diff --git a/consts/validation.js b/consts/validation.js index 49457bc3..12d73e97 100644 --- a/consts/validation.js +++ b/consts/validation.js @@ -22,7 +22,8 @@ const enums = { COOPERATION_STATUS_ENUM: ['pending', 'active', 'declined', 'closed'], PARAMS_ENUM: ['id', 'categoryId', 'subjectId'], OFFER_STATUS_ENUM: ['active', 'draft', 'closed'], - NOTIFICATION_TYPE_ENUM: ['new', 'requested', 'active', 'declined', 'updated', 'closed', 'deleted'] + NOTIFICATION_TYPE_ENUM: ['new', 'requested', 'active', 'declined', 'updated', 'closed', 'deleted'], + QUESTION_TYPE_ENUM: ['multipleChoice', 'openAnswer', 'oneAnswer'] } module.exports = { diff --git a/controllers/question.js b/controllers/question.js index 42e6d510..247dec52 100644 --- a/controllers/question.js +++ b/controllers/question.js @@ -13,6 +13,16 @@ const getQuestions = async (req, res) => { res.status(200).json(questions) } +const createQuestion = async (req, res) => { + const { id: author } = req.user + const data = req.body + + const newQuestion = await questionService.createQuestion(author, data) + + res.status(201).json(newQuestion) +} + module.exports = { - getQuestions + getQuestions, + createQuestion } diff --git a/docs/questions/questions.yaml b/docs/questions/questions.yaml new file mode 100644 index 00000000..661cb2f9 --- /dev/null +++ b/docs/questions/questions.yaml @@ -0,0 +1,64 @@ +paths: + /questions: + post: + security: + - bearerAuth: [] + tags: + - Questions + summary: Creates a new question. + description: Creates a new question. + produces: + - application/json + requestBody: + required: true + content: + application/json: + schema: + $ref: '#definitions/questionBody' + example: + title: What is the chemical symbol for water? + answers: + - text: First answer + isCorrect: true + - text: Second answer + isCorrect: false + type: multipleChoice + responses: + '201': + description: Created + content: + application/json: + schema: + $ref: '#definitions/question' + example: + _id: 63ec1cd51e9d781cdb6f4b14 + title: What is the chemical symbol for water? + answers: + - text: First answer + isCorrect: true + - text: Second answer + isCorrect: false + type: multipleChoice + author: 63da8767c9ad4c9a0b0eacd3 + createdAt: 2023-02-14T23:44:21.334Z + updatedAt: 2023-02-14T23:44:21.334Z + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/Error' + example: + status: 401 + code: UNAUTHORIZED + message: The requested URL requires user authorization. + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/Error' + example: + status: 403 + code: FORBIDDEN + message: You do not have permission to perform this action. diff --git a/models/question.js b/models/question.js index 3026bd35..924002ea 100644 --- a/models/question.js +++ b/models/question.js @@ -1,7 +1,14 @@ const { Schema, model } = require('mongoose') - +const { + enums: { QUESTION_TYPE_ENUM } +} = require('~/consts/validation') const { QUESTION, USER } = require('~/consts/models') -const { FIELD_CANNOT_BE_EMPTY, FIELD_CANNOT_BE_LONGER, FIELD_CANNOT_BE_SHORTER } = require('~/consts/errors') +const { + FIELD_CANNOT_BE_EMPTY, + FIELD_CANNOT_BE_LONGER, + FIELD_CANNOT_BE_SHORTER, + ENUM_CAN_BE_ONE_OF +} = require('~/consts/errors') const questionSchema = new Schema( { @@ -27,6 +34,14 @@ const questionSchema = new Schema( } } ], + type: { + type: String, + enum: { + values: QUESTION_TYPE_ENUM, + message: ENUM_CAN_BE_ONE_OF('type', QUESTION_TYPE_ENUM) + }, + required: true + }, author: { type: Schema.Types.ObjectId, ref: USER, diff --git a/routes/question.js b/routes/question.js index 9baec467..5b60d07c 100644 --- a/routes/question.js +++ b/routes/question.js @@ -2,10 +2,15 @@ const router = require('express').Router() const questionController = require('~/controllers/question') const asyncWrapper = require('~/middlewares/asyncWrapper') -const { authMiddleware } = require('~/middlewares/auth') +const { authMiddleware, restrictTo } = require('~/middlewares/auth') +const { + roles: { TUTOR } +} = require('~/consts/auth') router.use(authMiddleware) router.get('/', asyncWrapper(questionController.getQuestions)) +router.use(restrictTo(TUTOR)) +router.post('/', asyncWrapper(questionController.createQuestion)) module.exports = router diff --git a/services/question.js b/services/question.js index 5d4de36c..52c3307b 100644 --- a/services/question.js +++ b/services/question.js @@ -13,7 +13,17 @@ const questionService = { const count = await Question.countDocuments(match) return { items, count } + }, + + createQuestion: async (author, data) => { + const { title, answers, type } = data + + return await Question.create({ + title, + answers, + type, + author + }) } } - module.exports = questionService diff --git a/test/integration/controllers/question.spec.js b/test/integration/controllers/question.spec.js index 550f9c06..f5bafd2a 100644 --- a/test/integration/controllers/question.spec.js +++ b/test/integration/controllers/question.spec.js @@ -1,12 +1,11 @@ const { serverInit, serverCleanup, stopServer } = require('~/test/setup') const { expectError } = require('~/test/helpers') -const { UNAUTHORIZED } = require('~/consts/errors') +const { UNAUTHORIZED, FORBIDDEN } = require('~/consts/errors') const testUserAuthentication = require('~/utils/testUserAuth') const TokenService = require('~/services/token') const { roles: { TUTOR } } = require('~/consts/auth') -const Question = require('~/models/question') const endpointUrl = '/questions/' @@ -21,12 +20,24 @@ const testQuestionData = { text: 'Yes, of course', isCorrect: false } - ] + ], + type: 'multipleChoice' } +const studentUserData = { + role: 'student', + firstName: 'Yamada', + lastName: 'Kizen', + email: 'yamakai@gmail.com', + password: 'ninpopass', + appLanguage: 'en', + isEmailConfirmed: true, + lastLogin: new Date().toJSON(), + lastLoginAs: 'student' +} describe('Question controller', () => { - let app, server, accessToken, currentUser, testQuestion + let app, server, accessToken, currentUser, studentAccessToken, testQuestion beforeAll(async () => { ;({ app, server } = await serverInit()) @@ -34,10 +45,11 @@ describe('Question controller', () => { beforeEach(async () => { accessToken = await testUserAuthentication(app, { role: TUTOR }) + studentAccessToken = await testUserAuthentication(app, studentUserData) currentUser = TokenService.validateAccessToken(accessToken) - testQuestion = await Question.create({ author: currentUser.id, ...testQuestionData }) + testQuestion = await app.post(endpointUrl).send(testQuestionData).set('Authorization', `Bearer ${accessToken}`) }) afterEach(async () => { @@ -50,22 +62,52 @@ describe('Question controller', () => { describe(`GET ${endpointUrl}`, () => { it('should return list of questions', async () => { - const questions = await app.get(endpointUrl).set('Authorization', accessToken) + const questions = await app.get(endpointUrl).set('Authorization', `Bearer ${accessToken}`) expect(questions.statusCode).toBe(200) - expect(questions.count).toBe(1) - expect(questions.items).toContainObject({ - _id: testQuestion._id, + expect(questions.body.count).toBe(1) + expect(questions.body.items).toMatchObject([ + { + _id: testQuestion.body._id, + createdAt: expect.any(String), + updatedAt: expect.any(String), + ...testQuestionData + } + ]) + }) + + it('should throw UNAUTHORIZED', async () => { + const response = await app.get(endpointUrl) + + expectError(401, UNAUTHORIZED, response) + }) + }) + + describe(`POST ${endpointUrl}`, () => { + it('should create a new question', async () => { + expect(testQuestion.statusCode).toBe(201) + expect(testQuestion._body).toMatchObject({ + _id: expect.any(String), createdAt: expect.any(String), updatedAt: expect.any(String), + author: currentUser.id, ...testQuestionData }) }) it('should throw UNAUTHORIZED', async () => { - const response = await app.get(endpointUrl) + const response = await app.post(endpointUrl) expectError(401, UNAUTHORIZED, response) }) + + it('should throw FORBIDDEN', async () => { + const response = await app + .post(endpointUrl) + .send(testQuestionData) + .set('Authorization', `Bearer ${studentAccessToken}`) + + expectError(403, FORBIDDEN, response) + }) }) })