Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EW-1008 Decouple lesson dependency from common-cartridge module #5300

Merged
merged 19 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './lesson.dto';
export * from './lesson-materials.dto';
export * from './lesson-contents.dto';
export * from './lesson-linked-task.dto';
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export class LessonContentDto {
content: object;

title: string;

component: LessonContentDtoComponent;

hidden: boolean;

constructor(props: LessonContentDto) {
this.content = props.content;
this.title = props.title;
this.component = props.component;
this.hidden = props.hidden;
}
}

export const LessonContentDtoComponentValues = {
ETHERPAD: 'Etherpad',
GEO_GEBRA: 'geoGebra',
INTERNAL: 'internal',
RESOURCES: 'resources',
TEXT: 'text',
NE_XBOARD: 'neXboard',
} as const;

export type LessonContentDtoComponent =
typeof LessonContentDtoComponentValues[keyof typeof LessonContentDtoComponentValues];
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
export class LessonLinkedTaskDto {
name: string;

description: string;

descriptionInputFormat: LessonLinkedTaskDescriptionInputFormatType;

availableDate: string | null;

dueDate: string | null;

private: boolean;

publicSubmissions: boolean | null;

teamSubmissions: boolean | null;

creator: string | null;

courseId: string | null;

submissionIds: string[];

finishedIds: string[];

constructor(props: LessonLinkedTaskDto) {
this.name = props.name;
this.description = props.description;
this.descriptionInputFormat = props.descriptionInputFormat;
this.availableDate = props.availableDate;
this.dueDate = props.dueDate;
this.private = props.private;
this.creator = props.creator;
this.courseId = props.courseId;
this.publicSubmissions = props.publicSubmissions;
this.teamSubmissions = props.teamSubmissions;
this.submissionIds = props.submissionIds;
this.finishedIds = props.finishedIds;
}
}

export const LessonLinkedTaskDescriptionInputFormat = {
PLAIN_TEXT: 'plainText',
RICH_TEXT_CK5_SIMPLE: 'richTextCk5Simple',
RICH_TEXT_CK4: 'richTextCk4',
RICH_TEXT_CK5: 'richTextCk5',
} as const;

export type LessonLinkedTaskDescriptionInputFormatType =
typeof LessonLinkedTaskDescriptionInputFormat[keyof typeof LessonLinkedTaskDescriptionInputFormat];
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export class LessonMaterialsDto {
materialsId: string;

title: string;

relatedResources: string[];

url: string;

client: string;

license: string[];

merlinReference: string;

constructor(props: LessonMaterialsDto) {
this.materialsId = props.materialsId;
this.title = props.title;
this.relatedResources = props.relatedResources;
this.url = props.url;
this.client = props.client;
this.license = props.license;
this.merlinReference = props.merlinReference;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { LessonContentDto } from './lesson-contents.dto';
import { LessonMaterialsDto } from './lesson-materials.dto';

export class LessonDto {
lessonId: string;

name: string;

courseId?: string;

courseGroupId?: string;

hidden: boolean;

position: number;

contents: LessonContentDto[];

materials: LessonMaterialsDto[];

constructor(props: LessonDto) {
this.lessonId = props.lessonId;
this.name = props.name;
this.courseId = props.courseId;
this.courseGroupId = props.courseGroupId;
this.hidden = props.hidden;
this.position = props.position;
this.contents = props.contents;
this.materials = props.materials;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import { faker } from '@faker-js/faker';
import { createMock, DeepMocked } from '@golevelup/ts-jest';
import { UnauthorizedException } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { Test, TestingModule } from '@nestjs/testing';
import { AxiosResponse } from 'axios';
import { Request } from 'express';
import { LessonClientAdapter } from './lesson-client.adapter';
import { LessonApi, LessonLinkedTaskResponse, LessonResponse } from './lessons-api-client';

describe(LessonClientAdapter.name, () => {
let module: TestingModule;
let sut: LessonClientAdapter;
let lessonApiMock: DeepMocked<LessonApi>;
const jwtToken = faker.string.alphanumeric(20);

beforeAll(async () => {
module = await Test.createTestingModule({
providers: [
LessonClientAdapter,
{
provide: LessonApi,
useValue: createMock<LessonApi>(),
},
{
provide: REQUEST,
useValue: createMock<Request>({
headers: {
authorization: `Bearer ${jwtToken}`,
},
}),
},
],
}).compile();

sut = module.get(LessonClientAdapter);
lessonApiMock = module.get(LessonApi);
});

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

afterEach(() => {
jest.resetAllMocks();
});

it('should be defined', () => {
expect(sut).toBeDefined();
});

describe('getLessonById', () => {
describe('When getLessonById is called', () => {
const setup = () => {
const response = createMock<AxiosResponse<LessonResponse>>({
data: {
_id: faker.string.uuid(),
id: faker.string.uuid(),
name: faker.lorem.sentence(),
courseId: faker.string.uuid(),
courseGroupId: faker.string.uuid(),
hidden: faker.datatype.boolean(),
position: faker.number.int(),
contents: [
{
content: { text: faker.lorem.sentence() },
_id: faker.string.uuid(),
id: faker.string.uuid(),
title: faker.lorem.sentence(),
component: faker.helpers.arrayElement(['Etherpad', 'geoGebra', 'neXboard']),
hidden: faker.datatype.boolean(),
},
],
materials: [
{
_id: faker.string.uuid(),
id: faker.string.uuid(),
title: faker.lorem.sentence(),
relatedResources: [faker.lorem.sentence()],
url: faker.internet.url(),
client: faker.lorem.sentence(),
license: [faker.lorem.sentence()],
merlinReference: faker.lorem.sentence(),
},
],
},
});

lessonApiMock.lessonControllerGetLesson.mockResolvedValue(response);

return { lessonId: response.data.id };
};

it('should call lessonControllerGetLesson', async () => {
const { lessonId } = setup();

await sut.getLessonById(lessonId);

expect(lessonApiMock.lessonControllerGetLesson).toHaveBeenCalled();
});
});

describe('When getLessonById is called with invalid id', () => {
const setup = () => {
const lessonResponseId = faker.string.uuid();

lessonApiMock.lessonControllerGetLesson.mockRejectedValueOnce(new Error('error'));

return { lessonResponseId };
};

it('should throw an error', async () => {
const { lessonResponseId } = setup();

const result = sut.getLessonById(lessonResponseId);

await expect(result).rejects.toThrowError('error');
});
});

describe('When no JWT token is found', () => {
const setup = () => {
const lessonResponseId = faker.string.uuid();
const request = createMock<Request>({
headers: {},
}) as Request;

const adapter: LessonClientAdapter = new LessonClientAdapter(lessonApiMock, request);

return { lessonResponseId, adapter };
};

it('should throw an UnauthorizedError', async () => {
const { lessonResponseId, adapter } = setup();

await expect(adapter.getLessonById(lessonResponseId)).rejects.toThrowError(UnauthorizedException);
});
});
});

describe('getLessonTasks', () => {
describe('When getLessonTasks is called', () => {
const setup = () => {
const lessonId = faker.string.uuid();
const response = createMock<AxiosResponse<LessonLinkedTaskResponse[]>>({
data: [
{
name: faker.lorem.sentence(),
description: faker.lorem.sentence(),
descriptionInputFormat: faker.helpers.arrayElement(['plainText', 'richTextCk4', 'richTextCk5Simple']),
availableDate: faker.date.recent().toString(),
dueDate: faker.date.future().toString(),
private: faker.datatype.boolean(),
publicSubmissions: faker.datatype.boolean(),
teamSubmissions: faker.datatype.boolean(),
},
],
});

lessonApiMock.lessonControllerGetLessonTasks.mockResolvedValue(response);

return { lessonId };
};

it('should call lessonControllerGetLessonTasks', async () => {
const { lessonId } = setup();

await sut.getLessonTasks(lessonId);

expect(lessonApiMock.lessonControllerGetLessonTasks).toHaveBeenCalled();
});
});

describe('When getLessonTasks is called with invalid id', () => {
const setup = () => {
const lessonResponseId = faker.string.uuid();

lessonApiMock.lessonControllerGetLessonTasks.mockRejectedValueOnce(new Error('error'));

return { lessonResponseId };
};
it('should throw an error', async () => {
psachmann marked this conversation as resolved.
Show resolved Hide resolved
const { lessonResponseId } = setup();

const result = sut.getLessonTasks(lessonResponseId);

await expect(result).rejects.toThrowError('error');
});
});

describe('When no JWT token is found', () => {
const setup = () => {
const lessonResponseId = faker.string.uuid();
const request = createMock<Request>({
headers: {},
}) as Request;

const adapter: LessonClientAdapter = new LessonClientAdapter(lessonApiMock, request);

return { lessonResponseId, adapter };
};

it('should throw an UnauthorizedError', async () => {
const { lessonResponseId, adapter } = setup();

await expect(adapter.getLessonTasks(lessonResponseId)).rejects.toThrowError(UnauthorizedException);
});
});
});
});
Loading
Loading