├── renovate.json ├── views ├── partials │ ├── footer.hbs │ └── header.hbs ├── error.hbs ├── users │ ├── instructors │ │ ├── beInstructor.hbs │ │ ├── new-course.hbs │ │ ├── student-list.hbs │ │ ├── edit-course.hbs │ │ ├── new-lesson.hbs │ │ ├── course-taught.hbs │ │ └── edit-lesson.hbs │ ├── common │ │ ├── lesson-details.hbs │ │ ├── profile.hbs │ │ ├── all-courses.hbs │ │ ├── course-details.hbs │ │ ├── all-course-lessons.hbs │ │ ├── my-courses.hbs │ │ └── edit-profile.hbs │ ├── students │ │ └── course-learned.hbs │ └── auth │ │ ├── login.hbs │ │ └── register.hbs ├── index.hbs └── layout.hbs ├── globalConfig.json ├── assets └── db-diagram.png ├── public ├── images │ └── favicon.ico └── stylesheets │ ├── style.css │ └── reset.css ├── utils ├── global-utils │ ├── isString.utils.js │ ├── handleAsyncFunction.utils.js │ ├── index.js │ ├── ensureDataInVulnerableOfInjectionAttacks.utils.js │ └── __tests__ │ │ └── handleAsyncFunction.utils.test.js └── test-utils │ ├── courseControllerDeps.js │ ├── dbHandler.utils.js │ ├── interceptors.utils.js │ └── validators.utils.js ├── prettier.config.js ├── database ├── services │ ├── helpers │ │ ├── index.js │ │ └── hashPassword.helper.js │ └── modelServices │ │ ├── lessonServices │ │ ├── index.js │ │ ├── findAllLessons.service.js │ │ ├── findOneLesson.service.js │ │ ├── createNewLesson.service.js │ │ └── __tests__ │ │ │ ├── findOneLesson.service.test.js │ │ │ ├── createNewLesson.service.test.js │ │ │ └── findAllLessons.service.test.js │ │ ├── courseServices │ │ ├── findAllCourses.service.js │ │ ├── deleteCourse.service.js │ │ ├── createNewCourse.service.js │ │ ├── findOneCourse.service.js │ │ ├── index.js │ │ ├── updateCourse.service.js │ │ └── __tests__ │ │ │ ├── findOneCourse.service.test.js │ │ │ ├── findAllCourses.service.test.js │ │ │ ├── createNewCourse.service.test.js │ │ │ ├── deleteCourse.service.test.js │ │ │ └── updateCourse.service.test.js │ │ └── userServices │ │ ├── findUserById.service.js │ │ ├── updateUserRole.service.js │ │ ├── index.js │ │ ├── updateUserProfileData.service.js │ │ ├── createNewUser.service.js │ │ ├── createNewGoogleUser.service.js │ │ └── __tests__ │ │ ├── updateUserRole.service.test.js │ │ ├── createNewUser.service.test.js │ │ ├── createNewGoogleUser.service.test.js │ │ ├── findUserById.service.test.js │ │ └── updateUserProfileData.service.test.js ├── models │ ├── lesson.model.js │ ├── course.model.js │ ├── user.model.js │ └── __tests__ │ │ ├── lesson.model.test.js │ │ ├── user.model.test.js │ │ └── course.model.test.js └── fixtures │ └── index.js ├── api ├── middleware │ ├── isLoggedInUser.middleware.js │ ├── isInstructor.middleware.js │ ├── index.js │ ├── __tests__ │ │ ├── isLoggedInUser.middleware.test.js │ │ ├── isInstructor.middleware.test.js │ │ └── validateRegistrationForm.middleware.test.js │ └── validateRegistrationForm.middleware.js ├── controllers │ ├── helpers │ │ ├── includesUserType.helper.js │ │ ├── checkIfValidObjectId.helper.js │ │ ├── duplicateErrorMessage.helper.js │ │ ├── redirectNonExistentData.helper.js │ │ ├── renderLoginFormWithErrors.helper.js │ │ ├── authenticateUser.helper.js │ │ ├── getDuplicateErrorMessage.helper.js │ │ ├── __tests__ │ │ │ ├── authenticateUser.helper.test.js │ │ │ ├── duplicateErrorMessage.helper.test.js │ │ │ ├── render500Error.helper.test.js │ │ │ ├── checkIfValidObjectId.helper.test.js │ │ │ ├── renderLoginFormWithErrors.helper.test.js │ │ │ ├── redirectNonexistentCourse.helper.test.js │ │ │ ├── renderRegistrationFormWithErrors.helper.test.js │ │ │ ├── getDuplicateErrorMessage.helper.test.js │ │ │ ├── createNewUser.helper.test.js │ │ │ ├── handleIfAsyncError.helper.test.js │ │ │ ├── handleUpdatedPassword.helper.test.js │ │ │ ├── setUserInSessionAndLogin.helper.test.js │ │ │ ├── checkCurrentUserRelationToCourse.helper.test.js │ │ │ └── filterCourses.helper.test.js │ │ ├── renderRegistrationFormWithErrors.helper.js │ │ ├── render500Error.helper.js │ │ ├── handleUpdatedPassword.helper.js │ │ ├── setUserInSessionAndLogin.helper.js │ │ ├── filterCourses.helper.js │ │ ├── handleIfAsyncError.helper.js │ │ ├── createNewUser.helper.js │ │ ├── checkCurrentUserRelationToCourse.helper.js │ │ └── index.js │ ├── usersAuthControllers │ │ ├── renderRegister.controller.js │ │ ├── logoutUser.controller.js │ │ ├── renderLogin.controller.js │ │ ├── authenticateGoogleLogin.controller.js │ │ ├── authenticateGoogleCallback.controller.js │ │ ├── loginUser.controller.js │ │ ├── registerNewUser.controller.js │ │ ├── __tests__ │ │ │ ├── logoutUser.controller.test.js │ │ │ ├── renderRegister.controller.test.js │ │ │ ├── renderLogin.controller.test.js │ │ │ ├── registerNewUser.controller.test.js │ │ │ └── loginUser.controller.test.js │ │ └── index.js │ ├── indexControllers │ │ ├── index.js │ │ ├── renderIndexView.controller.js │ │ └── __tests__ │ │ │ └── renderIndexView.controller.test.js │ ├── coursesControllers │ │ ├── renderCreateNewCourse.controller.js │ │ ├── renderAllCourses.controller.js │ │ ├── deleteTaughtCourse.controller.js │ │ ├── registerToCourse.controller.js │ │ ├── renderStudentList.controller.js │ │ ├── renderLearnedCourse.controller.js │ │ ├── renderEditTaughtCourse.controller.js │ │ ├── renderTaughtCourse.controller.js │ │ ├── renderMyCourses.controller.js │ │ ├── updateTaughtCourse.controller.js │ │ ├── createNewCourse.controller.js │ │ ├── __tests__ │ │ │ ├── renderCreateNewCourse.controller.test.js │ │ │ ├── renderAllCourses.controller.test.js │ │ │ ├── deleteTaughtCourse.controller.test.js │ │ │ ├── createNewCourse.controller.test.js │ │ │ ├── registerToCourse.controller.test.js │ │ │ ├── renderLearnedCourse.controller.test.js │ │ │ ├── renderStudentList.controller.test.js │ │ │ ├── renderMyCourses.controller.test.js │ │ │ ├── renderEditTaughtCourse.controller.test.js │ │ │ ├── updateTaughtCourse.controller.test.js │ │ │ ├── renderTaughtCourse.controller.test.js │ │ │ └── renderCourseDetails.controller.test.js │ │ ├── renderCourseDetails.controller.js │ │ └── index.js │ ├── lessonsControllers │ │ ├── renderCreateNewLesson.controller.js │ │ ├── index.js │ │ ├── renderEditLesson.controller.js │ │ ├── renderLessonDetails.controller.js │ │ ├── renderAllLessonsTaught.controller.js │ │ ├── __tests__ │ │ │ ├── renderCreateNewLesson.controller.test.js │ │ │ ├── renderEditLesson.controller.test.js │ │ │ ├── renderAllLessonsTaught.controller.test.js │ │ │ ├── renderLessonDetails.controller.test.js │ │ │ └── createNewLesson.controller.test.js │ │ └── createNewLesson.controller.js │ ├── usersControllers │ │ ├── renderUserProfile.controller.js │ │ ├── index.js │ │ ├── renderEditUserProfile.controller.js │ │ ├── __tests__ │ │ │ ├── renderUserProfile.controller.test.js │ │ │ └── renderEditUserProfile.controller.test.js │ │ └── updateUserProfile.controller.js │ ├── instructorsControllers │ │ ├── index.js │ │ ├── renderBeInstructor.controller.js │ │ ├── changeRoleToInstructor.controller.js │ │ └── __tests__ │ │ │ ├── renderBeInstructor.controller.test.js │ │ │ └── changeRoleToInstructor.controller.test.js │ ├── __tests__ │ │ └── index.test.js │ └── index.js └── routes │ ├── index.routes.js │ ├── instructors.routes.js │ ├── users.routes.js │ ├── users-auth.routes.js │ ├── lessons.routes.js │ └── courses.routes.js ├── passport ├── helpers │ ├── index.js │ └── comparePassword.helper.js ├── serializeSession.js ├── localStrategy.js └── googleStrategy.js ├── .gitignore ├── src └── app.js ├── configs ├── __tests__ │ └── index.test.js ├── cloudinary │ ├── index.js │ └── __tests__ │ │ └── index.test.js └── index.js ├── loaders ├── __tests__ │ ├── mongoose.loader.test.js │ ├── routes.loader.test.js │ └── express.loader.test.js ├── mongoose.loader.js ├── routes.loader.js └── express.loader.js ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── tests-coverage.yml │ └── deployment-automerge.yml ├── .eslintrc.json ├── CONTRIBUTING.md ├── LICENSE ├── bin └── www └── package.json /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /views/partials/footer.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /globalConfig.json: -------------------------------------------------------------------------------- 1 | {"mongoUri":"mongodb://127.0.0.1:61900/jest?","mongoDBName":"jest"} -------------------------------------------------------------------------------- /assets/db-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reMRKableDev/OnLearn/HEAD/assets/db-diagram.png -------------------------------------------------------------------------------- /views/error.hbs: -------------------------------------------------------------------------------- 1 |

{{message}}

2 |

{{error.status}}

3 |
{{error.stack}}
4 | -------------------------------------------------------------------------------- /public/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reMRKableDev/OnLearn/HEAD/public/images/favicon.ico -------------------------------------------------------------------------------- /utils/global-utils/isString.utils.js: -------------------------------------------------------------------------------- 1 | exports.isString = (value) => 2 | typeof value === 'string' || value instanceof String; 3 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | tabWidth: 2, 3 | semi: true, 4 | singleQuote: true, 5 | trailingComma: 'es5', 6 | }; 7 | -------------------------------------------------------------------------------- /database/services/helpers/index.js: -------------------------------------------------------------------------------- 1 | const { hashPasswordHelper } = require('./hashPassword.helper'); 2 | 3 | module.exports = { hashPasswordHelper }; 4 | -------------------------------------------------------------------------------- /api/middleware/isLoggedInUser.middleware.js: -------------------------------------------------------------------------------- 1 | exports.isLoggedInUser = (req, res, next) => 2 | req.isAuthenticated() ? next() : res.redirect(302, '/'); 3 | -------------------------------------------------------------------------------- /api/controllers/helpers/includesUserType.helper.js: -------------------------------------------------------------------------------- 1 | exports.includesUserTypeHelper = (courseItemUserType, userId) => 2 | courseItemUserType.includes(userId); 3 | -------------------------------------------------------------------------------- /passport/helpers/index.js: -------------------------------------------------------------------------------- 1 | const { comparePasswordHelper } = require('./comparePassword.helper'); 2 | 3 | module.exports = { 4 | comparePasswordHelper, 5 | }; 6 | -------------------------------------------------------------------------------- /api/controllers/usersAuthControllers/renderRegister.controller.js: -------------------------------------------------------------------------------- 1 | exports.renderRegisterController = (_req, res) => 2 | res.status(200).render('users/auth/register'); 3 | -------------------------------------------------------------------------------- /api/controllers/indexControllers/index.js: -------------------------------------------------------------------------------- 1 | const { renderIndexViewController } = require('./renderIndexView.controller'); 2 | 3 | module.exports = { renderIndexViewController }; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node Modules 2 | node_modules/ 3 | 4 | # DS_Store & .vscode 5 | DS_Store 6 | .vscode 7 | 8 | # Environment variables 9 | .env 10 | 11 | # Coverage 12 | coverage/ -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | const app = require('../loaders/express.loader'); 2 | require('../loaders/mongoose.loader')(); 3 | require('../loaders/routes.loader')(app); 4 | 5 | module.exports = app; 6 | -------------------------------------------------------------------------------- /api/controllers/helpers/checkIfValidObjectId.helper.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | exports.checkIfValidObjectIdHelper = (id) => 4 | mongoose.Types.ObjectId.isValid(id); 5 | -------------------------------------------------------------------------------- /views/users/instructors/beInstructor.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
-------------------------------------------------------------------------------- /api/controllers/usersAuthControllers/logoutUser.controller.js: -------------------------------------------------------------------------------- 1 | exports.logoutUserController = (req, res) => { 2 | req.logout(); 3 | req.session.destroy(() => res.redirect(302, '/')); 4 | }; 5 | -------------------------------------------------------------------------------- /utils/global-utils/handleAsyncFunction.utils.js: -------------------------------------------------------------------------------- 1 | exports.handleAsyncFunction = (promise) => 2 | promise 3 | .then((data) => [data, undefined]) 4 | .catch((error) => [undefined, error]); 5 | -------------------------------------------------------------------------------- /api/controllers/coursesControllers/renderCreateNewCourse.controller.js: -------------------------------------------------------------------------------- 1 | exports.renderCreateNewCourseController = (req, res) => 2 | res.status(200).render('users/instructors/new-course', req.user); 3 | -------------------------------------------------------------------------------- /api/controllers/helpers/duplicateErrorMessage.helper.js: -------------------------------------------------------------------------------- 1 | exports.duplicateErrorMessageHelper = (keyValueType) => 2 | `Sorry, ${keyValueType} already exists! Please register with another one or login to your account`; 3 | -------------------------------------------------------------------------------- /views/users/common/lesson-details.hbs: -------------------------------------------------------------------------------- 1 |
2 |

{{isLesson.topic}}

3 | 4 |

{{isLesson.content}}

5 | 6 | See lesson video 7 |
-------------------------------------------------------------------------------- /api/controllers/helpers/redirectNonExistentData.helper.js: -------------------------------------------------------------------------------- 1 | exports.redirectNonExistentDataHelper = (request, response) => { 2 | request.flash('error_msg', "Sorry, this doesn't exist!"); 3 | response.redirect('/'); 4 | }; 5 | -------------------------------------------------------------------------------- /api/controllers/usersAuthControllers/renderLogin.controller.js: -------------------------------------------------------------------------------- 1 | exports.renderLoginController = (req, res) => { 2 | const [message] = req.flash('message'); 3 | res.status(200).render('users/auth/login', { message }); 4 | }; 5 | -------------------------------------------------------------------------------- /public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | @import 'reset.css'; 2 | 3 | html { 4 | box-sizing: border-box; 5 | } 6 | *, 7 | *:before, 8 | *:after { 9 | box-sizing: inherit; 10 | } 11 | 12 | a { 13 | color: #00b7ff; 14 | } 15 | -------------------------------------------------------------------------------- /api/controllers/usersAuthControllers/authenticateGoogleLogin.controller.js: -------------------------------------------------------------------------------- 1 | const passport = require('passport'); 2 | 3 | exports.authenticateGoogleLoginController = passport.authenticate('google', { 4 | scope: ['profile', 'email'], 5 | }); 6 | -------------------------------------------------------------------------------- /api/routes/index.routes.js: -------------------------------------------------------------------------------- 1 | const { Router } = require('express'); 2 | const { renderIndexViewController } = require('../controllers'); 3 | 4 | const router = Router(); 5 | 6 | router.get('/', renderIndexViewController); 7 | 8 | module.exports = router; 9 | -------------------------------------------------------------------------------- /views/index.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{#if error_msg}} 4 |

{{error_msg}}

5 | {{/if}} 6 |
7 |
8 |

{{title}}

9 |

Welcome to {{title}}

10 |
11 |
-------------------------------------------------------------------------------- /api/controllers/helpers/renderLoginFormWithErrors.helper.js: -------------------------------------------------------------------------------- 1 | exports.renderLoginFormWithErrorsHelper = (response, user, password) => 2 | response.status(400).render('users/auth/login', { 3 | user, 4 | password, 5 | message: 'Please fill in all the fields!', 6 | }); 7 | -------------------------------------------------------------------------------- /api/controllers/lessonsControllers/renderCreateNewLesson.controller.js: -------------------------------------------------------------------------------- 1 | exports.renderCreateNewLessonController = (req, res) => { 2 | const { local } = req.user; 3 | const { id } = req.params; 4 | 5 | res.status(200).render('users/instructors/new-lesson', { local, id }); 6 | }; 7 | -------------------------------------------------------------------------------- /api/controllers/usersAuthControllers/authenticateGoogleCallback.controller.js: -------------------------------------------------------------------------------- 1 | const passport = require('passport'); 2 | 3 | exports.authenticateGoogleCallbackController = passport.authenticate('google', { 4 | successRedirect: '/profile', 5 | failureRedirect: '/', 6 | failureFlash: true, 7 | }); 8 | -------------------------------------------------------------------------------- /api/controllers/helpers/authenticateUser.helper.js: -------------------------------------------------------------------------------- 1 | const passport = require('passport'); 2 | 3 | exports.authenticateUserHelper = (req, res, next) => { 4 | passport.authenticate('local', { 5 | successRedirect: '/profile', 6 | failureRedirect: '/login', 7 | failureFlash: true, 8 | })(req, res, next); 9 | }; 10 | -------------------------------------------------------------------------------- /api/controllers/usersControllers/renderUserProfile.controller.js: -------------------------------------------------------------------------------- 1 | exports.renderUserProfileController = (req, res) => { 2 | const { _id, role, local, profilePictureUrl } = req.user; 3 | res.status(200).render('users/common/profile', { 4 | _id, 5 | role, 6 | local, 7 | profilePictureUrl, 8 | }); 9 | }; 10 | -------------------------------------------------------------------------------- /configs/__tests__/index.test.js: -------------------------------------------------------------------------------- 1 | const configsObject = require('../index'); 2 | const { validateNotEmpty } = require('../../utils/test-utils/validators.utils'); 3 | 4 | describe('Configuration Test Suite', () => { 5 | test('should validate configuration object', () => { 6 | validateNotEmpty(configsObject); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /api/controllers/indexControllers/renderIndexView.controller.js: -------------------------------------------------------------------------------- 1 | exports.renderIndexViewController = (req, res) => 2 | req.user 3 | ? res.status(200).render('index', { 4 | title: 'Express', 5 | local: { username: req.user.local.username }, 6 | }) 7 | : res.status(200).render('index', { title: 'Express' }); 8 | -------------------------------------------------------------------------------- /loaders/__tests__/mongoose.loader.test.js: -------------------------------------------------------------------------------- 1 | const connectDB = require('../mongoose.loader'); 2 | const { validateNotEmpty } = require('../../utils/test-utils/validators.utils'); 3 | 4 | describe('Mongoose Loader Test Suite', () => { 5 | test('should validate connectDB not empty', () => { 6 | validateNotEmpty(connectDB); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /loaders/__tests__/routes.loader.test.js: -------------------------------------------------------------------------------- 1 | const routesLoader = require('../routes.loader'); 2 | const { validateNotEmpty } = require('../../utils/test-utils/validators.utils'); 3 | 4 | describe('Routes Loader Test Suite', () => { 5 | test('should validate connectDB not empty', () => { 6 | validateNotEmpty(routesLoader); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /api/controllers/helpers/getDuplicateErrorMessage.helper.js: -------------------------------------------------------------------------------- 1 | const { 2 | duplicateErrorMessageHelper, 3 | } = require('./duplicateErrorMessage.helper'); 4 | 5 | exports.getDuplicateErrorMessageHelper = (keyValue) => 6 | keyValue.email 7 | ? duplicateErrorMessageHelper(keyValue.email) 8 | : duplicateErrorMessageHelper(keyValue.username); 9 | -------------------------------------------------------------------------------- /api/controllers/instructorsControllers/index.js: -------------------------------------------------------------------------------- 1 | const { 2 | changeRoleToInstructor, 3 | } = require('./changeRoleToInstructor.controller'); 4 | const { 5 | renderBeInstructorController, 6 | } = require('./renderBeInstructor.controller'); 7 | 8 | module.exports = { 9 | changeRoleToInstructor, 10 | renderBeInstructorController, 11 | }; 12 | -------------------------------------------------------------------------------- /api/middleware/isInstructor.middleware.js: -------------------------------------------------------------------------------- 1 | exports.isInstructor = (req, res, next) => { 2 | const { role } = req.user; 3 | 4 | const noAccessMessage = () => { 5 | req.flash('error_msg', 'Only instructors can access this route'); 6 | res.redirect(302, '/'); 7 | }; 8 | 9 | return role === 'instructor' ? next() : noAccessMessage(); 10 | }; 11 | -------------------------------------------------------------------------------- /api/middleware/index.js: -------------------------------------------------------------------------------- 1 | const { isInstructor } = require('./isInstructor.middleware'); 2 | const { isLoggedInUser } = require('./isLoggedInUser.middleware'); 3 | const { 4 | validateRegistrationForm, 5 | } = require('./validateRegistrationForm.middleware'); 6 | 7 | module.exports = { 8 | isInstructor, 9 | isLoggedInUser, 10 | validateRegistrationForm, 11 | }; 12 | -------------------------------------------------------------------------------- /api/controllers/__tests__/index.test.js: -------------------------------------------------------------------------------- 1 | const mainControllersObject = require('../index'); 2 | const { 3 | validateNotEmpty, 4 | } = require('../../../utils/test-utils/validators.utils'); 5 | 6 | describe('All Controllers Test Suite', () => { 7 | test('should validate that main controller object not empty', () => { 8 | validateNotEmpty(mainControllersObject); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /passport/helpers/comparePassword.helper.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcrypt'); 2 | const { handleAsyncFunction } = require('../../utils/global-utils'); 3 | 4 | exports.comparePasswordHelper = async (password, passwordToCompare) => { 5 | const [results, error] = await handleAsyncFunction( 6 | bcrypt.compare(password, passwordToCompare) 7 | ); 8 | 9 | return results || error; 10 | }; 11 | -------------------------------------------------------------------------------- /database/services/modelServices/lessonServices/index.js: -------------------------------------------------------------------------------- 1 | const { findAllLessonsService } = require('./findAllLessons.service'); 2 | const { createNewLessonService } = require('./createNewLesson.service'); 3 | const { findOneLessonService } = require('./findOneLesson.service'); 4 | 5 | module.exports = { 6 | findOneLessonService, 7 | findAllLessonsService, 8 | createNewLessonService, 9 | }; 10 | -------------------------------------------------------------------------------- /database/services/helpers/hashPassword.helper.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcrypt'); 2 | const { handleAsyncFunction } = require('../../../utils/global-utils'); 3 | 4 | const saltRounds = 10; 5 | 6 | exports.hashPasswordHelper = async (userPassword) => { 7 | const [results, error] = await handleAsyncFunction( 8 | bcrypt.hash(userPassword, saltRounds) 9 | ); 10 | 11 | return results || error; 12 | }; 13 | -------------------------------------------------------------------------------- /utils/global-utils/index.js: -------------------------------------------------------------------------------- 1 | const { isString } = require('./isString.utils'); 2 | const { handleAsyncFunction } = require('./handleAsyncFunction.utils'); 3 | const { 4 | ensureDataInVulnerableOfInjectionAttacks, 5 | } = require('./ensureDataInVulnerableOfInjectionAttacks.utils'); 6 | 7 | module.exports = { 8 | isString, 9 | handleAsyncFunction, 10 | ensureDataInVulnerableOfInjectionAttacks, 11 | }; 12 | -------------------------------------------------------------------------------- /api/controllers/helpers/__tests__/authenticateUser.helper.test.js: -------------------------------------------------------------------------------- 1 | const { authenticateUserHelper } = require('../authenticateUser.helper'); 2 | const { 3 | validateNotEmpty, 4 | } = require('../../../../utils/test-utils/validators.utils'); 5 | 6 | describe('authenticateUser Helper Test Suite', () => { 7 | test('should validate not empty', () => { 8 | validateNotEmpty(authenticateUserHelper); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /database/models/lesson.model.js: -------------------------------------------------------------------------------- 1 | const { Schema, model } = require('mongoose'); 2 | 3 | const lessonSchema = new Schema({ 4 | topic: { type: String, trim: true, unique: true }, 5 | content: { type: String, trim: true }, 6 | videoUrl: { type: String, default: 'https://via.placeholder.com/200x150' }, 7 | isComplete: { type: Boolean, default: false }, 8 | }); 9 | 10 | module.exports = model('Lesson', lessonSchema); 11 | -------------------------------------------------------------------------------- /api/controllers/helpers/renderRegistrationFormWithErrors.helper.js: -------------------------------------------------------------------------------- 1 | exports.renderRegistrationFormWithErrorsHelper = (response, request, error) => { 2 | const { firstName, lastName, email, username, password } = request.body; 3 | 4 | response.status(400).render('users/auth/register', { 5 | firstName, 6 | lastName, 7 | email, 8 | username, 9 | password, 10 | errors: error.array(), 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /api/controllers/usersAuthControllers/loginUser.controller.js: -------------------------------------------------------------------------------- 1 | const { 2 | authenticateUserHelper, 3 | renderLoginFormWithErrorsHelper, 4 | } = require('../helpers'); 5 | 6 | exports.loginUserController = (req, res, next) => { 7 | const { user, password } = req.body; 8 | 9 | return !user || !password 10 | ? renderLoginFormWithErrorsHelper(res, user, password) 11 | : authenticateUserHelper(req, res, next); 12 | }; 13 | -------------------------------------------------------------------------------- /database/services/modelServices/courseServices/findAllCourses.service.js: -------------------------------------------------------------------------------- 1 | const Course = require('../../../models/course.model'); 2 | const { 3 | handleAsyncFunction, 4 | } = require('../../../../utils/global-utils/handleAsyncFunction.utils'); 5 | 6 | exports.findAllCoursesService = async () => { 7 | const [foundResults, foundError] = await handleAsyncFunction(Course.find({})); 8 | 9 | return foundResults || foundError; 10 | }; 11 | -------------------------------------------------------------------------------- /database/services/modelServices/lessonServices/findAllLessons.service.js: -------------------------------------------------------------------------------- 1 | const Lesson = require('../../../models/lesson.model'); 2 | const { 3 | handleAsyncFunction, 4 | } = require('../../../../utils/global-utils/handleAsyncFunction.utils'); 5 | 6 | exports.findAllLessonsService = async () => { 7 | const [foundResults, foundError] = await handleAsyncFunction(Lesson.find({})); 8 | 9 | return foundResults || foundError; 10 | }; 11 | -------------------------------------------------------------------------------- /database/services/modelServices/courseServices/deleteCourse.service.js: -------------------------------------------------------------------------------- 1 | const Course = require('../../../models/course.model'); 2 | const { 3 | handleAsyncFunction, 4 | } = require('../../../../utils/global-utils/handleAsyncFunction.utils'); 5 | 6 | exports.deleteCourseService = async (courseId) => { 7 | const [results, error] = await handleAsyncFunction( 8 | Course.findByIdAndDelete(courseId) 9 | ); 10 | 11 | return results || error; 12 | }; 13 | -------------------------------------------------------------------------------- /public/stylesheets/reset.css: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | font-size: 16px; 4 | } 5 | 6 | *, 7 | *:before, 8 | *:after { 9 | box-sizing: inherit; 10 | } 11 | 12 | body, 13 | h1, 14 | h2, 15 | h3, 16 | h4, 17 | h5, 18 | h6, 19 | p, 20 | ol, 21 | ul { 22 | margin: 0; 23 | padding: 0; 24 | font-weight: normal; 25 | } 26 | 27 | ol, 28 | ul { 29 | list-style: none; 30 | } 31 | 32 | img { 33 | max-width: 100%; 34 | height: auto; 35 | } 36 | -------------------------------------------------------------------------------- /views/layout.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{title}} 6 | 7 | 8 | 9 | 10 | 11 | 12 | {{> header}} 13 | 14 | {{{body}}} 15 | 16 | {{> footer}} 17 | 18 | 19 | -------------------------------------------------------------------------------- /api/controllers/helpers/render500Error.helper.js: -------------------------------------------------------------------------------- 1 | exports.render500ErrorHelper = (response) => 2 | response.status(500).render('error', { 3 | message: 4 | 'Oops! An unexpected error seems to have occurred while processing your request.', 5 | error: { 6 | status: '500', 7 | stack: `We're sorry for the trouble. We've been notified and will correct this as soon as possible. Please try your request again after a little while`, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /database/services/modelServices/lessonServices/findOneLesson.service.js: -------------------------------------------------------------------------------- 1 | const Lesson = require('../../../models/lesson.model'); 2 | const { 3 | handleAsyncFunction, 4 | } = require('../../../../utils/global-utils/handleAsyncFunction.utils'); 5 | 6 | exports.findOneLessonService = async (lessonId) => { 7 | const [lessonFound, lessonError] = await handleAsyncFunction( 8 | Lesson.findById(lessonId) 9 | ); 10 | 11 | return lessonFound || lessonError; 12 | }; 13 | -------------------------------------------------------------------------------- /database/services/modelServices/lessonServices/createNewLesson.service.js: -------------------------------------------------------------------------------- 1 | const Lesson = require('../../../models/lesson.model'); 2 | const { handleAsyncFunction } = require('../../../../utils/global-utils'); 3 | 4 | exports.createNewLessonService = async (topic, content, videoUrl) => { 5 | const [lessonResults, lessonError] = await handleAsyncFunction( 6 | Lesson.create({ topic, content, videoUrl }) 7 | ); 8 | 9 | return lessonResults || lessonError; 10 | }; 11 | -------------------------------------------------------------------------------- /utils/test-utils/courseControllerDeps.js: -------------------------------------------------------------------------------- 1 | const { mockRequest, mockResponse } = require('./interceptors.utils'); 2 | 3 | const setupReqRes = () => { 4 | const request = mockRequest(); 5 | const response = mockResponse(); 6 | 7 | request.user = { 8 | local: expect.anything(), 9 | }; 10 | 11 | return { request, response }; 12 | }; 13 | 14 | const clearMocks = () => jest.clearAllMocks(); 15 | 16 | module.exports = { 17 | setupReqRes, 18 | clearMocks, 19 | }; 20 | -------------------------------------------------------------------------------- /api/controllers/helpers/handleUpdatedPassword.helper.js: -------------------------------------------------------------------------------- 1 | const { hashPasswordHelper } = require('../../../database/services/helpers'); 2 | const { render500ErrorHelper } = require('./render500Error.helper'); 3 | 4 | exports.handleUpdatedPasswordHelper = async (newPassword, response) => { 5 | const isHashedPassword = await hashPasswordHelper(newPassword); 6 | 7 | return isHashedPassword instanceof Error 8 | ? render500ErrorHelper(response) 9 | : isHashedPassword; 10 | }; 11 | -------------------------------------------------------------------------------- /api/controllers/usersControllers/index.js: -------------------------------------------------------------------------------- 1 | const { 2 | updateUserProfileController, 3 | } = require('./updateUserProfile.controller'); 4 | const { 5 | renderUserProfileController, 6 | } = require('./renderUserProfile.controller'); 7 | const { 8 | renderEditUserProfileController, 9 | } = require('./renderEditUserProfile.controller'); 10 | 11 | module.exports = { 12 | updateUserProfileController, 13 | renderUserProfileController, 14 | renderEditUserProfileController, 15 | }; 16 | -------------------------------------------------------------------------------- /database/services/modelServices/courseServices/createNewCourse.service.js: -------------------------------------------------------------------------------- 1 | const Course = require('../../../models/course.model'); 2 | const { 3 | handleAsyncFunction, 4 | } = require('../../../../utils/global-utils/handleAsyncFunction.utils'); 5 | 6 | exports.createNewCourseService = async (title, description) => { 7 | const [courseResults, courseError] = await handleAsyncFunction( 8 | Course.create({ title, description }) 9 | ); 10 | 11 | return courseResults || courseError; 12 | }; 13 | -------------------------------------------------------------------------------- /database/services/modelServices/userServices/findUserById.service.js: -------------------------------------------------------------------------------- 1 | const User = require('../../../models/user.model'); 2 | const { 3 | handleAsyncFunction, 4 | } = require('../../../../utils/global-utils/handleAsyncFunction.utils'); 5 | 6 | exports.findUserByIdService = async (id) => { 7 | const [userResults, userError] = await handleAsyncFunction(User.findById(id)); 8 | 9 | if (userResults === null) { 10 | return userResults; 11 | } 12 | 13 | return userResults || userError; 14 | }; 15 | -------------------------------------------------------------------------------- /views/users/students/course-learned.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{#if success_msg}} 4 |

{{success_msg}}

5 | {{/if}} 6 |
7 | 8 |
9 |

{{isCourse.title}}

10 |
11 | course cover image 12 |
13 |

{{isCourse.description}}

14 |
15 |
16 |

Modules

17 |

Coming soon

18 |
19 |
-------------------------------------------------------------------------------- /api/controllers/helpers/setUserInSessionAndLogin.helper.js: -------------------------------------------------------------------------------- 1 | exports.setUserInSessionAndLoginHelper = ( 2 | request, 3 | isHandledResults, 4 | response 5 | ) => 6 | request.login(isHandledResults, (err) => { 7 | if (err) { 8 | response.status(500).render('users/auth/register', { 9 | message: 'Login after signup went bad.', 10 | }); 11 | return; 12 | } 13 | 14 | request.flash('success_msg', 'New User Added'); 15 | response.redirect(302, '/profile'); 16 | }); 17 | -------------------------------------------------------------------------------- /api/controllers/usersAuthControllers/registerNewUser.controller.js: -------------------------------------------------------------------------------- 1 | const { validationResult } = require('express-validator'); 2 | const { 3 | createNewUserHelper, 4 | renderRegistrationFormWithErrorsHelper, 5 | } = require('../helpers'); 6 | 7 | exports.registerNewUserController = async (req, res) => { 8 | const errors = validationResult(req); 9 | 10 | return errors.array().length 11 | ? renderRegistrationFormWithErrorsHelper(res, req, errors) 12 | : createNewUserHelper(req, res); 13 | }; 14 | -------------------------------------------------------------------------------- /database/services/modelServices/courseServices/findOneCourse.service.js: -------------------------------------------------------------------------------- 1 | const Course = require('../../../models/course.model'); 2 | const { 3 | handleAsyncFunction, 4 | } = require('../../../../utils/global-utils/handleAsyncFunction.utils'); 5 | 6 | exports.findOneCourseService = async (courseId) => { 7 | const [foundResults, foundError] = await handleAsyncFunction( 8 | Course.findById(courseId).populate('instructors students lessons') 9 | ); 10 | 11 | return foundResults || foundError; 12 | }; 13 | -------------------------------------------------------------------------------- /api/routes/instructors.routes.js: -------------------------------------------------------------------------------- 1 | const { Router } = require('express'); 2 | const { instructorPrefix } = require('../../configs'); 3 | const { 4 | changeRoleToInstructor, 5 | renderBeInstructorController, 6 | } = require('../controllers'); 7 | const { isLoggedInUser } = require('../middleware'); 8 | 9 | const router = Router(); 10 | 11 | router.get(instructorPrefix, isLoggedInUser, renderBeInstructorController); 12 | router.post(instructorPrefix, changeRoleToInstructor); 13 | 14 | module.exports = router; 15 | -------------------------------------------------------------------------------- /api/controllers/instructorsControllers/renderBeInstructor.controller.js: -------------------------------------------------------------------------------- 1 | exports.renderBeInstructorController = (req, res) => { 2 | const { _id, role, local } = req.user; 3 | const { email, username, lastName, firstName } = local; 4 | 5 | return role === 'student' 6 | ? res.status(200).render('users/instructors/beInstructor', { 7 | _id, 8 | role, 9 | email, 10 | username, 11 | lastName, 12 | firstName, 13 | }) 14 | : res.redirect(302, '/profile'); 15 | }; 16 | -------------------------------------------------------------------------------- /api/controllers/helpers/filterCourses.helper.js: -------------------------------------------------------------------------------- 1 | const { includesUserTypeHelper } = require('./includesUserType.helper'); 2 | 3 | exports.filterCoursesHelper = (allCoursesList, incomingUserId) => { 4 | const coursesTaught = allCoursesList.filter((courseItem) => 5 | includesUserTypeHelper(courseItem.instructors, incomingUserId) 6 | ); 7 | 8 | const coursesLearned = allCoursesList.filter((courseItem) => 9 | includesUserTypeHelper(courseItem.students, incomingUserId) 10 | ); 11 | return { coursesTaught, coursesLearned }; 12 | }; 13 | -------------------------------------------------------------------------------- /api/controllers/helpers/handleIfAsyncError.helper.js: -------------------------------------------------------------------------------- 1 | const { 2 | getDuplicateErrorMessageHelper, 3 | } = require('./getDuplicateErrorMessage.helper'); 4 | 5 | exports.handleIfAsyncErrorHelper = (incomingObject) => { 6 | let handleResults; 7 | 8 | if (incomingObject instanceof Error) { 9 | if (incomingObject.code && incomingObject.code === 11000) { 10 | handleResults = getDuplicateErrorMessageHelper(incomingObject.keyValue); 11 | } 12 | } else { 13 | handleResults = incomingObject; 14 | } 15 | return handleResults; 16 | }; 17 | -------------------------------------------------------------------------------- /database/models/course.model.js: -------------------------------------------------------------------------------- 1 | const { Schema, model } = require('mongoose'); 2 | 3 | const courseSchema = new Schema({ 4 | title: { type: String, trim: true, unique: true }, 5 | description: { type: String, trim: true }, 6 | imageUrl: { type: String, default: 'https://via.placeholder.com/200x150' }, 7 | instructors: [{ type: Schema.Types.ObjectId, ref: 'User' }], 8 | students: [{ type: Schema.Types.ObjectId, ref: 'User' }], 9 | lessons: [{ type: Schema.Types.ObjectId, ref: 'Lesson' }], 10 | }); 11 | 12 | module.exports = model('Course', courseSchema); 13 | -------------------------------------------------------------------------------- /api/controllers/helpers/__tests__/duplicateErrorMessage.helper.test.js: -------------------------------------------------------------------------------- 1 | const { 2 | duplicateErrorMessageHelper, 3 | } = require('../duplicateErrorMessage.helper'); 4 | const { 5 | validateNotEmpty, 6 | validateTypeOf, 7 | } = require('../../../../utils/test-utils/validators.utils'); 8 | 9 | describe('duplicateErrorMessage Test Suite', () => { 10 | test('should validate message is not empty', () => { 11 | const results = duplicateErrorMessageHelper('dummy'); 12 | 13 | validateNotEmpty(results); 14 | validateTypeOf(results, 'string'); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /api/controllers/instructorsControllers/changeRoleToInstructor.controller.js: -------------------------------------------------------------------------------- 1 | const { 2 | updateUserRoleService, 3 | } = require('../../../database/services/modelServices/userServices'); 4 | const { render500ErrorHelper } = require('../helpers'); 5 | 6 | exports.changeRoleToInstructor = async (req, res) => { 7 | const { _id: userId } = req.user; 8 | const updatedResults = await updateUserRoleService(userId); 9 | 10 | if (updatedResults instanceof Error) { 11 | render500ErrorHelper(res); 12 | return; 13 | } 14 | 15 | res.redirect(302, '/profile'); 16 | }; 17 | -------------------------------------------------------------------------------- /passport/serializeSession.js: -------------------------------------------------------------------------------- 1 | const passport = require('passport'); 2 | const User = require('../database/models/user.model'); 3 | const { handleAsyncFunction } = require('../utils/global-utils'); 4 | 5 | passport.serializeUser((loggedInUser, done) => { 6 | const { _id } = loggedInUser; 7 | 8 | done(null, _id); 9 | }); 10 | 11 | passport.deserializeUser(async (userIdFromSession, done) => { 12 | const [results, error] = await handleAsyncFunction( 13 | User.findById(userIdFromSession) 14 | ); 15 | 16 | return error ? done(error) : done(null, results); 17 | }); 18 | -------------------------------------------------------------------------------- /database/services/modelServices/courseServices/index.js: -------------------------------------------------------------------------------- 1 | const { deleteCourseService } = require('./deleteCourse.service'); 2 | const { updateCourseService } = require('./updateCourse.service'); 3 | const { findOneCourseService } = require('./findOneCourse.service'); 4 | const { findAllCoursesService } = require('./findAllCourses.service'); 5 | const { createNewCourseService } = require('./createNewCourse.service'); 6 | 7 | module.exports = { 8 | deleteCourseService, 9 | updateCourseService, 10 | findOneCourseService, 11 | findAllCoursesService, 12 | createNewCourseService, 13 | }; 14 | -------------------------------------------------------------------------------- /views/users/common/profile.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{#if error_msg}} 4 |

{{error_msg}}

5 | {{/if}} 6 |
7 | {{role}} 8 |

Welcome {{local.username}}

9 |
10 | profile-avatar 11 |
12 |

Edit Profile

13 | {{#ifEqualsHelper role 'instructor'}} 14 |

Register New Course

15 | {{/ifEqualsHelper}} 16 |

My Courses

17 |
-------------------------------------------------------------------------------- /database/services/modelServices/userServices/updateUserRole.service.js: -------------------------------------------------------------------------------- 1 | const User = require('../../../models/user.model'); 2 | const { 3 | handleAsyncFunction, 4 | } = require('../../../../utils/global-utils/handleAsyncFunction.utils'); 5 | 6 | exports.updateUserRoleService = async (userId) => { 7 | const [updateResults, updateError] = await handleAsyncFunction( 8 | User.findOneAndUpdate( 9 | { _id: userId }, 10 | { 11 | role: 'instructor', 12 | }, 13 | { upsert: true, new: true } 14 | ) 15 | ); 16 | 17 | return updateResults || updateError; 18 | }; 19 | -------------------------------------------------------------------------------- /api/controllers/coursesControllers/renderAllCourses.controller.js: -------------------------------------------------------------------------------- 1 | const { render500ErrorHelper } = require('../helpers'); 2 | const { 3 | findAllCoursesService, 4 | } = require('../../../database/services/modelServices/courseServices'); 5 | 6 | exports.renderAllCoursesController = async (req, res) => { 7 | const { local } = req.user; 8 | 9 | const isAllCourses = await findAllCoursesService(); 10 | 11 | if (isAllCourses instanceof Error) { 12 | render500ErrorHelper(res); 13 | return; 14 | } 15 | 16 | res.status(200).render('users/common/all-courses', { local, isAllCourses }); 17 | }; 18 | -------------------------------------------------------------------------------- /database/services/modelServices/courseServices/updateCourse.service.js: -------------------------------------------------------------------------------- 1 | const Course = require('../../../models/course.model'); 2 | const { 3 | handleAsyncFunction, 4 | } = require('../../../../utils/global-utils/handleAsyncFunction.utils'); 5 | 6 | exports.updateCourseService = async (id, title, description) => { 7 | const [updateResults, updateError] = await handleAsyncFunction( 8 | Course.findByIdAndUpdate( 9 | id, 10 | { 11 | title, 12 | description, 13 | }, 14 | { upsert: true, new: true } 15 | ) 16 | ); 17 | 18 | return updateResults || updateError; 19 | }; 20 | -------------------------------------------------------------------------------- /loaders/mongoose.loader.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const { localMongoUri } = require('../configs'); 3 | 4 | module.exports = async () => { 5 | try { 6 | const db = await mongoose.connect(localMongoUri, { 7 | useNewUrlParser: true, 8 | useCreateIndex: true, 9 | useUnifiedTopology: true, 10 | useFindAndModify: false, 11 | }); 12 | console.log( 13 | `Connected to Mongo! Database name: "${db.connections[0].name}"` 14 | ); 15 | return db; 16 | } catch (err) { 17 | console.error(`Error connecting to mongo: ${err}`); 18 | return err; 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /api/controllers/coursesControllers/deleteTaughtCourse.controller.js: -------------------------------------------------------------------------------- 1 | const { 2 | deleteCourseService, 3 | } = require('../../../database/services/modelServices/courseServices'); 4 | const { render500ErrorHelper } = require('../helpers'); 5 | 6 | exports.deleteTaughtCourseController = async (req, res) => { 7 | const { id } = req.params; 8 | 9 | const isDeletedCourse = await deleteCourseService(id); 10 | 11 | if (isDeletedCourse instanceof Error) { 12 | render500ErrorHelper(res); 13 | return; 14 | } 15 | 16 | req.flash('success_msg', 'Course successfully deleted!'); 17 | res.redirect('/my-courses'); 18 | }; 19 | -------------------------------------------------------------------------------- /utils/global-utils/ensureDataInVulnerableOfInjectionAttacks.utils.js: -------------------------------------------------------------------------------- 1 | exports.ensureDataInVulnerableOfInjectionAttacks = (incomingRequestBodyObj) => { 2 | const incomingObjKeysList = Object.keys(incomingRequestBodyObj); 3 | const incomingObjValuesList = Object.values(incomingRequestBodyObj); 4 | 5 | const parsedValuesToStringToPreventInjectionAttacks = incomingObjValuesList.map( 6 | (objKey) => objKey.toString() 7 | ); 8 | 9 | return Object.assign( 10 | ...incomingObjKeysList.map((objKeyItem, index) => ({ 11 | [objKeyItem]: parsedValuesToStringToPreventInjectionAttacks[index], 12 | })) 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /database/services/modelServices/userServices/index.js: -------------------------------------------------------------------------------- 1 | const { findUserByIdService } = require('./findUserById.service'); 2 | const { createNewUserService } = require('./createNewUser.service'); 3 | const { updateUserRoleService } = require('./updateUserRole.service'); 4 | const { 5 | updateUserProfileDataService, 6 | } = require('./updateUserProfileData.service'); 7 | const { createNewGoogleUserService } = require('./createNewGoogleUser.service'); 8 | 9 | module.exports = { 10 | findUserByIdService, 11 | createNewUserService, 12 | updateUserRoleService, 13 | createNewGoogleUserService, 14 | updateUserProfileDataService, 15 | }; 16 | -------------------------------------------------------------------------------- /views/users/common/all-courses.hbs: -------------------------------------------------------------------------------- 1 |
2 |

All Courses

3 | {{#if isAllCourses.length}} 4 | 17 | 18 | {{else}} 19 |

There are currently no courses registered in our system!

20 | {{/if}} 21 |
-------------------------------------------------------------------------------- /views/users/common/course-details.hbs: -------------------------------------------------------------------------------- 1 |
2 |

{{isCourse.title}}

3 |
4 | course cover image 5 |
6 | 7 |

{{isCourse.description}}

8 | 9 | {{#if isCurrentUserTheCourseInstructor}} 10 | See Course Overview 11 | {{else if isCurrentUserInStudentList}} 12 | Continue Course 13 | {{else}} 14 |
15 | 16 |
17 | {{/if}} 18 |
-------------------------------------------------------------------------------- /configs/cloudinary/index.js: -------------------------------------------------------------------------------- 1 | const multer = require('multer'); 2 | const cloudinary = require('cloudinary').v2; 3 | const { CloudinaryStorage } = require('multer-storage-cloudinary'); 4 | const { cloudKey, cloudName, cloudSecret } = require('..'); 5 | 6 | cloudinary.config({ 7 | cloud_name: cloudName, 8 | api_key: cloudKey, 9 | api_secret: cloudSecret, 10 | }); 11 | 12 | const storage = new CloudinaryStorage({ 13 | cloudinary, 14 | params: { 15 | folder: 'onLearn', 16 | allowedFormats: ['jpg', 'png', 'pdf'], 17 | use_filename: true, 18 | }, 19 | }); 20 | 21 | const uploadCloud = multer({ storage, limits: { fileSize: 8000000 } }); 22 | 23 | module.exports = uploadCloud; 24 | -------------------------------------------------------------------------------- /utils/test-utils/dbHandler.utils.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const { MongoMemoryServer } = require('mongodb-memory-server'); 3 | 4 | const mongoServer = new MongoMemoryServer(); 5 | 6 | exports.dbConnect = async () => { 7 | const uri = await mongoServer.getUri(); 8 | 9 | const mongooseOpts = { 10 | useNewUrlParser: true, 11 | useCreateIndex: true, 12 | useUnifiedTopology: true, 13 | useFindAndModify: false, 14 | }; 15 | 16 | await mongoose.connect(uri, mongooseOpts); 17 | }; 18 | 19 | exports.dbDisconnect = async () => { 20 | await mongoose.connection.dropDatabase(); 21 | await mongoose.connection.close(); 22 | await mongoServer.stop(); 23 | }; 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /views/users/instructors/new-course.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{#if message}} 4 |

{{message}}

5 | {{/if}} 6 |
7 |
8 |
9 |
10 | 11 | 12 |
13 |
14 | 15 | 16 |
17 | 18 |
19 |
20 |
-------------------------------------------------------------------------------- /api/controllers/coursesControllers/registerToCourse.controller.js: -------------------------------------------------------------------------------- 1 | const { render500ErrorHelper } = require('../helpers'); 2 | const { 3 | findOneCourseService, 4 | } = require('../../../database/services/modelServices/courseServices'); 5 | 6 | exports.registerToCourseController = async (req, res) => { 7 | const { _id } = req.user; 8 | const { id } = req.params; 9 | 10 | const isFoundCourse = await findOneCourseService(id); 11 | 12 | if (isFoundCourse instanceof Error) { 13 | render500ErrorHelper(res); 14 | return; 15 | } 16 | 17 | isFoundCourse.students.push(_id); 18 | await isFoundCourse.save(); 19 | 20 | req.flash('success_msg', 'Successfully registered to course'); 21 | res.redirect('/my-courses'); 22 | }; 23 | -------------------------------------------------------------------------------- /api/controllers/lessonsControllers/index.js: -------------------------------------------------------------------------------- 1 | const { createNewLessonController } = require('./createNewLesson.controller'); 2 | const { 3 | renderCreateNewLessonController, 4 | } = require('./renderCreateNewLesson.controller'); 5 | const { 6 | renderLessonDetailsController, 7 | } = require('./renderLessonDetails.controller'); 8 | const { 9 | renderAllLessonsTaughtController, 10 | } = require('./renderAllLessonsTaught.controller'); 11 | const { renderEditLessonController } = require('./renderEditLesson.controller'); 12 | 13 | module.exports = { 14 | createNewLessonController, 15 | renderEditLessonController, 16 | renderLessonDetailsController, 17 | renderAllLessonsTaughtController, 18 | renderCreateNewLessonController, 19 | }; 20 | -------------------------------------------------------------------------------- /views/partials/header.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /api/routes/users.routes.js: -------------------------------------------------------------------------------- 1 | const { Router } = require('express'); 2 | const { isLoggedInUser } = require('../middleware'); 3 | const { profilePrefix, profileEditPrefix } = require('../../configs'); 4 | const { 5 | updateUserProfileController, 6 | renderUserProfileController, 7 | renderEditUserProfileController, 8 | } = require('../controllers'); 9 | const fileUploader = require('../../configs/cloudinary'); 10 | 11 | const router = Router(); 12 | 13 | router.get(profilePrefix, isLoggedInUser, renderUserProfileController); 14 | router.get(profileEditPrefix, isLoggedInUser, renderEditUserProfileController); 15 | router.post( 16 | profileEditPrefix, 17 | fileUploader.single('image'), 18 | updateUserProfileController 19 | ); 20 | 21 | module.exports = router; 22 | -------------------------------------------------------------------------------- /api/controllers/lessonsControllers/renderEditLesson.controller.js: -------------------------------------------------------------------------------- 1 | const { 2 | render500ErrorHelper, 3 | redirectNonExistentDataHelper, 4 | } = require('../helpers'); 5 | const { 6 | findOneLessonService, 7 | } = require('../../../database/services/modelServices/lessonServices'); 8 | 9 | exports.renderEditLessonController = async (req, res) => { 10 | const { local } = req.user; 11 | const { id } = req.params; 12 | 13 | const isLesson = await findOneLessonService(id); 14 | 15 | if (isLesson instanceof Error) { 16 | render500ErrorHelper(res); 17 | return; 18 | } 19 | 20 | if (isLesson == null) { 21 | redirectNonExistentDataHelper(req, res); 22 | } 23 | 24 | res.status(200).render('users/instructors/edit-lesson', { local, isLesson }); 25 | }; 26 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es2020": true, 5 | "node": true, 6 | "jest/globals": true, 7 | "amd": true 8 | }, 9 | "extends": ["airbnb-base", "prettier"], 10 | "plugins": ["prettier", "jest"], 11 | "globals": { 12 | "Atomics": "readonly", 13 | "SharedArrayBuffer": "readonly" 14 | }, 15 | "parserOptions": { 16 | "ecmaVersion": 11 17 | }, 18 | "rules": { 19 | "prettier/prettier": ["error", { "singleQuote": true }], 20 | "no-console": "off", 21 | "import/no-unresolved": "off", 22 | "jest/no-disabled-tests": "warn", 23 | "jest/no-focused-tests": "error", 24 | "jest/no-identical-title": "error", 25 | "jest/prefer-to-have-length": "warn", 26 | "jest/valid-expect": "error" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /views/users/instructors/student-list.hbs: -------------------------------------------------------------------------------- 1 |
2 |

Students List

3 | {{#if isCourse.students.length}} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | {{#each isCourse.students}} 13 | 14 | 15 | 16 | 17 | {{/each}} 18 | 19 |
FirstnameLastname
{{this.local.firstName}}{{this.local.lastName}}
20 | {{else}} 21 |

Your course doesn't have students registered yet!

22 | {{/if}} 23 | 24 |

Back to Course Details Overview

25 |
-------------------------------------------------------------------------------- /api/controllers/helpers/__tests__/render500Error.helper.test.js: -------------------------------------------------------------------------------- 1 | const { render500ErrorHelper } = require('../index'); 2 | const { 3 | mockResponse, 4 | } = require('../../../../utils/test-utils/interceptors.utils'); 5 | 6 | const { 7 | validateMockValueToHaveBeenCalled, 8 | } = require('../../../../utils/test-utils/validators.utils'); 9 | 10 | describe('render500Error Helper Test Suite', () => { 11 | afterEach(() => { 12 | jest.clearAllMocks(); 13 | }); 14 | 15 | test('should validate res.status & res.render being called', () => { 16 | const res = mockResponse(); 17 | 18 | render500ErrorHelper(res); 19 | 20 | const { status, render } = res; 21 | 22 | validateMockValueToHaveBeenCalled(status); 23 | validateMockValueToHaveBeenCalled(render); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /api/controllers/lessonsControllers/renderLessonDetails.controller.js: -------------------------------------------------------------------------------- 1 | const { 2 | findOneLessonService, 3 | } = require('../../../database/services/modelServices/lessonServices'); 4 | const { 5 | render500ErrorHelper, 6 | redirectNonExistentDataHelper, 7 | } = require('../helpers'); 8 | 9 | exports.renderLessonDetailsController = async (req, res) => { 10 | const { id } = req.params; 11 | const { local } = req.user; 12 | 13 | const isLesson = await findOneLessonService(id); 14 | 15 | if (isLesson instanceof Error) { 16 | render500ErrorHelper(res); 17 | return; 18 | } 19 | 20 | if (isLesson === null) { 21 | redirectNonExistentDataHelper(req, res); 22 | return; 23 | } 24 | 25 | res.status(200).render('users/common/lesson-details', { local, isLesson }); 26 | }; 27 | -------------------------------------------------------------------------------- /api/controllers/coursesControllers/renderStudentList.controller.js: -------------------------------------------------------------------------------- 1 | const { 2 | findOneCourseService, 3 | } = require('../../../database/services/modelServices/courseServices'); 4 | 5 | const { 6 | render500ErrorHelper, 7 | redirectNonExistentDataHelper, 8 | } = require('../helpers'); 9 | 10 | exports.renderStudentListController = async (req, res) => { 11 | const { local } = req.user; 12 | const { id } = req.params; 13 | 14 | const isCourse = await findOneCourseService(id); 15 | 16 | if (isCourse instanceof Error) { 17 | render500ErrorHelper(res); 18 | return; 19 | } 20 | 21 | if (isCourse === null) { 22 | redirectNonExistentDataHelper(req, res); 23 | return; 24 | } 25 | 26 | res.status(200).render('users/instructors/student-list', { local, isCourse }); 27 | }; 28 | -------------------------------------------------------------------------------- /api/controllers/coursesControllers/renderLearnedCourse.controller.js: -------------------------------------------------------------------------------- 1 | const { 2 | findOneCourseService, 3 | } = require('../../../database/services/modelServices/courseServices'); 4 | const { 5 | render500ErrorHelper, 6 | redirectNonExistentDataHelper, 7 | } = require('../helpers'); 8 | 9 | exports.renderLearnedCourseController = async (req, res) => { 10 | const { id } = req.params; 11 | const { local } = req.user; 12 | 13 | const isCourse = await findOneCourseService(id); 14 | 15 | if (isCourse instanceof Error) { 16 | render500ErrorHelper(res); 17 | return; 18 | } 19 | 20 | if (isCourse === null) { 21 | redirectNonExistentDataHelper(req, res); 22 | return; 23 | } 24 | 25 | res.status(200).render('users/students/course-learned', { 26 | local, 27 | isCourse, 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /api/controllers/coursesControllers/renderEditTaughtCourse.controller.js: -------------------------------------------------------------------------------- 1 | const { 2 | render500ErrorHelper, 3 | redirectNonExistentDataHelper, 4 | } = require('../helpers'); 5 | const { 6 | findOneCourseService, 7 | } = require('../../../database/services/modelServices/courseServices'); 8 | 9 | exports.renderEditTaughtCourseController = async (req, res) => { 10 | const { local } = req.user; 11 | const { id } = req.params; 12 | 13 | const isCourse = await findOneCourseService(id); 14 | 15 | if (isCourse instanceof Error) { 16 | render500ErrorHelper(res); 17 | return; 18 | } 19 | 20 | if (isCourse === null) { 21 | redirectNonExistentDataHelper(req, res); 22 | return; 23 | } 24 | 25 | res.status(200).render('users/instructors/edit-course', { 26 | local, 27 | isCourse, 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /api/controllers/coursesControllers/renderTaughtCourse.controller.js: -------------------------------------------------------------------------------- 1 | const { 2 | findOneCourseService, 3 | } = require('../../../database/services/modelServices/courseServices'); 4 | 5 | const { 6 | render500ErrorHelper, 7 | redirectNonExistentDataHelper, 8 | } = require('../helpers'); 9 | 10 | exports.renderTaughtCourseController = async (req, res) => { 11 | const { local } = req.user; 12 | const { id } = req.params; 13 | 14 | const isCourse = await findOneCourseService(id); 15 | 16 | if (isCourse instanceof Error) { 17 | render500ErrorHelper(res); 18 | return; 19 | } 20 | 21 | if (isCourse === null) { 22 | redirectNonExistentDataHelper(req, res); 23 | return; 24 | } 25 | 26 | res.status(200).render('users/instructors/course-taught', { 27 | local, 28 | isCourse, 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /database/services/modelServices/userServices/updateUserProfileData.service.js: -------------------------------------------------------------------------------- 1 | const User = require('../../../models/user.model'); 2 | const { 3 | handleAsyncFunction, 4 | } = require('../../../../utils/global-utils/handleAsyncFunction.utils'); 5 | 6 | exports.updateUserProfileDataService = async ( 7 | id, 8 | requestBody, 9 | userPassword, 10 | profilePictureUrl 11 | ) => { 12 | const { email, username, firstName, lastName } = requestBody; 13 | 14 | const [updateResults, updateError] = await handleAsyncFunction( 15 | User.findByIdAndUpdate( 16 | id, 17 | { 18 | profilePictureUrl, 19 | local: { email, username, firstName, lastName, password: userPassword }, 20 | }, 21 | { upsert: true, new: true } 22 | ) 23 | ); 24 | 25 | return updateResults || updateError; 26 | }; 27 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # New Features / Improvements 2 | 3 | Any new features and/or improvements are required to have unit tests. Tests help ensure that as many lines of code within this project are accounted for. This helps upkeep the code quality. 4 | 5 | Once a feature/improvement is tested and accounted for, feel free to make a Pull Request. 6 | # Pull Request Process 7 | 8 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a build. 9 | 2. Include a detailed overview of what has been added/improved/removed. 10 | 3. Update the README.md with details of changes to the interface, this includes new environment 11 | variables, exposed ports, useful file locations and container parameters. 12 | 4. You may merge the Pull Request in once the changes have been reviewed and accepted developers. 13 | -------------------------------------------------------------------------------- /api/controllers/coursesControllers/renderMyCourses.controller.js: -------------------------------------------------------------------------------- 1 | const { filterCoursesHelper, render500ErrorHelper } = require('../helpers'); 2 | const { 3 | findAllCoursesService, 4 | } = require('../../../database/services/modelServices/courseServices'); 5 | 6 | exports.renderMyCoursesController = async (req, res) => { 7 | const { _id, local, role } = req.user; 8 | 9 | const isAllCourses = await findAllCoursesService(); 10 | 11 | if (isAllCourses instanceof Error) { 12 | render500ErrorHelper(res); 13 | return; 14 | } 15 | 16 | const { coursesTaught, coursesLearned } = filterCoursesHelper( 17 | isAllCourses, 18 | _id 19 | ); 20 | 21 | const user = { role, coursesTaught, coursesLearned }; 22 | 23 | res.status(200).render('users/common/my-courses', { 24 | user, 25 | local, 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /api/controllers/lessonsControllers/renderAllLessonsTaught.controller.js: -------------------------------------------------------------------------------- 1 | const { 2 | findOneCourseService, 3 | } = require('../../../database/services/modelServices/courseServices'); 4 | const { 5 | render500ErrorHelper, 6 | redirectNonExistentDataHelper, 7 | } = require('../helpers'); 8 | 9 | exports.renderAllLessonsTaughtController = async (req, res) => { 10 | const { id } = req.params; 11 | const { local } = req.user; 12 | 13 | const isCourse = await findOneCourseService(id); 14 | 15 | if (isCourse instanceof Error) { 16 | render500ErrorHelper(res); 17 | return; 18 | } 19 | 20 | if (isCourse === null) { 21 | redirectNonExistentDataHelper(req, res); 22 | return; 23 | } 24 | 25 | res.status(200).render('users/common/all-course-lessons', { 26 | local, 27 | isCourse, 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /api/controllers/helpers/__tests__/checkIfValidObjectId.helper.test.js: -------------------------------------------------------------------------------- 1 | const { checkIfValidObjectIdHelper } = require('../index'); 2 | const { 3 | validateEquality, 4 | } = require('../../../../utils/test-utils/validators.utils'); 5 | 6 | describe('checkIfValidObjectId Helper Test Suite', () => { 7 | test('should validate true for incoming id with correct Mongo ObjectId format', () => { 8 | const id = `5fe0f5e398cf4637b715db3f`; 9 | 10 | const isValidObjectId = checkIfValidObjectIdHelper(id); 11 | 12 | validateEquality(isValidObjectId, true); 13 | }); 14 | 15 | test('should validate false for incoming id with correct Mongo ObjectId format', () => { 16 | const id = `chicken`; 17 | 18 | const isValidObjectId = checkIfValidObjectIdHelper(id); 19 | 20 | validateEquality(isValidObjectId, false); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /api/controllers/usersAuthControllers/__tests__/logoutUser.controller.test.js: -------------------------------------------------------------------------------- 1 | const { logoutUserController } = require('../index'); 2 | const { 3 | mockRequest, 4 | mockResponse, 5 | } = require('../../../../utils/test-utils/interceptors.utils'); 6 | const { 7 | validateMockValueToHaveBeenCalled, 8 | } = require('../../../../utils/test-utils/validators.utils'); 9 | 10 | describe('logoutUser Controller Test Suite', () => { 11 | afterEach(() => { 12 | jest.clearAllMocks(); 13 | }); 14 | 15 | test('should validate logout path', () => { 16 | const req = mockRequest(); 17 | const res = mockResponse(); 18 | 19 | logoutUserController(req, res); 20 | 21 | const { logout, session } = req; 22 | 23 | validateMockValueToHaveBeenCalled(logout); 24 | validateMockValueToHaveBeenCalled(session.destroy); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /api/controllers/usersAuthControllers/__tests__/renderRegister.controller.test.js: -------------------------------------------------------------------------------- 1 | const { renderRegisterController } = require('../index'); 2 | const { 3 | validateMockValueToHaveBeenCalled, 4 | } = require('../../../../utils/test-utils/validators.utils'); 5 | const { 6 | mockRequest, 7 | mockResponse, 8 | } = require('../../../../utils/test-utils/interceptors.utils'); 9 | 10 | describe('renderRegisterController Test Suite', () => { 11 | afterEach(() => { 12 | jest.clearAllMocks(); 13 | }); 14 | 15 | test('should validate res.status & res.render is called', () => { 16 | const req = mockRequest(); 17 | const res = mockResponse(); 18 | 19 | renderRegisterController(req, res); 20 | const { status, render } = res; 21 | 22 | validateMockValueToHaveBeenCalled(status); 23 | validateMockValueToHaveBeenCalled(render); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /api/controllers/coursesControllers/updateTaughtCourse.controller.js: -------------------------------------------------------------------------------- 1 | const { 2 | updateCourseService, 3 | } = require('../../../database/services/modelServices/courseServices'); 4 | const { 5 | render500ErrorHelper, 6 | redirectNonExistentDataHelper, 7 | } = require('../helpers'); 8 | 9 | exports.updateTaughtCourseController = async (req, res) => { 10 | const { id } = req.params; 11 | const { title, description } = req.body; 12 | 13 | const isUpdatedCourse = await updateCourseService(id, title, description); 14 | 15 | if (isUpdatedCourse instanceof Error) { 16 | render500ErrorHelper(res); 17 | return; 18 | } 19 | 20 | if (isUpdatedCourse === null) { 21 | redirectNonExistentDataHelper(req, res); 22 | return; 23 | } 24 | 25 | req.flash('success_msg', 'Course successfully updated!'); 26 | res.redirect(`/my-courses/teach/${id}`); 27 | }; 28 | -------------------------------------------------------------------------------- /views/users/instructors/edit-course.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Edit Course

4 |
5 | 6 |
7 |
8 |
9 | 10 | 11 |
12 |
13 | 14 | 15 | 16 |
17 | 18 |
19 |
20 |
-------------------------------------------------------------------------------- /api/controllers/helpers/createNewUser.helper.js: -------------------------------------------------------------------------------- 1 | const { handleIfAsyncErrorHelper } = require('./handleIfAsyncError.helper'); 2 | const { 3 | setUserInSessionAndLoginHelper, 4 | } = require('./setUserInSessionAndLogin.helper'); 5 | const { isString } = require('../../../utils/global-utils'); 6 | const { 7 | createNewUserService, 8 | } = require('../../../database/services/modelServices/userServices'); 9 | 10 | exports.createNewUserHelper = async (request, response) => { 11 | const isNewUser = await createNewUserService(request.body); 12 | 13 | const isHandledResults = handleIfAsyncErrorHelper(isNewUser); 14 | 15 | if (isString(isHandledResults)) { 16 | response 17 | .status(409) 18 | .render('users/auth/register', { message: isHandledResults }); 19 | return; 20 | } 21 | 22 | setUserInSessionAndLoginHelper(request, isHandledResults, response); 23 | }; 24 | -------------------------------------------------------------------------------- /api/controllers/coursesControllers/createNewCourse.controller.js: -------------------------------------------------------------------------------- 1 | const { 2 | createNewCourseService, 3 | } = require('../../../database/services/modelServices/courseServices'); 4 | const { render500ErrorHelper } = require('../helpers'); 5 | const { 6 | ensureDataInVulnerableOfInjectionAttacks, 7 | } = require('../../../utils/global-utils'); 8 | 9 | exports.createNewCourseController = async (req, res) => { 10 | const secureRequestBody = ensureDataInVulnerableOfInjectionAttacks(req.body); 11 | 12 | const { title, description } = secureRequestBody; 13 | const { _id } = req.user; 14 | 15 | const newCourse = await createNewCourseService(title, description); 16 | 17 | if (newCourse instanceof Error) { 18 | render500ErrorHelper(res); 19 | return; 20 | } 21 | 22 | newCourse.instructors.push(_id); 23 | await newCourse.save(); 24 | 25 | res.redirect('/my-courses'); 26 | }; 27 | -------------------------------------------------------------------------------- /api/controllers/usersControllers/renderEditUserProfile.controller.js: -------------------------------------------------------------------------------- 1 | const { 2 | findUserByIdService, 3 | } = require('../../../database/services/modelServices/userServices'); 4 | const { checkIfValidObjectIdHelper } = require('../helpers'); 5 | 6 | exports.renderEditUserProfileController = async (req, res) => { 7 | const { id } = req.params; 8 | 9 | const isValidObjectId = checkIfValidObjectIdHelper(id); 10 | 11 | if (!isValidObjectId) { 12 | req.flash('error_msg', 'Wrong format on the user ID'); 13 | res.redirect(302, '/profile'); 14 | return; 15 | } 16 | 17 | const isFoundUser = await findUserByIdService(id); 18 | 19 | if (isFoundUser === null) { 20 | req.flash('error_msg', `A user with this ID doesn't exist!`); 21 | res.redirect(302, '/profile'); 22 | return; 23 | } 24 | 25 | res.status(200).render('users/common/edit-profile', isFoundUser); 26 | }; 27 | -------------------------------------------------------------------------------- /api/controllers/helpers/__tests__/renderLoginFormWithErrors.helper.test.js: -------------------------------------------------------------------------------- 1 | const { renderLoginFormWithErrorsHelper } = require('../index'); 2 | const { 3 | validateMockValueToHaveBeenCalled, 4 | } = require('../../../../utils/test-utils/validators.utils'); 5 | const { 6 | mockResponse, 7 | } = require('../../../../utils/test-utils/interceptors.utils'); 8 | 9 | describe('renderLoginFormWithErrors Test Suite', () => { 10 | afterEach(() => { 11 | jest.clearAllMocks(); 12 | }); 13 | 14 | test('should validate res.status & res.render get called', () => { 15 | const res = mockResponse(); 16 | const user = 'dummy'; 17 | const password = expect.anything(); 18 | 19 | renderLoginFormWithErrorsHelper(res, user, password); 20 | const { status, render } = res; 21 | 22 | validateMockValueToHaveBeenCalled(status); 23 | validateMockValueToHaveBeenCalled(render); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /api/controllers/usersAuthControllers/index.js: -------------------------------------------------------------------------------- 1 | const { loginUserController } = require('./loginUser.controller'); 2 | const { logoutUserController } = require('./logoutUser.controller'); 3 | const { renderLoginController } = require('./renderLogin.controller'); 4 | const { renderRegisterController } = require('./renderRegister.controller'); 5 | const { registerNewUserController } = require('./registerNewUser.controller'); 6 | const { 7 | authenticateGoogleLoginController, 8 | } = require('./authenticateGoogleLogin.controller'); 9 | const { 10 | authenticateGoogleCallbackController, 11 | } = require('./authenticateGoogleCallback.controller'); 12 | 13 | module.exports = { 14 | loginUserController, 15 | logoutUserController, 16 | renderLoginController, 17 | renderRegisterController, 18 | registerNewUserController, 19 | authenticateGoogleLoginController, 20 | authenticateGoogleCallbackController, 21 | }; 22 | -------------------------------------------------------------------------------- /api/controllers/helpers/__tests__/redirectNonexistentCourse.helper.test.js: -------------------------------------------------------------------------------- 1 | const { redirectNonExistentDataHelper } = require('../index'); 2 | const { 3 | mockRequest, 4 | mockResponse, 5 | } = require('../../../../utils/test-utils/interceptors.utils'); 6 | 7 | const { 8 | validateMockValueToHaveBeenCalled, 9 | } = require('../../../../utils/test-utils/validators.utils'); 10 | 11 | describe('redirectNonexistentCourse Helper Test Suite', () => { 12 | afterEach(() => { 13 | jest.clearAllMocks(); 14 | }); 15 | 16 | test('should validate res.status & res.render being called', () => { 17 | const req = mockRequest(); 18 | const res = mockResponse(); 19 | 20 | redirectNonExistentDataHelper(req, res); 21 | 22 | const { flash } = req; 23 | const { redirect } = res; 24 | 25 | validateMockValueToHaveBeenCalled(flash); 26 | validateMockValueToHaveBeenCalled(redirect); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /api/controllers/helpers/checkCurrentUserRelationToCourse.helper.js: -------------------------------------------------------------------------------- 1 | exports.checkCurrentUserRelationToCourseHelper = ( 2 | instructorsList, 3 | currentUserId, 4 | studentsList 5 | ) => { 6 | let isCurrentUserTheCourseInstructor; 7 | let isCurrentUserInStudentList = false; 8 | 9 | if (instructorsList && instructorsList.length) { 10 | const courseInstructorsList = instructorsList.map( 11 | ({ _id: instructorId }) => instructorId 12 | ); 13 | 14 | isCurrentUserTheCourseInstructor = courseInstructorsList.includes( 15 | currentUserId 16 | ); 17 | } 18 | 19 | if (studentsList && studentsList.length) { 20 | const courseStudentList = studentsList.map( 21 | ({ _id: studentId }) => studentId 22 | ); 23 | isCurrentUserInStudentList = courseStudentList.includes(currentUserId); 24 | } 25 | 26 | return { isCurrentUserInStudentList, isCurrentUserTheCourseInstructor }; 27 | }; 28 | -------------------------------------------------------------------------------- /api/controllers/usersControllers/__tests__/renderUserProfile.controller.test.js: -------------------------------------------------------------------------------- 1 | const { renderUserProfileController } = require('../index'); 2 | const { 3 | validateMockValueToHaveBeenCalled, 4 | } = require('../../../../utils/test-utils/validators.utils'); 5 | const { 6 | mockRequest, 7 | mockResponse, 8 | } = require('../../../../utils/test-utils/interceptors.utils'); 9 | 10 | describe('renderUserProfileController Test Suite', () => { 11 | afterEach(() => { 12 | jest.clearAllMocks(); 13 | }); 14 | 15 | test('should validate res.status & res.render is called', () => { 16 | const req = mockRequest(); 17 | const res = mockResponse(); 18 | req.user = jest.fn(); 19 | req.user.local = jest.fn(); 20 | 21 | renderUserProfileController(req, res); 22 | const { status, render } = res; 23 | 24 | validateMockValueToHaveBeenCalled(status); 25 | validateMockValueToHaveBeenCalled(render); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /api/controllers/helpers/__tests__/renderRegistrationFormWithErrors.helper.test.js: -------------------------------------------------------------------------------- 1 | const { renderRegistrationFormWithErrorsHelper } = require('../index'); 2 | const { 3 | validateMockValueToHaveBeenCalled, 4 | } = require('../../../../utils/test-utils/validators.utils'); 5 | 6 | const { 7 | mockResponse, 8 | mockErrorRequest, 9 | } = require('../../../../utils/test-utils/interceptors.utils'); 10 | 11 | describe('renderRegistrationForm Test Suite', () => { 12 | afterEach(() => { 13 | jest.clearAllMocks(); 14 | }); 15 | 16 | test('should validate res.status & res.render get called', () => { 17 | const req = mockErrorRequest(); 18 | const res = mockResponse(); 19 | 20 | const dummyError = { array: jest.fn() }; 21 | 22 | renderRegistrationFormWithErrorsHelper(res, req, dummyError); 23 | const { status, render } = res; 24 | 25 | validateMockValueToHaveBeenCalled(status); 26 | validateMockValueToHaveBeenCalled(render); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /api/controllers/lessonsControllers/__tests__/renderCreateNewLesson.controller.test.js: -------------------------------------------------------------------------------- 1 | const { 2 | renderCreateNewLessonController, 3 | } = require('../renderCreateNewLesson.controller'); 4 | const { 5 | validateMockValueToHaveBeenCalled, 6 | } = require('../../../../utils/test-utils/validators.utils'); 7 | const { 8 | mockRequest, 9 | mockResponse, 10 | } = require('../../../../utils/test-utils/interceptors.utils'); 11 | 12 | describe('renderCreateNewLesson Controller Test Suite', () => { 13 | afterEach(() => { 14 | jest.clearAllMocks(); 15 | }); 16 | 17 | test('should validate res.status & res.render are called', () => { 18 | const req = mockRequest(); 19 | const res = mockResponse(); 20 | 21 | req.user = expect.anything(); 22 | 23 | renderCreateNewLessonController(req, res); 24 | 25 | const { status, render } = res; 26 | 27 | validateMockValueToHaveBeenCalled(status); 28 | validateMockValueToHaveBeenCalled(render); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /views/users/instructors/new-lesson.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{#if message}} 4 |

{{message}}

5 | {{/if}} 6 |
7 |
8 |
9 |
10 | 11 | 12 |
13 |
14 | 15 | 17 |
18 |
19 | 20 | 21 |
22 | 23 |
24 |
25 |
-------------------------------------------------------------------------------- /api/controllers/coursesControllers/__tests__/renderCreateNewCourse.controller.test.js: -------------------------------------------------------------------------------- 1 | const { renderCreateNewCourseController } = require('../index'); 2 | const { 3 | mockRequest, 4 | mockResponse, 5 | } = require('../../../../utils/test-utils/interceptors.utils'); 6 | const { 7 | validateMockValueToHaveBeenCalled, 8 | } = require('../../../../utils/test-utils/validators.utils'); 9 | 10 | let req; 11 | let res; 12 | 13 | describe('renderCreateNewCourse Controller Test Suite', () => { 14 | beforeEach(() => { 15 | req = mockRequest(); 16 | res = mockResponse(); 17 | }); 18 | 19 | afterEach(() => { 20 | jest.clearAllMocks(); 21 | }); 22 | 23 | test('should validate res.status & res.render are called', () => { 24 | req.user = expect.anything(); 25 | 26 | renderCreateNewCourseController(req, res); 27 | 28 | const { status, render } = res; 29 | 30 | validateMockValueToHaveBeenCalled(status); 31 | validateMockValueToHaveBeenCalled(render); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /views/users/common/all-course-lessons.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{#if success_msg}} 4 |

{{success_msg}}

5 | {{/if}} 6 |
7 | 8 |
9 |

{{isCourse.title}} lessons

10 |
11 |
12 | {{#if isCourse.lessons}} 13 | {{#each isCourse.lessons}} 14 |
15 |

{{this.topic}}

16 | See Lesson 17 | Edit Lesson 18 |
19 | {{/each}} 20 | 21 | {{else}} 22 |
23 |

There are no lessons yet!

24 |
25 | {{/if}} 26 | 27 | Add New Lesson 28 |
29 |
30 |
31 |
-------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help improve this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /database/services/modelServices/userServices/createNewUser.service.js: -------------------------------------------------------------------------------- 1 | const User = require('../../../models/user.model'); 2 | const { 3 | handleAsyncFunction, 4 | ensureDataInVulnerableOfInjectionAttacks, 5 | } = require('../../../../utils/global-utils'); 6 | const { hashPasswordHelper } = require('../../helpers'); 7 | 8 | exports.createNewUserService = async (requestBody) => { 9 | const secureRequestBody = ensureDataInVulnerableOfInjectionAttacks( 10 | requestBody 11 | ); 12 | 13 | const { 14 | firstName, 15 | lastName, 16 | email, 17 | username, 18 | password, 19 | role, 20 | } = secureRequestBody; 21 | 22 | const isHashedPassword = await hashPasswordHelper(password); 23 | 24 | const [results, error] = await handleAsyncFunction( 25 | User.create({ 26 | role, 27 | local: { 28 | email, 29 | username, 30 | lastName, 31 | firstName, 32 | password: isHashedPassword, 33 | }, 34 | }) 35 | ); 36 | 37 | return results || error; 38 | }; 39 | -------------------------------------------------------------------------------- /api/controllers/usersAuthControllers/__tests__/renderLogin.controller.test.js: -------------------------------------------------------------------------------- 1 | const { renderLoginController } = require('../index'); 2 | const { 3 | validateStringEquality, 4 | validateMockValueToHaveBeenCalled, 5 | } = require('../../../../utils/test-utils/validators.utils'); 6 | const { 7 | mockRequest, 8 | mockResponse, 9 | } = require('../../../../utils/test-utils/interceptors.utils'); 10 | 11 | describe('renderLoginController Test Suite', () => { 12 | afterEach(() => { 13 | jest.clearAllMocks(); 14 | }); 15 | 16 | test('should validate res.status & res.render is called', () => { 17 | const req = mockRequest(); 18 | const res = mockResponse(); 19 | const message = `Missing credentials`; 20 | 21 | req.flash = () => message; 22 | 23 | renderLoginController(req, res); 24 | const { status, render } = res; 25 | const { flash } = req; 26 | 27 | validateStringEquality(flash(), message); 28 | validateMockValueToHaveBeenCalled(status); 29 | validateMockValueToHaveBeenCalled(render); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /database/models/user.model.js: -------------------------------------------------------------------------------- 1 | const { Schema, model } = require('mongoose'); 2 | 3 | const userSchema = new Schema({ 4 | local: { 5 | firstName: { 6 | type: String, 7 | trim: true, 8 | }, 9 | lastName: { 10 | type: String, 11 | trim: true, 12 | }, 13 | username: { 14 | type: String, 15 | trim: true, 16 | unique: true, 17 | }, 18 | email: { 19 | type: String, 20 | match: [/^\S+@\S+\.\S+$/, 'Please use a valid email address.'], 21 | unique: true, 22 | lowercase: true, 23 | trim: true, 24 | }, 25 | password: { type: String }, 26 | }, 27 | google: { 28 | id: String, 29 | token: String, 30 | email: String, 31 | name: String, 32 | }, 33 | profilePictureUrl: { 34 | type: 'String', 35 | default: 'https://via.placeholder.com/150', 36 | }, 37 | role: { 38 | type: String, 39 | enum: ['student', 'instructor', 'admin'], 40 | default: 'student', 41 | }, 42 | }); 43 | 44 | module.exports = model('User', userSchema); 45 | -------------------------------------------------------------------------------- /database/services/modelServices/userServices/createNewGoogleUser.service.js: -------------------------------------------------------------------------------- 1 | const User = require('../../../models/user.model'); 2 | const { 3 | handleAsyncFunction, 4 | } = require('../../../../utils/global-utils/handleAsyncFunction.utils'); 5 | 6 | exports.createNewGoogleUserService = async (googleProfileData, accessToken) => { 7 | const [googleResults, googleError] = await handleAsyncFunction( 8 | User.create({ 9 | local: { 10 | email: googleProfileData.emails[0].value.toLocaleLowerCase(), 11 | username: googleProfileData.displayName, 12 | firstName: googleProfileData.name.givenName, 13 | lastName: googleProfileData.name.familyName, 14 | }, 15 | google: { 16 | id: googleProfileData.id, 17 | token: accessToken, 18 | name: googleProfileData.displayName, 19 | email: googleProfileData.emails[0].value.toLocaleLowerCase(), 20 | }, 21 | profilePictureUrl: googleProfileData.photos[0].value, 22 | }) 23 | ); 24 | 25 | return googleResults || googleError; 26 | }; 27 | -------------------------------------------------------------------------------- /api/middleware/__tests__/isLoggedInUser.middleware.test.js: -------------------------------------------------------------------------------- 1 | const { isLoggedInUser } = require('../index'); 2 | const { 3 | mockRequest, 4 | mockResponse, 5 | } = require('../../../utils/test-utils/interceptors.utils'); 6 | const { 7 | validateMockValueToHaveBeenCalled, 8 | } = require('../../../utils/test-utils/validators.utils'); 9 | 10 | let req; 11 | let res; 12 | let next; 13 | 14 | describe('isLoggedInUser Test Suite', () => { 15 | beforeEach(() => { 16 | req = mockRequest(); 17 | res = mockResponse(); 18 | next = jest.fn(); 19 | }); 20 | 21 | afterEach(() => { 22 | jest.clearAllMocks(); 23 | }); 24 | 25 | test('should validate next being called', () => { 26 | req.isAuthenticated = () => true; 27 | 28 | isLoggedInUser(req, res, next); 29 | validateMockValueToHaveBeenCalled(next); 30 | }); 31 | 32 | test('should validate redirect is being called', () => { 33 | req.isAuthenticated = () => false; 34 | 35 | const { redirect } = res; 36 | 37 | isLoggedInUser(req, res, next); 38 | validateMockValueToHaveBeenCalled(redirect); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /api/controllers/usersAuthControllers/__tests__/registerNewUser.controller.test.js: -------------------------------------------------------------------------------- 1 | jest.mock('express-validator'); 2 | const { validationResult } = require('express-validator'); 3 | const { registerNewUserController } = require('../index'); 4 | const { 5 | validateMockValueToHaveBeenCalled, 6 | } = require('../../../../utils/test-utils/validators.utils'); 7 | const { 8 | mockRequest, 9 | mockResponse, 10 | } = require('../../../../utils/test-utils/interceptors.utils'); 11 | 12 | describe('registerNewUserController Test Suite', () => { 13 | afterEach(() => { 14 | jest.clearAllMocks(); 15 | }); 16 | 17 | test('should validate req.login gets called', async () => { 18 | validationResult.mockImplementation(() => ({ 19 | isEmpty: jest.fn().mockReturnValue(false), 20 | array: jest 21 | .fn() 22 | .mockReturnValue([{ test1: 'error1' }, { test2: 'error2' }]), 23 | })); 24 | 25 | const req = mockRequest(); 26 | const res = mockResponse(); 27 | 28 | await registerNewUserController(req, res); 29 | validateMockValueToHaveBeenCalled(validationResult); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /database/fixtures/index.js: -------------------------------------------------------------------------------- 1 | const { dummyPassword } = require('../../configs'); 2 | 3 | exports.fakeCourseData = { 4 | title: 'Dummy Course', 5 | description: 'Dummy Description', 6 | instructors: [], 7 | students: [], 8 | lessons: [], 9 | }; 10 | 11 | exports.fakeLessonData = { 12 | topic: 'Dummy Topic', 13 | content: 'Dummy Content', 14 | videoUrl: 'www.dummy.video', 15 | }; 16 | 17 | exports.fakeUserData = { 18 | firstName: 'Dummy', 19 | lastName: 'User', 20 | username: 'dummyUser', 21 | email: 'dummy@user.com', 22 | password: dummyPassword, 23 | role: 'instructor', 24 | }; 25 | 26 | exports.fakeUserDataTwo = { 27 | firstName: 'Fake', 28 | lastName: 'User', 29 | username: 'fakeUser', 30 | email: 'fake@user.com', 31 | password: dummyPassword, 32 | role: 'student', 33 | }; 34 | 35 | exports.fakeUserDataEmptyFields = { 36 | firstName: '', 37 | lastName: '', 38 | username: '', 39 | email: '', 40 | password: '', 41 | role: 'student', 42 | }; 43 | 44 | exports.fakeIdFormatData = { 45 | correctFormat: '5fe0f5e398cf4637b715db3f', 46 | incorrectFormat: 'dummy', 47 | }; 48 | -------------------------------------------------------------------------------- /api/controllers/helpers/__tests__/getDuplicateErrorMessage.helper.test.js: -------------------------------------------------------------------------------- 1 | const { 2 | getDuplicateErrorMessageHelper, 3 | } = require('../getDuplicateErrorMessage.helper'); 4 | const { 5 | validateStringEquality, 6 | } = require('../../../../utils/test-utils/validators.utils'); 7 | 8 | const expectedResult = (input) => 9 | `Sorry, ${input} already exists! Please register with another one or login to your account`; 10 | 11 | describe('getDuplicateErrorMessage Test Suite', () => { 12 | test('should validate duplicate message function is called for email', () => { 13 | const keyValue = { 14 | email: 'dummy@dummy.com', 15 | }; 16 | 17 | const results = getDuplicateErrorMessageHelper(keyValue); 18 | validateStringEquality(results, expectedResult(keyValue.email)); 19 | }); 20 | 21 | test('should validate duplicate message function is called for username', () => { 22 | const keyValue = { 23 | username: 'dummy', 24 | }; 25 | 26 | const results = getDuplicateErrorMessageHelper(keyValue); 27 | validateStringEquality(results, expectedResult(keyValue.username)); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /views/users/instructors/course-taught.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{#if success_msg}} 4 |

{{success_msg}}

5 | {{/if}} 6 |
7 | 8 |

{{isCourse.title}}

9 |

{{isCourse.description}}

10 | 11 |
12 |
13 | course cover image 14 |
15 |
16 |
17 |
18 |

Registered Students: {{isCourse.students.length}}

19 | See Student List 20 |
21 |
22 |

Edit Course

23 | Edit Course 24 |
25 |
26 |

All Lessons

27 | See All Lessons 28 |
29 |
30 |
31 |
32 |
-------------------------------------------------------------------------------- /api/routes/users-auth.routes.js: -------------------------------------------------------------------------------- 1 | const { Router } = require('express'); 2 | const { validateRegistrationForm } = require('../middleware'); 3 | const { 4 | registerPrefix, 5 | loginPrefix, 6 | logoutPrefix, 7 | googleAuthPrefix, 8 | googleAuthCallbackPrefix, 9 | } = require('../../configs'); 10 | const { 11 | loginUserController, 12 | logoutUserController, 13 | renderLoginController, 14 | renderRegisterController, 15 | registerNewUserController, 16 | authenticateGoogleLoginController, 17 | authenticateGoogleCallbackController, 18 | } = require('../controllers'); 19 | 20 | const router = Router(); 21 | 22 | router.get(registerPrefix, renderRegisterController); 23 | router.post( 24 | registerPrefix, 25 | validateRegistrationForm(), 26 | registerNewUserController 27 | ); 28 | router.get(loginPrefix, renderLoginController); 29 | router.post(loginPrefix, loginUserController); 30 | router.post(logoutPrefix, logoutUserController); 31 | router.get(googleAuthPrefix, authenticateGoogleLoginController); 32 | router.get(googleAuthCallbackPrefix, authenticateGoogleCallbackController); 33 | 34 | module.exports = router; 35 | -------------------------------------------------------------------------------- /passport/localStrategy.js: -------------------------------------------------------------------------------- 1 | const LocalStrategy = require('passport-local').Strategy; 2 | const User = require('../database/models/user.model'); 3 | const { handleAsyncFunction } = require('../utils/global-utils'); 4 | const { comparePasswordHelper } = require('./helpers'); 5 | 6 | exports.localStrategy = new LocalStrategy( 7 | { usernameField: 'user', passwordField: 'password', passReqToCallback: true }, 8 | async (req, user, password, done) => { 9 | const [userResults, error] = await handleAsyncFunction( 10 | User.findOne({ 'local.email': user }) 11 | ); 12 | 13 | if (error) { 14 | return done(error); 15 | } 16 | 17 | if (!userResults) { 18 | return done( 19 | null, 20 | false, 21 | req.flash('message', 'Incorrect username / email.') 22 | ); 23 | } 24 | 25 | const isMatch = await comparePasswordHelper( 26 | password, 27 | userResults.local.password 28 | ); 29 | 30 | if (!isMatch) { 31 | return done(null, false, req.flash('message', 'Incorrect password.')); 32 | } 33 | 34 | return done(null, userResults); 35 | } 36 | ); 37 | -------------------------------------------------------------------------------- /views/users/instructors/edit-lesson.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Edit Lesson

4 |
5 | 6 |
7 |
8 |
9 | 10 | 11 |
12 |
13 | 14 | 16 |
17 |
18 | 19 | 20 | 21 |
22 | 23 |
24 |
25 |
-------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Malcolm R. Kente (reMRKable Dev) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /views/users/auth/login.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{#if errors}} 4 | 13 | {{/if}} 14 |
15 |
16 | {{#if message}} 17 |

{{message}}

18 | {{/if}} 19 |
20 |
21 |

Login

22 |
23 |
24 | 25 | 26 |
27 |
28 | 29 | 30 |
31 | 32 |
33 |
34 |

Don't have an account? Register

35 |

Login with Google+

36 |
37 |
-------------------------------------------------------------------------------- /api/middleware/validateRegistrationForm.middleware.js: -------------------------------------------------------------------------------- 1 | const { check } = require('express-validator'); 2 | 3 | exports.validateRegistrationForm = () => [ 4 | check('firstName').notEmpty().withMessage('Please provide your First Name.'), 5 | check('lastName').notEmpty().withMessage('Please provide your Last Name.'), 6 | check('email').normalizeEmail(), 7 | check('email').notEmpty().withMessage('Please provide your Email.'), 8 | check('email') 9 | .isEmail() 10 | .withMessage("Please use correct Email format, 'foo@bar.com'."), 11 | check('username').notEmpty().withMessage('Please provide your Username.'), 12 | check('password').notEmpty().withMessage('Please provide a Password.'), 13 | check('password') 14 | .isLength({ min: 6 }) 15 | .matches(/(?=.*\d)(?=.*[a-z])(?=.*[A-Z])/) 16 | .withMessage( 17 | 'Password needs to have at least 6 chars and must contain at least one number, one lowercase and one uppercase letter.' 18 | ), 19 | check('password2').custom((value, { req }) => { 20 | if (value !== req.body.password) { 21 | throw new Error('Password confirmation does not match password'); 22 | } 23 | return true; 24 | }), 25 | ]; 26 | -------------------------------------------------------------------------------- /utils/global-utils/__tests__/handleAsyncFunction.utils.test.js: -------------------------------------------------------------------------------- 1 | const { handleAsyncFunction } = require('../index'); 2 | const { 3 | validateNotEmpty, 4 | validateUndefined, 5 | validateTypeOf, 6 | } = require('../../test-utils/validators.utils'); 7 | 8 | const doSomethingAsync = (promiseState) => 9 | new Promise((resolve, reject) => 10 | promiseState 11 | ? resolve('Async Promise resolved') 12 | : reject(new Error('Async Promise rejected')) 13 | ); 14 | 15 | describe('Handle Async Functions Test Suite', () => { 16 | test('should validate handleAsyncFunction as typeof function', () => { 17 | validateTypeOf(handleAsyncFunction, 'function'); 18 | }); 19 | 20 | test('should validate handleAsyncFunction returns error', async () => { 21 | const [results, error] = await handleAsyncFunction(doSomethingAsync(false)); 22 | 23 | validateUndefined(results); 24 | validateNotEmpty(error); 25 | }); 26 | 27 | test('should validate handleAsyncFunction returns results', async () => { 28 | const [results, error] = await handleAsyncFunction(doSomethingAsync(true)); 29 | 30 | validateNotEmpty(results); 31 | validateUndefined(error); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /api/middleware/__tests__/isInstructor.middleware.test.js: -------------------------------------------------------------------------------- 1 | const { isInstructor } = require('../index'); 2 | const { 3 | mockRequest, 4 | mockResponse, 5 | } = require('../../../utils/test-utils/interceptors.utils'); 6 | const { 7 | validateMockValueToHaveBeenCalled, 8 | } = require('../../../utils/test-utils/validators.utils'); 9 | 10 | let req; 11 | let res; 12 | let next; 13 | 14 | describe('isInstructor Test Suite', () => { 15 | beforeEach(() => { 16 | req = mockRequest(); 17 | res = mockResponse(); 18 | next = jest.fn(); 19 | }); 20 | 21 | afterEach(() => { 22 | jest.clearAllMocks(); 23 | }); 24 | 25 | test('should validate req.flash & res.redirect being called', () => { 26 | req.user.role = 'student'; 27 | 28 | isInstructor(req, res, next); 29 | 30 | const { redirect } = res; 31 | const { flash } = req; 32 | 33 | validateMockValueToHaveBeenCalled(redirect); 34 | validateMockValueToHaveBeenCalled(flash); 35 | }); 36 | 37 | test('should validate next is being called', () => { 38 | req.user.role = 'instructor'; 39 | 40 | isInstructor(req, res, next); 41 | 42 | validateMockValueToHaveBeenCalled(next); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /api/routes/lessons.routes.js: -------------------------------------------------------------------------------- 1 | const { Router } = require('express'); 2 | const { isLoggedInUser, isInstructor } = require('../middleware'); 3 | const { 4 | myCoursesLessonEditPrefix, 5 | myCoursesLessonDetailsPrefix, 6 | myCoursesTeachNewLessonPrefix, 7 | myCoursesTeachAllLessonsPrefix, 8 | } = require('../../configs'); 9 | const { 10 | createNewLessonController, 11 | renderEditLessonController, 12 | renderLessonDetailsController, 13 | renderCreateNewLessonController, 14 | renderAllLessonsTaughtController, 15 | } = require('../controllers'); 16 | 17 | const router = Router(); 18 | 19 | router.get( 20 | myCoursesTeachAllLessonsPrefix, 21 | isLoggedInUser, 22 | isInstructor, 23 | renderAllLessonsTaughtController 24 | ); 25 | router.get( 26 | myCoursesTeachNewLessonPrefix, 27 | isLoggedInUser, 28 | isInstructor, 29 | renderCreateNewLessonController 30 | ); 31 | router.post(myCoursesTeachNewLessonPrefix, createNewLessonController); 32 | router.get( 33 | myCoursesLessonDetailsPrefix, 34 | isLoggedInUser, 35 | renderLessonDetailsController 36 | ); 37 | router.get( 38 | myCoursesLessonEditPrefix, 39 | isLoggedInUser, 40 | renderEditLessonController 41 | ); 42 | 43 | module.exports = router; 44 | -------------------------------------------------------------------------------- /api/controllers/coursesControllers/renderCourseDetails.controller.js: -------------------------------------------------------------------------------- 1 | const { 2 | findOneCourseService, 3 | } = require('../../../database/services/modelServices/courseServices'); 4 | const { 5 | render500ErrorHelper, 6 | redirectNonExistentDataHelper, 7 | checkCurrentUserRelationToCourseHelper, 8 | } = require('../helpers'); 9 | 10 | exports.renderCourseDetailsController = async (req, res) => { 11 | const { local, _id: currentUserId } = req.user; 12 | const { id } = req.params; 13 | 14 | const isCourse = await findOneCourseService(id); 15 | 16 | if (isCourse instanceof Error) { 17 | render500ErrorHelper(res); 18 | return; 19 | } 20 | 21 | if (isCourse === null) { 22 | redirectNonExistentDataHelper(req, res); 23 | return; 24 | } 25 | 26 | const { students, instructors } = isCourse; 27 | 28 | const { 29 | isCurrentUserInStudentList, 30 | isCurrentUserTheCourseInstructor, 31 | } = checkCurrentUserRelationToCourseHelper( 32 | instructors, 33 | currentUserId, 34 | students 35 | ); 36 | 37 | res.status(200).render('users/common/course-details', { 38 | local, 39 | isCourse, 40 | isCurrentUserInStudentList, 41 | isCurrentUserTheCourseInstructor, 42 | }); 43 | }; 44 | -------------------------------------------------------------------------------- /api/controllers/lessonsControllers/createNewLesson.controller.js: -------------------------------------------------------------------------------- 1 | const { 2 | createNewLessonService, 3 | } = require('../../../database/services/modelServices/lessonServices'); 4 | const { 5 | findOneCourseService, 6 | } = require('../../../database/services/modelServices/courseServices'); 7 | const { render500ErrorHelper } = require('../helpers'); 8 | const { 9 | ensureDataInVulnerableOfInjectionAttacks, 10 | } = require('../../../utils/global-utils'); 11 | 12 | exports.createNewLessonController = async (req, res) => { 13 | const secureRequestBody = ensureDataInVulnerableOfInjectionAttacks(req.body); 14 | 15 | const { topic, content, videoUrl } = secureRequestBody; 16 | const { id } = req.params; 17 | 18 | const newLesson = await createNewLessonService(topic, content, videoUrl); 19 | 20 | if (newLesson instanceof Error) { 21 | render500ErrorHelper(res); 22 | return; 23 | } 24 | 25 | const isFoundCourse = await findOneCourseService(id); 26 | 27 | if (isFoundCourse instanceof Error) { 28 | render500ErrorHelper(res); 29 | return; 30 | } 31 | 32 | const { _id } = newLesson; 33 | 34 | isFoundCourse.lessons.push(_id); 35 | await isFoundCourse.save(); 36 | 37 | res.redirect(`/my-courses/teach/${id}/all-lessons`); 38 | }; 39 | -------------------------------------------------------------------------------- /api/middleware/__tests__/validateRegistrationForm.middleware.test.js: -------------------------------------------------------------------------------- 1 | const { CustomValidation } = require('express-validator/src/context-items'); 2 | const { validateRegistrationForm } = require('../index'); 3 | const { 4 | validateNotEmpty, 5 | validateArrayLength, 6 | validateInstanceOf, 7 | } = require('../../../utils/test-utils/validators.utils'); 8 | 9 | let validationResults; 10 | describe('validateRegistrationForm Middleware Test Suite', () => { 11 | beforeEach(() => { 12 | validationResults = validateRegistrationForm(); 13 | }); 14 | 15 | test('should validate incoming object to contain data', () => { 16 | validateNotEmpty(validationResults); 17 | }); 18 | 19 | test('should validate incoming object to be an Array instance', () => { 20 | validateInstanceOf(validationResults, Array); 21 | }); 22 | 23 | test('should validate length of validation array', () => { 24 | validateArrayLength(validationResults, 9); 25 | }); 26 | 27 | test('should validate custom validation for password 2', () => { 28 | const pwdCustomValidator = validationResults[validationResults.length - 1]; 29 | 30 | const { stack } = pwdCustomValidator.builderOrContext; 31 | 32 | validateInstanceOf(stack[0], CustomValidation); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /utils/test-utils/interceptors.utils.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mockRequest: () => { 3 | const req = {}; 4 | req.body = jest.fn().mockReturnValue(req); 5 | req.params = jest.fn().mockReturnValue(req); 6 | req.login = jest.fn().mockReturnValue(req); 7 | req.logout = jest.fn().mockReturnValue(req); 8 | req.session = { 9 | destroy: jest.fn(), 10 | }; 11 | req.flash = jest.fn().mockReturnValue(req); 12 | req.user = jest.fn().mockReturnValue(req); 13 | req.file = jest.fn().mockReturnValue(req); 14 | return req; 15 | }, 16 | 17 | mockErrorRequest: () => { 18 | const req = {}; 19 | req.body = { 20 | role: 'student', 21 | firstName: '', 22 | lastName: '', 23 | email: '@', 24 | username: '', 25 | password: '', 26 | password2: '', 27 | }; 28 | return req; 29 | }, 30 | 31 | mockResponse: () => { 32 | const res = {}; 33 | res.status = jest.fn().mockReturnValue(res); 34 | res.render = jest.fn().mockReturnValue(res); 35 | res.send = jest.fn().mockReturnValue(res); 36 | res.redirect = jest.fn().mockReturnValue(res); 37 | return res; 38 | }, 39 | 40 | mockServer: () => { 41 | const server = {}; 42 | server.listen = jest.fn(); 43 | return server; 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /database/services/modelServices/courseServices/__tests__/findOneCourse.service.test.js: -------------------------------------------------------------------------------- 1 | const { findOneCourseService, createNewCourseService } = require('../index'); 2 | const { fakeCourseData, fakeIdFormatData } = require('../../../../fixtures'); 3 | const { 4 | dbConnect, 5 | dbDisconnect, 6 | } = require('../../../../../utils/test-utils/dbHandler.utils'); 7 | const { 8 | validateNotEmpty, 9 | validateInstanceOf, 10 | } = require('../../../../../utils/test-utils/validators.utils'); 11 | 12 | describe('findOneCourse Service Test Suite', () => { 13 | beforeEach(async () => dbConnect()); 14 | afterEach(async () => dbDisconnect()); 15 | 16 | test('should validate returns error when course id is incorrect', async () => { 17 | const { incorrectFormat } = fakeIdFormatData; 18 | 19 | const results = await findOneCourseService(incorrectFormat); 20 | 21 | validateInstanceOf(results, Error); 22 | }); 23 | 24 | test('should validate successfully finding one saved course', async () => { 25 | const { title, description } = fakeCourseData; 26 | 27 | const newCourse = await createNewCourseService(title, description); 28 | 29 | const { _id } = newCourse; 30 | 31 | const results = await findOneCourseService(_id); 32 | 33 | validateNotEmpty(results); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /loaders/routes.loader.js: -------------------------------------------------------------------------------- 1 | const createError = require('http-errors'); 2 | const { apiPrefix } = require('../configs'); 3 | const indexRouter = require('../api/routes/index.routes'); 4 | const usersRouter = require('../api/routes/users.routes'); 5 | const coursesRouter = require('../api/routes/courses.routes'); 6 | const lessonsRouter = require('../api/routes/lessons.routes'); 7 | const usersAuthRouter = require('../api/routes/users-auth.routes'); 8 | const instructorsRouter = require('../api/routes/instructors.routes'); 9 | 10 | module.exports = (app) => { 11 | app.use(apiPrefix, indexRouter); 12 | app.use(apiPrefix, usersRouter); 13 | app.use(apiPrefix, coursesRouter); 14 | app.use(apiPrefix, lessonsRouter); 15 | app.use(apiPrefix, usersAuthRouter); 16 | app.use(apiPrefix, instructorsRouter); 17 | 18 | // Catch 404's and forward to error handler below 19 | app.use((req, res, next) => { 20 | next(createError(404)); 21 | }); 22 | 23 | // Error handler 24 | app.use((err, req, res) => { 25 | // set locals, only providing error in development 26 | res.locals.message = err.message; 27 | res.locals.error = req.app.get('env') === 'development' ? err : {}; 28 | 29 | // render the error page 30 | res.status(err.status || 500); 31 | res.render('error'); 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /database/services/modelServices/lessonServices/__tests__/findOneLesson.service.test.js: -------------------------------------------------------------------------------- 1 | const { findOneLessonService, createNewLessonService } = require('../index'); 2 | const { fakeLessonData, fakeIdFormatData } = require('../../../../fixtures'); 3 | const { 4 | validateNotEmpty, 5 | validateInstanceOf, 6 | } = require('../../../../../utils/test-utils/validators.utils'); 7 | const { 8 | dbConnect, 9 | dbDisconnect, 10 | } = require('../../../../../utils/test-utils/dbHandler.utils'); 11 | 12 | describe('findOneLesson Service Test Suite', () => { 13 | beforeEach(async () => dbConnect()); 14 | afterEach(async () => dbDisconnect()); 15 | 16 | test('should validate returns error when course id is incorrect', async () => { 17 | const { incorrectFormat } = fakeIdFormatData; 18 | 19 | const results = await findOneLessonService(incorrectFormat); 20 | 21 | validateInstanceOf(results, Error); 22 | }); 23 | 24 | test('should validate successfully finding one saved course', async () => { 25 | const { topic, content, videoUrl } = fakeLessonData; 26 | 27 | const newCourse = await createNewLessonService(topic, content, videoUrl); 28 | 29 | const { _id } = newCourse; 30 | 31 | const results = await findOneLessonService(_id); 32 | 33 | validateNotEmpty(results); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /api/controllers/helpers/index.js: -------------------------------------------------------------------------------- 1 | const { filterCoursesHelper } = require('./filterCourses.helper'); 2 | const { createNewUserHelper } = require('./createNewUser.helper'); 3 | const { render500ErrorHelper } = require('./render500Error.helper'); 4 | const { authenticateUserHelper } = require('./authenticateUser.helper'); 5 | const { checkIfValidObjectIdHelper } = require('./checkIfValidObjectId.helper'); 6 | const { 7 | handleUpdatedPasswordHelper, 8 | } = require('./handleUpdatedPassword.helper'); 9 | const { 10 | renderLoginFormWithErrorsHelper, 11 | } = require('./renderLoginFormWithErrors.helper'); 12 | const { 13 | renderRegistrationFormWithErrorsHelper, 14 | } = require('./renderRegistrationFormWithErrors.helper'); 15 | const { 16 | redirectNonExistentDataHelper, 17 | } = require('./redirectNonExistentData.helper'); 18 | const { 19 | checkCurrentUserRelationToCourseHelper, 20 | } = require('./checkCurrentUserRelationToCourse.helper'); 21 | 22 | module.exports = { 23 | filterCoursesHelper, 24 | createNewUserHelper, 25 | render500ErrorHelper, 26 | authenticateUserHelper, 27 | checkIfValidObjectIdHelper, 28 | handleUpdatedPasswordHelper, 29 | redirectNonExistentDataHelper, 30 | renderLoginFormWithErrorsHelper, 31 | checkCurrentUserRelationToCourseHelper, 32 | renderRegistrationFormWithErrorsHelper, 33 | }; 34 | -------------------------------------------------------------------------------- /database/services/modelServices/courseServices/__tests__/findAllCourses.service.test.js: -------------------------------------------------------------------------------- 1 | const { findAllCoursesService, createNewCourseService } = require('../index'); 2 | const { fakeCourseData } = require('../../../../fixtures'); 3 | const { 4 | dbConnect, 5 | dbDisconnect, 6 | } = require('../../../../../utils/test-utils/dbHandler.utils'); 7 | const { 8 | validateNotEmpty, 9 | validateArrayLength, 10 | validateStringEquality, 11 | } = require('../../../../../utils/test-utils/validators.utils'); 12 | 13 | let newCourse; 14 | 15 | describe('findAllCourses Service Test Suite', () => { 16 | beforeEach(async () => { 17 | await dbConnect(); 18 | 19 | const { title, description } = fakeCourseData; 20 | 21 | newCourse = await createNewCourseService(title, description); 22 | }); 23 | 24 | afterEach(async () => dbDisconnect()); 25 | 26 | test('should validate successfully finding saved course', async () => { 27 | const results = await findAllCoursesService(); 28 | 29 | validateNotEmpty(results); 30 | validateArrayLength(results, 1); 31 | 32 | const [expectedCourseData] = results; 33 | 34 | validateStringEquality(expectedCourseData.title, newCourse.title); 35 | validateStringEquality( 36 | expectedCourseData.description, 37 | newCourse.description 38 | ); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /api/controllers/indexControllers/__tests__/renderIndexView.controller.test.js: -------------------------------------------------------------------------------- 1 | const { renderIndexViewController } = require('../index'); 2 | const { 3 | validateMockValueToHaveBeenCalled, 4 | } = require('../../../../utils/test-utils/validators.utils'); 5 | const { 6 | mockRequest, 7 | mockResponse, 8 | } = require('../../../../utils/test-utils/interceptors.utils'); 9 | 10 | let req; 11 | let res; 12 | 13 | describe('renderIndexView Test Suite', () => { 14 | beforeEach(() => { 15 | req = mockRequest(); 16 | res = mockResponse(); 17 | }); 18 | 19 | afterEach(() => { 20 | jest.clearAllMocks(); 21 | }); 22 | 23 | test('should validate res.status & res.render is called when a user is logged in', () => { 24 | req.user = { local: { username: expect.anything() } }; 25 | 26 | renderIndexViewController(req, res); 27 | const { status, render } = res; 28 | 29 | validateMockValueToHaveBeenCalled(status); 30 | validateMockValueToHaveBeenCalled(render); 31 | }); 32 | 33 | test('should validate res.status & res.render is called when there is no user logged in', () => { 34 | req.user = null; 35 | 36 | renderIndexViewController(req, res); 37 | const { status, render } = res; 38 | 39 | validateMockValueToHaveBeenCalled(status); 40 | validateMockValueToHaveBeenCalled(render); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /api/controllers/helpers/__tests__/createNewUser.helper.test.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../../../database/services/modelServices/userServices'); 2 | jest.mock('../handleIfAsyncError.helper'); 3 | 4 | const { createNewUserHelper } = require('../index'); 5 | const { 6 | mockResponse, 7 | mockRequest, 8 | } = require('../../../../utils/test-utils/interceptors.utils'); 9 | const { 10 | validateMockValueToHaveBeenCalled, 11 | } = require('../../../../utils/test-utils/validators.utils'); 12 | const { 13 | createNewUserService, 14 | } = require('../../../../database/services/modelServices/userServices'); 15 | const { handleIfAsyncErrorHelper } = require('../handleIfAsyncError.helper'); 16 | const { fakeUserData } = require('../../../../database/fixtures'); 17 | 18 | describe('creatNewUser Test Suite', () => { 19 | afterEach(() => { 20 | jest.clearAllMocks(); 21 | }); 22 | 23 | test('should validate incoming data follows a successful flow', async () => { 24 | const req = mockRequest(); 25 | const res = mockResponse(); 26 | 27 | req.body = fakeUserData; 28 | 29 | await createNewUserHelper(req, res); 30 | 31 | validateMockValueToHaveBeenCalled(createNewUserService); 32 | validateMockValueToHaveBeenCalled(handleIfAsyncErrorHelper); 33 | 34 | const { login } = req; 35 | validateMockValueToHaveBeenCalled(login); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /api/controllers/helpers/__tests__/handleIfAsyncError.helper.test.js: -------------------------------------------------------------------------------- 1 | jest.mock('../getDuplicateErrorMessage.helper'); 2 | const { handleIfAsyncErrorHelper } = require('../handleIfAsyncError.helper'); 3 | const { 4 | getDuplicateErrorMessageHelper, 5 | } = require('../getDuplicateErrorMessage.helper'); 6 | const { 7 | validateNotEmpty, 8 | validateObjectMatch, 9 | validateMockValueToHaveBeenCalled, 10 | } = require('../../../../utils/test-utils/validators.utils'); 11 | 12 | const { fakeUserData } = require('../../../../database/fixtures'); 13 | 14 | describe('Handle Async Error Helper Test Suite', () => { 15 | test('should validate incoming object as Error with duplicate code', () => { 16 | function DummyError() { 17 | this.name = 'DummyMongoError'; 18 | this.code = 11000; 19 | } 20 | DummyError.prototype = Error.prototype; 21 | 22 | try { 23 | throw new DummyError(); 24 | } catch (error) { 25 | validateNotEmpty(error); 26 | 27 | handleIfAsyncErrorHelper(error); 28 | 29 | validateMockValueToHaveBeenCalled(getDuplicateErrorMessageHelper); 30 | } 31 | }); 32 | 33 | test('should validate incoming object with user data', () => { 34 | const results = handleIfAsyncErrorHelper(fakeUserData); 35 | 36 | validateNotEmpty(results); 37 | validateObjectMatch(results, fakeUserData); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /api/controllers/helpers/__tests__/handleUpdatedPassword.helper.test.js: -------------------------------------------------------------------------------- 1 | jest.mock('../render500Error.helper'); 2 | const { handleUpdatedPasswordHelper } = require('../index'); 3 | const { render500ErrorHelper } = require('../render500Error.helper'); 4 | const { 5 | mockResponse, 6 | } = require('../../../../utils/test-utils/interceptors.utils'); 7 | 8 | const { 9 | validateEquality, 10 | validateMockValueToHaveBeenCalled, 11 | } = require('../../../../utils/test-utils/validators.utils'); 12 | 13 | let res; 14 | 15 | describe('handleUpdatedPassword Helper Test Suite', () => { 16 | beforeEach(() => { 17 | res = mockResponse(); 18 | }); 19 | 20 | afterEach(() => { 21 | jest.clearAllMocks(); 22 | }); 23 | 24 | test('should validate render500ErrorHelper to have been called', async () => { 25 | const dummyPassword = new Error(); 26 | 27 | await handleUpdatedPasswordHelper(dummyPassword, res); 28 | 29 | validateMockValueToHaveBeenCalled(render500ErrorHelper); 30 | }); 31 | 32 | test('should validate new password is hashed correctly', async () => { 33 | const dummyPwd = 'hello'; 34 | 35 | const results = await handleUpdatedPasswordHelper(dummyPwd, res); 36 | 37 | const bcryptRegEx = /^\$2[ayb]\$.{56}$/gm; 38 | const isHashMatch = bcryptRegEx.test(results); 39 | 40 | validateEquality(isHashMatch, true); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /api/controllers/helpers/__tests__/setUserInSessionAndLogin.helper.test.js: -------------------------------------------------------------------------------- 1 | const { 2 | setUserInSessionAndLoginHelper, 3 | } = require('../setUserInSessionAndLogin.helper'); 4 | const { 5 | mockResponse, 6 | mockRequest, 7 | } = require('../../../../utils/test-utils/interceptors.utils'); 8 | const { 9 | validateNotEmpty, 10 | validateObjectMatch, 11 | } = require('../../../../utils/test-utils/validators.utils'); 12 | const { fakeUserData } = require('../../../../database/fixtures'); 13 | 14 | describe('setUserInSessionAndLoginHelper Test Suite', () => { 15 | afterEach(() => { 16 | jest.clearAllMocks(); 17 | }); 18 | 19 | test('should ', () => { 20 | const req = mockRequest(); 21 | const res = mockResponse(); 22 | 23 | req.login = (handledResults, callback) => { 24 | callback(handledResults); 25 | }; 26 | 27 | req.flash = (messageType, message) => ({ 28 | messageType, 29 | message, 30 | }); 31 | 32 | setUserInSessionAndLoginHelper(req, fakeUserData, res); 33 | 34 | const expectedFlashObject = { 35 | messageType: 'success_msg', 36 | message: 'New User Added', 37 | }; 38 | const { flash } = req; 39 | 40 | const flashResults = flash('success_msg', 'New User Added'); 41 | 42 | validateNotEmpty(flashResults); 43 | validateObjectMatch(flashResults, expectedFlashObject); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /api/controllers/instructorsControllers/__tests__/renderBeInstructor.controller.test.js: -------------------------------------------------------------------------------- 1 | const { renderBeInstructorController } = require('../index'); 2 | const { 3 | mockRequest, 4 | mockResponse, 5 | } = require('../../../../utils/test-utils/interceptors.utils'); 6 | const { 7 | validateMockValueToHaveBeenCalled, 8 | } = require('../../../../utils/test-utils/validators.utils'); 9 | 10 | let req; 11 | let res; 12 | 13 | describe('renderBeInstructor Controller Test Suite', () => { 14 | beforeEach(() => { 15 | req = mockRequest(); 16 | res = mockResponse(); 17 | }); 18 | 19 | afterEach(() => { 20 | jest.clearAllMocks(); 21 | }); 22 | 23 | test('should validate res.status & res.render is called when role is student', () => { 24 | req.user = { _id: expect.anything(), role: 'student', local: jest.fn() }; 25 | 26 | renderBeInstructorController(req, res); 27 | 28 | const { status, render } = res; 29 | validateMockValueToHaveBeenCalled(status); 30 | validateMockValueToHaveBeenCalled(render); 31 | }); 32 | 33 | test('should validate res.redirect is called when role is instructor', () => { 34 | req.user = { _id: expect.anything(), role: 'instructor', local: jest.fn() }; 35 | 36 | renderBeInstructorController(req, res); 37 | 38 | const { redirect } = res; 39 | validateMockValueToHaveBeenCalled(redirect); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /loaders/__tests__/express.loader.test.js: -------------------------------------------------------------------------------- 1 | const app = require('../express.loader'); 2 | const { 3 | validateNotEmpty, 4 | validateArrayLength, 5 | validateStringEquality, 6 | validateArrayContaining, 7 | } = require('../../utils/test-utils/validators.utils'); 8 | 9 | describe('Express Loaders Test Suite', () => { 10 | test(`should validate that loaded object isn't empty`, () => { 11 | validateNotEmpty(app); 12 | }); 13 | 14 | test('should validate the view engine to be hbs', () => { 15 | const { settings } = app.locals; 16 | const viewEngine = settings['view engine']; 17 | 18 | validateNotEmpty(viewEngine); 19 | validateStringEquality(viewEngine, 'hbs'); 20 | }); 21 | 22 | test('should validate middleware set to router', () => { 23 | const { _router } = app; 24 | const { stack } = _router; 25 | const receivedStackLayerNames = stack.map((element) => element.name); 26 | const expectedStackLayerNames = [ 27 | 'query', 28 | 'expressInit', 29 | 'logger', 30 | 'cookieParser', 31 | 'jsonParser', 32 | 'urlencodedParser', 33 | 'serveStatic', 34 | 'session', 35 | 'initialize', 36 | 'authenticate', 37 | 'favicon', 38 | ]; 39 | 40 | validateNotEmpty(stack); 41 | validateArrayLength(stack, 13); 42 | validateArrayContaining(receivedStackLayerNames, expectedStackLayerNames); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /api/controllers/helpers/__tests__/checkCurrentUserRelationToCourse.helper.test.js: -------------------------------------------------------------------------------- 1 | const { checkCurrentUserRelationToCourseHelper } = require('../index'); 2 | const { 3 | validateEquality, 4 | } = require('../../../../utils/test-utils/validators.utils'); 5 | 6 | let dummyInstructorsList; 7 | let dummyStudentsList; 8 | 9 | const dummyInstructorOneId = 'fedfe6ed'; 10 | const dummyInstructorTwoId = 'f7e52938'; 11 | 12 | const dummyStudentOneId = '94aa6839'; 13 | const dummyStudentTwoId = 'd30320f3'; 14 | const dummyStudentThreeId = '9c622219'; 15 | 16 | describe('checkCurrentUserRelationToCourse Helper Test Suite', () => { 17 | beforeEach(() => { 18 | dummyInstructorsList = [ 19 | { _id: dummyInstructorOneId }, 20 | { _id: dummyInstructorTwoId }, 21 | ]; 22 | dummyStudentsList = [ 23 | { _id: dummyStudentOneId }, 24 | { _id: dummyStudentTwoId }, 25 | { _id: dummyStudentThreeId }, 26 | ]; 27 | }); 28 | 29 | test('should validate current user as an instructor', () => { 30 | const dummyCurrentUserId = dummyInstructorTwoId; 31 | 32 | const { 33 | isCurrentUserInStudentList, 34 | isCurrentUserTheCourseInstructor, 35 | } = checkCurrentUserRelationToCourseHelper( 36 | dummyInstructorsList, 37 | dummyCurrentUserId, 38 | dummyStudentsList 39 | ); 40 | 41 | validateEquality(isCurrentUserInStudentList, false); 42 | validateEquality(isCurrentUserTheCourseInstructor, true); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /database/services/modelServices/courseServices/__tests__/createNewCourse.service.test.js: -------------------------------------------------------------------------------- 1 | const { createNewCourseService } = require('../index'); 2 | const { fakeCourseData } = require('../../../../fixtures'); 3 | const { 4 | dbConnect, 5 | dbDisconnect, 6 | } = require('../../../../../utils/test-utils/dbHandler.utils'); 7 | const { 8 | validateNotEmpty, 9 | validateInstanceOf, 10 | validateStringEquality, 11 | } = require('../../../../../utils/test-utils/validators.utils'); 12 | 13 | describe('createNewCourse Service Test Suite', () => { 14 | beforeEach(async () => dbConnect()); 15 | afterEach(async () => dbDisconnect()); 16 | 17 | test('should validate returning error while saving course', async () => { 18 | const { title, description } = fakeCourseData; 19 | 20 | const newCourse = await createNewCourseService(title, description); 21 | 22 | validateNotEmpty(newCourse); 23 | 24 | const newCourseDuplicate = await createNewCourseService(title, description); 25 | 26 | validateInstanceOf(newCourseDuplicate, Error); 27 | }); 28 | 29 | test('should validate successfully saving course', async () => { 30 | const { title, description } = fakeCourseData; 31 | 32 | const newCourse = await createNewCourseService(title, description); 33 | 34 | validateNotEmpty(newCourse); 35 | validateStringEquality(newCourse.title, fakeCourseData.title); 36 | validateStringEquality(newCourse.description, fakeCourseData.description); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /api/controllers/coursesControllers/__tests__/renderAllCourses.controller.test.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../helpers'); 2 | jest.mock('../../../../database/services/modelServices/courseServices'); 3 | 4 | const { render500ErrorHelper } = require('../../helpers'); 5 | const { renderAllCoursesController } = require('../index'); 6 | const { 7 | findAllCoursesService, 8 | } = require('../../../../database/services/modelServices/courseServices'); 9 | const { 10 | setupReqRes, 11 | clearMocks, 12 | } = require('../../../../utils/test-utils/courseControllerDeps'); 13 | const { 14 | validateMockValueToHaveBeenCalled, 15 | } = require('../../../../utils/test-utils/validators.utils'); 16 | 17 | let req; 18 | let res; 19 | 20 | beforeEach(() => { 21 | const { request, response } = setupReqRes(); 22 | req = request; 23 | res = response; 24 | }); 25 | 26 | afterEach(() => { 27 | clearMocks(); 28 | }); 29 | 30 | describe('renderAllCourses Controller Test Suite', () => { 31 | test('should validate render500ErrorHelper is called', async () => { 32 | findAllCoursesService.mockReturnValueOnce(new Error()); 33 | 34 | await renderAllCoursesController(req, res); 35 | 36 | validateMockValueToHaveBeenCalled(render500ErrorHelper); 37 | }); 38 | 39 | test('should validate res.status & res.render is called', async () => { 40 | await renderAllCoursesController(req, res); 41 | 42 | const { status, render } = res; 43 | 44 | validateMockValueToHaveBeenCalled(status); 45 | validateMockValueToHaveBeenCalled(render); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /.github/workflows/tests-coverage.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [ 'lts/*', 'current' ] 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | 25 | - name: Create .env file 26 | run: | 27 | touch .env 28 | echo DUMMY_PASSWORD=${{ secrets.DUMMY_PASSWORD }} >> .env 29 | echo DUMMY_EDIT_PASSWORD_WEAK=${{ secrets.DUMMY_EDIT_PASSWORD_WEAK }} >> .env 30 | echo DUMMY_EDIT_PASSWORD_STRONG=${{ secrets.DUMMY_EDIT_PASSWORD_STRONG }} >> .env 31 | echo GOOGLE_CLIENT_ID=${{ secrets.GOOGLE_CLIENT_ID }} >> .env 32 | echo GOOGLE_CLIENT_SECRET=${{ secrets.GOOGLE_CLIENT_SECRET }} >> .env 33 | echo SESSION_SECRET=${{ secrets.SESSION_SECRET }} >> .env 34 | echo CLOUDINARY_KEY=${{ secrets.CLOUDINARY_KEY }} >> .env 35 | echo CLOUDINARY_NAME=${{ secrets.CLOUDINARY_NAME }} >> .env 36 | echo CLOUDINARY_SECRET=${{ secrets.CLOUDINARY_SECRET }} >> .env 37 | 38 | - run: npm ci 39 | - run: npm run build --if-present 40 | - run: npm run coverage 41 | env: 42 | CI: true 43 | 44 | - name: Collect coverage to Codecov 45 | uses: codecov/codecov-action@v5.5.1 46 | with: 47 | token: ${{ secrets.CODECOV_TOKEN }} 48 | -------------------------------------------------------------------------------- /api/controllers/usersControllers/updateUserProfile.controller.js: -------------------------------------------------------------------------------- 1 | const { 2 | render500ErrorHelper, 3 | handleUpdatedPasswordHelper, 4 | } = require('../helpers'); 5 | const { 6 | updateUserProfileDataService, 7 | } = require('../../../database/services/modelServices/userServices'); 8 | 9 | const strongPasswordRegex = /(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{6,}/; 10 | 11 | exports.updateUserProfileController = async (req, res) => { 12 | const { local } = req.user; 13 | const { id } = req.params; 14 | const { password, password2, existingImage } = req.body; 15 | 16 | if (password !== password2) { 17 | req.flash('error_msg', `Passwords do not match`); 18 | res.redirect(302, `/profile/${id}/edit`); 19 | return; 20 | } 21 | 22 | if (password && !strongPasswordRegex.test(password)) { 23 | req.flash( 24 | 'error_msg', 25 | 'Password needs to have at least 6 chars and must contain at least one number, one lowercase and one uppercase letter.' 26 | ); 27 | res.redirect(302, `/profile/${id}/edit`); 28 | return; 29 | } 30 | 31 | const userPassword = password 32 | ? await handleUpdatedPasswordHelper(password, res) 33 | : local.password; 34 | 35 | const profilePictureUrl = req.file ? req.file.path : existingImage; 36 | 37 | const isUpdatedUserProfile = await updateUserProfileDataService( 38 | id, 39 | req.body, 40 | userPassword, 41 | profilePictureUrl 42 | ); 43 | 44 | if (isUpdatedUserProfile instanceof Error) { 45 | render500ErrorHelper(res); 46 | return; 47 | } 48 | 49 | res.redirect(302, '/profile'); 50 | }; 51 | -------------------------------------------------------------------------------- /api/controllers/coursesControllers/__tests__/deleteTaughtCourse.controller.test.js: -------------------------------------------------------------------------------- 1 | const { deleteTaughtCourseController } = require('../index'); 2 | const { render500ErrorHelper } = require('../../helpers'); 3 | 4 | const { 5 | validateMockValueToHaveBeenCalled, 6 | } = require('../../../../utils/test-utils/validators.utils'); 7 | const { 8 | setupReqRes, 9 | clearMocks, 10 | } = require('../../../../utils/test-utils/courseControllerDeps'); 11 | const { 12 | deleteCourseService, 13 | } = require('../../../../database/services/modelServices/courseServices'); 14 | 15 | jest.mock('../../helpers'); 16 | jest.mock('../../../../database/services/modelServices/courseServices'); 17 | 18 | let req; 19 | let res; 20 | 21 | describe('deleteTaughtCourse Controller Test Suite', () => { 22 | beforeEach(() => { 23 | const { request, response } = setupReqRes(); 24 | 25 | req = request; 26 | res = response; 27 | }); 28 | 29 | afterEach(() => { 30 | clearMocks(); 31 | }); 32 | 33 | test('should validate render500ErrorHelper is called', async () => { 34 | deleteCourseService.mockReturnValueOnce(new Error()); 35 | 36 | await deleteTaughtCourseController(req, res); 37 | 38 | validateMockValueToHaveBeenCalled(render500ErrorHelper); 39 | }); 40 | 41 | test('should validate res.status & res.render is called', async () => { 42 | await deleteTaughtCourseController(req, res); 43 | 44 | const { flash } = req; 45 | const { redirect } = res; 46 | 47 | validateMockValueToHaveBeenCalled(flash); 48 | validateMockValueToHaveBeenCalled(redirect); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /database/services/modelServices/userServices/__tests__/updateUserRole.service.test.js: -------------------------------------------------------------------------------- 1 | const { updateUserRoleService, createNewUserService } = require('../index'); 2 | const { fakeUserData, fakeIdFormatData } = require('../../../../fixtures'); 3 | const { 4 | dbConnect, 5 | dbDisconnect, 6 | } = require('../../../../../utils/test-utils/dbHandler.utils'); 7 | const { 8 | validateNotEmpty, 9 | validateInstanceOf, 10 | validateStringEquality, 11 | } = require('../../../../../utils/test-utils/validators.utils'); 12 | 13 | let validUser; 14 | 15 | describe('updateUserRole Service Test Suite', () => { 16 | beforeEach(async () => { 17 | await dbConnect(); 18 | 19 | fakeUserData.role = 'student'; 20 | 21 | validUser = await createNewUserService({ 22 | local: fakeUserData, 23 | role: fakeUserData.role, 24 | }); 25 | }); 26 | 27 | afterEach(async () => dbDisconnect()); 28 | 29 | test('should validate an Error for incorrect id format', async () => { 30 | const { incorrectFormat } = fakeIdFormatData; 31 | 32 | const results = await updateUserRoleService(incorrectFormat); 33 | 34 | validateInstanceOf(results, Error); 35 | }); 36 | 37 | test('should validate updating role from student to instructor successfully', async () => { 38 | validateNotEmpty(validUser); 39 | validateStringEquality(validUser.role, 'student'); 40 | 41 | const { _id } = validUser; 42 | const updatedUserResults = await updateUserRoleService(_id); 43 | 44 | validateNotEmpty(updatedUserResults); 45 | validateStringEquality(updatedUserResults.role, 'instructor'); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /database/services/modelServices/lessonServices/__tests__/createNewLesson.service.test.js: -------------------------------------------------------------------------------- 1 | const { createNewLessonService } = require('../index'); 2 | const { fakeLessonData } = require('../../../../fixtures'); 3 | const { 4 | dbConnect, 5 | dbDisconnect, 6 | } = require('../../../../../utils/test-utils/dbHandler.utils'); 7 | const { 8 | validateNotEmpty, 9 | validateInstanceOf, 10 | validateStringEquality, 11 | } = require('../../../../../utils/test-utils/validators.utils'); 12 | 13 | describe('createNewLesson Service Test Suite', () => { 14 | beforeEach(async () => dbConnect()); 15 | afterEach(async () => dbDisconnect()); 16 | 17 | test('should validate returning error while saving lesson', async () => { 18 | const { topic, content, videoUrl } = fakeLessonData; 19 | 20 | const newLesson = await createNewLessonService(topic, content, videoUrl); 21 | 22 | validateNotEmpty(newLesson); 23 | 24 | const newLessonDuplicate = await createNewLessonService( 25 | topic, 26 | content, 27 | videoUrl 28 | ); 29 | 30 | validateInstanceOf(newLessonDuplicate, Error); 31 | }); 32 | 33 | test('should validate successfully saving lesson', async () => { 34 | const { topic, content, videoUrl } = fakeLessonData; 35 | 36 | const newLesson = await createNewLessonService(topic, content, videoUrl); 37 | 38 | validateNotEmpty(newLesson); 39 | validateStringEquality(newLesson.topic, fakeLessonData.topic); 40 | validateStringEquality(newLesson.content, fakeLessonData.content); 41 | validateStringEquality(newLesson.videoUrl, fakeLessonData.videoUrl); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /views/users/common/my-courses.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{#if success_msg}} 3 | {{success_msg}} 4 | {{/if}} 5 | 6 | {{#if user.coursesTaught.length}} 7 |
8 |

Courses I'm Teaching

9 | 20 |
21 | {{else}} 22 | {{#ifEqualsHelper user.role 'instructor'}} 23 |
24 |

Courses I'm Teaching

25 |

You aren't teaching any courses yet!

26 |

Create New Course

27 |
28 | {{/ifEqualsHelper}} 29 | {{/if}} 30 | 31 | 32 | 33 |
34 |

Courses I'm Taking

35 | {{#if user.coursesLearned.length}} 36 | 46 | {{else}} 47 |
48 |

You aren't taking any courses yet!

49 |

Register To A Course

50 |
51 | {{/if}} 52 |
53 |
-------------------------------------------------------------------------------- /api/controllers/coursesControllers/index.js: -------------------------------------------------------------------------------- 1 | const { createNewCourseController } = require('./createNewCourse.controller'); 2 | const { renderMyCoursesController } = require('./renderMyCourses.controller'); 3 | const { renderAllCoursesController } = require('./renderAllCourses.controller'); 4 | const { registerToCourseController } = require('./registerToCourse.controller'); 5 | const { 6 | renderCourseDetailsController, 7 | } = require('./renderCourseDetails.controller'); 8 | const { 9 | renderTaughtCourseController, 10 | } = require('./renderTaughtCourse.controller'); 11 | const { 12 | renderStudentListController, 13 | } = require('./renderStudentList.controller'); 14 | const { 15 | renderCreateNewCourseController, 16 | } = require('./renderCreateNewCourse.controller'); 17 | const { 18 | renderEditTaughtCourseController, 19 | } = require('./renderEditTaughtCourse.controller'); 20 | const { 21 | updateTaughtCourseController, 22 | } = require('./updateTaughtCourse.controller'); 23 | const { 24 | deleteTaughtCourseController, 25 | } = require('./deleteTaughtCourse.controller'); 26 | const { 27 | renderLearnedCourseController, 28 | } = require('./renderLearnedCourse.controller'); 29 | 30 | module.exports = { 31 | createNewCourseController, 32 | renderMyCoursesController, 33 | renderAllCoursesController, 34 | registerToCourseController, 35 | renderStudentListController, 36 | deleteTaughtCourseController, 37 | updateTaughtCourseController, 38 | renderTaughtCourseController, 39 | renderCourseDetailsController, 40 | renderLearnedCourseController, 41 | renderCreateNewCourseController, 42 | renderEditTaughtCourseController, 43 | }; 44 | -------------------------------------------------------------------------------- /database/services/modelServices/courseServices/__tests__/deleteCourse.service.test.js: -------------------------------------------------------------------------------- 1 | const { 2 | deleteCourseService, 3 | findAllCoursesService, 4 | createNewCourseService, 5 | } = require('../index'); 6 | const { fakeCourseData, fakeIdFormatData } = require('../../../../fixtures'); 7 | const { 8 | validateNotEmpty, 9 | validateInstanceOf, 10 | validateArrayLength, 11 | } = require('../../../../../utils/test-utils/validators.utils'); 12 | const { 13 | dbConnect, 14 | dbDisconnect, 15 | } = require('../../../../../utils/test-utils/dbHandler.utils'); 16 | 17 | let newCourse; 18 | 19 | describe('deleteCourse Service Test Suite ', () => { 20 | beforeEach(async () => { 21 | await dbConnect(); 22 | 23 | const { title, description } = fakeCourseData; 24 | 25 | newCourse = await createNewCourseService(title, description); 26 | 27 | validateNotEmpty(newCourse); 28 | }); 29 | 30 | afterEach(async () => dbDisconnect()); 31 | 32 | test('should validate error when wrong incorrect id is used', async () => { 33 | const { incorrectFormat } = fakeIdFormatData; 34 | 35 | const results = await deleteCourseService(incorrectFormat); 36 | 37 | validateInstanceOf(results, Error); 38 | }); 39 | 40 | test('should validate successfully deleting course', async () => { 41 | const results = await findAllCoursesService(); 42 | 43 | validateNotEmpty(results); 44 | validateArrayLength(results, 1); 45 | 46 | const { _id } = newCourse; 47 | 48 | await deleteCourseService(_id); 49 | 50 | const resultsAfterDelete = await findAllCoursesService(); 51 | 52 | validateArrayLength(resultsAfterDelete, 0); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /api/controllers/instructorsControllers/__tests__/changeRoleToInstructor.controller.test.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../../../database/services/modelServices/userServices'); 2 | const { changeRoleToInstructor } = require('../index'); 3 | const { 4 | updateUserRoleService, 5 | } = require('../../../../database/services/modelServices/userServices'); 6 | const { 7 | validateMockValueToHaveBeenCalled, 8 | } = require('../../../../utils/test-utils/validators.utils'); 9 | const { 10 | mockRequest, 11 | mockResponse, 12 | } = require('../../../../utils/test-utils/interceptors.utils'); 13 | 14 | let req; 15 | let res; 16 | 17 | describe('changeRoleToInstructor Controller Test Suite', () => { 18 | beforeEach(() => { 19 | req = mockRequest(); 20 | res = mockResponse(); 21 | }); 22 | 23 | afterEach(() => { 24 | jest.clearAllMocks(); 25 | }); 26 | 27 | test('should validate res.redirect called ', async () => { 28 | req.user = { _id: expect.anything() }; 29 | await changeRoleToInstructor(req, res); 30 | 31 | validateMockValueToHaveBeenCalled(updateUserRoleService); 32 | 33 | const { redirect } = res; 34 | validateMockValueToHaveBeenCalled(redirect); 35 | }); 36 | 37 | test('should validate res.status & res.render called ', async () => { 38 | req.user = { _id: expect.anything() }; 39 | 40 | updateUserRoleService.mockImplementationOnce(() => new Error()); 41 | 42 | await changeRoleToInstructor(req, res); 43 | 44 | validateMockValueToHaveBeenCalled(updateUserRoleService); 45 | 46 | const { status, render } = res; 47 | validateMockValueToHaveBeenCalled(status); 48 | validateMockValueToHaveBeenCalled(render); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /configs/cloudinary/__tests__/index.test.js: -------------------------------------------------------------------------------- 1 | const { CloudinaryStorage } = require('multer-storage-cloudinary'); 2 | const fileUploader = require('../index'); 3 | const { 4 | validateTypeOf, 5 | validateEquality, 6 | validateNotEmpty, 7 | validateInstanceOf, 8 | validateArrayLength, 9 | validateStringEquality, 10 | validateArrayContaining, 11 | } = require('../../../utils/test-utils/validators.utils'); 12 | 13 | describe('Cloudinary uploader Test Suite', () => { 14 | test('should validate fileUploader object to not be empty ', () => { 15 | validateNotEmpty(fileUploader); 16 | }); 17 | 18 | test('should validate fileUploader is typeof object', () => { 19 | validateTypeOf(fileUploader, 'object'); 20 | }); 21 | 22 | test('should validate fileUploader to use CloudinaryStorage as storage', () => { 23 | const { storage } = fileUploader; 24 | validateInstanceOf(storage, CloudinaryStorage); 25 | }); 26 | 27 | test('should validate fileUploader storage params', () => { 28 | const { params } = fileUploader.storage; 29 | const expectedParams = { 30 | folder: 'onLearn', 31 | allowedFormats: ['jpg', 'png', 'pdf'], 32 | use_filename: true, 33 | }; 34 | 35 | validateStringEquality(params.folder, expectedParams.folder); 36 | validateArrayLength(params.allowedFormats, 3); 37 | validateArrayContaining( 38 | params.allowedFormats, 39 | expectedParams.allowedFormats 40 | ); 41 | validateEquality(params.use_filename, expectedParams.use_filename); 42 | }); 43 | 44 | test('should validate fileUploader limits.fileSize to be 8MB', () => { 45 | const { limits } = fileUploader; 46 | 47 | validateEquality(limits.fileSize, 8000000); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /database/models/__tests__/lesson.model.test.js: -------------------------------------------------------------------------------- 1 | const Lesson = require('../lesson.model'); 2 | const Course = require('../course.model'); 3 | const User = require('../user.model'); 4 | 5 | const { 6 | fakeUserData, 7 | fakeCourseData, 8 | fakeLessonData, 9 | } = require('../../fixtures'); 10 | const { 11 | validateNotEmpty, 12 | validateObjectMatch, 13 | validateArrayLength, 14 | validateStringEquality, 15 | } = require('../../../utils/test-utils/validators.utils'); 16 | const { 17 | dbConnect, 18 | dbDisconnect, 19 | } = require('../../../utils/test-utils/dbHandler.utils'); 20 | 21 | let validCourse; 22 | let validLesson; 23 | let validInstructor; 24 | 25 | describe('Lesson Model Test Suite', () => { 26 | beforeAll(async () => { 27 | await dbConnect(); 28 | 29 | validInstructor = await User.create({ 30 | local: fakeUserData, 31 | role: fakeUserData.role, 32 | }); 33 | 34 | const { _id: instructorId } = validInstructor; 35 | 36 | fakeCourseData.instructors.push(instructorId); 37 | 38 | validCourse = await Course.create(fakeCourseData); 39 | 40 | validLesson = await Lesson.create(fakeLessonData); 41 | }); 42 | 43 | afterAll(async () => dbDisconnect()); 44 | 45 | test('should validate Lesson created & saved', () => { 46 | const { _id: lessonId } = validLesson; 47 | 48 | validCourse.lessons.push(lessonId); 49 | 50 | validateNotEmpty(validLesson); 51 | validateObjectMatch(validLesson, fakeLessonData); 52 | 53 | validateNotEmpty(validCourse.lessons); 54 | const { lessons } = validCourse; 55 | 56 | validateNotEmpty(lessons); 57 | 58 | validateArrayLength(lessons, 1); 59 | validateStringEquality(lessons[0], lessonId); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /database/services/modelServices/courseServices/__tests__/updateCourse.service.test.js: -------------------------------------------------------------------------------- 1 | const { updateCourseService, createNewCourseService } = require('../index'); 2 | const { fakeCourseData, fakeIdFormatData } = require('../../../../fixtures'); 3 | const { 4 | dbConnect, 5 | dbDisconnect, 6 | } = require('../../../../../utils/test-utils/dbHandler.utils'); 7 | const { 8 | validateNotEmpty, 9 | validateInstanceOf, 10 | validateStringEquality, 11 | } = require('../../../../../utils/test-utils/validators.utils'); 12 | 13 | let newCourse; 14 | 15 | describe('updateCourse Service Test Suite ', () => { 16 | beforeEach(async () => { 17 | await dbConnect(); 18 | 19 | const { title, description } = fakeCourseData; 20 | 21 | newCourse = await createNewCourseService(title, description); 22 | 23 | validateNotEmpty(newCourse); 24 | }); 25 | 26 | afterEach(async () => dbDisconnect()); 27 | 28 | test('should validate error when wrong incorrect id is used', async () => { 29 | const { incorrectFormat } = fakeIdFormatData; 30 | const { title, description } = newCourse; 31 | 32 | const results = await updateCourseService( 33 | incorrectFormat, 34 | title, 35 | description 36 | ); 37 | 38 | validateInstanceOf(results, Error); 39 | }); 40 | 41 | test('should validate successfully updating course', async () => { 42 | const title = 'New Dummy Title'; 43 | const description = 'New Dummy Description'; 44 | const { _id } = newCourse; 45 | 46 | const results = await updateCourseService(_id, title, description); 47 | 48 | validateNotEmpty(results); 49 | validateStringEquality(results.title, title); 50 | validateStringEquality(results.description, description); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /database/services/modelServices/lessonServices/__tests__/findAllLessons.service.test.js: -------------------------------------------------------------------------------- 1 | const { createNewCourseService } = require('../../courseServices'); 2 | const { findAllLessonsService, createNewLessonService } = require('../index'); 3 | const { fakeCourseData, fakeLessonData } = require('../../../../fixtures'); 4 | const { 5 | dbConnect, 6 | dbDisconnect, 7 | } = require('../../../../../utils/test-utils/dbHandler.utils'); 8 | const { 9 | validateNotEmpty, 10 | validateArrayLength, 11 | validateStringEquality, 12 | } = require('../../../../../utils/test-utils/validators.utils'); 13 | 14 | let newCourse; 15 | let newLesson; 16 | 17 | describe('findAllLessons Service Test Suite', () => { 18 | beforeEach(async () => { 19 | await dbConnect(); 20 | 21 | const { title, description } = fakeCourseData; 22 | newCourse = await createNewCourseService(title, description); 23 | }); 24 | 25 | afterEach(async () => dbDisconnect()); 26 | 27 | test('should validate successfully finding saved lesson', async () => { 28 | validateArrayLength(newCourse.lessons, 0); 29 | 30 | const { topic, content, videoUrl } = fakeLessonData; 31 | newLesson = await createNewLessonService(topic, content, videoUrl); 32 | 33 | newCourse.lessons.push(newLesson); 34 | const results = await findAllLessonsService(); 35 | 36 | validateNotEmpty(results); 37 | validateArrayLength(results, 1); 38 | 39 | const [expectedLessonData] = results; 40 | 41 | validateStringEquality(expectedLessonData.topic, fakeLessonData.topic); 42 | validateStringEquality(expectedLessonData.content, fakeLessonData.content); 43 | validateStringEquality( 44 | expectedLessonData.videoUrl, 45 | fakeLessonData.videoUrl 46 | ); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /database/services/modelServices/userServices/__tests__/createNewUser.service.test.js: -------------------------------------------------------------------------------- 1 | const { fakeUserData } = require('../../../../fixtures'); 2 | const { createNewUserService } = require('../index'); 3 | const { 4 | dbConnect, 5 | dbDisconnect, 6 | } = require('../../../../../utils/test-utils/dbHandler.utils'); 7 | const { 8 | validateNotEmpty, 9 | validateInstanceOf, 10 | validateStringEquality, 11 | } = require('../../../../../utils/test-utils/validators.utils'); 12 | 13 | describe('Create New User Test Suite', () => { 14 | beforeEach(async () => dbConnect()); 15 | afterEach(async () => dbDisconnect()); 16 | 17 | test('should validate an Error for incorrect id format', async () => { 18 | const results = await createNewUserService({ role: 123 }); 19 | 20 | validateInstanceOf(results, Error); 21 | }); 22 | 23 | test('should validate new user created from incoming data', async () => { 24 | const newUser = await createNewUserService(fakeUserData); 25 | 26 | validateNotEmpty(newUser); 27 | 28 | const { role, local } = newUser; 29 | 30 | validateNotEmpty(role); 31 | validateStringEquality(role, fakeUserData.role); 32 | 33 | const { email, firstName, lastName, password, username } = local; 34 | 35 | validateNotEmpty(email); 36 | validateStringEquality(email, fakeUserData.email); 37 | 38 | validateNotEmpty(firstName); 39 | validateStringEquality(firstName, fakeUserData.firstName); 40 | 41 | validateNotEmpty(lastName); 42 | validateStringEquality(lastName, fakeUserData.lastName); 43 | 44 | validateNotEmpty(password); 45 | validateStringEquality(password, expect.anything()); 46 | 47 | validateNotEmpty(username); 48 | validateStringEquality(username, fakeUserData.username); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /database/services/modelServices/userServices/__tests__/createNewGoogleUser.service.test.js: -------------------------------------------------------------------------------- 1 | const { createNewGoogleUserService } = require('../index'); 2 | const { 3 | dbConnect, 4 | dbDisconnect, 5 | } = require('../../../../../utils/test-utils/dbHandler.utils'); 6 | const { 7 | validateNotEmpty, 8 | validateStringEquality, 9 | validateMongoDuplicationError, 10 | } = require('../../../../../utils/test-utils/validators.utils'); 11 | 12 | const fakeGoogleProfileData = { 13 | id: '1a2b3c', 14 | name: { givenName: 'Google', familyName: 'User' }, 15 | displayName: 'DummyGoogleUser', 16 | emails: [{ value: 'duMmy@dummy.com' }], 17 | photos: [{ value: 'https://photo-goes-here.com' }], 18 | }; 19 | const fakeAccessToken = expect.anything(); 20 | 21 | describe('createNewGoogleUser Service Test Suite', () => { 22 | beforeEach(async () => dbConnect()); 23 | afterAll(async () => dbDisconnect()); 24 | 25 | test('should validate successfully adding Google user', async () => { 26 | const results = await createNewGoogleUserService( 27 | fakeGoogleProfileData, 28 | fakeAccessToken 29 | ); 30 | 31 | validateNotEmpty(results); 32 | 33 | const { profilePictureUrl, local, google } = results; 34 | 35 | validateStringEquality( 36 | profilePictureUrl, 37 | fakeGoogleProfileData.photos[0].value 38 | ); 39 | validateStringEquality(local.username, fakeGoogleProfileData.displayName); 40 | validateStringEquality(google.name, fakeGoogleProfileData.displayName); 41 | }); 42 | 43 | test('should validate error when saving Google user', async () => { 44 | try { 45 | await createNewGoogleUserService(fakeGoogleProfileData, fakeAccessToken); 46 | } catch (error) { 47 | const { name, code } = error; 48 | validateMongoDuplicationError(name, code); 49 | } 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /api/controllers/coursesControllers/__tests__/createNewCourse.controller.test.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../helpers'); 2 | jest.mock('../../../../database/services/modelServices/courseServices'); 3 | 4 | const { createNewCourseController } = require('../index'); 5 | const { render500ErrorHelper } = require('../../helpers'); 6 | const { 7 | validateMockValueToHaveBeenCalled, 8 | } = require('../../../../utils/test-utils/validators.utils'); 9 | const { 10 | mockRequest, 11 | mockResponse, 12 | } = require('../../../../utils/test-utils/interceptors.utils'); 13 | const { 14 | createNewCourseService, 15 | } = require('../../../../database/services/modelServices/courseServices'); 16 | 17 | let req; 18 | let res; 19 | 20 | describe('createNewCourse Controller Test Suite', () => { 21 | beforeEach(() => { 22 | req = mockRequest(); 23 | res = mockResponse(); 24 | 25 | req.user = { _id: expect.anything() }; 26 | req.body = { title: expect.anything(), description: expect.anything() }; 27 | }); 28 | 29 | afterEach(() => { 30 | jest.clearAllMocks(); 31 | }); 32 | 33 | test('should validate render500ErrorHelper is called ', async () => { 34 | createNewCourseService.mockReturnValueOnce(new Error()); 35 | 36 | await createNewCourseController(req, res); 37 | 38 | validateMockValueToHaveBeenCalled(createNewCourseService); 39 | validateMockValueToHaveBeenCalled(render500ErrorHelper); 40 | }); 41 | 42 | test('should validate res.redirect called ', async () => { 43 | createNewCourseService.mockImplementationOnce(() => ({ 44 | instructors: [], 45 | save: jest.fn(), 46 | })); 47 | 48 | await createNewCourseController(req, res); 49 | 50 | validateMockValueToHaveBeenCalled(createNewCourseService); 51 | 52 | const { redirect } = res; 53 | validateMockValueToHaveBeenCalled(redirect); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /api/controllers/coursesControllers/__tests__/registerToCourse.controller.test.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../helpers'); 2 | jest.mock('../../../../database/services/modelServices/courseServices'); 3 | 4 | const { registerToCourseController } = require('../index'); 5 | const { render500ErrorHelper } = require('../../helpers'); 6 | const { 7 | validateMockValueToHaveBeenCalled, 8 | } = require('../../../../utils/test-utils/validators.utils'); 9 | const { 10 | mockRequest, 11 | mockResponse, 12 | } = require('../../../../utils/test-utils/interceptors.utils'); 13 | const { 14 | findOneCourseService, 15 | } = require('../../../../database/services/modelServices/courseServices'); 16 | 17 | let req; 18 | let res; 19 | 20 | describe('registerToCourse Controller Test Suite', () => { 21 | beforeEach(() => { 22 | req = mockRequest(); 23 | res = mockResponse(); 24 | 25 | req.user = { _id: expect.anything() }; 26 | }); 27 | 28 | afterEach(() => { 29 | jest.clearAllMocks(); 30 | }); 31 | 32 | test('should validate render500ErrorHelper is called ', async () => { 33 | findOneCourseService.mockReturnValueOnce(new Error()); 34 | 35 | await registerToCourseController(req, res); 36 | 37 | validateMockValueToHaveBeenCalled(findOneCourseService); 38 | validateMockValueToHaveBeenCalled(render500ErrorHelper); 39 | }); 40 | 41 | test('should validate res.redirect called ', async () => { 42 | findOneCourseService.mockImplementationOnce(() => ({ 43 | students: [], 44 | save: jest.fn(), 45 | })); 46 | 47 | await registerToCourseController(req, res); 48 | 49 | validateMockValueToHaveBeenCalled(findOneCourseService); 50 | 51 | const { flash } = req; 52 | const { redirect } = res; 53 | 54 | validateMockValueToHaveBeenCalled(flash); 55 | validateMockValueToHaveBeenCalled(redirect); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /api/controllers/lessonsControllers/__tests__/renderEditLesson.controller.test.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../helpers'); 2 | jest.mock('../../../../database/services/modelServices/lessonServices'); 3 | 4 | const { renderEditLessonController } = require('../index'); 5 | const { 6 | findOneLessonService, 7 | } = require('../../../../database/services/modelServices/lessonServices'); 8 | const { 9 | validateMockValueToHaveBeenCalled, 10 | } = require('../../../../utils/test-utils/validators.utils'); 11 | const { 12 | setupReqRes, 13 | clearMocks, 14 | } = require('../../../../utils/test-utils/courseControllerDeps'); 15 | const { 16 | render500ErrorHelper, 17 | redirectNonExistentDataHelper, 18 | } = require('../../helpers'); 19 | 20 | let req; 21 | let res; 22 | 23 | describe('renderEditLesson Controller Test Suite', () => { 24 | beforeEach(() => { 25 | const { request, response } = setupReqRes(); 26 | 27 | req = request; 28 | res = response; 29 | }); 30 | 31 | afterEach(() => { 32 | clearMocks(); 33 | }); 34 | 35 | test('should validate render500ErrorHelper is called', async () => { 36 | findOneLessonService.mockReturnValueOnce(new Error()); 37 | 38 | await renderEditLessonController(req, res); 39 | 40 | validateMockValueToHaveBeenCalled(render500ErrorHelper); 41 | }); 42 | 43 | test('should validate redirectNonExistentDataHelper is called', async () => { 44 | findOneLessonService.mockReturnValueOnce(null); 45 | 46 | await renderEditLessonController(req, res); 47 | 48 | validateMockValueToHaveBeenCalled(redirectNonExistentDataHelper); 49 | }); 50 | 51 | test('should validate res.status & res.render is called', async () => { 52 | await renderEditLessonController(req, res); 53 | 54 | const { status, render } = res; 55 | 56 | validateMockValueToHaveBeenCalled(status); 57 | validateMockValueToHaveBeenCalled(render); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /api/controllers/coursesControllers/__tests__/renderLearnedCourse.controller.test.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../helpers'); 2 | jest.mock('../../../../database/services/modelServices/courseServices'); 3 | 4 | const { renderLearnedCourseController } = require('../index'); 5 | const { 6 | render500ErrorHelper, 7 | redirectNonExistentDataHelper, 8 | } = require('../../helpers'); 9 | const { 10 | validateMockValueToHaveBeenCalled, 11 | } = require('../../../../utils/test-utils/validators.utils'); 12 | const { 13 | findOneCourseService, 14 | } = require('../../../../database/services/modelServices/courseServices'); 15 | const { 16 | setupReqRes, 17 | clearMocks, 18 | } = require('../../../../utils/test-utils/courseControllerDeps'); 19 | 20 | let req; 21 | let res; 22 | 23 | describe('renderLearnedCourse Controller Test Suite', () => { 24 | beforeEach(() => { 25 | const { request, response } = setupReqRes(); 26 | req = request; 27 | res = response; 28 | }); 29 | 30 | afterEach(() => { 31 | clearMocks(); 32 | }); 33 | 34 | test('should validate render500ErrorHelper is called', async () => { 35 | findOneCourseService.mockReturnValueOnce(new Error()); 36 | 37 | await renderLearnedCourseController(req, res); 38 | 39 | validateMockValueToHaveBeenCalled(render500ErrorHelper); 40 | }); 41 | 42 | test('should validate redirectNonExistentDataHelper is called', async () => { 43 | findOneCourseService.mockReturnValueOnce(null); 44 | 45 | await renderLearnedCourseController(req, res); 46 | 47 | validateMockValueToHaveBeenCalled(redirectNonExistentDataHelper); 48 | }); 49 | 50 | test('should validate res.status & res.render is called', async () => { 51 | await renderLearnedCourseController(req, res); 52 | 53 | const { status, render } = res; 54 | 55 | validateMockValueToHaveBeenCalled(status); 56 | validateMockValueToHaveBeenCalled(render); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /api/controllers/coursesControllers/__tests__/renderStudentList.controller.test.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../helpers'); 2 | jest.mock('../../../../database/services/modelServices/courseServices'); 3 | 4 | const { 5 | setupReqRes, 6 | clearMocks, 7 | } = require('../../../../utils/test-utils/courseControllerDeps'); 8 | const { 9 | render500ErrorHelper, 10 | redirectNonExistentDataHelper, 11 | } = require('../../helpers'); 12 | const { 13 | findOneCourseService, 14 | } = require('../../../../database/services/modelServices/courseServices'); 15 | const { 16 | validateMockValueToHaveBeenCalled, 17 | } = require('../../../../utils/test-utils/validators.utils'); 18 | const { 19 | renderStudentListController, 20 | } = require('../renderStudentList.controller'); 21 | 22 | let req; 23 | let res; 24 | 25 | beforeEach(() => { 26 | const { request, response } = setupReqRes(); 27 | 28 | req = request; 29 | res = response; 30 | }); 31 | 32 | afterEach(() => { 33 | clearMocks(); 34 | }); 35 | 36 | describe('renderStudentList Controller Test Suite', () => { 37 | test('should validate render500ErrorHelper is called', async () => { 38 | findOneCourseService.mockReturnValueOnce(new Error()); 39 | 40 | await renderStudentListController(req, res); 41 | 42 | validateMockValueToHaveBeenCalled(render500ErrorHelper); 43 | }); 44 | 45 | test('should validate redirectNonExistentDataHelper is called', async () => { 46 | findOneCourseService.mockReturnValueOnce(null); 47 | 48 | await renderStudentListController(req, res); 49 | 50 | validateMockValueToHaveBeenCalled(redirectNonExistentDataHelper); 51 | }); 52 | 53 | test('should validate res.status & res.render is called', async () => { 54 | await renderStudentListController(req, res); 55 | 56 | const { status, render } = res; 57 | 58 | validateMockValueToHaveBeenCalled(status); 59 | validateMockValueToHaveBeenCalled(render); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /api/controllers/coursesControllers/__tests__/renderMyCourses.controller.test.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../helpers'); 2 | jest.mock('../../../../database/services/modelServices/courseServices'); 3 | 4 | const { renderMyCoursesController } = require('../index'); 5 | const { filterCoursesHelper, render500ErrorHelper } = require('../../helpers'); 6 | const { 7 | findAllCoursesService, 8 | } = require('../../../../database/services/modelServices/courseServices'); 9 | const { 10 | validateMockValueToHaveBeenCalled, 11 | } = require('../../../../utils/test-utils/validators.utils'); 12 | const { 13 | mockRequest, 14 | mockResponse, 15 | } = require('../../../../utils/test-utils/interceptors.utils'); 16 | 17 | let req; 18 | let res; 19 | 20 | describe('renderMyCourses Controller Test Suite', () => { 21 | beforeEach(() => { 22 | req = mockRequest(); 23 | res = mockResponse(); 24 | 25 | req.user = { 26 | _id: expect.anything(), 27 | local: expect.anything(), 28 | role: expect.anything(), 29 | }; 30 | }); 31 | 32 | afterEach(() => { 33 | jest.clearAllMocks(); 34 | }); 35 | 36 | test('should validate render500ErrorHelper is called', async () => { 37 | findAllCoursesService.mockReturnValueOnce(new Error()); 38 | 39 | await renderMyCoursesController(req, res); 40 | 41 | validateMockValueToHaveBeenCalled(render500ErrorHelper); 42 | }); 43 | 44 | test('should validate filterCoursesHelper, res.status & res.render is called', async () => { 45 | filterCoursesHelper.mockReturnValueOnce({ 46 | coursesTaught: expect.anything(), 47 | coursesLearned: expect.anything(), 48 | }); 49 | 50 | await renderMyCoursesController(req, res); 51 | 52 | validateMockValueToHaveBeenCalled(filterCoursesHelper); 53 | 54 | const { status, render } = res; 55 | 56 | validateMockValueToHaveBeenCalled(status); 57 | validateMockValueToHaveBeenCalled(render); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /loaders/express.loader.js: -------------------------------------------------------------------------------- 1 | const hbs = require('hbs'); 2 | const path = require('path'); 3 | const logger = require('morgan'); 4 | const express = require('express'); 5 | const passport = require('passport'); 6 | const flash = require('connect-flash'); 7 | const favicon = require('serve-favicon'); 8 | const session = require('express-session'); 9 | const cookieParser = require('cookie-parser'); 10 | const { sessSecret } = require('../configs'); 11 | const { localStrategy } = require('../passport/localStrategy'); 12 | const { googleAuthStrategy } = require('../passport/googleStrategy'); 13 | 14 | const app = express(); 15 | app.disable('x-powered-by'); 16 | 17 | hbs.registerHelper('ifEqualsHelper', (value1, value2, options) => 18 | value1 === value2 ? options.fn(this) : options.inverse(this) 19 | ); 20 | hbs.registerPartials(path.join(__dirname, '../views/partials')); 21 | app.set('views', path.join(__dirname, '../views')); 22 | app.set('view engine', 'hbs'); 23 | 24 | app.use(logger('dev')); 25 | app.use(cookieParser()); 26 | app.use(express.json()); 27 | app.use(express.urlencoded({ extended: false })); 28 | app.use(express.static(path.join(__dirname, '../public'))); 29 | app.use(favicon(path.join(__dirname, '../public', '/images/', 'favicon.ico'))); 30 | 31 | app.set('trust proxy', 1); 32 | app.use( 33 | session({ 34 | secret: sessSecret, 35 | resave: false, 36 | saveUninitialized: true, 37 | }) 38 | ); 39 | 40 | app.use(flash()); 41 | 42 | app.use(passport.initialize()); 43 | app.use(passport.session()); 44 | passport.use(localStrategy); 45 | passport.use(googleAuthStrategy); 46 | require('../passport/serializeSession'); 47 | 48 | // Global Variables - messages for the view 49 | app.use((req, res, next) => { 50 | res.locals.success_msg = req.flash('success_msg'); 51 | res.locals.error_msg = req.flash('error_msg'); 52 | res.locals.error = req.flash('error'); 53 | next(); 54 | }); 55 | 56 | module.exports = app; 57 | -------------------------------------------------------------------------------- /api/controllers/coursesControllers/__tests__/renderEditTaughtCourse.controller.test.js: -------------------------------------------------------------------------------- 1 | const { renderEditTaughtCourseController } = require('../index'); 2 | const { 3 | findOneCourseService, 4 | } = require('../../../../database/services/modelServices/courseServices'); 5 | const { 6 | validateMockValueToHaveBeenCalled, 7 | } = require('../../../../utils/test-utils/validators.utils'); 8 | const { 9 | setupReqRes, 10 | clearMocks, 11 | } = require('../../../../utils/test-utils/courseControllerDeps'); 12 | const { 13 | render500ErrorHelper, 14 | redirectNonExistentDataHelper, 15 | } = require('../../helpers'); 16 | 17 | jest.mock('../../helpers'); 18 | jest.mock('../../../../database/services/modelServices/courseServices'); 19 | 20 | let req; 21 | let res; 22 | 23 | describe('renderEditTaughtCourse Controller Test Suite', () => { 24 | beforeEach(() => { 25 | const { request, response } = setupReqRes(); 26 | 27 | req = request; 28 | res = response; 29 | }); 30 | 31 | afterEach(() => { 32 | clearMocks(); 33 | }); 34 | 35 | test('should validate render500ErrorHelper is called', async () => { 36 | findOneCourseService.mockReturnValueOnce(new Error()); 37 | 38 | await renderEditTaughtCourseController(req, res); 39 | 40 | validateMockValueToHaveBeenCalled(render500ErrorHelper); 41 | }); 42 | 43 | test('should validate redirectNonExistentDataHelper is called', async () => { 44 | findOneCourseService.mockReturnValueOnce(null); 45 | 46 | await renderEditTaughtCourseController(req, res); 47 | 48 | validateMockValueToHaveBeenCalled(redirectNonExistentDataHelper); 49 | }); 50 | 51 | test('should validate res.status & res.render is called', async () => { 52 | await renderEditTaughtCourseController(req, res); 53 | 54 | const { status, render } = res; 55 | 56 | validateMockValueToHaveBeenCalled(status); 57 | validateMockValueToHaveBeenCalled(render); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /api/controllers/coursesControllers/__tests__/updateTaughtCourse.controller.test.js: -------------------------------------------------------------------------------- 1 | const { updateTaughtCourseController } = require('../index'); 2 | const { 3 | setupReqRes, 4 | clearMocks, 5 | } = require('../../../../utils/test-utils/courseControllerDeps'); 6 | const { 7 | updateCourseService, 8 | } = require('../../../../database/services/modelServices/courseServices'); 9 | const { 10 | render500ErrorHelper, 11 | redirectNonExistentDataHelper, 12 | } = require('../../helpers'); 13 | const { 14 | validateMockValueToHaveBeenCalled, 15 | } = require('../../../../utils/test-utils/validators.utils'); 16 | 17 | jest.mock('../../helpers'); 18 | jest.mock('../../../../database/services/modelServices/courseServices'); 19 | 20 | let req; 21 | let res; 22 | 23 | describe('updateTaughtCourse Controller Test Suite', () => { 24 | beforeEach(() => { 25 | const { request, response } = setupReqRes(); 26 | 27 | req = request; 28 | res = response; 29 | }); 30 | 31 | afterEach(() => { 32 | clearMocks(); 33 | }); 34 | 35 | test('should validate render500ErrorHelper is called', async () => { 36 | updateCourseService.mockReturnValueOnce(new Error()); 37 | 38 | await updateTaughtCourseController(req, res); 39 | 40 | validateMockValueToHaveBeenCalled(render500ErrorHelper); 41 | }); 42 | 43 | test('should validate redirectNonExistentDataHelper is called', async () => { 44 | updateCourseService.mockReturnValueOnce(null); 45 | 46 | await updateTaughtCourseController(req, res); 47 | 48 | validateMockValueToHaveBeenCalled(redirectNonExistentDataHelper); 49 | }); 50 | 51 | test('should validate res.status & res.render is called', async () => { 52 | await updateTaughtCourseController(req, res); 53 | 54 | const { flash } = req; 55 | const { redirect } = res; 56 | 57 | validateMockValueToHaveBeenCalled(flash); 58 | validateMockValueToHaveBeenCalled(redirect); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /api/controllers/coursesControllers/__tests__/renderTaughtCourse.controller.test.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../helpers'); 2 | jest.mock('../../../../database/services/modelServices/courseServices'); 3 | 4 | const { 5 | renderTaughtCourseController, 6 | } = require('../renderTaughtCourse.controller'); 7 | const { 8 | render500ErrorHelper, 9 | redirectNonExistentDataHelper, 10 | } = require('../../helpers'); 11 | const { 12 | findOneCourseService, 13 | } = require('../../../../database/services/modelServices/courseServices'); 14 | const { 15 | validateMockValueToHaveBeenCalled, 16 | } = require('../../../../utils/test-utils/validators.utils'); 17 | const { 18 | setupReqRes, 19 | clearMocks, 20 | } = require('../../../../utils/test-utils/courseControllerDeps'); 21 | 22 | let req; 23 | let res; 24 | 25 | describe('renderTaughtCourse Controller Test Suite', () => { 26 | beforeEach(() => { 27 | const { request, response } = setupReqRes(); 28 | req = request; 29 | res = response; 30 | }); 31 | 32 | afterEach(() => { 33 | clearMocks(); 34 | }); 35 | 36 | test('should validate render500ErrorHelper is called', async () => { 37 | findOneCourseService.mockReturnValueOnce(new Error()); 38 | 39 | await renderTaughtCourseController(req, res); 40 | 41 | validateMockValueToHaveBeenCalled(render500ErrorHelper); 42 | }); 43 | 44 | test('should validate redirectNonExistentDataHelper is called', async () => { 45 | findOneCourseService.mockReturnValueOnce(null); 46 | 47 | await renderTaughtCourseController(req, res); 48 | 49 | validateMockValueToHaveBeenCalled(redirectNonExistentDataHelper); 50 | }); 51 | 52 | test('should validate res.status & res.render is called', async () => { 53 | await renderTaughtCourseController(req, res); 54 | 55 | const { status, render } = res; 56 | 57 | validateMockValueToHaveBeenCalled(status); 58 | validateMockValueToHaveBeenCalled(render); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /database/models/__tests__/user.model.test.js: -------------------------------------------------------------------------------- 1 | const User = require('../user.model'); 2 | const { fakeUserData } = require('../../fixtures'); 3 | const { 4 | validateNotEmpty, 5 | validateStringEquality, 6 | validateMongoDuplicationError, 7 | } = require('../../../utils/test-utils/validators.utils'); 8 | const { 9 | dbConnect, 10 | dbDisconnect, 11 | } = require('../../../utils/test-utils/dbHandler.utils'); 12 | 13 | beforeAll(async () => dbConnect()); 14 | afterAll(async () => dbDisconnect()); 15 | 16 | describe('User Model Test Suite', () => { 17 | test('should validate saving a new student user successfully', async () => { 18 | const validStudentUser = new User({ 19 | local: fakeUserData, 20 | role: fakeUserData.role, 21 | }); 22 | const savedStudentUser = await validStudentUser.save(); 23 | 24 | validateNotEmpty(savedStudentUser); 25 | validateStringEquality(savedStudentUser.role, fakeUserData.role); 26 | validateStringEquality(savedStudentUser.local.email, fakeUserData.email); 27 | validateStringEquality( 28 | savedStudentUser.local.username, 29 | fakeUserData.username 30 | ); 31 | validateStringEquality( 32 | savedStudentUser.local.password, 33 | fakeUserData.password 34 | ); 35 | validateStringEquality( 36 | savedStudentUser.local.firstName, 37 | fakeUserData.firstName 38 | ); 39 | validateStringEquality( 40 | savedStudentUser.local.lastName, 41 | fakeUserData.lastName 42 | ); 43 | }); 44 | 45 | test('should validate MongoError duplicate error with code 11000', async () => { 46 | expect.assertions(4); 47 | const validStudentUser = new User({ 48 | local: fakeUserData, 49 | role: fakeUserData.role, 50 | }); 51 | 52 | try { 53 | await validStudentUser.save(); 54 | } catch (error) { 55 | const { name, code } = error; 56 | validateMongoDuplicationError(name, code); 57 | } 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /api/controllers/lessonsControllers/__tests__/renderAllLessonsTaught.controller.test.js: -------------------------------------------------------------------------------- 1 | const { 2 | renderAllLessonsTaughtController, 3 | } = require('../renderAllLessonsTaught.controller'); 4 | const { 5 | setupReqRes, 6 | clearMocks, 7 | } = require('../../../../utils/test-utils/courseControllerDeps'); 8 | const { 9 | render500ErrorHelper, 10 | redirectNonExistentDataHelper, 11 | } = require('../../helpers'); 12 | const { 13 | validateMockValueToHaveBeenCalled, 14 | } = require('../../../../utils/test-utils/validators.utils'); 15 | const { 16 | findOneCourseService, 17 | } = require('../../../../database/services/modelServices/courseServices'); 18 | 19 | jest.mock('../../helpers'); 20 | jest.mock('../../../../database/services/modelServices/courseServices'); 21 | 22 | let req; 23 | let res; 24 | 25 | describe('renderAllLessonsTaught Controller Test Suite', () => { 26 | beforeEach(() => { 27 | const { request, response } = setupReqRes(); 28 | req = request; 29 | res = response; 30 | }); 31 | 32 | afterEach(() => { 33 | clearMocks(); 34 | }); 35 | 36 | test('should validate render500ErrorHelper is called', async () => { 37 | findOneCourseService.mockReturnValueOnce(new Error()); 38 | 39 | await renderAllLessonsTaughtController(req, res); 40 | 41 | validateMockValueToHaveBeenCalled(render500ErrorHelper); 42 | }); 43 | 44 | test('should validate redirectNonExistentDataHelper is called', async () => { 45 | findOneCourseService.mockReturnValueOnce(null); 46 | 47 | await renderAllLessonsTaughtController(req, res); 48 | 49 | validateMockValueToHaveBeenCalled(redirectNonExistentDataHelper); 50 | }); 51 | 52 | test('should validate res.status & res.render is called', async () => { 53 | await renderAllLessonsTaughtController(req, res); 54 | 55 | const { status, render } = res; 56 | 57 | validateMockValueToHaveBeenCalled(status); 58 | validateMockValueToHaveBeenCalled(render); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | const debug = require('debug')('node-onlearn:server'); 8 | const http = require('http'); 9 | const app = require('../src/app'); 10 | 11 | /** 12 | * Normalize a port into a number, string, or false. 13 | */ 14 | 15 | function normalizePort(val) { 16 | const chosenPort = parseInt(val, 10); 17 | 18 | if (Number.isNaN(chosenPort)) { 19 | // named pipe 20 | return val; 21 | } 22 | 23 | if (chosenPort >= 0) { 24 | // port number 25 | return chosenPort; 26 | } 27 | 28 | return false; 29 | } 30 | 31 | /** 32 | * Get port from environment and store in Express. 33 | */ 34 | 35 | const port = normalizePort(process.env.PORT || '3000'); 36 | app.set('port', port); 37 | 38 | /** 39 | * Create HTTP server. 40 | */ 41 | 42 | const server = http.createServer(app); 43 | 44 | /** 45 | * Event listener for HTTP server "error" event. 46 | */ 47 | 48 | function onError(error) { 49 | if (error.syscall !== 'listen') { 50 | throw error; 51 | } 52 | 53 | const bind = typeof port === 'string' ? `Pipe ${port}` : `Port ${port}`; 54 | 55 | // handle specific listen errors with friendly messages 56 | switch (error.code) { 57 | case 'EACCES': 58 | console.error(`${bind} requires elevated privileges`); 59 | process.exit(1); 60 | break; 61 | case 'EADDRINUSE': 62 | console.error(`${bind} is already in use`); 63 | process.exit(1); 64 | break; 65 | default: 66 | throw error; 67 | } 68 | } 69 | 70 | /** 71 | * Event listener for HTTP server "listening" event. 72 | */ 73 | 74 | function onListening() { 75 | const addr = server.address(); 76 | const bind = typeof addr === 'string' ? `pipe ${addr}` : `port ${addr.port}`; 77 | debug(`Listening on ${bind}`); 78 | } 79 | 80 | /** 81 | * Listen on provided port, on all network interfaces. 82 | */ 83 | 84 | server.listen(port); 85 | server.on('error', onError); 86 | server.on('listening', onListening); 87 | -------------------------------------------------------------------------------- /api/controllers/lessonsControllers/__tests__/renderLessonDetails.controller.test.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../helpers'); 2 | jest.mock('../../../../database/services/modelServices/lessonServices'); 3 | 4 | const { renderLessonDetailsController } = require('../index'); 5 | const { 6 | setupReqRes, 7 | clearMocks, 8 | } = require('../../../../utils/test-utils/courseControllerDeps'); 9 | const { 10 | render500ErrorHelper, 11 | redirectNonExistentDataHelper, 12 | } = require('../../helpers'); 13 | const { 14 | findOneLessonService, 15 | } = require('../../../../database/services/modelServices/lessonServices'); 16 | const { 17 | validateMockValueToHaveBeenCalled, 18 | } = require('../../../../utils/test-utils/validators.utils'); 19 | 20 | let req; 21 | let res; 22 | 23 | describe('renderLessonDetails Controller Test Suite', () => { 24 | beforeEach(() => { 25 | const { request, response } = setupReqRes(); 26 | req = request; 27 | res = response; 28 | }); 29 | afterEach(() => { 30 | clearMocks(); 31 | }); 32 | 33 | test('should validate render500ErrorHelper is called', async () => { 34 | findOneLessonService.mockReturnValueOnce(new Error()); 35 | 36 | await renderLessonDetailsController(req, res); 37 | 38 | validateMockValueToHaveBeenCalled(render500ErrorHelper); 39 | }); 40 | 41 | test('should validate redirectNonExistentDataHelper is called', async () => { 42 | findOneLessonService.mockReturnValueOnce(null); 43 | 44 | await renderLessonDetailsController(req, res); 45 | 46 | validateMockValueToHaveBeenCalled(redirectNonExistentDataHelper); 47 | }); 48 | 49 | test('should validate res.status & res.render is called', async () => { 50 | findOneLessonService.mockReturnValueOnce({ 51 | students: expect.anything(), 52 | instructors: expect.anything(), 53 | }); 54 | 55 | await renderLessonDetailsController(req, res); 56 | 57 | const { status, render } = res; 58 | 59 | validateMockValueToHaveBeenCalled(status); 60 | validateMockValueToHaveBeenCalled(render); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /api/controllers/usersAuthControllers/__tests__/loginUser.controller.test.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../helpers'); 2 | const { loginUserController } = require('../index'); 3 | const { 4 | mockRequest, 5 | mockResponse, 6 | } = require('../../../../utils/test-utils/interceptors.utils'); 7 | const { 8 | validateMockValueToHaveBeenCalled, 9 | } = require('../../../../utils/test-utils/validators.utils'); 10 | const { 11 | authenticateUserHelper, 12 | renderLoginFormWithErrorsHelper, 13 | } = require('../../helpers'); 14 | 15 | let req; 16 | let res; 17 | let next; 18 | 19 | const loginFormTestHelper = (body, request, response, done, validation) => { 20 | request.body = body; 21 | loginUserController(request, response, done); 22 | validateMockValueToHaveBeenCalled(validation); 23 | }; 24 | 25 | describe('loginUser Controller Test Suite', () => { 26 | beforeEach(() => { 27 | req = mockRequest(); 28 | res = mockResponse(); 29 | next = () => jest.fn(); 30 | }); 31 | 32 | afterEach(() => { 33 | jest.clearAllMocks(); 34 | }); 35 | 36 | test('should validate login form with errors when req.body is empty', () => { 37 | loginFormTestHelper( 38 | { user: '', password: '' }, 39 | req, 40 | res, 41 | next, 42 | renderLoginFormWithErrorsHelper 43 | ); 44 | }); 45 | 46 | test('should validate login form with errors when req.body.password is empty', () => { 47 | loginFormTestHelper( 48 | { user: 'dummy', password: '' }, 49 | req, 50 | res, 51 | next, 52 | renderLoginFormWithErrorsHelper 53 | ); 54 | }); 55 | 56 | test('should validate login form with errors when req.body.user is empty', () => { 57 | loginFormTestHelper( 58 | { user: '', password: '*****' }, 59 | req, 60 | res, 61 | next, 62 | renderLoginFormWithErrorsHelper 63 | ); 64 | }); 65 | 66 | test('should validate user authentication', () => { 67 | loginFormTestHelper( 68 | { user: 'dummy', password: '*****' }, 69 | req, 70 | res, 71 | next, 72 | authenticateUserHelper 73 | ); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /database/models/__tests__/course.model.test.js: -------------------------------------------------------------------------------- 1 | const Course = require('../course.model'); 2 | const User = require('../user.model'); 3 | const { 4 | fakeUserData, 5 | fakeUserDataTwo, 6 | fakeCourseData, 7 | } = require('../../fixtures'); 8 | const { 9 | validateNotEmpty, 10 | validateArrayLength, 11 | validateStringEquality, 12 | validateArrayContaining, 13 | } = require('../../../utils/test-utils/validators.utils'); 14 | const { 15 | dbConnect, 16 | dbDisconnect, 17 | } = require('../../../utils/test-utils/dbHandler.utils'); 18 | 19 | let validCourse; 20 | let validStudentUser; 21 | let validInstructorUser; 22 | 23 | describe('Course Model Test Suite', () => { 24 | beforeAll(async () => { 25 | await dbConnect(); 26 | 27 | validInstructorUser = await User.create({ 28 | local: fakeUserData, 29 | role: fakeUserData.role, 30 | }); 31 | 32 | validStudentUser = await User.create({ 33 | local: fakeUserDataTwo, 34 | role: fakeUserDataTwo.role, 35 | }); 36 | 37 | const { _id: instructorId } = validInstructorUser; 38 | const { _id: studentId } = validStudentUser; 39 | 40 | fakeCourseData.instructors.push(instructorId); 41 | fakeCourseData.students.push(studentId); 42 | 43 | validCourse = await Course.create(fakeCourseData); 44 | }); 45 | 46 | afterAll(async () => dbDisconnect()); 47 | 48 | test('should validate Course successfully saved', async () => { 49 | validateNotEmpty(validCourse); 50 | 51 | const { _id: validStudentId } = validStudentUser; 52 | const { _id: validInstructorId } = validInstructorUser; 53 | 54 | const { imageUrl, title, description, instructors, students } = validCourse; 55 | 56 | validateStringEquality(imageUrl, 'https://via.placeholder.com/200x150'); 57 | validateStringEquality(title, fakeCourseData.title); 58 | validateStringEquality(description, fakeCourseData.description); 59 | validateArrayLength(instructors, 1); 60 | validateArrayContaining(instructors, [validInstructorId]); 61 | validateArrayLength(students, 1); 62 | validateArrayContaining(students, [validStudentId]); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /database/services/modelServices/userServices/__tests__/findUserById.service.test.js: -------------------------------------------------------------------------------- 1 | const User = require('../../../../models/user.model'); 2 | const { findUserByIdService } = require('../index'); 3 | const { fakeIdFormatData, fakeUserData } = require('../../../../fixtures'); 4 | const { 5 | dbConnect, 6 | dbDisconnect, 7 | } = require('../../../../../utils/test-utils/dbHandler.utils'); 8 | const { 9 | validateNotEmpty, 10 | validateEquality, 11 | validateInstanceOf, 12 | validateStringEquality, 13 | } = require('../../../../../utils/test-utils/validators.utils'); 14 | 15 | let validUser; 16 | 17 | describe('findUserById Service Test Suite', () => { 18 | beforeEach(async () => { 19 | await dbConnect(); 20 | validUser = await User.create({ 21 | local: fakeUserData, 22 | role: fakeUserData.role, 23 | }); 24 | }); 25 | 26 | afterEach(async () => dbDisconnect()); 27 | 28 | test('should validate null for non-existent user', async () => { 29 | const { correctFormat } = fakeIdFormatData; 30 | 31 | const results = await findUserByIdService(correctFormat); 32 | 33 | validateEquality(results, null); 34 | }); 35 | 36 | test('should validate an Error for incorrect id format', async () => { 37 | const { incorrectFormat } = fakeIdFormatData; 38 | 39 | const results = await findUserByIdService(incorrectFormat); 40 | 41 | validateInstanceOf(results, Error); 42 | }); 43 | 44 | test('should validate successfully finding id of an existing user', async () => { 45 | const { _id } = validUser; 46 | 47 | const results = await findUserByIdService(_id); 48 | 49 | validateNotEmpty(results); 50 | 51 | const { local, role } = results; 52 | validateStringEquality(role, fakeUserData.role); 53 | 54 | const { firstName, lastName, email, username, password } = local; 55 | validateStringEquality(firstName, fakeUserData.firstName); 56 | validateStringEquality(lastName, fakeUserData.lastName); 57 | validateStringEquality(email, fakeUserData.email); 58 | validateStringEquality(username, fakeUserData.username); 59 | validateStringEquality(password, fakeUserData.password); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /configs/index.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | const { 4 | LOCAL_MONGO_URI, 5 | SESSION_SECRET, 6 | DUMMY_PASSWORD, 7 | DUMMY_EDIT_PASSWORD_WEAK, 8 | DUMMY_EDIT_PASSWORD_STRONG, 9 | GOOGLE_CLIENT_ID, 10 | GOOGLE_CLIENT_SECRET, 11 | CLOUDINARY_KEY, 12 | CLOUDINARY_NAME, 13 | CLOUDINARY_SECRET, 14 | } = process.env; 15 | 16 | module.exports = { 17 | /* Route prefixes */ 18 | apiPrefix: '/', 19 | registerPrefix: '/register', 20 | loginPrefix: '/login', 21 | logoutPrefix: '/logout', 22 | profilePrefix: '/profile', 23 | profileEditPrefix: '/profile/:id/edit', 24 | googleAuthPrefix: '/auth/google', 25 | googleAuthCallbackPrefix: '/auth/google/callback', 26 | instructorPrefix: '/instructor', 27 | newCoursePrefix: '/course/new', 28 | courseRegistrationPrefix: '/course/:id/register', 29 | courseDetailsPrefix: '/course/:id/details', 30 | allCoursesPrefix: '/all-courses', 31 | myCoursesPrefix: '/my-courses', 32 | myCoursesTeachPrefix: '/my-courses/teach/:id', 33 | myCoursesTeachStudentListPrefix: '/my-courses/teach/:id/studentList', 34 | myCoursesTeachEditCoursePrefix: '/my-courses/teach/:id/edit', 35 | myCoursesTeachDeleteCoursePrefix: '/my-courses/teach/:id/delete', 36 | myCoursesTeachAllLessonsPrefix: '/my-courses/teach/:id/all-lessons', 37 | myCoursesTeachNewLessonPrefix: '/my-courses/teach/:id/lesson/new', 38 | myCoursesLessonDetailsPrefix: '/my-courses/teach/lesson/:id/', 39 | myCoursesLessonEditPrefix: '/my-courses/teach/lesson/:id/edit', 40 | myCoursesLearnPrefix: '/my-courses/learn/:id', 41 | 42 | /* Database */ 43 | localMongoUri: LOCAL_MONGO_URI, 44 | sessSecret: SESSION_SECRET, 45 | 46 | /* Test Fixture Data */ 47 | dummyPassword: DUMMY_PASSWORD, 48 | dummyEditPasswordWeak: DUMMY_EDIT_PASSWORD_WEAK, 49 | dummyEditPasswordStrong: DUMMY_EDIT_PASSWORD_STRONG, 50 | 51 | /* Google Authentication */ 52 | googleAuth: { 53 | clientId: GOOGLE_CLIENT_ID, 54 | clientSecret: GOOGLE_CLIENT_SECRET, 55 | callbackUrl: '/auth/google/callback', 56 | }, 57 | 58 | /* Cloudinary */ 59 | cloudName: CLOUDINARY_NAME, 60 | cloudKey: CLOUDINARY_KEY, 61 | cloudSecret: CLOUDINARY_SECRET, 62 | }; 63 | -------------------------------------------------------------------------------- /utils/test-utils/validators.utils.js: -------------------------------------------------------------------------------- 1 | exports.validateNotEmpty = (received) => { 2 | expect(received).not.toBeNull(); 3 | expect(received).not.toBeUndefined(); 4 | expect(received).toBeTruthy(); 5 | }; 6 | 7 | exports.validateUndefined = (received) => { 8 | expect(received).toBeUndefined(); 9 | }; 10 | 11 | exports.validateEquality = (received, expected) => { 12 | expect(received).not.toBe('dummydummy'); 13 | expect(received).toBe(expected); 14 | }; 15 | 16 | exports.validateStringEquality = (received, expected) => { 17 | expect(received).not.toEqual('dummydfasfsdfsdfasdsd'); 18 | expect(received).toEqual(expected); 19 | }; 20 | 21 | exports.validateArrayLength = (received, expected) => { 22 | expect(received).not.toHaveLength(10000000000); 23 | expect(received).toHaveLength(expected); 24 | }; 25 | 26 | exports.validateArrayContaining = (received, expected) => { 27 | expect(received).not.toEqual(expect.arrayContaining(['dummyData'])); 28 | expect(received).toEqual(expect.arrayContaining(expected)); 29 | }; 30 | 31 | exports.validateMockValueToHaveBeenCalled = (mockValue) => { 32 | expect(mockValue).not.toHaveBeenCalledTimes(100); 33 | expect(mockValue).toHaveBeenCalled(); 34 | }; 35 | 36 | exports.validateControllerUsed = (received, controller) => { 37 | expect(received).not.toBe(() => 'dummy'); 38 | expect(received).toBe(controller); 39 | }; 40 | 41 | exports.validateObjectMatch = (received, expected) => { 42 | expect(received).not.toMatchObject({ dfsdaf: 905 }); 43 | expect(received).toMatchObject(expected); 44 | }; 45 | 46 | exports.validateMongoDuplicationError = (name, code) => { 47 | expect(name).not.toEqual(/dummy/i); 48 | expect(name).toEqual('MongoError'); 49 | expect(code).not.toBe(255); 50 | expect(code).toBe(11000); 51 | }; 52 | 53 | exports.validateTypeOf = (received, expected) => { 54 | expect(typeof received).not.toBe('dummyType'); 55 | expect(typeof received).toBe(expected); 56 | }; 57 | 58 | exports.validateInstanceOf = (received, expected) => { 59 | function DummyInstance(dummy) { 60 | this.dummy = dummy; 61 | } 62 | 63 | expect(received instanceof DummyInstance).toBe(false); 64 | expect(received instanceof expected).toBe(true); 65 | }; 66 | -------------------------------------------------------------------------------- /views/users/common/edit-profile.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{#if error_msg}} 4 |

{{error_msg}}

5 | {{/if}} 6 |
7 |
8 | {{#if message}} 9 |

{{message}}

10 | {{/if}} 11 |
12 |
13 |

Edit Profile

14 |
15 |
16 |
17 | profile-avatar 18 |
19 |

Change your profile picture

20 | 21 | 22 |
23 |
24 |
25 | 26 | 27 |
28 | {{#if google}} 29 | {{/if}} 30 |
31 | 32 | 33 |
34 |
35 | 36 | 37 |
38 |
39 | 40 | 41 |
42 | 43 | {{#if local.password}} 44 |
45 | 46 | 47 |
48 |
49 | 50 | 51 |
52 | {{/if}} 53 | 54 | 55 |
56 |
57 |
-------------------------------------------------------------------------------- /api/routes/courses.routes.js: -------------------------------------------------------------------------------- 1 | const { Router } = require('express'); 2 | const { isInstructor, isLoggedInUser } = require('../middleware'); 3 | const { 4 | newCoursePrefix, 5 | myCoursesPrefix, 6 | allCoursesPrefix, 7 | courseDetailsPrefix, 8 | myCoursesTeachPrefix, 9 | myCoursesLearnPrefix, 10 | courseRegistrationPrefix, 11 | myCoursesTeachEditCoursePrefix, 12 | myCoursesTeachDeleteCoursePrefix, 13 | myCoursesTeachStudentListPrefix, 14 | } = require('../../configs'); 15 | const { 16 | createNewCourseController, 17 | renderMyCoursesController, 18 | renderAllCoursesController, 19 | registerToCourseController, 20 | renderStudentListController, 21 | renderTaughtCourseController, 22 | updateTaughtCourseController, 23 | deleteTaughtCourseController, 24 | renderCourseDetailsController, 25 | renderLearnedCourseController, 26 | renderCreateNewCourseController, 27 | renderEditTaughtCourseController, 28 | } = require('../controllers'); 29 | 30 | const router = Router(); 31 | 32 | router.get( 33 | newCoursePrefix, 34 | isLoggedInUser, 35 | isInstructor, 36 | renderCreateNewCourseController 37 | ); 38 | router.post(newCoursePrefix, isLoggedInUser, createNewCourseController); 39 | router.get(courseDetailsPrefix, isLoggedInUser, renderCourseDetailsController); 40 | router.post( 41 | courseRegistrationPrefix, 42 | isLoggedInUser, 43 | registerToCourseController 44 | ); 45 | router.get(allCoursesPrefix, isLoggedInUser, renderAllCoursesController); 46 | router.get(myCoursesPrefix, isLoggedInUser, renderMyCoursesController); 47 | router.get(myCoursesLearnPrefix, isLoggedInUser, renderLearnedCourseController); 48 | router.get( 49 | myCoursesTeachPrefix, 50 | isLoggedInUser, 51 | isInstructor, 52 | renderTaughtCourseController 53 | ); 54 | router.get( 55 | myCoursesTeachStudentListPrefix, 56 | isLoggedInUser, 57 | isInstructor, 58 | renderStudentListController 59 | ); 60 | router.get( 61 | myCoursesTeachEditCoursePrefix, 62 | isLoggedInUser, 63 | isInstructor, 64 | renderEditTaughtCourseController 65 | ); 66 | router.post( 67 | myCoursesTeachEditCoursePrefix, 68 | isLoggedInUser, 69 | updateTaughtCourseController 70 | ); 71 | router.post( 72 | myCoursesTeachDeleteCoursePrefix, 73 | isLoggedInUser, 74 | deleteTaughtCourseController 75 | ); 76 | 77 | module.exports = router; 78 | -------------------------------------------------------------------------------- /views/users/auth/register.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{#if errors}} 4 | 13 | {{/if}} 14 |
15 |
16 | {{#if message}} 17 |

{{message}}

18 | {{/if}} 19 |
20 |
21 |

Register

22 |
23 |
24 | 25 |
26 | 30 |
31 |
32 | 33 | 34 |
35 |
36 | 37 | 38 |
39 |
40 | 41 | 42 |
43 |
44 | 45 | 46 |
47 |
48 | 49 | 50 |
51 |
52 | 53 | 54 |
55 | 56 |
57 |
58 |

Already have an account? Login

59 |

Register with Google+

60 |
61 |
-------------------------------------------------------------------------------- /.github/workflows/deployment-automerge.yml: -------------------------------------------------------------------------------- 1 | name: Auto-Merge Dependency PRs 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - synchronize 8 | - reopened 9 | - labeled 10 | 11 | permissions: 12 | pull-requests: write 13 | contents: write 14 | actions: read 15 | 16 | jobs: 17 | process-prs: 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | # ✅ Step 1: Checkout the repo (Fixes "not a git repository" issue) 22 | - name: Checkout Repository 23 | uses: actions/checkout@v4 24 | 25 | # ✅ Step 2: Authenticate GitHub CLI (Ensure gh CLI works) 26 | - name: Authenticate GitHub CLI 27 | run: gh auth setup-git 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | 31 | # ✅ Step 3: Fetch all PRs sorted from oldest to newest 32 | - name: Get Oldest PRs 33 | id: get-prs 34 | run: | 35 | prs=$(gh pr list --state open --json number,createdAt --jq 'sort_by(.createdAt) | .[].number') 36 | echo "prs=$prs" >> $GITHUB_ENV 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | 40 | # ✅ Step 4: Process PRs One by One 41 | - name: Process PRs 42 | run: | 43 | for pr in $prs; do 44 | echo "Processing PR #$pr" 45 | # Checkout PR branch 46 | gh pr checkout $pr 47 | # Handle conflicts: Ensure latest package.json dependency version is used 48 | git checkout --ours package.json || true 49 | git add package.json || true 50 | # If package.json had conflicts, fix package-lock.json too 51 | if git diff --name-only | grep "package.json"; then 52 | echo "package.json had conflicts, regenerating package-lock.json..." 53 | npm install 54 | git add package-lock.json 55 | fi 56 | # Commit & push resolved conflicts 57 | git commit -m "Resolve dependency conflicts in PR #$pr" || true 58 | git push origin HEAD || true 59 | # Approve PR before merging 60 | gh pr review $pr --approve 61 | # Merge PR when all checks pass 62 | gh pr merge $pr --squash --auto 63 | echo "Merged PR #$pr successfully!" 64 | done 65 | env: 66 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-onlearn", 3 | "author": "Malcolm R. Kente (reMRKable Dev)", 4 | "version": "0.0.0", 5 | "private": true, 6 | "scripts": { 7 | "start": "node ./bin/www", 8 | "dev": "DEBUG=node-onlearn:* nodemon ./bin/www", 9 | "test": "jest --watchAll=true --runInBand --detectOpenHandles", 10 | "coverage": "jest --coverage --runInBand" 11 | }, 12 | "jest": { 13 | "testEnvironment": "node", 14 | "coverageDirectory": "./coverage/", 15 | "collectCoverage": false, 16 | "collectCoverageFrom": [ 17 | "*.js", 18 | "configs/**/*.js", 19 | "api/**/*.js", 20 | "database/**/*.js", 21 | "utils/global-utils/**/*.js", 22 | "loaders/*", 23 | "!prettier.config.js" 24 | ], 25 | "coverageThreshold": { 26 | "global": { 27 | "branches": 86, 28 | "functions": 87, 29 | "lines": 95, 30 | "statements": 94 31 | } 32 | } 33 | }, 34 | "dependencies": { 35 | "bcrypt": "5.1.1", 36 | "cloudinary": "2.7.0", 37 | "connect-flash": "0.1.1", 38 | "connect-mongo": "5.1.0", 39 | "cookie-parser": "1.4.7", 40 | "debug": "4.4.3", 41 | "dotenv": "16.6.1", 42 | "express": "5.1.0", 43 | "express-session": "1.18.1", 44 | "express-validator": "7.2.1", 45 | "hbs": "4.2.0", 46 | "http-errors": "2.0.0", 47 | "mongodb-memory-server": "10.1.4", 48 | "mongoose": "8.19.0", 49 | "morgan": "1.10.0", 50 | "multer": "2.0.2", 51 | "multer-storage-cloudinary": "4.0.0", 52 | "passport": "0.7.0", 53 | "passport-google-oauth": "2.0.0", 54 | "passport-local": "1.0.0", 55 | "serve-favicon": "2.5.0" 56 | }, 57 | "devDependencies": { 58 | "@shelf/jest-mongodb": "5.2.2", 59 | "eslint": "9.25.1", 60 | "eslint-config-airbnb-base": "15.0.0", 61 | "eslint-config-prettier": "10.1.8", 62 | "eslint-plugin-import": "2.31.0", 63 | "eslint-plugin-jest": "28.11.0", 64 | "eslint-plugin-prettier": "5.5.4", 65 | "jest": "29.7.0", 66 | "mongodb": "6.16.0", 67 | "prettier": "3.5.3", 68 | "supertest": "7.1.4" 69 | }, 70 | "repository": { 71 | "type": "git", 72 | "url": "git@github.com:reMRKableDev/OnLearn.git" 73 | }, 74 | "bugs": { 75 | "url": "https://github.com/reMRKableDev/OnLearn/issues" 76 | }, 77 | "homepage": "https://github.com/reMRKableDev/OnLearn#readme" 78 | } 79 | -------------------------------------------------------------------------------- /api/controllers/coursesControllers/__tests__/renderCourseDetails.controller.test.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../helpers'); 2 | jest.mock('../../../../database/services/modelServices/courseServices'); 3 | 4 | const { 5 | setupReqRes, 6 | clearMocks, 7 | } = require('../../../../utils/test-utils/courseControllerDeps'); 8 | const { 9 | findOneCourseService, 10 | } = require('../../../../database/services/modelServices/courseServices'); 11 | const { 12 | render500ErrorHelper, 13 | redirectNonExistentDataHelper, 14 | checkCurrentUserRelationToCourseHelper, 15 | } = require('../../helpers'); 16 | const { 17 | validateMockValueToHaveBeenCalled, 18 | } = require('../../../../utils/test-utils/validators.utils'); 19 | const { renderCourseDetailsController } = require('../index'); 20 | 21 | let req; 22 | let res; 23 | 24 | describe('renderCourseDetails Controller Test Suite', () => { 25 | beforeEach(() => { 26 | const { request, response } = setupReqRes(); 27 | req = request; 28 | res = response; 29 | }); 30 | 31 | afterEach(() => { 32 | clearMocks(); 33 | }); 34 | 35 | test('should validate render500ErrorHelper is called', async () => { 36 | findOneCourseService.mockReturnValueOnce(new Error()); 37 | 38 | await renderCourseDetailsController(req, res); 39 | 40 | validateMockValueToHaveBeenCalled(render500ErrorHelper); 41 | }); 42 | 43 | test('should validate redirectNonExistentDataHelper is called', async () => { 44 | findOneCourseService.mockReturnValueOnce(null); 45 | 46 | await renderCourseDetailsController(req, res); 47 | 48 | validateMockValueToHaveBeenCalled(redirectNonExistentDataHelper); 49 | }); 50 | 51 | test('should validate res.status & res.render is called', async () => { 52 | findOneCourseService.mockReturnValueOnce({ 53 | students: expect.anything(), 54 | instructors: expect.anything(), 55 | }); 56 | 57 | checkCurrentUserRelationToCourseHelper.mockReturnValueOnce({ 58 | isCurrentUserInStudentList: expect.anything(), 59 | isCurrentUserTheCourseInstructor: expect.anything(), 60 | }); 61 | 62 | await renderCourseDetailsController(req, res); 63 | 64 | validateMockValueToHaveBeenCalled(checkCurrentUserRelationToCourseHelper); 65 | 66 | const { status, render } = res; 67 | 68 | validateMockValueToHaveBeenCalled(status); 69 | validateMockValueToHaveBeenCalled(render); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /api/controllers/index.js: -------------------------------------------------------------------------------- 1 | const { renderIndexViewController } = require('./indexControllers'); 2 | const { 3 | loginUserController, 4 | logoutUserController, 5 | renderLoginController, 6 | renderRegisterController, 7 | registerNewUserController, 8 | authenticateGoogleLoginController, 9 | authenticateGoogleCallbackController, 10 | } = require('./usersAuthControllers'); 11 | const { 12 | updateUserProfileController, 13 | renderUserProfileController, 14 | renderEditUserProfileController, 15 | } = require('./usersControllers'); 16 | const { 17 | changeRoleToInstructor, 18 | renderBeInstructorController, 19 | } = require('./instructorsControllers'); 20 | const { 21 | createNewCourseController, 22 | renderMyCoursesController, 23 | registerToCourseController, 24 | renderAllCoursesController, 25 | renderStudentListController, 26 | updateTaughtCourseController, 27 | deleteTaughtCourseController, 28 | renderTaughtCourseController, 29 | renderLearnedCourseController, 30 | renderCourseDetailsController, 31 | renderCreateNewCourseController, 32 | renderEditTaughtCourseController, 33 | } = require('./coursesControllers'); 34 | const { 35 | createNewLessonController, 36 | renderLessonDetailsController, 37 | renderEditLessonController, 38 | renderCreateNewLessonController, 39 | renderAllLessonsTaughtController, 40 | } = require('./lessonsControllers'); 41 | 42 | module.exports = { 43 | loginUserController, 44 | logoutUserController, 45 | renderLoginController, 46 | changeRoleToInstructor, 47 | renderRegisterController, 48 | renderIndexViewController, 49 | registerNewUserController, 50 | createNewCourseController, 51 | createNewLessonController, 52 | renderMyCoursesController, 53 | renderEditLessonController, 54 | renderAllCoursesController, 55 | registerToCourseController, 56 | renderStudentListController, 57 | updateUserProfileController, 58 | renderUserProfileController, 59 | deleteTaughtCourseController, 60 | updateTaughtCourseController, 61 | renderTaughtCourseController, 62 | renderBeInstructorController, 63 | renderLearnedCourseController, 64 | renderCourseDetailsController, 65 | renderLessonDetailsController, 66 | renderCreateNewCourseController, 67 | renderCreateNewLessonController, 68 | renderEditUserProfileController, 69 | renderAllLessonsTaughtController, 70 | renderEditTaughtCourseController, 71 | authenticateGoogleLoginController, 72 | authenticateGoogleCallbackController, 73 | }; 74 | -------------------------------------------------------------------------------- /api/controllers/usersControllers/__tests__/renderEditUserProfile.controller.test.js: -------------------------------------------------------------------------------- 1 | const { renderEditUserProfileController } = require('../index'); 2 | const { 3 | createNewUserService, 4 | } = require('../../../../database/services/modelServices/userServices/createNewUser.service'); 5 | const { 6 | dbConnect, 7 | dbDisconnect, 8 | } = require('../../../../utils/test-utils/dbHandler.utils'); 9 | const { 10 | mockRequest, 11 | mockResponse, 12 | } = require('../../../../utils/test-utils/interceptors.utils'); 13 | const { 14 | validateMockValueToHaveBeenCalled, 15 | } = require('../../../../utils/test-utils/validators.utils'); 16 | const { 17 | fakeUserData, 18 | fakeIdFormatData, 19 | } = require('../../../../database/fixtures'); 20 | 21 | let res; 22 | let req; 23 | let validUser; 24 | 25 | describe('renderEditUserProfile Controller Test Suite', () => { 26 | beforeAll(async () => { 27 | await dbConnect(); 28 | validUser = await createNewUserService(fakeUserData); 29 | }); 30 | afterAll(async () => dbDisconnect()); 31 | 32 | beforeEach(() => { 33 | req = mockRequest(); 34 | res = mockResponse(); 35 | }); 36 | afterEach(() => { 37 | jest.clearAllMocks(); 38 | }); 39 | 40 | test('should validate that req.flash & res.redirect are called when ObjectId is invalid', async () => { 41 | const { incorrectFormat } = fakeIdFormatData; 42 | req.params = { id: incorrectFormat }; 43 | 44 | await renderEditUserProfileController(req, res); 45 | 46 | const { flash } = req; 47 | const { redirect } = res; 48 | 49 | validateMockValueToHaveBeenCalled(flash); 50 | validateMockValueToHaveBeenCalled(redirect); 51 | }); 52 | 53 | test('should validate req.flash & res.redirect when there is no user found with given Id', async () => { 54 | const { correctFormat } = fakeIdFormatData; 55 | req.params = { id: correctFormat }; 56 | 57 | await renderEditUserProfileController(req, res); 58 | 59 | const { redirect } = res; 60 | const { flash } = req; 61 | 62 | validateMockValueToHaveBeenCalled(redirect); 63 | validateMockValueToHaveBeenCalled(flash); 64 | }); 65 | 66 | test('should validate that res.status is called when the user if found', async () => { 67 | const { _id } = validUser; 68 | req.params = { id: _id }; 69 | 70 | await renderEditUserProfileController(req, res); 71 | 72 | const { status } = res; 73 | 74 | validateMockValueToHaveBeenCalled(status); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /database/services/modelServices/userServices/__tests__/updateUserProfileData.service.test.js: -------------------------------------------------------------------------------- 1 | const { 2 | updateUserProfileDataService, 3 | createNewUserService, 4 | } = require('../index'); 5 | const { fakeUserData, fakeIdFormatData } = require('../../../../fixtures'); 6 | const { 7 | dbConnect, 8 | dbDisconnect, 9 | } = require('../../../../../utils/test-utils/dbHandler.utils'); 10 | const { 11 | validateNotEmpty, 12 | validateInstanceOf, 13 | validateStringEquality, 14 | } = require('../../../../../utils/test-utils/validators.utils'); 15 | 16 | let validUser; 17 | 18 | describe('updateUserProfileData Service Test Suite', () => { 19 | beforeEach(async () => { 20 | await dbConnect(); 21 | 22 | validUser = await createNewUserService(fakeUserData); 23 | }); 24 | 25 | afterEach(async () => dbDisconnect()); 26 | 27 | test('should validate an Error for incorrect id format', async () => { 28 | const { incorrectFormat: dummyId } = fakeIdFormatData; 29 | const dummyReqBody = jest.fn(); 30 | const dummyUserPwd = expect.anything(); 31 | const dummyProfPicUrl = expect.anything(); 32 | 33 | const results = await updateUserProfileDataService( 34 | dummyId, 35 | dummyReqBody, 36 | dummyUserPwd, 37 | dummyProfPicUrl 38 | ); 39 | 40 | validateInstanceOf(results, Error); 41 | }); 42 | 43 | test('should validate successfully updating fields for user', async () => { 44 | validateNotEmpty(validUser); 45 | 46 | const { _id } = validUser; 47 | const dummyUserPwd = expect.anything(); 48 | const dummyProfPicUrl = 'https://dummy.image.com/dummyImg.jpg'; 49 | const dummyRequestBody = { 50 | email: 'updated@email.com', 51 | username: 'updatedDummyUser', 52 | firstName: 'Updated Dummy', 53 | lastName: 'Updated User', 54 | }; 55 | 56 | const results = await updateUserProfileDataService( 57 | _id, 58 | dummyRequestBody, 59 | dummyUserPwd, 60 | dummyProfPicUrl 61 | ); 62 | 63 | validateNotEmpty(results); 64 | 65 | const { local, profilePictureUrl } = results; 66 | 67 | validateStringEquality(profilePictureUrl, dummyProfPicUrl); 68 | 69 | const { email, username, firstName, lastName, password } = local; 70 | 71 | validateStringEquality(email, dummyRequestBody.email); 72 | validateStringEquality(username, dummyRequestBody.username); 73 | validateStringEquality(firstName, dummyRequestBody.firstName); 74 | validateStringEquality(lastName, dummyRequestBody.lastName); 75 | validateStringEquality(password, 'Anything'); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /passport/googleStrategy.js: -------------------------------------------------------------------------------- 1 | const GoogleStrategy = require('passport-google-oauth').OAuth2Strategy; 2 | const { googleAuth } = require('../configs'); 3 | const User = require('../database/models/user.model'); 4 | const { 5 | createNewGoogleUserService, 6 | } = require('../database/services/modelServices/userServices'); 7 | const { handleAsyncFunction } = require('../utils/global-utils'); 8 | 9 | exports.googleAuthStrategy = new GoogleStrategy( 10 | { 11 | clientID: googleAuth.clientId, 12 | clientSecret: googleAuth.clientSecret, 13 | callbackURL: googleAuth.callbackUrl, 14 | passReqToCallback: true, 15 | }, 16 | async (req, accessToken, _refreshToken, profile, done) => { 17 | // Check if user is logged in 18 | if (!req.user) { 19 | const [userResults, error] = await handleAsyncFunction( 20 | User.findOne({ 'google.id': profile.id }) 21 | ); 22 | 23 | if (error) { 24 | return done(error); 25 | } 26 | 27 | if (userResults) { 28 | // if there is a user id already but no token (user was linked at one point and then removed) 29 | if (!userResults.google.token) { 30 | userResults.google.token = accessToken; 31 | userResults.google.name = profile.displayName; 32 | userResults.google.email = profile.emails[0].value.toLocaleLowerCase(); 33 | userResults.profilePictureUrl = profile.photos[0].value; 34 | 35 | const [updatedUserResults, saveError] = await handleAsyncFunction( 36 | userResults.save() 37 | ); 38 | 39 | if (saveError) { 40 | return done(saveError); 41 | } 42 | 43 | return done(null, updatedUserResults); 44 | } 45 | 46 | return done(null, userResults); 47 | } 48 | 49 | const isGoogleUser = await createNewGoogleUserService( 50 | profile, 51 | accessToken 52 | ); 53 | 54 | if (isGoogleUser instanceof Error) { 55 | return done(isGoogleUser); 56 | } 57 | 58 | return done(null, isGoogleUser); 59 | } 60 | 61 | // user already exists and is logged in, we have to link accounts 62 | const { user } = req; 63 | 64 | user.google.id = profile.id; 65 | user.google.token = accessToken; 66 | user.google.name = profile.displayName; 67 | user.google.email = (profile.emails[0].value || '').toLocaleLowerCase(); 68 | user.profilePictureUrl = profile.photos[0].value || ''; 69 | 70 | const [updatedUser, updateError] = await handleAsyncFunction(user.save()); 71 | 72 | if (updateError) { 73 | return done(updateError); 74 | } 75 | 76 | return done(null, updatedUser); 77 | } 78 | ); 79 | -------------------------------------------------------------------------------- /api/controllers/lessonsControllers/__tests__/createNewLesson.controller.test.js: -------------------------------------------------------------------------------- 1 | const { createNewLessonController } = require('../index'); 2 | const { render500ErrorHelper } = require('../../helpers'); 3 | const { 4 | createNewLessonService, 5 | } = require('../../../../database/services/modelServices/lessonServices'); 6 | const { 7 | findOneCourseService, 8 | } = require('../../../../database/services/modelServices/courseServices'); 9 | const { 10 | validateMockValueToHaveBeenCalled, 11 | } = require('../../../../utils/test-utils/validators.utils'); 12 | const { 13 | mockRequest, 14 | mockResponse, 15 | } = require('../../../../utils/test-utils/interceptors.utils'); 16 | 17 | jest.mock('../../helpers'); 18 | jest.mock('../../../../database/services/modelServices/lessonServices'); 19 | jest.mock('../../../../database/services/modelServices/courseServices'); 20 | 21 | let req; 22 | let res; 23 | 24 | describe('createNewLesson Controller Test Suite', () => { 25 | beforeEach(() => { 26 | req = mockRequest(); 27 | res = mockResponse(); 28 | 29 | req.params = { id: expect.anything() }; 30 | req.body = { 31 | topic: expect.anything(), 32 | content: expect.anything(), 33 | videoUrl: expect.anything(), 34 | }; 35 | }); 36 | 37 | afterEach(() => { 38 | jest.clearAllMocks(); 39 | }); 40 | 41 | test('should validate render500ErrorHelper is called when createNewLessonService returns an error ', async () => { 42 | createNewLessonService.mockReturnValueOnce(new Error()); 43 | 44 | await createNewLessonController(req, res); 45 | 46 | validateMockValueToHaveBeenCalled(createNewLessonService); 47 | validateMockValueToHaveBeenCalled(render500ErrorHelper); 48 | }); 49 | 50 | test('should validate render500ErrorHelper is called when findOneCourse returns an error ', async () => { 51 | findOneCourseService.mockReturnValueOnce(new Error()); 52 | 53 | await createNewLessonController(req, res); 54 | 55 | validateMockValueToHaveBeenCalled(findOneCourseService); 56 | validateMockValueToHaveBeenCalled(render500ErrorHelper); 57 | }); 58 | 59 | test('should validate res.redirect called ', async () => { 60 | createNewLessonService.mockImplementationOnce(() => ({ 61 | _id: jest.fn(), 62 | })); 63 | findOneCourseService.mockImplementationOnce(() => ({ 64 | lessons: [], 65 | save: jest.fn(), 66 | })); 67 | 68 | await createNewLessonController(req, res); 69 | 70 | validateMockValueToHaveBeenCalled(createNewLessonService); 71 | validateMockValueToHaveBeenCalled(findOneCourseService); 72 | 73 | const { redirect } = res; 74 | validateMockValueToHaveBeenCalled(redirect); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /api/controllers/helpers/__tests__/filterCourses.helper.test.js: -------------------------------------------------------------------------------- 1 | const { filterCoursesHelper } = require('../index'); 2 | const { 3 | validateArrayLength, 4 | } = require('../../../../utils/test-utils/validators.utils'); 5 | 6 | let allCoursesListDummy; 7 | let incomingUserIdDummy; 8 | 9 | const firstInstructorIdDummy = '43833ad4-77ea-4669-a6d3-996b72eb3595'; 10 | const secondInstructorIdDummy = '67767882-eebd-47e4-bcf4-2b92c202f857'; 11 | 12 | const firstStudentIdDummy = '9aa29da0-8662-4685-8699-b191a3de162a'; 13 | 14 | describe('filterCourses Helper Test Suite', () => { 15 | beforeEach(() => { 16 | allCoursesListDummy = [ 17 | { 18 | dummyTitle: expect.anything(), 19 | dummyDescription: expect.anything(), 20 | instructors: [firstInstructorIdDummy], 21 | students: [firstStudentIdDummy, secondInstructorIdDummy], 22 | modules: expect.anything(), 23 | }, 24 | { 25 | dummyTitle: expect.anything(), 26 | dummyDescription: expect.anything(), 27 | instructors: [firstInstructorIdDummy], 28 | students: [firstStudentIdDummy], 29 | modules: expect.anything(), 30 | }, 31 | { 32 | dummyTitle: expect.anything(), 33 | dummyDescription: expect.anything(), 34 | instructors: [secondInstructorIdDummy], 35 | students: [firstStudentIdDummy], 36 | modules: expect.anything(), 37 | }, 38 | ]; 39 | }); 40 | 41 | test('should validate two empty arrays returned when the incomingUserId does not exist in the list', () => { 42 | incomingUserIdDummy = 1; 43 | 44 | const { coursesTaught, coursesLearned } = filterCoursesHelper( 45 | allCoursesListDummy, 46 | incomingUserIdDummy 47 | ); 48 | 49 | validateArrayLength(coursesTaught, 0); 50 | validateArrayLength(coursesLearned, 0); 51 | }); 52 | 53 | test('should validate coursesTaught to contain data associated to incomingUserId and coursesLearned to be empty', () => { 54 | incomingUserIdDummy = firstInstructorIdDummy; 55 | 56 | const { coursesTaught, coursesLearned } = filterCoursesHelper( 57 | allCoursesListDummy, 58 | incomingUserIdDummy 59 | ); 60 | 61 | validateArrayLength(coursesTaught, 2); 62 | validateArrayLength(coursesLearned, 0); 63 | }); 64 | 65 | test('should validate coursesLearned to contain data associated to incomingUserId and coursesTaught to be empty', () => { 66 | incomingUserIdDummy = firstStudentIdDummy; 67 | 68 | const { coursesTaught, coursesLearned } = filterCoursesHelper( 69 | allCoursesListDummy, 70 | incomingUserIdDummy 71 | ); 72 | 73 | validateArrayLength(coursesTaught, 0); 74 | validateArrayLength(coursesLearned, 3); 75 | }); 76 | 77 | test('should validate coursesTaught & coursesLearned to contain data associated to incomingUserId', () => { 78 | incomingUserIdDummy = secondInstructorIdDummy; 79 | 80 | const { coursesTaught, coursesLearned } = filterCoursesHelper( 81 | allCoursesListDummy, 82 | incomingUserIdDummy 83 | ); 84 | 85 | validateArrayLength(coursesTaught, 1); 86 | validateArrayLength(coursesLearned, 1); 87 | }); 88 | }); 89 | --------------------------------------------------------------------------------