├── 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 |
2 | Made by reMRKableDev
3 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
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 |
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 |
5 | {{#each isAllCourses}}
6 |
7 |
14 |
15 | {{/each}}
16 |
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 |
5 |
6 |
7 | {{isCourse.description}}
8 |
9 | {{#if isCurrentUserTheCourseInstructor}}
10 | See Course Overview
11 | {{else if isCurrentUserInStudentList}}
12 | Continue Course
13 | {{else}}
14 |
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 |
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 |
2 |
22 |
--------------------------------------------------------------------------------
/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 | Firstname
8 | Lastname
9 |
10 |
11 |
12 | {{#each isCourse.students}}
13 |
14 | {{this.local.firstName}}
15 | {{this.local.lastName}}
16 |
17 | {{/each}}
18 |
19 |
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 | Delete Course
6 |
7 |
8 |
9 | Course Title
10 |
11 |
12 |
13 | Course Description
14 | {{isCourse.description}}
15 |
16 |
17 | Submit
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 |
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 |
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 |
14 |
15 |
16 |
17 |
18 |
Registered Students: {{isCourse.students.length}}
19 |
See Student List
20 |
21 |
25 |
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 | Delete Lesson
6 |
7 |
8 |
9 | Lesson Topic
10 |
11 |
12 |
13 | Lesson Video Url
14 |
16 |
17 |
18 | Lesson Description
19 | {{isLesson.content}}
20 |
21 |
22 | Submit
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 |
5 | {{#each errors}}
6 |
7 |
8 | {{msg}}
9 |
10 |
11 | {{/each}}
12 |
13 | {{/if}}
14 |
15 |
16 | {{#if message}}
17 |
{{message}}
18 | {{/if}}
19 |
20 |
21 |
Login
22 |
23 |
24 | Email
25 |
26 |
27 |
28 | Password
29 |
30 |
31 | Login
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 |
10 | {{#each user.coursesTaught}}
11 |
12 |
13 |
{{this.title}}
14 |
{{this.description}}
15 |
More Details
16 |
17 |
18 | {{/each}}
19 |
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 |
37 | {{#each user.coursesLearned}}
38 |
39 |
43 |
44 | {{/each}}
45 |
46 | {{else}}
47 |
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 |
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 |
5 | {{#each errors}}
6 |
7 |
8 | {{msg}}
9 |
10 |
11 | {{/each}}
12 |
13 | {{/if}}
14 |
15 |
16 | {{#if message}}
17 |
{{message}}
18 | {{/if}}
19 |
20 |
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 |
--------------------------------------------------------------------------------