Skip to content

Commit f4fab81

Browse files
authored
Add rsvp service (#85)
* added GET /rsvp/ and GET /rsvp/:USERID/ and PUT /rsvp/ * added rsvp-router.ts and rsvp-router.test.ts
1 parent 1e442f4 commit f4fab81

File tree

6 files changed

+7073
-6715
lines changed

6 files changed

+7073
-6715
lines changed

.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
.eslintrc.cjs
22
tsconfig.json
3+
coverage

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,4 +134,4 @@ dist
134134
# Auto-generated HTML files
135135
docs/
136136
apidocs/
137-
devdocs/
137+
devdocs/

src/app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import eventRouter from "./services/event/event-router.js";
99
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";
12+
import rsvpRouter from "./services/rsvp/rsvp-router.js";
1213
import versionRouter from "./services/version/version-router.js";
1314
import admissionRouter from "./services/admission/admission-router.js";
1415

@@ -36,6 +37,7 @@ app.use("/auth/", authRouter);
3637
app.use("/event/", eventRouter);
3738
app.use("/newsletter/", newsletterRouter);
3839
app.use("/profile/", profileRouter);
40+
app.use("/rsvp/", rsvpRouter);
3941
app.use("/staff/", staffRouter);
4042
app.use("/user/", userRouter);
4143
app.use("/admission/", admissionRouter);

src/services/rsvp/rsvp-router.test.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { describe, expect, it, beforeEach } from "@jest/globals";
2+
import { TESTER, getAsAttendee, getAsStaff, putAsApplicant } from "../../testTools.js";
3+
import { DecisionInfo, DecisionStatus, DecisionResponse } from "../../database/decision-db.js";
4+
import Models from "../../database/models.js";
5+
import { StatusCode } from "status-code-enum";
6+
7+
const TESTER_DECISION_INFO = {
8+
userId: TESTER.id,
9+
status: DecisionStatus.ACCEPTED,
10+
response: DecisionResponse.PENDING,
11+
reviewer: "reviewer1",
12+
emailSent: true,
13+
} satisfies DecisionInfo;
14+
15+
beforeEach(async () => {
16+
Models.initialize();
17+
await Models.DecisionInfo.create(TESTER_DECISION_INFO);
18+
});
19+
20+
describe("GET /rsvp", () => {
21+
it("gives a UserNotFound error for an non-existent user", async () => {
22+
await Models.DecisionInfo.deleteOne({
23+
userId: TESTER.id,
24+
});
25+
26+
const response = await getAsAttendee("/rsvp/").expect(StatusCode.ClientErrorBadRequest);
27+
28+
expect(JSON.parse(response.text)).toHaveProperty("error", "UserNotFound");
29+
});
30+
31+
it("works for an attendee user and returns filtered data", async () => {
32+
const response = await getAsAttendee("/rsvp/").expect(StatusCode.SuccessOK);
33+
34+
expect(JSON.parse(response.text)).toMatchObject({
35+
userId: TESTER.id,
36+
status: DecisionStatus.ACCEPTED,
37+
response: DecisionResponse.PENDING,
38+
});
39+
});
40+
41+
it("works for a staff user and returns unfiltered data", async () => {
42+
const response = await getAsStaff("/rsvp/").expect(StatusCode.SuccessOK);
43+
44+
expect(JSON.parse(response.text)).toMatchObject(TESTER_DECISION_INFO);
45+
});
46+
});
47+
48+
describe("GET /rsvp/:USERID", () => {
49+
it("returns forbidden error if caller doesn't have elevated perms", async () => {
50+
const response = await getAsAttendee(`/rsvp/${TESTER.id}`).expect(StatusCode.ClientErrorForbidden);
51+
52+
expect(JSON.parse(response.text)).toHaveProperty("error", "Forbidden");
53+
});
54+
55+
it("gets if caller has elevated perms", async () => {
56+
const response = await getAsStaff(`/rsvp/${TESTER.id}`).expect(StatusCode.SuccessOK);
57+
58+
expect(JSON.parse(response.text)).toMatchObject(TESTER_DECISION_INFO);
59+
});
60+
61+
it("returns UserNotFound error if user doesn't exist", async () => {
62+
const response = await getAsStaff("/rsvp/idontexist").expect(StatusCode.ClientErrorBadRequest);
63+
64+
expect(JSON.parse(response.text)).toHaveProperty("error", "UserNotFound");
65+
});
66+
});
67+
68+
describe("PUT /rsvp", () => {
69+
it("error checking for empty query works", async () => {
70+
const response = await putAsApplicant("/rsvp/").send({}).expect(StatusCode.ClientErrorBadRequest);
71+
72+
expect(JSON.parse(response.text)).toHaveProperty("error", "InvalidParams");
73+
});
74+
75+
it("returns UserNotFound for nonexistent user", async () => {
76+
await Models.DecisionInfo.deleteOne({
77+
userId: TESTER.id,
78+
});
79+
const response = await putAsApplicant("/rsvp/").send({ isAttending: true }).expect(StatusCode.ClientErrorBadRequest);
80+
81+
expect(JSON.parse(response.text)).toHaveProperty("error", "UserNotFound");
82+
});
83+
84+
it("lets applicant accept accepted decision", async () => {
85+
await putAsApplicant("/rsvp/").send({ isAttending: true }).expect(StatusCode.SuccessOK);
86+
const stored = await Models.DecisionInfo.findOne({ userId: TESTER.id });
87+
88+
if (stored) {
89+
const storedObject = stored.toObject();
90+
expect(storedObject).toHaveProperty("response", DecisionResponse.ACCEPTED);
91+
} else {
92+
expect(stored).not.toBeNull();
93+
}
94+
});
95+
96+
it("lets applicant reject accepted decision", async () => {
97+
await putAsApplicant("/rsvp/").send({ isAttending: false }).expect(StatusCode.SuccessOK);
98+
const stored = await Models.DecisionInfo.findOne({ userId: TESTER.id });
99+
100+
if (stored) {
101+
const storedObject = stored.toObject();
102+
expect(storedObject).toHaveProperty("response", DecisionResponse.DECLINED);
103+
} else {
104+
expect(stored).not.toBeNull();
105+
}
106+
});
107+
108+
it("doesn't let applicant accept rejected decision", async () => {
109+
await Models.DecisionInfo.findOneAndUpdate({ userId: TESTER.id }, { status: DecisionStatus.REJECTED });
110+
111+
const response = await putAsApplicant("/rsvp/").send({ isAttending: false }).expect(StatusCode.ClientErrorForbidden);
112+
113+
expect(JSON.parse(response.text)).toHaveProperty("error", "NotAccepted");
114+
});
115+
});

src/services/rsvp/rsvp-router.ts

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import { Request, Response, Router } from "express";
2+
import { StatusCode } from "status-code-enum";
3+
import { strongJwtVerification } from "../../middleware/verify-jwt.js";
4+
import { JwtPayload } from "../auth/auth-models.js";
5+
import { hasElevatedPerms } from "../auth/auth-lib.js";
6+
import { DecisionStatus, DecisionResponse, DecisionInfo } from "../../database/decision-db.js";
7+
import Models from "../../database/models.js";
8+
9+
const rsvpRouter: Router = Router();
10+
11+
/**
12+
* @api {get} /rsvp/:USERID/ GET /rsvp/:USERID/
13+
* @apiGroup rsvp
14+
* @apiDescription Check RSVP decision for a given userId, provided that the current user has elevated perms
15+
*
16+
*
17+
* @apiSuccess (200: Success) {string} userId
18+
* @apiSuccess (200: Success) {string} User's applicatoin status
19+
* @apiSuccess (200: Success) {string} User's Response (whether or whether not they're attending)
20+
* @apiSuccess (200: Success) {string} Reviwer
21+
* @apiSuccess (200: Success) {boolean} Whether email has been sent
22+
* @apiSuccessExample Example Success Response:
23+
* HTTP/1.1 200 OK
24+
* {
25+
* "userId": "github0000001",
26+
* "status": "ACCEPTED",
27+
* "response": "PENDING",
28+
* "reviewer": "reviewer1",
29+
* "emailSent": true
30+
* }
31+
*
32+
* @apiUse strongVerifyErrors
33+
*/
34+
rsvpRouter.get("/:USERID", strongJwtVerification, async (req: Request, res: Response) => {
35+
const userId: string | undefined = req.params.USERID;
36+
37+
const payload: JwtPayload = res.locals.payload as JwtPayload;
38+
39+
//Sends error if caller doesn't have elevated perms
40+
if (!hasElevatedPerms(payload)) {
41+
return res.status(StatusCode.ClientErrorForbidden).send({ error: "Forbidden" });
42+
}
43+
44+
const queryResult: DecisionInfo | null = await Models.DecisionInfo.findOne({ userId: userId });
45+
46+
//Returns error if query is empty
47+
if (!queryResult) {
48+
return res.status(StatusCode.ClientErrorBadRequest).send({ error: "UserNotFound" });
49+
}
50+
51+
return res.status(StatusCode.SuccessOK).send(queryResult);
52+
});
53+
54+
/**
55+
* @api {get} /rsvp/ GET /rsvp/
56+
* @apiGroup rsvp
57+
* @apiDescription Check RSVP decision for current user, returns filtered info for attendees and unfiltered info for staff/admin
58+
*
59+
*
60+
* @apiSuccess (200: Success) {string} userId
61+
* @apiSuccess (200: Success) {string} User's applicatoin status
62+
* @apiSuccess (200: Success) {string} User's Response (whether or whether not they're attending)
63+
* @apiSuccessExample Example Success Response (caller is a user):
64+
* HTTP/1.1 200 OK
65+
* {
66+
* "userId": "github0000001",
67+
* "status": "ACCEPTED",
68+
* "response": "ACCEPTED",
69+
* }
70+
*
71+
* @apiSuccessExample Example Success Response (caller is a staff/admin):
72+
* HTTP/1.1 200 OK
73+
* {
74+
* "userId": "github0000001",
75+
* "status": "ACCEPTED",
76+
* "response": "ACCEPTED",
77+
* "reviewer": "reviewer1",
78+
* "emailSent": true,
79+
* }
80+
*
81+
* @apiUse strongVerifyErrors
82+
*/
83+
rsvpRouter.get("/", strongJwtVerification, async (_: Request, res: Response) => {
84+
const payload: JwtPayload = res.locals.payload as JwtPayload;
85+
86+
const userId: string = payload.id;
87+
88+
const queryResult: DecisionInfo | null = await Models.DecisionInfo.findOne({ userId: userId });
89+
90+
//Returns error if query is empty
91+
if (!queryResult) {
92+
return res.status(StatusCode.ClientErrorBadRequest).send({ error: "UserNotFound" });
93+
}
94+
95+
//Filters data if caller doesn't have elevated perms
96+
if (!hasElevatedPerms(payload)) {
97+
return res
98+
.status(StatusCode.SuccessOK)
99+
.send({ userId: queryResult.userId, status: queryResult.status, response: queryResult.response });
100+
}
101+
102+
return res.status(StatusCode.SuccessOK).send(queryResult);
103+
});
104+
105+
/**
106+
* @api {put} /rsvp/ Put /rsvp/
107+
* @apiGroup rsvp
108+
* @apiDescription Updates an rsvp for the currently authenticated user (determined by the JWT in the Authorization header).
109+
*
110+
* @apiBody {boolean} isAttending Whether or whether not the currently authenticated user is attending
111+
* @apiParamExample {json} Example Request:
112+
* {
113+
* "isAttending": false
114+
* }
115+
*
116+
* @apiSuccess (200: Success) {string} userId
117+
* @apiSuccess (200: Success) {string} User's applicatoin status
118+
* @apiSuccess (200: Success) {string} User's Response (whether or whether not they're attending)
119+
* @apiSuccess (200: Success) {string} Reviwer
120+
* @apiSuccess (200: Success) {boolean} Whether email has been sent
121+
* @apiSuccessExample Example Success Response:
122+
* HTTP/1.1 200 OK
123+
* {
124+
* "userId": "github0000001",
125+
* "status": "ACCEPTED",
126+
* "response": "DECLINED",
127+
* "reviewer": "reviewer1",
128+
* "emailSent": true
129+
* }
130+
*
131+
* @apiUse strongVerifyErrors
132+
*/
133+
rsvpRouter.put("/", strongJwtVerification, async (req: Request, res: Response) => {
134+
const rsvp: boolean | undefined = req.body.isAttending;
135+
136+
//Returns error if request body has no isAttending parameter
137+
if (rsvp === undefined) {
138+
return res.status(StatusCode.ClientErrorBadRequest).send({ error: "InvalidParams" });
139+
}
140+
141+
const payload: JwtPayload = res.locals.payload as JwtPayload;
142+
143+
const userid: string = payload.id;
144+
145+
const queryResult: DecisionInfo | null = await Models.DecisionInfo.findOne({ userId: userid });
146+
147+
//Returns error if query is empty
148+
if (!queryResult) {
149+
return res.status(StatusCode.ClientErrorBadRequest).send({ error: "UserNotFound" });
150+
}
151+
152+
//If the current user has not been accepted, send an error
153+
if (queryResult.status != DecisionStatus.ACCEPTED) {
154+
return res.status(StatusCode.ClientErrorForbidden).send({ error: "NotAccepted" });
155+
}
156+
157+
//If current user has been accepted, update their RSVP decision to "ACCEPTED"/"DECLINED" acoordingly
158+
const updatedDecision: DecisionInfo | null = await Models.DecisionInfo.findOneAndUpdate(
159+
{ userId: queryResult.userId },
160+
{
161+
status: queryResult.status,
162+
response: rsvp ? DecisionResponse.ACCEPTED : DecisionResponse.DECLINED,
163+
},
164+
{ new: true },
165+
);
166+
167+
if (updatedDecision) {
168+
//return res.status(StatusCode.SuccessOK).send(updatedDecision.toObject());
169+
return res.status(StatusCode.SuccessOK).send(updatedDecision);
170+
} else {
171+
return res.status(StatusCode.ServerErrorInternal).send({ error: "InternalError" });
172+
}
173+
});
174+
175+
export default rsvpRouter;

0 commit comments

Comments
 (0)