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 rsvp service #85

Merged
merged 38 commits into from
Nov 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
8e0cc84
yes
aletya Oct 8, 2023
900a0a1
Merge branch 'main' of https://github.com/HackIllinois/adonix
aletya Oct 8, 2023
3b76d14
added GET /rsvp/ and GET /rsvp/:USERID/
aletya Oct 8, 2023
d0e598d
bug fix
aletya Oct 8, 2023
52d15aa
added rsvp-router.ts
aletya Oct 15, 2023
187c65d
formating
aletya Oct 15, 2023
7529cb3
fixed JWT token processing
aletya Oct 15, 2023
8c54885
formatting
aletya Oct 15, 2023
1be07cf
resolved comments
aletya Oct 16, 2023
d0421d0
formatting
aletya Oct 16, 2023
bfa26a5
merge
aletya Oct 17, 2023
e1d800f
merge
aletya Oct 17, 2023
b0c97e8
merge conflicts
aletya Oct 17, 2023
d10b9b3
pr changes
aletya Oct 18, 2023
a680fdd
everything done except return stuffs
aletya Oct 18, 2023
d11633b
everything done except return stuffs
aletya Oct 18, 2023
1eeb6a4
return format stuffs
aletya Oct 19, 2023
e806ac5
Merge branch 'main' into dev/alex/rsvp
aletya Oct 19, 2023
d9d77f6
started working on testing
aletya Oct 20, 2023
3bacadd
Merge branch 'main' of https://github.com/HackIllinois/adonix
aletya Oct 20, 2023
34b90a0
Merge branch 'dev/alex/rsvp' of https://github.com/HackIllinois/adoni…
aletya Oct 20, 2023
9d9ec71
2/3 endpoints testing done
aletya Oct 20, 2023
c644b62
testing done
aletya Oct 22, 2023
6c56a15
formatting
aletya Oct 22, 2023
4acb35c
workaround
aletya Oct 22, 2023
0368699
pr comments resolved
aletya Oct 24, 2023
73e88a7
pr comments resolved
aletya Oct 24, 2023
be216d6
Merge branch 'main' of https://github.com/HackIllinois/adonix
aletya Oct 26, 2023
3996a4f
comments
aletya Oct 26, 2023
20f3f8e
comments
aletya Oct 26, 2023
b0f6441
Merge branch 'main' into dev/alex/rsvp
aletya Oct 26, 2023
7c9e27c
new status codes
aletya Oct 26, 2023
8df6db6
fixed tests
aletya Oct 29, 2023
8bb7851
comments
aletya Nov 1, 2023
1effbba
comments
aletya Nov 1, 2023
39d6182
changed redirect to forbidden
aletya Nov 2, 2023
8c4a4cb
changed redirect to forbidden
aletya Nov 2, 2023
b0c2000
comment
aletya Nov 2, 2023
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
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.eslintrc.cjs
tsconfig.json
coverage
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,4 @@ dist
# Auto-generated HTML files
docs/
apidocs/
devdocs/
devdocs/
2 changes: 2 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import eventRouter from "./services/event/event-router.js";
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 rsvpRouter from "./services/rsvp/rsvp-router.js";
import versionRouter from "./services/version/version-router.js";

import { InitializeConfigReader } from "./middleware/config-reader.js";
Expand All @@ -35,6 +36,7 @@ app.use("/auth/", authRouter);
app.use("/event/", eventRouter);
app.use("/newsletter/", newsletterRouter);
app.use("/profile/", profileRouter);
app.use("/rsvp/", rsvpRouter);
app.use("/staff/", staffRouter);
app.use("/user/", userRouter);
app.use("/version/", versionRouter);
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
115 changes: 115 additions & 0 deletions src/services/rsvp/rsvp-router.test.ts
aletya marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { describe, expect, it, beforeEach } from "@jest/globals";
import { TESTER, getAsAttendee, getAsStaff, putAsApplicant } from "../../testTools.js";
import { DecisionInfo, DecisionStatus, DecisionResponse } from "../../database/decision-db.js";
import Models from "../../database/models.js";
import { StatusCode } from "status-code-enum";

const TESTER_DECISION_INFO = {
userId: TESTER.id,
status: DecisionStatus.ACCEPTED,
response: DecisionResponse.PENDING,
reviewer: "reviewer1",
emailSent: true,
} satisfies DecisionInfo;

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

describe("GET /rsvp", () => {
it("gives a UserNotFound error for an non-existent user", async () => {
await Models.DecisionInfo.deleteOne({
userId: TESTER.id,
});

const response = await getAsAttendee("/rsvp/").expect(StatusCode.ClientErrorBadRequest);

expect(JSON.parse(response.text)).toHaveProperty("error", "UserNotFound");
});

it("works for an attendee user and returns filtered data", async () => {
const response = await getAsAttendee("/rsvp/").expect(StatusCode.SuccessOK);

expect(JSON.parse(response.text)).toMatchObject({
userId: TESTER.id,
status: DecisionStatus.ACCEPTED,
response: DecisionResponse.PENDING,
});
});

it("works for a staff user and returns unfiltered data", async () => {
const response = await getAsStaff("/rsvp/").expect(StatusCode.SuccessOK);

expect(JSON.parse(response.text)).toMatchObject(TESTER_DECISION_INFO);
});
});

describe("GET /rsvp/:USERID", () => {
it("returns forbidden error if caller doesn't have elevated perms", async () => {
const response = await getAsAttendee(`/rsvp/${TESTER.id}`).expect(StatusCode.ClientErrorForbidden);

expect(JSON.parse(response.text)).toHaveProperty("error", "Forbidden");
});

it("gets if caller has elevated perms", async () => {
const response = await getAsStaff(`/rsvp/${TESTER.id}`).expect(StatusCode.SuccessOK);

expect(JSON.parse(response.text)).toMatchObject(TESTER_DECISION_INFO);
});

it("returns UserNotFound error if user doesn't exist", async () => {
const response = await getAsStaff("/rsvp/idontexist").expect(StatusCode.ClientErrorBadRequest);

expect(JSON.parse(response.text)).toHaveProperty("error", "UserNotFound");
});
});

describe("PUT /rsvp", () => {
it("error checking for empty query works", async () => {
const response = await putAsApplicant("/rsvp/").send({}).expect(StatusCode.ClientErrorBadRequest);

expect(JSON.parse(response.text)).toHaveProperty("error", "InvalidParams");
});

it("returns UserNotFound for nonexistent user", async () => {
await Models.DecisionInfo.deleteOne({
userId: TESTER.id,
});
const response = await putAsApplicant("/rsvp/").send({ isAttending: true }).expect(StatusCode.ClientErrorBadRequest);

expect(JSON.parse(response.text)).toHaveProperty("error", "UserNotFound");
});

it("lets applicant accept accepted decision", async () => {
await putAsApplicant("/rsvp/").send({ isAttending: true }).expect(StatusCode.SuccessOK);
const stored = await Models.DecisionInfo.findOne({ userId: TESTER.id });

if (stored) {
const storedObject = stored.toObject();
expect(storedObject).toHaveProperty("response", DecisionResponse.ACCEPTED);
} else {
expect(stored).not.toBeNull();
}
});

it("lets applicant reject accepted decision", async () => {
await putAsApplicant("/rsvp/").send({ isAttending: false }).expect(StatusCode.SuccessOK);
const stored = await Models.DecisionInfo.findOne({ userId: TESTER.id });

if (stored) {
const storedObject = stored.toObject();
expect(storedObject).toHaveProperty("response", DecisionResponse.DECLINED);
} else {
expect(stored).not.toBeNull();
}
});

it("doesn't let applicant accept rejected decision", async () => {
await Models.DecisionInfo.findOneAndUpdate({ userId: TESTER.id }, { status: DecisionStatus.REJECTED });

const response = await putAsApplicant("/rsvp/").send({ isAttending: false }).expect(StatusCode.ClientErrorForbidden);

expect(JSON.parse(response.text)).toHaveProperty("error", "NotAccepted");
});
});
175 changes: 175 additions & 0 deletions src/services/rsvp/rsvp-router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import { Request, Response, Router } from "express";
import { StatusCode } from "status-code-enum";
import { strongJwtVerification } from "../../middleware/verify-jwt.js";
import { JwtPayload } from "../auth/auth-models.js";
import { hasElevatedPerms } from "../auth/auth-lib.js";
import { DecisionStatus, DecisionResponse, DecisionInfo } from "../../database/decision-db.js";
import Models from "../../database/models.js";

const rsvpRouter: Router = Router();

/**
* @api {get} /rsvp/:USERID/ GET /rsvp/:USERID/
* @apiGroup rsvp
* @apiDescription Check RSVP decision for a given userId, provided that the current user has elevated perms
*
*
* @apiSuccess (200: Success) {string} userId
* @apiSuccess (200: Success) {string} User's applicatoin status
* @apiSuccess (200: Success) {string} User's Response (whether or whether not they're attending)
* @apiSuccess (200: Success) {string} Reviwer
* @apiSuccess (200: Success) {boolean} Whether email has been sent
* @apiSuccessExample Example Success Response:
* HTTP/1.1 200 OK
* {
* "userId": "github0000001",
* "status": "ACCEPTED",
* "response": "PENDING",
* "reviewer": "reviewer1",
* "emailSent": true
* }
*
* @apiUse strongVerifyErrors
*/
rsvpRouter.get("/:USERID", strongJwtVerification, async (req: Request, res: Response) => {
const userId: string | undefined = req.params.USERID;

const payload: JwtPayload = res.locals.payload as JwtPayload;

//Sends error if caller doesn't have elevated perms
if (!hasElevatedPerms(payload)) {
return res.status(StatusCode.ClientErrorForbidden).send({ error: "Forbidden" });
}

const queryResult: DecisionInfo | null = await Models.DecisionInfo.findOne({ userId: userId });

//Returns error if query is empty
if (!queryResult) {
return res.status(StatusCode.ClientErrorBadRequest).send({ error: "UserNotFound" });
}

return res.status(StatusCode.SuccessOK).send(queryResult);
});

/**
* @api {get} /rsvp/ GET /rsvp/
* @apiGroup rsvp
* @apiDescription Check RSVP decision for current user, returns filtered info for attendees and unfiltered info for staff/admin
*
*
* @apiSuccess (200: Success) {string} userId
* @apiSuccess (200: Success) {string} User's applicatoin status
* @apiSuccess (200: Success) {string} User's Response (whether or whether not they're attending)
* @apiSuccessExample Example Success Response (caller is a user):
* HTTP/1.1 200 OK
* {
* "userId": "github0000001",
* "status": "ACCEPTED",
* "response": "ACCEPTED",
* }
*
* @apiSuccessExample Example Success Response (caller is a staff/admin):
* HTTP/1.1 200 OK
* {
* "userId": "github0000001",
* "status": "ACCEPTED",
* "response": "ACCEPTED",
* "reviewer": "reviewer1",
* "emailSent": true,
* }
*
* @apiUse strongVerifyErrors
*/
rsvpRouter.get("/", strongJwtVerification, async (_: Request, res: Response) => {
const payload: JwtPayload = res.locals.payload as JwtPayload;

const userId: string = payload.id;

const queryResult: DecisionInfo | null = await Models.DecisionInfo.findOne({ userId: userId });

//Returns error if query is empty
if (!queryResult) {
return res.status(StatusCode.ClientErrorBadRequest).send({ error: "UserNotFound" });
}

//Filters data if caller doesn't have elevated perms
if (!hasElevatedPerms(payload)) {
return res
.status(StatusCode.SuccessOK)
.send({ userId: queryResult.userId, status: queryResult.status, response: queryResult.response });
}

return res.status(StatusCode.SuccessOK).send(queryResult);
});

/**
* @api {put} /rsvp/ Put /rsvp/
* @apiGroup rsvp
* @apiDescription Updates an rsvp for the currently authenticated user (determined by the JWT in the Authorization header).
*
* @apiBody {boolean} isAttending Whether or whether not the currently authenticated user is attending
* @apiParamExample {json} Example Request:
* {
* "isAttending": false
* }
*
* @apiSuccess (200: Success) {string} userId
* @apiSuccess (200: Success) {string} User's applicatoin status
* @apiSuccess (200: Success) {string} User's Response (whether or whether not they're attending)
* @apiSuccess (200: Success) {string} Reviwer
* @apiSuccess (200: Success) {boolean} Whether email has been sent
* @apiSuccessExample Example Success Response:
* HTTP/1.1 200 OK
* {
* "userId": "github0000001",
* "status": "ACCEPTED",
* "response": "DECLINED",
* "reviewer": "reviewer1",
* "emailSent": true
* }
*
* @apiUse strongVerifyErrors
*/
rsvpRouter.put("/", strongJwtVerification, async (req: Request, res: Response) => {
const rsvp: boolean | undefined = req.body.isAttending;

//Returns error if request body has no isAttending parameter
if (rsvp === undefined) {
return res.status(StatusCode.ClientErrorBadRequest).send({ error: "InvalidParams" });
}

const payload: JwtPayload = res.locals.payload as JwtPayload;

const userid: string = payload.id;

const queryResult: DecisionInfo | null = await Models.DecisionInfo.findOne({ userId: userid });

//Returns error if query is empty
if (!queryResult) {
return res.status(StatusCode.ClientErrorBadRequest).send({ error: "UserNotFound" });
}

//If the current user has not been accepted, send an error
if (queryResult.status != DecisionStatus.ACCEPTED) {
return res.status(StatusCode.ClientErrorForbidden).send({ error: "NotAccepted" });
}

//If current user has been accepted, update their RSVP decision to "ACCEPTED"/"DECLINED" acoordingly
const updatedDecision: DecisionInfo | null = await Models.DecisionInfo.findOneAndUpdate(
{ userId: queryResult.userId },
{
status: queryResult.status,
response: rsvp ? DecisionResponse.ACCEPTED : DecisionResponse.DECLINED,
},
{ new: true },
);

if (updatedDecision) {
//return res.status(StatusCode.SuccessOK).send(updatedDecision.toObject());
return res.status(StatusCode.SuccessOK).send(updatedDecision);
} else {
return res.status(StatusCode.ServerErrorInternal).send({ error: "InternalError" });
}
});

export default rsvpRouter;
Loading