Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add admission service #101

Merged
merged 19 commits into from
Oct 31, 2023
Merged
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
2 changes: 2 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import profileRouter from "./services/profile/profile-router.js";
import staffRouter from "./services/staff/staff-router.js";
import newsletterRouter from "./services/newsletter/newsletter-router.js";
import versionRouter from "./services/version/version-router.js";
import admissionRouter from "./services/admission/admission-router.js";

import { InitializeConfigReader } from "./middleware/config-reader.js";
import Models from "./database/models.js";
Expand Down Expand Up @@ -37,6 +38,7 @@ app.use("/newsletter/", newsletterRouter);
app.use("/profile/", profileRouter);
app.use("/staff/", staffRouter);
app.use("/user/", userRouter);
app.use("/admission/", admissionRouter);
app.use("/version/", versionRouter);

// Ensure that API is running
Expand Down
10 changes: 8 additions & 2 deletions src/database/decision-db.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { prop } from "@typegoose/typegoose";

enum DecisionStatus {
export enum DecisionStatus {
TBD = "TBD",
ACCEPTED = "ACCEPTED",
REJECTED = "REJECTED",
WAITLISTED = "WAITLISTED",
}

enum DecisionResponse {
export enum DecisionResponse {
PENDING = "PENDING",
ACCEPTED = "ACCEPTED",
DECLINED = "DECLINED",
Expand All @@ -22,6 +22,12 @@ export class DecisionInfo {

@prop({ required: true })
public response: DecisionResponse;

@prop({ required: true })
public reviewer: string;

@prop({ required: true })
public emailSent: boolean;
}

export class DecisionEntry {
Expand Down
6 changes: 6 additions & 0 deletions src/services/admission/admission-formats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { DecisionStatus } from "database/decision-db.js";

export interface ApplicantDecisionFormat {
userId: string;
status: DecisionStatus;
}
73 changes: 73 additions & 0 deletions src/services/admission/admission-router.test.ts
npunati27 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { beforeEach, describe, expect, it } from "@jest/globals";
import Models from "../../database/models.js";
import { DecisionStatus, DecisionResponse } from "../../database/decision-db.js";
import { getAsStaff, getAsUser, putAsStaff, putAsUser, TESTER } from "../../testTools.js";
import { DecisionInfo } from "../../database/decision-db.js";
import { StatusCode } from "status-code-enum";
import { ApplicantDecisionFormat } from "./admission-formats.js";

const TESTER_USER = {
userId: TESTER.id,
status: DecisionStatus.ACCEPTED,
response: DecisionResponse.PENDING,
emailSent: false,
reviewer: "tester-reviewer",
} satisfies DecisionInfo;

const OTHER_USER = {
userId: "other-user",
status: DecisionStatus.REJECTED,
response: DecisionResponse.DECLINED,
emailSent: true,
reviewer: "other-reviewer",
} satisfies DecisionInfo;

const updateData = [
{
userId: TESTER.id,
status: DecisionStatus.WAITLISTED,
},
{
userId: "other-user",
status: DecisionStatus.ACCEPTED,
},
] satisfies ApplicantDecisionFormat[];

beforeEach(async () => {
Models.initialize();
await Models.DecisionInfo.create(TESTER_USER);
await Models.DecisionInfo.create(OTHER_USER);
});

describe("GET /admission", () => {
it("gives forbidden error for user without elevated perms", async () => {
npunati27 marked this conversation as resolved.
Show resolved Hide resolved
const responseUser = await getAsUser("/admission/").expect(StatusCode.ClientErrorForbidden);
expect(JSON.parse(responseUser.text)).toHaveProperty("error", "Forbidden");
});
it("should return a list of applicants without email sent", async () => {
npunati27 marked this conversation as resolved.
Show resolved Hide resolved
const response = await getAsStaff("/admission/").expect(StatusCode.SuccessOK);
expect(JSON.parse(response.text)).toMatchObject(expect.arrayContaining([expect.objectContaining(TESTER_USER)]));
});
});

describe("PUT /admission", () => {
it("gives forbidden error for user without elevated perms", async () => {
const responseUser = await putAsUser("/admission/").send(updateData).expect(StatusCode.ClientErrorForbidden);
expect(JSON.parse(responseUser.text)).toHaveProperty("error", "Forbidden");
});
it("should update application status of applicants", async () => {
npunati27 marked this conversation as resolved.
Show resolved Hide resolved
const response = await putAsStaff("/admission/").send(updateData).expect(StatusCode.SuccessOK);
expect(JSON.parse(response.text)).toHaveProperty("message", "StatusSuccess");
const ops = updateData.map((entry) => {
return Models.DecisionInfo.findOne({ userId: entry.userId });
});
const retrievedEntries = await Promise.all(ops);
expect(retrievedEntries).toMatchObject(
expect.arrayContaining(
updateData.map((item) => {
return expect.objectContaining({ status: item.status, userId: item.userId });
}),
),
);
npunati27 marked this conversation as resolved.
Show resolved Hide resolved
});
});
110 changes: 110 additions & 0 deletions src/services/admission/admission-router.ts
npunati27 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { Router, Request, Response } from "express";
import { strongJwtVerification } from "../../middleware/verify-jwt.js";

import { JwtPayload } from "../auth/auth-models.js";
import { DecisionInfo } from "../../database/decision-db.js";
import Models from "../../database/models.js";
import { hasElevatedPerms } from "../auth/auth-lib.js";
import { ApplicantDecisionFormat } from "./admission-formats.js";
import { StatusCode } from "status-code-enum";

const admissionRouter: Router = Router();

/**
* @api {get} /admission/ GET /admission/
* @apiGroup Admission
* @apiDescription Gets all applicants who don't have an email sent
*
* @apiSuccess (200: Success) {Json} entries The list of applicants without email sent
* @apiSuccessExample Example Success Response (Staff POV)
* HTTP/1.1 200 OK
* [
* {
* "userId": "user1",
* "status": "ACCEPTED",
* "response": "ACCEPTED",
* "reviewer": "reviewer1",
* "emailSent": false
* },
* {
* "userId": "user3",
* "status": "WAITLISTED",
* "response": "PENDING",
* "reviewer": "reviewer1",
* "emailSent": false
* },
* {
* "userId": "user4",
* "status": "WAITLISTED",
* "response": "PENDING",
* "reviewer": "reviewer1",
* "emailSent": false
* }
* ]
* @apiUser strongVerifyErrors
* @apiError (500: Internal Server Error) {String} InternalError occurred on the server.
* @apiError (403: Forbidden) {String} Forbidden API accessed by user without valid perms.
* */
admissionRouter.get("/", strongJwtVerification, async (_: Request, res: Response) => {
const token: JwtPayload = res.locals.payload as JwtPayload;
if (!hasElevatedPerms(token)) {
return res.status(StatusCode.ClientErrorForbidden).send({ error: "Forbidden" });
}
try {
const filteredEntries: DecisionInfo[] = await Models.DecisionInfo.find({ emailSent: false });
return res.status(StatusCode.SuccessOK).send(filteredEntries);
} catch (error) {
console.error(error);
}
return res.status(StatusCode.ClientErrorBadRequest).send({ error: "InternalError" });
});
/**
* @api {put} /admission/ PUT /admission/
* @apiGroup Admission
* @apiDescription Updates the admission status of all applicants
*
* @apiHeader {String} Authorization Admin or Staff JWT Token
*
* @apiBody {Json} entries List of Applicants whose status needs to be updated
*
* @apiParamExample Example Request (Staff):
* HTTP/1.1 PUT /admission/
* [
* {
* "userId": "user1",
* "status": "ACCEPTED"
* },
* {
* "userId": "user2",
* "status": "REJECTED"
* },
* {
* "userId": "user3",
* "status": "WAITLISTED"
* }
* ]
*
* @apiSuccess (200: Success) {String} StatusSuccess
*
* @apiUse strongVerifyErrors
* @apiError (500: Internal Server Error) {String} InternalError occurred on the server.
* @apiError (403: Forbidden) {String} Forbidden API accessed by user without valid perms.
* */
admissionRouter.put("/", strongJwtVerification, async (req: Request, res: Response) => {
const token: JwtPayload = res.locals.payload as JwtPayload;
if (!hasElevatedPerms(token)) {
return res.status(StatusCode.ClientErrorForbidden).send({ error: "Forbidden" });
}
const updateEntries: ApplicantDecisionFormat[] = req.body as ApplicantDecisionFormat[];
const ops = updateEntries.map((entry) => {
return Models.DecisionInfo.findOneAndUpdate({ userId: entry.userId }, { $set: { status: entry.status } });
});
try {
await Promise.all(ops);
return res.status(StatusCode.SuccessOK).send({ message: "StatusSuccess" });
} catch (error) {
console.log(error);
}
return res.status(StatusCode.ClientErrorBadRequest).send("InternalError");
});
export default admissionRouter;