Skip to content

Commit 1e442f4

Browse files
authored
Add admission service (#101)
GET and PUT endpoints of the admission service.
1 parent b8c5c2a commit 1e442f4

File tree

5 files changed

+199
-2
lines changed

5 files changed

+199
-2
lines changed

src/app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import profileRouter from "./services/profile/profile-router.js";
1010
import staffRouter from "./services/staff/staff-router.js";
1111
import newsletterRouter from "./services/newsletter/newsletter-router.js";
1212
import versionRouter from "./services/version/version-router.js";
13+
import admissionRouter from "./services/admission/admission-router.js";
1314

1415
import { InitializeConfigReader } from "./middleware/config-reader.js";
1516
import Models from "./database/models.js";
@@ -37,6 +38,7 @@ app.use("/newsletter/", newsletterRouter);
3738
app.use("/profile/", profileRouter);
3839
app.use("/staff/", staffRouter);
3940
app.use("/user/", userRouter);
41+
app.use("/admission/", admissionRouter);
4042
app.use("/version/", versionRouter);
4143

4244
// Ensure that API is running

src/database/decision-db.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { prop } from "@typegoose/typegoose";
22

3-
enum DecisionStatus {
3+
export enum DecisionStatus {
44
TBD = "TBD",
55
ACCEPTED = "ACCEPTED",
66
REJECTED = "REJECTED",
77
WAITLISTED = "WAITLISTED",
88
}
99

10-
enum DecisionResponse {
10+
export enum DecisionResponse {
1111
PENDING = "PENDING",
1212
ACCEPTED = "ACCEPTED",
1313
DECLINED = "DECLINED",
@@ -22,6 +22,12 @@ export class DecisionInfo {
2222

2323
@prop({ required: true })
2424
public response: DecisionResponse;
25+
26+
@prop({ required: true })
27+
public reviewer: string;
28+
29+
@prop({ required: true })
30+
public emailSent: boolean;
2531
}
2632

2733
export class DecisionEntry {
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { DecisionStatus } from "database/decision-db.js";
2+
3+
export interface ApplicantDecisionFormat {
4+
userId: string;
5+
status: DecisionStatus;
6+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { beforeEach, describe, expect, it } from "@jest/globals";
2+
import Models from "../../database/models.js";
3+
import { DecisionStatus, DecisionResponse } from "../../database/decision-db.js";
4+
import { getAsStaff, getAsUser, putAsStaff, putAsUser, TESTER } from "../../testTools.js";
5+
import { DecisionInfo } from "../../database/decision-db.js";
6+
import { StatusCode } from "status-code-enum";
7+
import { ApplicantDecisionFormat } from "./admission-formats.js";
8+
9+
const TESTER_USER = {
10+
userId: TESTER.id,
11+
status: DecisionStatus.ACCEPTED,
12+
response: DecisionResponse.PENDING,
13+
emailSent: false,
14+
reviewer: "tester-reviewer",
15+
} satisfies DecisionInfo;
16+
17+
const OTHER_USER = {
18+
userId: "other-user",
19+
status: DecisionStatus.REJECTED,
20+
response: DecisionResponse.DECLINED,
21+
emailSent: true,
22+
reviewer: "other-reviewer",
23+
} satisfies DecisionInfo;
24+
25+
const updateData = [
26+
{
27+
userId: TESTER.id,
28+
status: DecisionStatus.WAITLISTED,
29+
},
30+
{
31+
userId: "other-user",
32+
status: DecisionStatus.ACCEPTED,
33+
},
34+
] satisfies ApplicantDecisionFormat[];
35+
36+
beforeEach(async () => {
37+
Models.initialize();
38+
await Models.DecisionInfo.create(TESTER_USER);
39+
await Models.DecisionInfo.create(OTHER_USER);
40+
});
41+
42+
describe("GET /admission", () => {
43+
it("gives forbidden error for user without elevated perms", async () => {
44+
const responseUser = await getAsUser("/admission/").expect(StatusCode.ClientErrorForbidden);
45+
expect(JSON.parse(responseUser.text)).toHaveProperty("error", "Forbidden");
46+
});
47+
it("should return a list of applicants without email sent", async () => {
48+
const response = await getAsStaff("/admission/").expect(StatusCode.SuccessOK);
49+
expect(JSON.parse(response.text)).toMatchObject(expect.arrayContaining([expect.objectContaining(TESTER_USER)]));
50+
});
51+
});
52+
53+
describe("PUT /admission", () => {
54+
it("gives forbidden error for user without elevated perms", async () => {
55+
const responseUser = await putAsUser("/admission/").send(updateData).expect(StatusCode.ClientErrorForbidden);
56+
expect(JSON.parse(responseUser.text)).toHaveProperty("error", "Forbidden");
57+
});
58+
it("should update application status of applicants", async () => {
59+
const response = await putAsStaff("/admission/").send(updateData).expect(StatusCode.SuccessOK);
60+
expect(JSON.parse(response.text)).toHaveProperty("message", "StatusSuccess");
61+
const ops = updateData.map((entry) => {
62+
return Models.DecisionInfo.findOne({ userId: entry.userId });
63+
});
64+
const retrievedEntries = await Promise.all(ops);
65+
expect(retrievedEntries).toMatchObject(
66+
expect.arrayContaining(
67+
updateData.map((item) => {
68+
return expect.objectContaining({ status: item.status, userId: item.userId });
69+
}),
70+
),
71+
);
72+
});
73+
});
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { Router, Request, Response } from "express";
2+
import { strongJwtVerification } from "../../middleware/verify-jwt.js";
3+
4+
import { JwtPayload } from "../auth/auth-models.js";
5+
import { DecisionInfo } from "../../database/decision-db.js";
6+
import Models from "../../database/models.js";
7+
import { hasElevatedPerms } from "../auth/auth-lib.js";
8+
import { ApplicantDecisionFormat } from "./admission-formats.js";
9+
import { StatusCode } from "status-code-enum";
10+
11+
const admissionRouter: Router = Router();
12+
13+
/**
14+
* @api {get} /admission/ GET /admission/
15+
* @apiGroup Admission
16+
* @apiDescription Gets all applicants who don't have an email sent
17+
*
18+
* @apiSuccess (200: Success) {Json} entries The list of applicants without email sent
19+
* @apiSuccessExample Example Success Response (Staff POV)
20+
* HTTP/1.1 200 OK
21+
* [
22+
* {
23+
* "userId": "user1",
24+
* "status": "ACCEPTED",
25+
* "response": "ACCEPTED",
26+
* "reviewer": "reviewer1",
27+
* "emailSent": false
28+
* },
29+
* {
30+
* "userId": "user3",
31+
* "status": "WAITLISTED",
32+
* "response": "PENDING",
33+
* "reviewer": "reviewer1",
34+
* "emailSent": false
35+
* },
36+
* {
37+
* "userId": "user4",
38+
* "status": "WAITLISTED",
39+
* "response": "PENDING",
40+
* "reviewer": "reviewer1",
41+
* "emailSent": false
42+
* }
43+
* ]
44+
* @apiUser strongVerifyErrors
45+
* @apiError (500: Internal Server Error) {String} InternalError occurred on the server.
46+
* @apiError (403: Forbidden) {String} Forbidden API accessed by user without valid perms.
47+
* */
48+
admissionRouter.get("/", strongJwtVerification, async (_: Request, res: Response) => {
49+
const token: JwtPayload = res.locals.payload as JwtPayload;
50+
if (!hasElevatedPerms(token)) {
51+
return res.status(StatusCode.ClientErrorForbidden).send({ error: "Forbidden" });
52+
}
53+
try {
54+
const filteredEntries: DecisionInfo[] = await Models.DecisionInfo.find({ emailSent: false });
55+
return res.status(StatusCode.SuccessOK).send(filteredEntries);
56+
} catch (error) {
57+
console.error(error);
58+
}
59+
return res.status(StatusCode.ClientErrorBadRequest).send({ error: "InternalError" });
60+
});
61+
/**
62+
* @api {put} /admission/ PUT /admission/
63+
* @apiGroup Admission
64+
* @apiDescription Updates the admission status of all applicants
65+
*
66+
* @apiHeader {String} Authorization Admin or Staff JWT Token
67+
*
68+
* @apiBody {Json} entries List of Applicants whose status needs to be updated
69+
*
70+
* @apiParamExample Example Request (Staff):
71+
* HTTP/1.1 PUT /admission/
72+
* [
73+
* {
74+
* "userId": "user1",
75+
* "status": "ACCEPTED"
76+
* },
77+
* {
78+
* "userId": "user2",
79+
* "status": "REJECTED"
80+
* },
81+
* {
82+
* "userId": "user3",
83+
* "status": "WAITLISTED"
84+
* }
85+
* ]
86+
*
87+
* @apiSuccess (200: Success) {String} StatusSuccess
88+
*
89+
* @apiUse strongVerifyErrors
90+
* @apiError (500: Internal Server Error) {String} InternalError occurred on the server.
91+
* @apiError (403: Forbidden) {String} Forbidden API accessed by user without valid perms.
92+
* */
93+
admissionRouter.put("/", strongJwtVerification, async (req: Request, res: Response) => {
94+
const token: JwtPayload = res.locals.payload as JwtPayload;
95+
if (!hasElevatedPerms(token)) {
96+
return res.status(StatusCode.ClientErrorForbidden).send({ error: "Forbidden" });
97+
}
98+
const updateEntries: ApplicantDecisionFormat[] = req.body as ApplicantDecisionFormat[];
99+
const ops = updateEntries.map((entry) => {
100+
return Models.DecisionInfo.findOneAndUpdate({ userId: entry.userId }, { $set: { status: entry.status } });
101+
});
102+
try {
103+
await Promise.all(ops);
104+
return res.status(StatusCode.SuccessOK).send({ message: "StatusSuccess" });
105+
} catch (error) {
106+
console.log(error);
107+
}
108+
return res.status(StatusCode.ClientErrorBadRequest).send("InternalError");
109+
});
110+
export default admissionRouter;

0 commit comments

Comments
 (0)