Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Lint codebase
name: Lint and test codebase

on:
push:
Expand All @@ -18,11 +18,11 @@ on:

jobs:
run-lint:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Filter changed files
uses: dorny/paths-filter@v2
id: changes
Expand All @@ -34,7 +34,7 @@ jobs:
- "backend/**"
python-backend:
- "backend/python/**"

- name: Set up Node.js
if: steps.changes.outputs.frontend == 'true' || steps.changes.outputs.typescript-backend == 'true'
uses: actions/setup-node@v4
Expand All @@ -58,7 +58,12 @@ jobs:
if: steps.changes.outputs.typescript-backend == 'true'
working-directory: ./backend
run: yarn lint


- name: Run backend unit tests
if: steps.changes.outputs.typescript-backend == 'true'
working-directory: ./backend
run: yarn test

- name: Lint Python backend
if: steps.changes.outputs.python-backend == 'true'
working-directory: ./backend/python
Expand Down
5 changes: 5 additions & 0 deletions backend/__mocks__/firebase-admin/storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const getDownloadURL = jest.fn().mockReturnValue("https://test.com/image.jpg");

module.exports = {
getDownloadURL,
};
148 changes: 148 additions & 0 deletions backend/__mocks__/mockData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/* eslint-disable no-underscore-dangle */
import { LearnerDTO, Role, Status } from "../types/userTypes";

export const testLessons = [
{
_id: "67e605c6fb8fbc9c9bbb6d81",
title: "Test Lesson",
type: "Lesson",
source: "https://test.com/lesson.pdf",
pageIndex: 0,
},
];

export const testActivities = [
{
_id: "67e60610fb8fbc9c9bbb6d82",
title: "Test Activity 1",
type: "Activity",
layout: [],
pageIndex: 0,
},
{
_id: "67e60617fb8fbc9c9bbb6d83",
title: "Test Activity 2",
type: "Activity",
layout: [],
pageIndex: 1,
},
{
_id: "67e60618fb8fbc9c9bbb6d84",
title: "Test Activity 3",
type: "Activity",
layout: [],
pageIndex: 2,
},
{
_id: "67e60619fb8fbc9c9bbb6d85",
title: "Test Activity 4",
type: "Activity",
layout: [],
pageIndex: 3,
},
];

export const testCourseModules = [
{
_id: "67e60622fb8fbc9c9bbb6d84",
displayIndex: 0,
title: "Test Course Module 1",
pages: [testActivities[0]._id],
},
{
_id: "67e60623fb8fbc9c9bbb6d85",
displayIndex: 1,
title: "Test Course Module 2",
pages: [testActivities[1]._id],
},
{
_id: "67e60624fb8fbc9c9bbb6d86",
displayIndex: 2,
title: "Test Course Module 3",
pages: [testActivities[2]._id, testActivities[3]._id],
},
];

export const testCourseUnits = [
{
_id: "67e6062cfb8fbc9c9bbb6d85",
displayIndex: 0,
title: "Test Course Unit 1",
modules: [testCourseModules[0]._id, testCourseModules[1]._id],
},
{
_id: "67e6062dfb8fbc9c9bbb6d86",
displayIndex: 1,
title: "Test Course Unit 2",
modules: [testCourseModules[2]._id],
},
];

export const testAdmins = [
{
firstName: "Peter",
lastName: "Pan",
_id: "67e6062ffb8fbc9c9bbb6d86",
authId: "67e6062ffb8fbc9c9bbb6d87",
email: "peter.pan@example.com",
status: "Active" as Status,
role: "Administrator" as Role,
bookmarks: [],
},
];

export const testFacilitators = [
{
firstName: "Wendy",
lastName: "Darling",
_id: "67e60630fb8fbc9c9bbb6d88",
authId: "67e60631fb8fbc9c9bbb6d89",
email: "wendy.darling@example.com",
status: "Active" as Status,
role: "Facilitator" as Role,
learners: ["67e60671fb8fbc9c9bbb6d8a"],
bookmarks: [],
},
];

export const testLearners = [
{
firstName: "John",
lastName: "Doe",
_id: "67e60671fb8fbc9c9bbb6d8a",
authId: "67e6068afb8fbc9c9bbb6d8b",
email: "john.doe@example.com",
status: "Active" as Status,
role: "Learner" as Role,
facilitator: testFacilitators[0]._id,
activitiesCompleted: {
[testCourseUnits[0]._id.toString()]: {
[testCourseModules[0]._id.toString()]: [testActivities[0]._id],
},
},
bookmarks: [],
},
];

export const testLearnersDTO: LearnerDTO[] = [
{
firstName: "John",
lastName: "Doe",
id: "67e60671fb8fbc9c9bbb6d8a",
email: "john.doe@example.com",
status: "Active" as Status,
role: "Learner" as Role,
facilitator: testFacilitators[0]._id,
activitiesCompleted: new Map([
[
testCourseUnits[0]._id.toString(),
new Map([
[testCourseModules[0]._id.toString(), [testActivities[0]._id]],
]),
],
]),
bookmarks: [],
},
];

export const testUsers = [...testAdmins, ...testFacilitators, ...testLearners];
2 changes: 1 addition & 1 deletion backend/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export default {
// moduleNameMapper: {},

// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
modulePathIgnorePatterns: ["<rootDir>/build/"],

// Activates notifications for test results
// notify: false,
Expand Down
20 changes: 20 additions & 0 deletions backend/middlewares/validators/userValidators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,23 @@ export const addBookmarkValidator = async (
return next();
};

export const completeActivityValidator = async (
req: Request,
res: Response,
next: NextFunction,
) => {
if (!validatePrimitive(req.body.unitId, "string")) {
return res.status(400).send(getApiValidationError("unitId", "string"));
}
if (!validatePrimitive(req.body.moduleId, "string")) {
return res.status(400).send(getApiValidationError("moduleId", "string"));
}
if (!validatePrimitive(req.body.pageId, "string")) {
return res.status(400).send(getApiValidationError("pageId", "string"));
}
return next();
};

export const deleteBookmarkValidator = async (
req: Request,
res: Response,
Expand All @@ -127,5 +144,8 @@ export const deleteBookmarkValidator = async (
if (!validatePrimitive(req.body.pageId, "string")) {
return res.status(400).send(getApiValidationError("pageId", "string"));
}
if (!validatePrimitive(req.body.activityId, "string")) {
return res.status(400).send(getApiValidationError("activityId", "string"));
}
return next();
};
6 changes: 3 additions & 3 deletions backend/models/activity.mgmodel.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Document, Schema } from "mongoose";
import { QuestionType } from "../types/activityTypes";
import CoursePageModel, { CoursePage } from "./coursepage.mgmodel";
import { QuestionType } from "../types/courseTypes";
import CoursePageModel, { CoursePageBase } from "./coursepage.mgmodel";

// Base Activity Interface
export interface Activity extends CoursePage {
export interface Activity extends CoursePageBase {
questionType: QuestionType;
activityNumber: string;
questionText: string;
Expand Down
10 changes: 7 additions & 3 deletions backend/models/coursepage.mgmodel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,25 @@ import { ElementSkeleton, PageType } from "../types/courseTypes";
// },
// });

export interface CoursePage extends Document {
export interface CoursePageBase extends Document {
id: string;
title: string;
type: PageType;
}

export interface LessonPage extends CoursePage {
export interface LessonPage extends CoursePageBase {
type: "Lesson";
source: string;
pageIndex: number;
}

export interface ActivityPage extends CoursePage {
export interface ActivityPage extends CoursePageBase {
type: "Activity";
layout: [ElementSkeleton];
}

export type CoursePage = LessonPage | ActivityPage;

const baseOptions = {
discriminatorKey: "type",
};
Expand Down
3 changes: 3 additions & 0 deletions backend/models/courseunit.mgmodel.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import mongoose, { Schema, Document, ObjectId } from "mongoose";
import mongooseLeanId from "mongoose-lean-id";

export interface CourseUnit extends Document {
id: string;
Expand Down Expand Up @@ -35,4 +36,6 @@ CourseUnitSchema.set("toObject", {
},
});

CourseUnitSchema.plugin(mongooseLeanId);

export default mongoose.model<CourseUnit>("CourseUnit", CourseUnitSchema);
16 changes: 16 additions & 0 deletions backend/models/user.mgmodel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export interface Bookmark {

export interface Learner extends User {
facilitator: ObjectId;
activitiesCompleted: Map<string, Map<string, Array<ObjectId>>>;
nextPage?: ObjectId;
}

export interface Facilitator extends User {
Expand Down Expand Up @@ -123,6 +125,20 @@ const LearnerSchema = new Schema(
ref: "User",
required: true,
},
activitiesCompleted: {
type: Map,
of: [
{
type: Map,
of: [{ type: mongoose.Schema.Types.ObjectId, ref: "Activity" }],
},
],
default: {},
},
nextPage: {
type: mongoose.Schema.Types.ObjectId,
ref: "CoursePage",
},
},
options,
);
Expand Down
2 changes: 1 addition & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"dotenv": "^16.5.0",
"express": "^4.19.2",
"express-rate-limit": "^6.2.0",
"firebase-admin": "^11.10.0",
"firebase-admin": "^13.2.0",
"generate-password": "^1.7.1",
"json2csv": "^5.0.6",
"lodash": "^4.17.21",
Expand Down
12 changes: 7 additions & 5 deletions backend/rest/authRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ import { CookieOptions, Router } from "express";
import { generate } from "generate-password";
import {
getAccessToken,
isAuthorizedByUserId,
isAuthorizedByRole,
isAuthorizedByUserId,
isFirstTimeInvitedUser,
} from "../middlewares/auth";
import {
forgotPasswordRequestValidator,
inviteUserRequestValidator,
loginRequestValidator,
signupRequestValidator,
inviteUserRequestValidator,
forgotPasswordRequestValidator,
updateTemporaryPasswordRequestValidator,
updateUserStatusRequestValidator,
} from "../middlewares/validators/authValidators";
Expand All @@ -22,8 +22,8 @@ import UserService from "../services/implementations/userService";
import IAuthService from "../services/interfaces/authService";
import IEmailService from "../services/interfaces/emailService";
import IUserService from "../services/interfaces/userService";
import { getErrorMessage } from "../utilities/errorUtils";
import { AuthError, AuthErrorCodes } from "../types/authTypes";
import { getErrorMessage } from "../utilities/errorUtils";

const authRouter: Router = Router();
const userService: IUserService = new UserService();
Expand Down Expand Up @@ -209,6 +209,8 @@ authRouter.post(
length: 20,
numbers: true,
});
const token = getAccessToken(req)!;
const facilitatorId = await authService.getUserIdFromAccessToken(token);
const invitedLearnerUser = await userService.createLearner(
{
firstName: req.body.firstName,
Expand All @@ -218,7 +220,7 @@ authRouter.post(
password: temporaryPassword,
status: "Invited",
},
req.body.facilitatorId,
facilitatorId.toString(),
);
await authService.sendLearnerInvite(
req.body.firstName,
Expand Down
Loading
Loading