diff --git a/docs/course/course-schema.yaml b/docs/course/course-schema.yaml index 6f9cb736..db7360ac 100644 --- a/docs/course/course-schema.yaml +++ b/docs/course/course-schema.yaml @@ -6,6 +6,8 @@ definitions: properties: _id: type: string + banner: + type: string title: type: string description: @@ -34,6 +36,8 @@ definitions: properties: _id: type: string + banner: + type: string title: type: string description: @@ -60,6 +64,8 @@ definitions: courseBody: type: object properties: + banner: + type: string title: type: string description: @@ -79,4 +85,4 @@ definitions: ref: '#/components/attachments' required: - title - - description \ No newline at end of file + - description diff --git a/docs/course/course.yaml b/docs/course/course.yaml index 5a430549..4a4e641f 100644 --- a/docs/course/course.yaml +++ b/docs/course/course.yaml @@ -28,7 +28,8 @@ paths: schema: $ref: '#/definitions/course' example: - - _id: 64045f98b131dd04d7896af6 + - _id: 64045f98b131dd04d7896af6, + banner: 247829-banner.jpg title: Advanced English description: Advanced english course with 5 modules and separated lessons for improving speaking, listening and reading. author: 63da8767c9ad4c9a0b0eacd3 @@ -80,6 +81,7 @@ paths: schema: $ref: '#definitions/courseBody' example: + banner: 247829-banner.jpg title: Advanced English description: Advanced english course with 5 modules and separated lessons for improving speaking, listening and reading. author: 63da8767c9ad4c9a0b0eacd3 @@ -93,6 +95,7 @@ paths: schema: $ref: '#definitions/course' example: + banner: 247829-banner.jpg title: Advanced English description: Advanced english course with 5 modules and separated lessons for improving speaking, listening and reading. author: 63da8767c9ad4c9a0b0eacd3 @@ -206,6 +209,7 @@ paths: schema: $ref: '#definitions/course' example: + banner: 247829-banner.jpg title: Advanced English description: Advanced english course with 5 modules and separated lessons for improving speaking, listening and reading. author: 63da8767c9ad4c9a0b0eacd3 @@ -281,4 +285,4 @@ paths: example: status: 404 code: DOCUMENT_NOT_FOUND - message: Course with the specified id was not found. \ No newline at end of file + message: Course with the specified id was not found. diff --git a/models/course.js b/models/course.js index 93621b11..6bd55e6b 100644 --- a/models/course.js +++ b/models/course.js @@ -1,40 +1,43 @@ -const { Schema, model } = require('mongoose') - -const { FIELD_CANNOT_BE_EMPTY, FIELD_CANNOT_BE_SHORTER, FIELD_CANNOT_BE_LONGER } = require('~/consts/errors') -const { COURSE, USER, LESSON, ATTACHMENT } = require('~/consts/models') - -const courseSchema = new Schema( - { - title: { - type: String, - required: [true, FIELD_CANNOT_BE_EMPTY('title')], - minLength: [1, FIELD_CANNOT_BE_SHORTER('title', 1)], - maxLength: [100, FIELD_CANNOT_BE_LONGER('title', 100)] - }, - description: { - type: String, - required: [true, FIELD_CANNOT_BE_EMPTY('description')], - minLength: [1, FIELD_CANNOT_BE_SHORTER('description', 1)], - maxLength: [1000, FIELD_CANNOT_BE_LONGER('description', 1000)] - }, - author: { - type: Schema.Types.ObjectId, - ref: USER, - required: [true, FIELD_CANNOT_BE_EMPTY('author')] - }, - lessons: { - type: [Schema.Types.ObjectId], - ref: LESSON - }, - attachments: { - type: [Schema.Types.ObjectId], - ref: ATTACHMENT - } - }, - { - versionKey: false, - timestamps: true - } -) - -module.exports = model(COURSE, courseSchema) +const { Schema, model } = require('mongoose') + +const { FIELD_CANNOT_BE_EMPTY, FIELD_CANNOT_BE_SHORTER, FIELD_CANNOT_BE_LONGER } = require('~/consts/errors') +const { COURSE, USER, LESSON, ATTACHMENT } = require('~/consts/models') + +const courseSchema = new Schema( + { + banner: { + type: String + }, + title: { + type: String, + required: [true, FIELD_CANNOT_BE_EMPTY('title')], + minLength: [1, FIELD_CANNOT_BE_SHORTER('title', 1)], + maxLength: [100, FIELD_CANNOT_BE_LONGER('title', 100)] + }, + description: { + type: String, + required: [true, FIELD_CANNOT_BE_EMPTY('description')], + minLength: [1, FIELD_CANNOT_BE_SHORTER('description', 1)], + maxLength: [1000, FIELD_CANNOT_BE_LONGER('description', 1000)] + }, + author: { + type: Schema.Types.ObjectId, + ref: USER, + required: [true, FIELD_CANNOT_BE_EMPTY('author')] + }, + lessons: { + type: [Schema.Types.ObjectId], + ref: LESSON + }, + attachments: { + type: [Schema.Types.ObjectId], + ref: ATTACHMENT + } + }, + { + versionKey: false, + timestamps: true + } +) + +module.exports = model(COURSE, courseSchema) diff --git a/services/course.js b/services/course.js index 9120525a..a202a623 100644 --- a/services/course.js +++ b/services/course.js @@ -1,77 +1,78 @@ -const Course = require('~/models/course') - -const { DOCUMENT_NOT_FOUND } = require('~/consts/errors') -const { createError, createForbiddenError } = require('~/utils/errorsHelper') - -const courseService = { - getCourses: async ({ author, skip, limit }) => { - const items = await Course.find({ author }).skip(skip).limit(limit).sort({ updatedAt: -1 }).lean().exec() - const count = await Course.countDocuments({ author }) - - return { items, count } - }, - - getCourseById: async (id) => { - return await Course.findById(id).lean().exec() - }, - - createCourse: async (author, data) => { - const { title, description, lessons, attachments } = data - - return await Course.create({ - title, - description, - author, - lessons, - attachments - }) - }, - - updateCourse: async (userId, data) => { - const { id, title, description, attachments, rewriteAttachments = false } = data - - const course = await Course.findById(id).exec() - - if (!course) { - throw createError(DOCUMENT_NOT_FOUND(Course.modelName)) - } - - const courseAuthor = course.author.toString() - - if (userId !== courseAuthor) { - throw createForbiddenError() - } - - if (attachments) { - course.attachments = rewriteAttachments ? attachments : course.attachments.concat(attachments) - } - - const updateData = { title, description } - - for (const key in updateData) { - const value = updateData[key] - if (value) course[key] = value - } - - await course.validate() - await course.save() - }, - - deleteCourse: async (id, currentUser) => { - const course = await Course.findById(id).exec() - - if (!course) { - throw createError(DOCUMENT_NOT_FOUND(Course.modelName)) - } - - const author = course.author.toString() - - if (author !== currentUser) { - throw createForbiddenError() - } - - await Course.findByIdAndRemove(id).exec() - } -} - -module.exports = courseService +const Course = require('~/models/course') + +const { DOCUMENT_NOT_FOUND } = require('~/consts/errors') +const { createError, createForbiddenError } = require('~/utils/errorsHelper') + +const courseService = { + getCourses: async ({ author, skip, limit }) => { + const items = await Course.find({ author }).skip(skip).limit(limit).sort({ updatedAt: -1 }).lean().exec() + const count = await Course.countDocuments({ author }) + + return { items, count } + }, + + getCourseById: async (id) => { + return await Course.findById(id).lean().exec() + }, + + createCourse: async (author, data) => { + const { banner, title, description, lessons, attachments } = data + + return await Course.create({ + banner, + title, + description, + author, + lessons, + attachments + }) + }, + + updateCourse: async (userId, data) => { + const { id, banner, title, description, attachments, rewriteAttachments = false } = data + + const course = await Course.findById(id).exec() + + if (!course) { + throw createError(DOCUMENT_NOT_FOUND(Course.modelName)) + } + + const courseAuthor = course.author.toString() + + if (userId !== courseAuthor) { + throw createForbiddenError() + } + + if (attachments) { + course.attachments = rewriteAttachments ? attachments : course.attachments.concat(attachments) + } + + const updateData = { banner, title, description } + + for (const key in updateData) { + const value = updateData[key] + if (value) course[key] = value + } + + await course.validate() + await course.save() + }, + + deleteCourse: async (id, currentUser) => { + const course = await Course.findById(id).exec() + + if (!course) { + throw createError(DOCUMENT_NOT_FOUND(Course.modelName)) + } + + const author = course.author.toString() + + if (author !== currentUser) { + throw createForbiddenError() + } + + await Course.findByIdAndRemove(id).exec() + } +} + +module.exports = courseService diff --git a/test/integration/controllers/course.spec.js b/test/integration/controllers/course.spec.js index e0fd14ef..821812e3 100644 --- a/test/integration/controllers/course.spec.js +++ b/test/integration/controllers/course.spec.js @@ -1,212 +1,215 @@ -const { serverInit, serverCleanup, stopServer } = require('~/test/setup') -const checkCategoryExistence = require('~/seed/checkCategoryExistence') -const testUserAuthentication = require('~/utils/testUserAuth') -const Course = require('~/models/course') -const { expectError } = require('~/test/helpers') -const { UNAUTHORIZED, DOCUMENT_NOT_FOUND, FORBIDDEN } = require('~/consts/errors') -const uploadService = require('~/services/upload') - -const endpointUrl = '/courses/' - -let mockUploadFile = jest.fn().mockResolvedValue('mocked-file-url') - -const nonExistingCourseId = '64a51e41de4debbccf0b39b0' - -let tutorUser = { - role: 'tutor', - firstName: 'albus', - lastName: 'dumbledore', - email: 'lovemagic@gmail.com', - password: 'supermagicpass123', - appLanguage: 'en', - FAQ: { student: [{ question: 'question1', answer: 'answer1' }] }, - isEmailConfirmed: true, - lastLogin: new Date().toJSON(), - lastLoginAs: 'tutor' -} - -const testCourseData = { - title: 'assembly', - description: 'you will learn some modern programming language for all your needs', - attachments: [ - { - src: 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAgAAAQABAAD...', - name: 'example1.jpg' - } - ], - lessons: [] -} - -const updateData = { - title: 'new title', - description: 'new description' -} - -describe('Course controller', () => { - let app, server, accessToken, studentAccessToken, testCourseResponse, testCourse - - beforeAll(async () => { - ;({ app, server } = await serverInit()) - }) - - beforeEach(async () => { - await checkCategoryExistence() - - accessToken = await testUserAuthentication(app, tutorUser) - studentAccessToken = await testUserAuthentication(app) - - uploadService.uploadFile = mockUploadFile - - testCourseResponse = await app.post(endpointUrl).set('Authorization', `Bearer ${accessToken}`).send(testCourseData) - testCourse = testCourseResponse.body - }) - - afterEach(async () => { - await serverCleanup() - }) - - afterAll(async () => { - await stopServer(server) - }) - - describe(`GET ${endpointUrl}`, () => { - it('should get all courses', async () => { - const response = await app.get(endpointUrl).set('Authorization', `Bearer ${accessToken}`) - - expect(response.statusCode).toBe(200) - expect(response.body).toEqual({ count: 1, items: [expect.objectContaining(testCourseData)] }) - }) - - it('should throw UNAUTHORIZED', async () => { - const response = await app.get(endpointUrl) - - expectError(401, UNAUTHORIZED, response) - }) - - it('should throw FORBIDDEN', async () => { - const response = await app.get(endpointUrl).set('Authorization', `Bearer ${studentAccessToken}`) - - expectError(403, FORBIDDEN, response) - }) - }) - - describe(`POST ${endpointUrl}`, () => { - it('should create a course', async () => { - expect(testCourseResponse.statusCode).toBe(201) - expect(testCourseResponse.body).toMatchObject({ - title: testCourseData.title, - description: testCourseData.description, - attachments: ['mocked-file-url'], - lessons: expect.any(Array) - }) - }) - - it('should throw UNAUTHORIZED', async () => { - const response = await app.post(endpointUrl) - - expectError(401, UNAUTHORIZED, response) - }) - - it('should throw FORBIDDEN', async () => { - const response = await app - .patch(endpointUrl) - .set('Authorization', `Bearer ${studentAccessToken}`) - .send(testCourseData) - - expectError(403, FORBIDDEN, response) - }) - }) - - describe(`PATCH ${endpointUrl}:id`, () => { - it('should update a course', async () => { - const response = await app - .patch(endpointUrl + testCourse._id) - .set('Authorization', `Bearer ${accessToken}`) - .send(updateData) - - const { title, description } = await Course.findById(testCourse._id) - - expect(response.statusCode).toBe(204) - expect({ title, description }).toMatchObject(updateData) - }) - - it('should throw UNAUTHORIZED', async () => { - const response = await app.patch(endpointUrl) - - expectError(401, UNAUTHORIZED, response) - }) - - it('should throw DOCUMENT_NOT_FOUND', async () => { - const response = await app.patch(endpointUrl + nonExistingCourseId).set('Authorization', `Bearer ${accessToken}`) - - expectError(404, DOCUMENT_NOT_FOUND([Course.modelName]), response) - }) - - it('should throw FORBIDDEN', async () => { - const response = await app - .patch(endpointUrl) - .set('Authorization', `Bearer ${studentAccessToken}`) - .send(updateData) - - expectError(403, FORBIDDEN, response) - }) - }) - - describe(`GET ${endpointUrl}:id`, () => { - it('should get course by id', async () => { - const response = await app - .get(endpointUrl + testCourseResponse.body._id) - .set('Authorization', `Bearer ${accessToken}`) - - expect(response.statusCode).toBe(200) - expect(response.body).toMatchObject({ - _id: expect.any(String), - author: expect.any(String), - title: 'assembly', - description: 'you will learn some modern programming language for all your needs', - attachments: ['mocked-file-url'], - lessons: expect.any(Array), - createdAt: expect.any(String), - updatedAt: expect.any(String) - }) - }) - - it('should throw DOCUMENT_NOT_FOUND', async () => { - const response = await app.get(endpointUrl + nonExistingCourseId).set('Authorization', `Bearer ${accessToken}`) - - expectError(404, DOCUMENT_NOT_FOUND([Course.modelName]), response) - }) - - it('should throw UNAUTHORIZED', async () => { - const response = await app.get(endpointUrl) - - expectError(401, UNAUTHORIZED, response) - }) - }) - - describe(`DELETE ${endpointUrl}:id`, () => { - it('should delete a course', async () => { - const response = await app.delete(endpointUrl + testCourse._id).set('Authorization', `Bearer ${accessToken}`) - - expect(response.statusCode).toBe(204) - }) - - it('should throw UNAUTHORIZED', async () => { - const response = await app.delete(endpointUrl) - - expectError(401, UNAUTHORIZED, response) - }) - - it('should throw DOCUMENT_NOT_FOUND', async () => { - const response = await app.delete(endpointUrl + nonExistingCourseId).set('Authorization', `Bearer ${accessToken}`) - - expectError(404, DOCUMENT_NOT_FOUND([Course.modelName]), response) - }) - - it('should throw FORBIDDEN', async () => { - const response = await app.delete(endpointUrl).set('Authorization', `Bearer ${studentAccessToken}`) - - expectError(403, FORBIDDEN, response) - }) - }) -}) +const { serverInit, serverCleanup, stopServer } = require('~/test/setup') +const checkCategoryExistence = require('~/seed/checkCategoryExistence') +const testUserAuthentication = require('~/utils/testUserAuth') +const Course = require('~/models/course') +const { expectError } = require('~/test/helpers') +const { UNAUTHORIZED, DOCUMENT_NOT_FOUND, FORBIDDEN } = require('~/consts/errors') +const uploadService = require('~/services/upload') + +const endpointUrl = '/courses/' + +let mockUploadFile = jest.fn().mockResolvedValue('mocked-file-url') + +const nonExistingCourseId = '64a51e41de4debbccf0b39b0' + +let tutorUser = { + role: 'tutor', + firstName: 'albus', + lastName: 'dumbledore', + email: 'lovemagic@gmail.com', + password: 'supermagicpass123', + appLanguage: 'en', + FAQ: { student: [{ question: 'question1', answer: 'answer1' }] }, + isEmailConfirmed: true, + lastLogin: new Date().toJSON(), + lastLoginAs: 'tutor' +} + +const testCourseData = { + banner: 'img-path', + title: 'assembly', + description: 'you will learn some modern programming language for all your needs', + attachments: [ + { + src: 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAgAAAQABAAD...', + name: 'example1.jpg' + } + ], + lessons: [] +} + +const updateData = { + title: 'new title', + description: 'new description' +} + +describe('Course controller', () => { + let app, server, accessToken, studentAccessToken, testCourseResponse, testCourse + + beforeAll(async () => { + ;({ app, server } = await serverInit()) + }) + + beforeEach(async () => { + await checkCategoryExistence() + + accessToken = await testUserAuthentication(app, tutorUser) + studentAccessToken = await testUserAuthentication(app) + + uploadService.uploadFile = mockUploadFile + + testCourseResponse = await app.post(endpointUrl).set('Authorization', `Bearer ${accessToken}`).send(testCourseData) + testCourse = testCourseResponse.body + }) + + afterEach(async () => { + await serverCleanup() + }) + + afterAll(async () => { + await stopServer(server) + }) + + describe(`GET ${endpointUrl}`, () => { + it('should get all courses', async () => { + const response = await app.get(endpointUrl).set('Authorization', `Bearer ${accessToken}`) + + expect(response.statusCode).toBe(200) + expect(response.body).toEqual({ count: 1, items: [expect.objectContaining(testCourseData)] }) + }) + + it('should throw UNAUTHORIZED', async () => { + const response = await app.get(endpointUrl) + + expectError(401, UNAUTHORIZED, response) + }) + + it('should throw FORBIDDEN', async () => { + const response = await app.get(endpointUrl).set('Authorization', `Bearer ${studentAccessToken}`) + + expectError(403, FORBIDDEN, response) + }) + }) + + describe(`POST ${endpointUrl}`, () => { + it('should create a course', async () => { + expect(testCourseResponse.statusCode).toBe(201) + expect(testCourseResponse.body).toMatchObject({ + banner: testCourseData.banner, + title: testCourseData.title, + description: testCourseData.description, + attachments: ['mocked-file-url'], + lessons: expect.any(Array) + }) + }) + + it('should throw UNAUTHORIZED', async () => { + const response = await app.post(endpointUrl) + + expectError(401, UNAUTHORIZED, response) + }) + + it('should throw FORBIDDEN', async () => { + const response = await app + .patch(endpointUrl) + .set('Authorization', `Bearer ${studentAccessToken}`) + .send(testCourseData) + + expectError(403, FORBIDDEN, response) + }) + }) + + describe(`PATCH ${endpointUrl}:id`, () => { + it('should update a course', async () => { + const response = await app + .patch(endpointUrl + testCourse._id) + .set('Authorization', `Bearer ${accessToken}`) + .send(updateData) + + const { title, description } = await Course.findById(testCourse._id) + + expect(response.statusCode).toBe(204) + expect({ title, description }).toMatchObject(updateData) + }) + + it('should throw UNAUTHORIZED', async () => { + const response = await app.patch(endpointUrl) + + expectError(401, UNAUTHORIZED, response) + }) + + it('should throw DOCUMENT_NOT_FOUND', async () => { + const response = await app.patch(endpointUrl + nonExistingCourseId).set('Authorization', `Bearer ${accessToken}`) + + expectError(404, DOCUMENT_NOT_FOUND([Course.modelName]), response) + }) + + it('should throw FORBIDDEN', async () => { + const response = await app + .patch(endpointUrl) + .set('Authorization', `Bearer ${studentAccessToken}`) + .send(updateData) + + expectError(403, FORBIDDEN, response) + }) + }) + + describe(`GET ${endpointUrl}:id`, () => { + it('should get course by id', async () => { + const response = await app + .get(endpointUrl + testCourseResponse.body._id) + .set('Authorization', `Bearer ${accessToken}`) + + expect(response.statusCode).toBe(200) + expect(response.body).toMatchObject({ + _id: expect.any(String), + banner: expect.any(String), + author: expect.any(String), + title: 'assembly', + description: 'you will learn some modern programming language for all your needs', + attachments: ['mocked-file-url'], + lessons: expect.any(Array), + createdAt: expect.any(String), + updatedAt: expect.any(String) + }) + }) + + it('should throw DOCUMENT_NOT_FOUND', async () => { + const response = await app.get(endpointUrl + nonExistingCourseId).set('Authorization', `Bearer ${accessToken}`) + + expectError(404, DOCUMENT_NOT_FOUND([Course.modelName]), response) + }) + + it('should throw UNAUTHORIZED', async () => { + const response = await app.get(endpointUrl) + + expectError(401, UNAUTHORIZED, response) + }) + }) + + describe(`DELETE ${endpointUrl}:id`, () => { + it('should delete a course', async () => { + const response = await app.delete(endpointUrl + testCourse._id).set('Authorization', `Bearer ${accessToken}`) + + expect(response.statusCode).toBe(204) + }) + + it('should throw UNAUTHORIZED', async () => { + const response = await app.delete(endpointUrl) + + expectError(401, UNAUTHORIZED, response) + }) + + it('should throw DOCUMENT_NOT_FOUND', async () => { + const response = await app.delete(endpointUrl + nonExistingCourseId).set('Authorization', `Bearer ${accessToken}`) + + expectError(404, DOCUMENT_NOT_FOUND([Course.modelName]), response) + }) + + it('should throw FORBIDDEN', async () => { + const response = await app.delete(endpointUrl).set('Authorization', `Bearer ${studentAccessToken}`) + + expectError(403, FORBIDDEN, response) + }) + }) +})