├── .gitignore ├── vercel.json ├── .env.example ├── api ├── unauthorized.ts ├── attendance │ ├── getAttendanceChange.ts │ ├── postAttendance.ts │ ├── postAttendanceChange.ts │ └── getAllAttendanceChanges.ts ├── event │ ├── getAllEvents.ts │ └── getEvent.ts ├── voting │ ├── getAllQuestions.ts │ ├── createVote.ts │ └── getVotingForMember.ts ├── member │ ├── getMemberTags.ts │ ├── updateMemberPreferences.ts │ ├── getMember.ts │ ├── createMember.ts │ └── getAllMembers.ts ├── record │ ├── getAttendanceEvents.ts │ └── getAttendanceRecordForMember.ts ├── auth.ts └── middleware.ts ├── src ├── types │ ├── tags.ts │ ├── event.ts │ ├── attendanceChange.ts │ ├── voting.ts │ ├── member.ts │ └── attendance.ts ├── controllers │ ├── authController.ts │ ├── recordController.ts │ ├── attendanceController.ts │ ├── votingController.ts │ ├── eventController.ts │ ├── attendanceChangeController.ts │ └── memberController.ts └── utils.ts ├── tsconfig.json ├── README.md ├── package.json └── tests ├── events.test.ts ├── attendanceChange.test.ts └── members.test.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .env 4 | dist 5 | /thunder-tests 6 | .vercel 7 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [{ "source": "/", "destination": "/api/unauthorized.js" }] 3 | } 4 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # db 2 | DB_HOST= 3 | DB_NAME= 4 | DB_USER= 5 | DB_PASS= 6 | 7 | # jwt 8 | JWT_SECRET= 9 | -------------------------------------------------------------------------------- /api/unauthorized.ts: -------------------------------------------------------------------------------- 1 | import { VercelRequest, VercelResponse } from "@vercel/node"; 2 | 3 | export default async(req: VercelRequest, res: VercelResponse) => { 4 | res.status(404).send("Endpoint does not exist") 5 | } -------------------------------------------------------------------------------- /src/types/tags.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const TagSchema = z.object({ 4 | membership_group: z.string(), 5 | }); 6 | 7 | export const parseTagType = (body: any) => { 8 | const parsedTag = TagSchema.parse(body); 9 | 10 | return parsedTag as Tags; 11 | }; 12 | 13 | export type Tags = z.infer; 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "Node", 5 | "target": "ES2020", 6 | "sourceMap": true, 7 | "outDir": "./dist", 8 | "esModuleInterop": true, 9 | "resolveJsonModule": true, 10 | "strict": true 11 | }, 12 | "include": ["src/**/*", "api"] 13 | } 14 | -------------------------------------------------------------------------------- /api/attendance/getAttendanceChange.ts: -------------------------------------------------------------------------------- 1 | import { VercelRequest, VercelResponse } from "@vercel/node"; 2 | import AttendanceChangeController from "../../src/controllers/attendanceChangeController"; 3 | 4 | const attendanceChangeController = new AttendanceChangeController(); 5 | 6 | export default async (req: VercelRequest, res: VercelResponse) => { 7 | try { 8 | const attendance = await attendanceChangeController.getAttendanceChange( 9 | req.query.id as string 10 | ); 11 | res.status(200).json({ attendance: attendance }); 12 | } catch (error) { 13 | res.status(500).send("Database Error"); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /api/event/getAllEvents.ts: -------------------------------------------------------------------------------- 1 | import { VercelRequest, VercelResponse } from "@vercel/node"; 2 | import EventsController from "../../src/controllers/eventController"; 3 | import { allowCors } from "../middleware"; 4 | 5 | const eventsController = new EventsController(); 6 | 7 | /* GET all events. */ 8 | const getAllEvents = async (req: VercelRequest, res: VercelResponse) => { 9 | try { 10 | const eventsList = await eventsController.getAllEvents(); 11 | res.status(200).json(eventsList); 12 | } catch (err: unknown) { 13 | res.status(500).send("Database Error"); 14 | } 15 | }; 16 | 17 | export default allowCors(getAllEvents); 18 | -------------------------------------------------------------------------------- /api/voting/getAllQuestions.ts: -------------------------------------------------------------------------------- 1 | import { VotingController } from "../../src/controllers/votingController"; 2 | import { allowCors } from "../middleware"; 3 | import { VercelRequest, VercelResponse } from "@vercel/node"; 4 | 5 | const votingController = new VotingController(); 6 | 7 | const getAllQuestions = async (req: VercelRequest, res: VercelResponse) => { 8 | try { 9 | const votingQuestions = await votingController.getAllQuestions(); 10 | res.status(200).json(votingQuestions); 11 | } catch (err: unknown) { 12 | res.status(500).send("Database Error"); 13 | } 14 | }; 15 | 16 | export default allowCors(getAllQuestions); 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SGA Tooling API 2 | 3 | This is the API for the SGA Tooling project 4 | 5 | ## Get Started 6 | 7 | Clone the project, add environment variables (listed below) in `.env`. 8 | 9 | ```env 10 | MYSQL DB 11 | DB_HOST= 12 | DB_NAME= 13 | DB_USER= 14 | DB_PASS= 15 | 16 | JWT 17 | JWT_SECRET= 18 | ``` 19 | 20 | Then do `yarn`, `yarn install `, and `yarn vercel dev` to get the project running locally. 21 | 22 | ## Tech Stack 23 | 24 | API built using Vercel's Serverless Functions and Typescript. 25 | 26 | ## Features 27 | 28 | - Authenticates users, and provides jwt on auth 29 | - Covers all relevant routes for the SGA Tooling project -------------------------------------------------------------------------------- /api/member/getMemberTags.ts: -------------------------------------------------------------------------------- 1 | import { VercelRequest, VercelResponse } from "@vercel/node"; 2 | import MembersController from "../../src/controllers/memberController"; 3 | import { allowCors } from "../middleware"; 4 | 5 | const membersController = new MembersController(); 6 | 7 | const getMemberTags = async function (req: VercelRequest, res: VercelResponse) { 8 | try { 9 | const memberTags = await membersController.getMemberTags( 10 | req.query.id as string 11 | ); 12 | res.status(200).json({ memberTags: memberTags }); 13 | } catch (error: unknown) { 14 | res.status(500).send("Database Error"); 15 | } 16 | }; 17 | 18 | export default allowCors(getMemberTags); 19 | -------------------------------------------------------------------------------- /api/record/getAttendanceEvents.ts: -------------------------------------------------------------------------------- 1 | import { VercelRequest, VercelResponse } from "@vercel/node"; 2 | import { allowCors } from "../middleware"; 3 | import EventsController from "../../src/controllers/eventController"; 4 | 5 | const eventsController = new EventsController(); 6 | 7 | const getAttendanceEvents = async (req: VercelRequest, res: VercelResponse) => { 8 | try { 9 | const events = await eventsController.getEventsForMemberRecord( 10 | req.query.id as string 11 | ); 12 | res.status(200).json({ events: events }); 13 | } catch (error: unknown) { 14 | res.status(500).send("Database Error"); 15 | } 16 | }; 17 | 18 | export default allowCors(getAttendanceEvents); 19 | -------------------------------------------------------------------------------- /api/record/getAttendanceRecordForMember.ts: -------------------------------------------------------------------------------- 1 | import { VercelRequest, VercelResponse } from "@vercel/node"; 2 | import { RecordController } from "../../src/controllers/recordController"; 3 | import { allowCors } from "../middleware"; 4 | 5 | const recordController = new RecordController(); 6 | 7 | const getAttendanceRecordForMember = async ( 8 | req: VercelRequest, 9 | res: VercelResponse 10 | ) => { 11 | try { 12 | const record = await recordController.getRecordForMember( 13 | req.query.id as string 14 | ); 15 | res.status(200).json(record); 16 | } catch (error: unknown) { 17 | res.status(500).send("Database Error"); 18 | } 19 | }; 20 | 21 | export default allowCors(getAttendanceRecordForMember); 22 | -------------------------------------------------------------------------------- /api/member/updateMemberPreferences.ts: -------------------------------------------------------------------------------- 1 | import { VercelRequest, VercelResponse } from "@vercel/node"; 2 | import MembersController from "../../src/controllers/memberController"; 3 | import { allowCors } from "../middleware"; 4 | 5 | const membersController = new MembersController(); 6 | 7 | const updateMemberPreferences = async function ( 8 | req: VercelRequest, 9 | res: VercelResponse 10 | ) { 11 | try { 12 | const member = await membersController.updateMemberPreferences( 13 | req.query.id as string 14 | ); 15 | res.status(201).json({ member: member }); 16 | } catch (error: unknown) { 17 | res.status(500).send("Database Error"); 18 | } 19 | }; 20 | 21 | export default allowCors(updateMemberPreferences); 22 | -------------------------------------------------------------------------------- /api/member/getMember.ts: -------------------------------------------------------------------------------- 1 | import { VercelRequest, VercelResponse } from "@vercel/node"; 2 | import MembersController from "../../src/controllers/memberController"; 3 | import { z } from "zod"; 4 | import { allowCors } from "../middleware"; 5 | 6 | const membersController = new MembersController(); 7 | 8 | const getMember = async function (req: VercelRequest, res: VercelResponse) { 9 | try { 10 | const member = await membersController.getMember(req.query.id as string); 11 | res.status(200).json(member); 12 | } catch (error: unknown) { 13 | error instanceof z.ZodError 14 | ? res.status(404).send("Member not found") 15 | : res.status(500).send("Database Error"); 16 | } 17 | }; 18 | 19 | export default allowCors(getMember); 20 | -------------------------------------------------------------------------------- /src/controllers/authController.ts: -------------------------------------------------------------------------------- 1 | import { Member, parseDataToMemberType } from "../types/member"; 2 | import { pool } from "../utils"; 3 | import { RowDataPacket } from "mysql2"; 4 | 5 | class AuthController { 6 | async getMember(nuid: string, lastName: string): Promise { 7 | const [memberInfo] = await pool.query( 8 | `SELECT * FROM Member WHERE nuid = ? AND last_name = ?`, 9 | [nuid, lastName] 10 | ); 11 | if (!(memberInfo as RowDataPacket[]).length) { 12 | throw new Error("Member not found"); 13 | } 14 | const member = (memberInfo as RowDataPacket[])[0]; 15 | const typedUser = parseDataToMemberType(member); 16 | return typedUser as Member; 17 | } 18 | } 19 | 20 | export default AuthController; 21 | -------------------------------------------------------------------------------- /api/voting/createVote.ts: -------------------------------------------------------------------------------- 1 | import { VotingController } from "../../src/controllers/votingController"; 2 | import { VercelRequest, VercelResponse } from "@vercel/node"; 3 | import { ZodError } from "zod"; 4 | import { allowCors } from "../middleware"; 5 | import { parseVote } from "../../src/types/voting"; 6 | 7 | const voteHistoryController = new VotingController(); 8 | 9 | const postAttendanceChange = async ( 10 | req: VercelRequest, 11 | res: VercelResponse 12 | ) => { 13 | try { 14 | const parsed = parseVote(req.body); 15 | const vote = await voteHistoryController.createVote(parsed); 16 | res.status(201).json({ vote }); 17 | } catch (error) { 18 | error instanceof ZodError 19 | ? res.status(400).send("Invalid Input") 20 | : res.status(500).send("Database Error"); 21 | } 22 | }; 23 | 24 | export default allowCors(postAttendanceChange); 25 | -------------------------------------------------------------------------------- /api/voting/getVotingForMember.ts: -------------------------------------------------------------------------------- 1 | import { allowCors } from "../middleware"; 2 | import { VercelRequest, VercelResponse } from "@vercel/node"; 3 | import { VotingController } from "../../src/controllers/votingController"; 4 | import { ZodError } from "zod"; 5 | import { parseVoteQuery } from "../../src/types/voting"; 6 | 7 | const votingController = new VotingController(); 8 | 9 | const getVotingForMember = async (req: VercelRequest, res: VercelResponse) => { 10 | try { 11 | const result = parseVoteQuery(req.query); 12 | const potentialVote = await votingController.getVotingHistory(result); 13 | res.status(200).json(potentialVote); 14 | } catch (error: unknown) { 15 | error instanceof ZodError 16 | ? res.status(400).send("Invalid Input") 17 | : res.status(500).send("Database error"); 18 | } 19 | }; 20 | 21 | export default allowCors(getVotingForMember); 22 | -------------------------------------------------------------------------------- /api/member/createMember.ts: -------------------------------------------------------------------------------- 1 | import { VercelRequest, VercelResponse } from "@vercel/node"; 2 | import MembersController from "../../src/controllers/memberController"; 3 | import { parseMemberType } from "../../src/types/member"; 4 | import { z } from "zod"; 5 | 6 | const membersController = new MembersController(); 7 | 8 | export default async function (req: VercelRequest, res: VercelResponse) { 9 | try { 10 | //try to parse the result 11 | const result = parseMemberType(req.body); 12 | const newMember = await membersController.createMember(result); 13 | res.status(201).json({ member: newMember }); 14 | } catch (err) { 15 | if (err instanceof z.ZodError) { 16 | //means we have bad inputs 17 | res.status(400).send("Invalid Data"); 18 | } else { 19 | //some database error when creating the new member 20 | res.status(500).send("Database Error"); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /api/event/getEvent.ts: -------------------------------------------------------------------------------- 1 | import { VercelRequest, VercelResponse } from "@vercel/node"; 2 | import EventsController from "../../src/controllers/eventController"; 3 | import { isEmpty } from "../../src/utils"; 4 | import { ZodError } from "zod"; 5 | import { allowCors } from "../middleware"; 6 | 7 | const eventsController = new EventsController(); 8 | 9 | /* Gets event data for a given event */ 10 | const getEvent = async (req: VercelRequest, res: VercelResponse) => { 11 | try { 12 | const event = await eventsController.getEvent(req.query.id as string); 13 | 14 | if (isEmpty(event)) { 15 | res.status(404).send("Event Not Found"); 16 | return; 17 | } 18 | 19 | res.status(200).json(event); 20 | } catch (error: unknown) { 21 | error instanceof ZodError 22 | ? res.status(400).send("Invalid Input") 23 | : res.status(500).send("Database error"); 24 | } 25 | }; 26 | 27 | export default allowCors(getEvent); 28 | -------------------------------------------------------------------------------- /api/attendance/postAttendance.ts: -------------------------------------------------------------------------------- 1 | import { VercelRequest, VercelResponse } from "@vercel/node"; 2 | import AttendanceController from "../../src/controllers/attendanceController"; 3 | import { z } from "zod"; 4 | import { allowCors } from "../middleware"; 5 | import { parseAttendanceType } from "../../src/types/attendance"; 6 | 7 | const attendanceController = new AttendanceController(); 8 | 9 | const postAttendance = async (req: VercelRequest, res: VercelResponse) => { 10 | try { 11 | const result = parseAttendanceType(req.body); 12 | const newAttendanceChange = await attendanceController.postAttendance( 13 | result 14 | ); 15 | res.status(201).json({ attendanceChange: newAttendanceChange }); 16 | } catch (error) { 17 | console.log(error); 18 | error instanceof z.ZodError 19 | ? res.status(400).send("Invalid Input") 20 | : res.status(500).send("Database Error"); 21 | } 22 | }; 23 | 24 | export default allowCors(postAttendance); 25 | -------------------------------------------------------------------------------- /src/controllers/recordController.ts: -------------------------------------------------------------------------------- 1 | import { pool } from "../utils"; 2 | import { RowDataPacket } from "mysql2"; 3 | import { EventType, parseEventType } from "../types/event"; 4 | import { 5 | HydratedAttendanceType, 6 | parseDataToHydratedAttendanceType, 7 | } from "../types/attendance"; 8 | 9 | export class RecordController { 10 | async getRecordForMember(id: string) { 11 | const [data] = await pool.query( 12 | `SELECT * FROM AttendanceRecord JOIN Event ON AttendanceRecord.event_id = Event.uuid WHERE person_id = ?`, 13 | [id] 14 | ); 15 | 16 | const parsedRowData = data as RowDataPacket[]; 17 | const record = parsedRowData 18 | .map((record): HydratedAttendanceType | null => { 19 | try { 20 | const typedRecord = parseDataToHydratedAttendanceType(record); 21 | return typedRecord as HydratedAttendanceType; 22 | } catch (err) { 23 | return null; 24 | } 25 | }) 26 | .filter((record) => record !== null); 27 | 28 | return record; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | dotenv.config(); 3 | import * as mysql2 from "mysql2"; 4 | import { v4 as uuidv4 } from "uuid"; 5 | 6 | //file to export useful functions for the rest of the files/tests 7 | export const isEmpty = (obj: any) => { 8 | for (const x in obj) { 9 | return false; 10 | } 11 | return true; 12 | }; 13 | 14 | //database 15 | export const pool = mysql2 16 | .createPool({ 17 | host: process.env.DB_HOST, 18 | user: process.env.DB_USER, 19 | password: process.env.DB_PASS, 20 | database: process.env.DB_NAME, 21 | port: 3306, 22 | }) 23 | .promise(); 24 | 25 | //as the name suggests function that creates the 32 character UID strings as uids 26 | export const createdRandomUID = (): string => { 27 | //uses the uuidv4 method and replaces the hypens with empty strings similar to how its implemented 28 | //in the python version 29 | return uuidv4().replace(/-/g, ""); 30 | }; 31 | 32 | export const castBufferToBoolean = (data: any) => { 33 | return data.readUInt8() === 1; 34 | }; 35 | -------------------------------------------------------------------------------- /api/attendance/postAttendanceChange.ts: -------------------------------------------------------------------------------- 1 | import { VercelRequest, VercelResponse } from "@vercel/node"; 2 | import { z } from "zod"; 3 | import { allowCors } from "../middleware"; 4 | import AttendanceChangeController from "../../src/controllers/attendanceChangeController"; 5 | import { AttendanceChangeRequestCreateSchema } from "../../src/types/attendanceChange"; 6 | 7 | const attendanceChangeController = new AttendanceChangeController(); 8 | 9 | const postAttendanceChange = async ( 10 | req: VercelRequest, 11 | res: VercelResponse 12 | ) => { 13 | try { 14 | const result = AttendanceChangeRequestCreateSchema.parse(req.body); 15 | const newAttendanceChange = 16 | await attendanceChangeController.postAttendanceChange(result); 17 | res.status(201).json({ attendanceChange: newAttendanceChange }); 18 | } catch (error) { 19 | console.log(error); 20 | error instanceof z.ZodError 21 | ? res.status(400).send("Invalid Input") 22 | : res.status(500).send("Database Error"); 23 | } 24 | }; 25 | 26 | export default allowCors(postAttendanceChange); 27 | -------------------------------------------------------------------------------- /src/controllers/attendanceController.ts: -------------------------------------------------------------------------------- 1 | // Controller class for the Attendance API endpoints 2 | import { RowDataPacket } from "mysql2"; 3 | import { pool } from "../utils"; 4 | import { Attendance, parseDataToAttendanceType } from "../types/attendance"; 5 | 6 | class AttendanceController { 7 | async getAttendance(memberId: string, eventId: string) { 8 | const [data] = await pool.query( 9 | "SELECT * FROM AttendanceRecord WHERE person_id = ? AND event_id = ?", 10 | [memberId, eventId] 11 | ); 12 | 13 | const parsedData = (data as RowDataPacket)[0]; 14 | return parseDataToAttendanceType(parsedData); 15 | } 16 | 17 | async postAttendance(attendance: Attendance) { 18 | let initialQuery = 19 | "INSERT INTO AttendanceRecord (person_id, event_id, attendance_status) VALUES (?, ?, ?)"; 20 | await pool.query(initialQuery, [ 21 | attendance.memberId, 22 | attendance.eventId, 23 | attendance.attendanceStatus, 24 | ]); 25 | return this.getAttendance(attendance.memberId, attendance.eventId); 26 | } 27 | } 28 | 29 | export default AttendanceController; 30 | -------------------------------------------------------------------------------- /api/member/getAllMembers.ts: -------------------------------------------------------------------------------- 1 | import { VercelRequest, VercelResponse } from "@vercel/node"; 2 | import { isEmpty } from "../../src/utils"; 3 | import MembersController from "../../src/controllers/memberController"; 4 | import { z } from "zod"; 5 | import { parseGetMemberParams } from "../../src/types/member"; 6 | 7 | const membersController = new MembersController(); 8 | 9 | export default async (req: VercelRequest, res: VercelResponse) => { 10 | let members; 11 | try { 12 | if (isEmpty(req.query)) { 13 | members = await membersController.getAllMembers(); 14 | } else { 15 | //else we validate that its one of/multiple of the supported enums 16 | const result = parseGetMemberParams(req.query); 17 | members = await membersController.getSpecificGroup(result); 18 | } 19 | //if no members found return a 404 else, send back the members 20 | !members 21 | ? res.status(404).send("Member Not Found") 22 | : res.status(200).json({ members: members }); 23 | } catch (error: unknown) { 24 | error instanceof z.ZodError 25 | ? res.status(400).send("Invalid Query Parameters") 26 | : res.status(500).send("Database Error"); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /api/attendance/getAllAttendanceChanges.ts: -------------------------------------------------------------------------------- 1 | import { VercelRequest, VercelResponse } from "@vercel/node"; 2 | import { isEmpty } from "../../src/utils"; 3 | import { ZodError } from "zod"; 4 | import { allowCors } from "../middleware"; 5 | import AttendanceChangeController from "../../src/controllers/attendanceChangeController"; 6 | import { parseAttendanceQueryType } from "../../src/types/attendance"; 7 | 8 | const attendanceChangeController = new AttendanceChangeController(); 9 | 10 | const getAllAttendanceChanges = async ( 11 | req: VercelRequest, 12 | res: VercelResponse 13 | ) => { 14 | try { 15 | let attendance; 16 | if (isEmpty(req.query)) { 17 | attendance = await attendanceChangeController.getAllAttendanceChanges(); 18 | } else { 19 | const result = parseAttendanceQueryType(req.query); 20 | attendance = await attendanceChangeController.getSpecificAttendanceChange( 21 | result 22 | ); 23 | } 24 | res.status(200).json(attendance); 25 | } catch (error: unknown) { 26 | error instanceof ZodError 27 | ? res.status(400).send(error) 28 | : res.status(500).send("Database Error"); 29 | } 30 | }; 31 | 32 | export default allowCors(getAllAttendanceChanges); 33 | -------------------------------------------------------------------------------- /src/types/event.ts: -------------------------------------------------------------------------------- 1 | import { RowDataPacket } from "mysql2"; 2 | import { z } from "zod"; 3 | 4 | const EventSchema = z 5 | .object({ 6 | id: z.string(), 7 | eventName: z.string(), 8 | startTime: z.date().optional(), 9 | endTime: z.date().optional(), 10 | signInClosed: z.boolean(), 11 | description: z.string(), 12 | location: z.string(), 13 | membershipGroup: z 14 | .array(z.enum(["New Senators Fall 2022", "All active"])) 15 | .optional(), 16 | }) 17 | .strict(); 18 | 19 | export const parseDataToEventType = (data: RowDataPacket) => { 20 | const splitMembership = data.membership_group.split(","); 21 | const typedEvent = EventSchema.parse({ 22 | id: data.uuid, 23 | eventName: data.event_name, 24 | ...(data.start_time && { startTime: new Date(data.start_time) }), 25 | ...(data.end_time && { endTime: new Date(data.end_time) }), 26 | signInClosed: !!data.sign_in_closed, 27 | description: data.description, 28 | location: data.location, 29 | membershipGroup: splitMembership, 30 | }); 31 | return typedEvent as EventType; 32 | }; 33 | 34 | export const parseEventType = (body: any): EventType => { 35 | const parsedEvent = EventSchema.parse(body); 36 | 37 | return parsedEvent as EventType; 38 | }; 39 | 40 | export type EventType = z.infer; 41 | -------------------------------------------------------------------------------- /api/auth.ts: -------------------------------------------------------------------------------- 1 | import { VercelRequest, VercelResponse } from "@vercel/node"; 2 | import AuthController from "../src/controllers/authController"; 3 | import dotenv from "dotenv"; 4 | dotenv.config(); 5 | import jwt from "jsonwebtoken"; 6 | import { allowCors } from "./middleware"; 7 | 8 | const secret = process.env.JWT_SECRET; 9 | const authController = new AuthController(); 10 | 11 | // TODO: come back to this route since auth should be used on each route as well in the middleware 12 | const auth = async (req: VercelRequest, res: VercelResponse) => { 13 | try { 14 | const member = await authController.getMember( 15 | req.body.nuid as string, 16 | req.body.lastName as string 17 | ); 18 | 19 | if (!secret) { 20 | res.status(500).json({ error: "JWT Key Error" }); 21 | return; 22 | } 23 | if (!member.activeMember) { 24 | res.status(403).json({ error: "User Not Active" }); 25 | } else if (member.signInBlocked) { 26 | res.status(403).json({ error: "User Blocked" }); 27 | } else { 28 | const token = jwt.sign({ data: member.id }, secret, { 29 | expiresIn: "1h", 30 | }); 31 | res.status(200).json({ auth: { jwt: token } }); 32 | } 33 | } catch (err) { 34 | console.log(err); 35 | res.status(400).json({ error: "User Not Found" }); 36 | } 37 | }; 38 | 39 | export default allowCors(auth, false); 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "build": "tsc", 9 | "prestart": "npm run build", 10 | "start": "node ./dist/api/index.js" 11 | }, 12 | "jest": { 13 | "verbose": true, 14 | "preset": "ts-jest", 15 | "testEnvironment": "node", 16 | "transform": { 17 | "node_modules/variables/.+\\.(j|t)sx?$": "ts-jest" 18 | } 19 | }, 20 | "keywords": [], 21 | "author": "", 22 | "license": "ISC", 23 | "devDependencies": { 24 | "@babel/core": "^7.22.9", 25 | "@babel/preset-env": "^7.22.9", 26 | "@babel/preset-typescript": "^7.22.5", 27 | "@types/express-session": "^1.17.10", 28 | "@types/jest": "^29.5.3", 29 | "@types/node": "^18.14.1", 30 | "@types/supertest": "^2.0.12", 31 | "@types/uuid": "^9.0.2", 32 | "babel-jest": "^29.6.1", 33 | "jest": "^29.7.0", 34 | "nodemon": "^2.0.20", 35 | "supertest": "^6.3.3", 36 | "ts-jest": "^29.1.1" 37 | }, 38 | "dependencies": { 39 | "@types/bcryptjs": "^2.4.6", 40 | "@types/jsonwebtoken": "^9.0.5", 41 | "@vercel/node": "^3.0.11", 42 | "bcryptjs": "^2.4.3", 43 | "dotenv": "^16.0.3", 44 | "joi": "^17.8.3", 45 | "jsonwebtoken": "^9.0.2", 46 | "mysql2": "^3.2.0", 47 | "rimraf": "^4.1.2", 48 | "typescript": "^4.9.5", 49 | "uuid": "^9.0.0", 50 | "vercel": "37.12.1", 51 | "zod": "^3.21.4" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /api/middleware.ts: -------------------------------------------------------------------------------- 1 | import { VercelApiHandler, VercelRequest, VercelResponse } from "@vercel/node"; 2 | import dotenv from "dotenv"; 3 | import jwt from "jsonwebtoken"; 4 | dotenv.config(); 5 | 6 | // Cors wrapper used on each function 7 | export const allowCors = 8 | (handler: VercelApiHandler, authenticated: boolean = true) => 9 | async (req: VercelRequest, res: VercelResponse) => { 10 | res.setHeader("Access-Control-Allow-Credentials", "true"); 11 | 12 | res.setHeader( 13 | "Access-Control-Allow-Methods", 14 | "GET,OPTIONS,PATCH,DELETE,POST,PUT" 15 | ); 16 | res.setHeader( 17 | "Access-Control-Allow-Headers", 18 | "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version" 19 | ); 20 | 21 | if (req.method === "OPTIONS") { 22 | return res.status(200).json({ 23 | body: "OK", 24 | }); 25 | } 26 | 27 | if (authenticated) { 28 | try { 29 | const token = req.cookies.token; 30 | 31 | if (!token) { 32 | return res.status(401).json({ error: "Unauthorized" }); 33 | } 34 | if (!process.env.JWT_SECRET) { 35 | return res.status(500).json({ error: "JWT Key Error" }); 36 | } 37 | 38 | jwt.verify(token, process.env.JWT_SECRET); 39 | return await handler(req, res); 40 | } catch (_e: any) { 41 | let e: Error = _e; 42 | if (e.name === "TokenExpiredError") { 43 | return res.status(401).json({ error: "Unauthorized" }); 44 | } 45 | return res.status(500).json({ error: "Unknown Error" }); 46 | } 47 | } 48 | 49 | return await handler(req, res); 50 | }; 51 | -------------------------------------------------------------------------------- /src/types/attendanceChange.ts: -------------------------------------------------------------------------------- 1 | import { RowDataPacket } from "mysql2"; 2 | import { z } from "zod"; 3 | 4 | //datetime type may be incorrect/annoying can change later 5 | export const AttendanceChangeRequestSchema = z.object({ 6 | id: z.string(), 7 | name: z.string(), 8 | timeSubmitted: z.string(), 9 | dateOfChange: z.string(), 10 | type: z.enum([ 11 | "absent", 12 | "arriving late", 13 | "leaving early", 14 | "arriving late, leaving early", 15 | ]), 16 | changeStatus: z.string(), 17 | reason: z.string(), 18 | timeArriving: z.string().optional(), 19 | timeLeaving: z.string().optional(), 20 | eventId: z.string(), 21 | memberId: z.string(), 22 | }); 23 | 24 | //datetime type may be incorrect/annoying can change later 25 | export const AttendanceChangeRequestCreateSchema = 26 | AttendanceChangeRequestSchema.partial({ 27 | id: true, 28 | changeStatus: true, 29 | }); 30 | 31 | export const parseDataToAttendanceChangeType = (data: RowDataPacket) => { 32 | const typedAttendance = AttendanceChangeRequestSchema.parse({ 33 | id: data.uuid, 34 | name: data.name, 35 | timeSubmitted: data.time_submitted, 36 | dateOfChange: data.date_of_change, 37 | type: data.type, 38 | changeStatus: data.change_status, 39 | reason: data.reason, 40 | ...(data.time_arriving && { timeArriving: data.time_arriving }), 41 | ...(data.time_leaving && { timeLeaving: data.time_leaving }), 42 | eventId: data.event_id, 43 | memberId: data.member_id, 44 | }); 45 | 46 | return typedAttendance as AttendanceChangeRequest; 47 | }; 48 | 49 | export type AttendanceChangeRequest = z.infer< 50 | typeof AttendanceChangeRequestSchema 51 | >; 52 | 53 | export type AttendanceChangeCreate = z.infer< 54 | typeof AttendanceChangeRequestCreateSchema 55 | >; 56 | -------------------------------------------------------------------------------- /src/types/voting.ts: -------------------------------------------------------------------------------- 1 | import { RowDataPacket } from "mysql2"; 2 | import { z } from "zod"; 3 | 4 | const VoteSchema = z 5 | .object({ 6 | member_id: z.string(), 7 | vote_id: z.string(), 8 | vote_selection: z.enum(["A", "Y", "N"]), 9 | }) 10 | .strict(); 11 | 12 | const VotingQuestionSchema = z 13 | .object({ 14 | uuid: z.string(), 15 | question: z.string(), 16 | description: z.string().optional(), 17 | time_start: z.string(), 18 | time_end: z.string(), 19 | }) 20 | .strict(); 21 | 22 | const VoteQuerySchema = z 23 | .object({ 24 | member_id: z.string(), 25 | vote_id: z.string().optional(), 26 | }) 27 | .strict(); 28 | 29 | export const parseDataToVote = (data: RowDataPacket) => { 30 | const typedVote = VoteSchema.parse({ 31 | member_id: data.member_id, 32 | vote_id: data.vote_id, 33 | vote_selection: data.vote_selection, 34 | }); 35 | 36 | return typedVote as VoteType; 37 | }; 38 | 39 | export const parseDataToVotingQuestion = (data: RowDataPacket) => { 40 | const typedQuestion = VotingQuestionSchema.parse({ 41 | uuid: data.uuid, 42 | question: data.question, 43 | ...(data.description && { description: data.description }), 44 | time_start: data.time_start, 45 | time_end: data.time_end, 46 | }); 47 | 48 | return typedQuestion as VotingQuestionType; 49 | }; 50 | 51 | export const parseVote = (body: any) => { 52 | const parsedVote = VoteSchema.parse(body); 53 | 54 | return parsedVote as VoteType; 55 | }; 56 | 57 | export const parseVoteQuery = (body: any) => { 58 | const parsedVoteQuery = VoteQuerySchema.parse(body); 59 | 60 | return parsedVoteQuery as VoteQueryType; 61 | }; 62 | 63 | export type VoteType = z.infer; 64 | export type VotingQuestionType = z.infer; 65 | export type VoteQueryType = z.infer; 66 | -------------------------------------------------------------------------------- /src/types/member.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { castBufferToBoolean } from "../utils"; 3 | import { RowDataPacket } from "mysql2"; 4 | 5 | const MemberSchema = z 6 | .object({ 7 | id: z.string(), 8 | nuid: z.string(), 9 | firstName: z.string(), 10 | lastName: z.string(), 11 | email: z.string(), 12 | activeMember: z.boolean(), 13 | votingRights: z.boolean(), 14 | includeInQuorum: z.boolean(), 15 | receiveNotPresentEmail: z.boolean(), 16 | signInBlocked: z.boolean(), 17 | }) 18 | .strict(); 19 | 20 | const MemberGroupSchema = z.object({ 21 | person_id: z.string(), 22 | membership_group: z.string(), 23 | }); 24 | 25 | const GetMembersParams = z 26 | .object({ 27 | group: z.string().optional(), 28 | active: z.string().optional(), 29 | includeInQuorum: z.string().optional(), 30 | }) 31 | .strict(); 32 | 33 | export const parseMemberType = (body: any) => { 34 | const parsedMember = MemberSchema.parse(body); 35 | 36 | return parsedMember as Member; 37 | }; 38 | 39 | export const parseGetMemberParams = (body: any) => { 40 | const parsedParams = GetMembersParams.parse(body); 41 | 42 | return parsedParams as GetMembersParamsType; 43 | }; 44 | 45 | export const parseDataToMemberType = (data: RowDataPacket) => { 46 | const parsedMember = MemberSchema.parse({ 47 | id: data.uuid, 48 | nuid: data.nuid, 49 | firstName: data.first_name, 50 | lastName: data.last_name, 51 | email: data.email, 52 | activeMember: castBufferToBoolean(data.active_member), 53 | votingRights: castBufferToBoolean(data.voting_rights), 54 | includeInQuorum: castBufferToBoolean(data.include_in_quorum), 55 | receiveNotPresentEmail: castBufferToBoolean(data.receive_not_present_email), 56 | signInBlocked: castBufferToBoolean(data.sign_in_blocked), 57 | }); 58 | 59 | return parsedMember as Member; 60 | }; 61 | 62 | export type Member = z.infer; 63 | export type MemberGroupType = z.infer; 64 | export type GetMembersParamsType = z.infer; 65 | -------------------------------------------------------------------------------- /src/types/attendance.ts: -------------------------------------------------------------------------------- 1 | import { RowDataPacket } from "mysql2"; 2 | import { z } from "zod"; 3 | import { EventType, parseEventType } from "./event"; 4 | 5 | const AttendanceSchema = z.object({ 6 | memberId: z.string(), 7 | eventId: z.string(), 8 | attendanceStatus: z.enum(["O", "LE"]), 9 | }); 10 | 11 | const HydratedAttendanceSchema = z 12 | .object({ 13 | memberId: z.string(), 14 | attendanceStatus: z.string(), 15 | event: z.custom(), 16 | }) 17 | .strict(); 18 | 19 | const AttendanceQuerySchema = z 20 | .object({ 21 | limit: z.string().optional(), 22 | memberId: z.string().optional(), 23 | eventId: z.string().optional(), 24 | }) 25 | .strict(); 26 | 27 | export const parseDataToAttendanceType = (data: RowDataPacket) => { 28 | const typedAttendance = AttendanceSchema.parse({ 29 | memberId: data.person_id, 30 | eventId: data.event_id, 31 | attendanceStatus: data.attendance_status, 32 | }); 33 | 34 | return typedAttendance as Attendance; 35 | }; 36 | 37 | export const parseDataToHydratedAttendanceType = (data: RowDataPacket) => { 38 | const typedRecord = HydratedAttendanceSchema.parse({ 39 | memberId: data.person_id, 40 | attendanceStatus: data.attendance_status, 41 | event: parseEventType({ 42 | id: data.event_id, 43 | eventName: data.event_name, 44 | ...(data.start_time && { 45 | startTime: new Date(data.start_time), 46 | }), 47 | ...(data.end_time && { endTime: new Date(data.end_time) }), 48 | signInClosed: !!data.sign_in_closed, 49 | description: data.description, 50 | location: data.location, 51 | membershipGroup: data.membership_group, 52 | }), 53 | }); 54 | return typedRecord as HydratedAttendanceType; 55 | }; 56 | 57 | export const parseAttendanceType = (body: any) => { 58 | const parsedAttendance = AttendanceSchema.parse(body); 59 | 60 | return parsedAttendance as Attendance; 61 | }; 62 | 63 | export const parseAttendanceQueryType = (body: any) => { 64 | const parsedQuery = AttendanceQuerySchema.parse(body); 65 | 66 | return parsedQuery as AttendanceQueryType; 67 | }; 68 | 69 | export type Attendance = z.infer; 70 | export type HydratedAttendanceType = z.infer; 71 | export type AttendanceQueryType = z.infer; 72 | -------------------------------------------------------------------------------- /src/controllers/votingController.ts: -------------------------------------------------------------------------------- 1 | import { RowDataPacket } from "mysql2"; 2 | import { pool } from "../utils"; 3 | import { parseDataToVote, VoteQueryType, VoteType } from "../types/voting"; 4 | 5 | export class VotingController { 6 | async getAllQuestions() { 7 | const [data] = await pool.query(`SELECT * from VoteQuestion`); 8 | 9 | const parsedData = data as RowDataPacket[]; 10 | const quesitonList = parsedData 11 | .map((question) => { 12 | try { 13 | return parseDataToVote(question); 14 | } catch (err) { 15 | return null; 16 | } 17 | }) 18 | .filter(Boolean); 19 | 20 | return quesitonList; 21 | } 22 | 23 | // if have both vote_id/member_id => we are trying to find whether they already voted for the event 24 | // if we have just member_id => we want to find the members voting Records to be displayed 25 | async getVotingHistory(queryParams: VoteQueryType) { 26 | const SELECTFROM = "SELECT * FROM VoteHistory"; 27 | let WHERE = ""; 28 | let data = []; 29 | 30 | const validParmsToQuery = new Map([ 31 | ["member_id", "member_id = ?"], 32 | ["vote_id", "vote_id = ?"], 33 | ]); 34 | 35 | for (let i = 0; i < Object.keys(queryParams).length; i++) { 36 | const currKey = Object.keys(queryParams)[i]; 37 | if (validParmsToQuery.has(currKey)) { 38 | if (WHERE) { 39 | WHERE += " AND " + validParmsToQuery.get(currKey); 40 | } else { 41 | WHERE += " WHERE " + validParmsToQuery.get(currKey); 42 | } 43 | data.push(Object.values(queryParams)[i]); 44 | } 45 | } 46 | 47 | const totalQuery = SELECTFROM + WHERE; 48 | const [result] = await pool.query(totalQuery, data); 49 | 50 | return result; 51 | } 52 | 53 | async createVote(vote: VoteType) { 54 | const keys = Object.keys(vote); 55 | const values = Object.values(vote); 56 | 57 | let initialQuery = "INSERT INTO VoteHistory ( "; 58 | let initialValue = " Values ( "; 59 | 60 | for (let index = 0; index < keys.length; index++) { 61 | const item = keys[index]; 62 | //unless we are at the last index: 63 | if (index !== keys.length - 1) { 64 | initialQuery += item + ", "; 65 | initialValue += "?, "; 66 | } else { 67 | initialQuery += item + ")"; 68 | initialValue += "?)"; 69 | } 70 | } 71 | 72 | const totalString = initialQuery + initialValue; 73 | // primary keys for 74 | const [result] = await pool.query(totalString, values); 75 | // subsequent query to get the item back 76 | const query = { 77 | member_id: vote.member_id, 78 | vote_id: vote.vote_id, 79 | }; 80 | 81 | const postedVote = this.getVotingHistory(query); 82 | return postedVote; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/controllers/eventController.ts: -------------------------------------------------------------------------------- 1 | // Controller class for the Event API endpoints 2 | // The controller has a class and method that call the repository. 3 | import { pool } from "../utils"; 4 | import { RowDataPacket } from "mysql2"; 5 | import { 6 | EventType, 7 | parseDataToEventType, 8 | parseEventType, 9 | } from "../types/event"; 10 | import { parseTagType } from "../types/tags"; 11 | 12 | class EventsController { 13 | async getAllEvents() { 14 | const [data] = await pool.query( 15 | "SELECT uuid, event_name, start_time, end_time,sign_in_closed, description, location, GROUP_CONCAT(membership_group) as membership_group FROM Event JOIN GroupExpectedAtEvent ON event_id = Event.uuid GROUP BY event_id" 16 | ); 17 | 18 | const parsedRowData = data as RowDataPacket[]; 19 | const Events = parsedRowData 20 | .map((event) => { 21 | try { 22 | const parsedEvent = parseDataToEventType(event); 23 | return parsedEvent; 24 | } catch (err) { 25 | return null; 26 | } 27 | }) 28 | .filter(Boolean); 29 | 30 | return Events; 31 | } 32 | 33 | async getEventsForMemberRecord(id: string) { 34 | const [data] = await pool.query( 35 | `SELECT uuid, event_name, start_time, end_time, sign_in_closed, description, location FROM AttendanceRecord JOIN Event ON AttendanceRecord.event_id = Event.uuid where person_id = ?`, 36 | [id] 37 | ); 38 | 39 | const parsedRowData = data as RowDataPacket[]; 40 | const record = parsedRowData.map((event) => { 41 | try { 42 | const typedEvent = parseEventType({ 43 | uuid: event.uuid, 44 | event_name: event.event_name, 45 | ...(event.start_time && { start_time: event.start_time }), 46 | ...(event.end_time && { end_time: event.end_time }), 47 | sign_in_closed: !!event.sign_in_closed, 48 | description: event.description, 49 | location: event.location, 50 | }); 51 | return typedEvent as EventType; 52 | } catch (err) { 53 | return null; 54 | } 55 | }); 56 | 57 | return record; 58 | } 59 | 60 | async getEvent(id: string) { 61 | const [data] = await pool.query(`SELECT * FROM Event WHERE uuid = ?`, [id]); 62 | const parsedRowData = (data as RowDataPacket)[0]; 63 | 64 | const [eventTags] = await pool.query( 65 | `SELECT membership_group FROM Event JOIN GroupExpectedAtEvent ON GroupExpectedAtEvent.event_id = Event.uuid WHERE event_id = ?`, 66 | [id] 67 | ); 68 | 69 | const parsedTags = eventTags as RowDataPacket[]; 70 | 71 | const Tags = parsedTags.map((tag) => 72 | parseTagType({ 73 | membership_group: tag.membership_group, 74 | }) 75 | ); 76 | 77 | //Tags returns a list of dictionaries 78 | // for instance [{membersip_group: 'abc}, {mebership_group: 'cba'}] 79 | // pull out the value for each dictionary 80 | const group_names = Tags.map((item) => item.membership_group); 81 | const joined_groups = group_names.join(","); 82 | 83 | // merge back with original Event 84 | const mergedEvent = { 85 | ...parsedRowData, 86 | ...{ membership_group: joined_groups }, 87 | }; 88 | 89 | // throw into parser 90 | const parsedEvent = parseDataToEventType(mergedEvent); 91 | 92 | return parsedEvent; 93 | } 94 | } 95 | 96 | export default EventsController; 97 | -------------------------------------------------------------------------------- /tests/events.test.ts: -------------------------------------------------------------------------------- 1 | import supertest from "supertest"; 2 | import { createServer } from "../src/utils"; 3 | import EventsController from "../src/controllers/eventController"; 4 | import { Event } from "../src/types/types"; 5 | 6 | const app = createServer(); 7 | 8 | const mockEvent: Event = { 9 | id: "some-test-uuid-stri", 10 | event_name: "Pretty Cool Event", 11 | start_time: "10:30", 12 | end_time: "11:30", 13 | sign_in_open: false, 14 | event_description: "test event", 15 | location: "your moms house", 16 | }; 17 | 18 | describe("Events Routes", () => { 19 | afterEach(() => { 20 | jest.resetAllMocks(); 21 | jest.clearAllMocks(); 22 | }); 23 | 24 | describe("Get all events Route", () => { 25 | it("should return the mock event ", async () => { 26 | const mockEventControllerGetAllEvents = jest 27 | .spyOn(EventsController.prototype, "getAllEvents") 28 | // @ts-ignore 29 | .mockReturnValue(mockEvent); 30 | 31 | const { statusCode, body } = await supertest(app).get("/events"); 32 | expect(statusCode).toBe(200); 33 | 34 | expect(body).toEqual(mockEvent); 35 | 36 | expect(mockEventControllerGetAllEvents).toBeCalledTimes(1); 37 | }); 38 | 39 | it("should return all the events when there are multiple events", async () => { 40 | const mockEventControllerGetAllEvents = jest 41 | .spyOn(EventsController.prototype, "getAllEvents") 42 | // @ts-ignore 43 | .mockReturnValue([mockEvent, mockEvent]); 44 | 45 | const { statusCode, body } = await supertest(app).get("/events"); 46 | expect(statusCode).toBe(200); 47 | 48 | expect(body).toEqual([mockEvent, mockEvent]); 49 | 50 | expect(mockEventControllerGetAllEvents).toBeCalledTimes(1); 51 | }); 52 | 53 | it("should return proper error when the db fails", async () => { 54 | const mockEventControllerGetAllEvents = jest 55 | .spyOn(EventsController.prototype, "getAllEvents") 56 | // @ts-ignore 57 | .mockRejectedValue("Rejecting"); 58 | 59 | const { statusCode } = await supertest(app).get("/events"); 60 | expect(statusCode).toBe(500); 61 | }); 62 | }); 63 | 64 | describe("Get individual Event Route", () => { 65 | it("should return the expected member on call", async () => { 66 | const mockEventControllerGetEvent = jest 67 | .spyOn(EventsController.prototype, "getEvent") 68 | // @ts-ignore 69 | .mockReturnValue(mockEvent); 70 | 71 | const { statusCode, body } = await supertest(app).get("/events/1"); 72 | expect(statusCode).toBe(200); 73 | 74 | expect(body).toEqual(mockEvent); 75 | }); 76 | 77 | it("should return a 400 error when there is no events with id", async () => { 78 | const mockEventControllerGetEvent = jest 79 | .spyOn(EventsController.prototype, "getEvent") 80 | // @ts-ignore 81 | .mockReturnValue([]); 82 | 83 | const { statusCode } = await supertest(app).get("/events/1"); 84 | expect(statusCode).toBe(404); 85 | }); 86 | 87 | it("should return a 500 error when the database fails", async () => { 88 | const mockEventControllerGetEvent = jest 89 | .spyOn(EventsController.prototype, "getEvent") 90 | // @ts-ignore 91 | .mockRejectedValue("Rejecting"); 92 | 93 | const { statusCode } = await supertest(app).get("/events/1"); 94 | expect(statusCode).toBe(500); 95 | }); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /src/controllers/attendanceChangeController.ts: -------------------------------------------------------------------------------- 1 | import { RowDataPacket } from "mysql2"; 2 | import { createdRandomUID, pool } from "../utils"; 3 | import { AttendanceQueryType } from "../types/attendance"; 4 | import { 5 | parseDataToAttendanceChangeType, 6 | AttendanceChangeCreate, 7 | } from "../types/attendanceChange"; 8 | 9 | class AttendanceChangeController { 10 | async getAllAttendanceChanges() { 11 | const [data] = await pool.query("SELECT * FROM AttendanceChangeRequest"); 12 | 13 | const parsedData = data as RowDataPacket[]; 14 | const AttendanceChanges = parsedData 15 | .map((attendance) => { 16 | try { 17 | const parsedAttendance = parseDataToAttendanceChangeType(attendance); 18 | return parsedAttendance; 19 | } catch (err) { 20 | return null; 21 | } 22 | }) 23 | .filter(Boolean); 24 | 25 | return AttendanceChanges; 26 | } 27 | 28 | async getAttendanceChange(id: string) { 29 | const [data] = await pool.query( 30 | "SELECT * FROM AttendanceChangeRequest WHERE uuid = ?", 31 | [id] 32 | ); 33 | 34 | const parsedData = (data as RowDataPacket)[0]; 35 | 36 | const AttendanceChange = parseDataToAttendanceChangeType(parsedData); 37 | return AttendanceChange; 38 | } 39 | 40 | async getSpecificAttendanceChange(urlArgs: AttendanceQueryType) { 41 | let SELECTFROM = "SELECT * FROM AttendanceChangeRequest"; 42 | let WHERE = ""; 43 | let LIMIT = ""; 44 | let data = []; 45 | 46 | const validParams = new Map([ 47 | ["eventID", "event_id = "], 48 | ["memberID", "member_id = "], 49 | ["limit", "LIMIT "], 50 | ]); 51 | 52 | Object.entries(urlArgs).forEach(([key, value]) => { 53 | if (validParams.has(key)) { 54 | if (WHERE) { 55 | WHERE += " AND " + validParams.get(key); 56 | } else { 57 | WHERE += " WHERE " + validParams.get(key); 58 | } 59 | data.push(value); 60 | } 61 | }); 62 | 63 | if (urlArgs.hasOwnProperty("limit")) { 64 | LIMIT += " LIMIT ?"; 65 | data.push(parseInt(urlArgs["limit"] as string)); 66 | } 67 | 68 | const totalQuery = SELECTFROM + WHERE + LIMIT; 69 | const [result] = await pool.query(totalQuery, data); 70 | 71 | // the user has no Attendance Changes 72 | if (!result) { 73 | return []; 74 | } 75 | 76 | const parsedData = result as RowDataPacket[]; 77 | const AttendanceChanges = parsedData 78 | .map((attendance) => { 79 | try { 80 | const parsedAttendance = parseDataToAttendanceChangeType(attendance); 81 | return parsedAttendance; 82 | } catch (err) { 83 | return null; 84 | } 85 | }) 86 | .filter(Boolean); 87 | 88 | return AttendanceChanges; 89 | } 90 | 91 | async postAttendanceChange(attendanceChange: AttendanceChangeCreate) { 92 | //generate the UUID(the API is responsible for creating this) 93 | const randomuuid = createdRandomUID(); 94 | //change_status is given to be pending since its just created 95 | let query = 96 | "INSERT INTO AttendanceChangeRequest (uuid, change_status, name, time_submitted, date_of_change, type, reason, time_arriving, time_leaving, event_id, member_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; 97 | const values = [randomuuid, "pending"].concat( 98 | Object.values(attendanceChange) 99 | ); 100 | await pool.query(query, values); 101 | 102 | //subsequent query to get the information of the item we just inserted 103 | const insertedAttendanceChange = this.getAttendanceChange(randomuuid); 104 | 105 | return insertedAttendanceChange; 106 | } 107 | } 108 | 109 | export default AttendanceChangeController; 110 | -------------------------------------------------------------------------------- /src/controllers/memberController.ts: -------------------------------------------------------------------------------- 1 | import { RowDataPacket } from "mysql2"; 2 | import { isEmpty, pool, createdRandomUID } from "../utils"; 3 | import { 4 | parseDataToMemberType, 5 | GetMembersParamsType, 6 | Member, 7 | } from "../types/member"; 8 | import { parseTagType } from "../types/tags"; 9 | 10 | class MembersController { 11 | async getAllMembers() { 12 | const [data] = await pool.query("SELECT * FROM Member"); 13 | const parsedData = data as RowDataPacket[]; 14 | 15 | const Members = parsedData 16 | .map((member) => { 17 | try { 18 | const parsedMember = parseDataToMemberType(member); 19 | return parsedMember; 20 | } catch (err) { 21 | return null; 22 | } 23 | }) 24 | .filter(Boolean); 25 | 26 | return Members; 27 | } 28 | 29 | async getSpecificGroup(urlArgs: GetMembersParamsType) { 30 | let SELECTFROM = 31 | "SELECT Member.id, nuid, first_name, last_name, email, active_member, can_vote, receive_email_notifs, include_in_quorum, can_log_in FROM Member "; 32 | let JOIN = ""; 33 | let WHERE = "WHERE "; 34 | 35 | let data; 36 | const validParams = new Map([ 37 | ["group", "MembershipGroup.group_name = ?"], 38 | ["active", "active_member"], 39 | ["includeInQuorum", "include_in_quorum"], 40 | ]); 41 | 42 | for (let index = 0; index < Object.keys(urlArgs).length; index++) { 43 | const item = Object.keys(urlArgs)[index]; 44 | 45 | if (index >= 1) { 46 | WHERE += " AND "; 47 | } 48 | 49 | if (item === "group") { 50 | //including this join separately because maybe its not assumed a member is in a group 51 | JOIN += 52 | "JOIN Membership ON Member.id = Membership.membership_id JOIN MembershipGroup ON Membership.group_id = MembershipGroup.id "; 53 | data = [urlArgs[item]]; 54 | } 55 | WHERE += validParams.get(item); 56 | } 57 | 58 | let totalString = SELECTFROM + JOIN + WHERE; 59 | 60 | const [result] = await pool.query(totalString, data); 61 | if (isEmpty(result)) return null; 62 | 63 | const members = result as RowDataPacket[]; 64 | const parsedMembers = members 65 | .map((member) => { 66 | try { 67 | const Member = parseDataToMemberType(member); 68 | return Member; 69 | } catch (err) { 70 | return null; 71 | } 72 | }) 73 | .filter(Boolean); 74 | 75 | return parsedMembers; 76 | } 77 | 78 | async createMember(bodyData: Member) { 79 | const randomuuid = createdRandomUID(); 80 | const keys = Object.keys(bodyData); 81 | const values = Object.values(bodyData); 82 | 83 | let initialQuery = "INSERT INTO Member (id, "; 84 | let initialValue = " Values (?,"; 85 | 86 | for (let index = 0; index < keys.length; index++) { 87 | const item = keys[index]; 88 | //unless we are at the last index: 89 | if (index !== keys.length - 1) { 90 | initialQuery += item + ", "; 91 | initialValue += "?, "; 92 | } else { 93 | initialQuery += item + ")"; 94 | initialValue += "?)"; 95 | } 96 | } 97 | 98 | const totalString = initialQuery + initialValue; 99 | const newValues = [randomuuid, ...values]; 100 | //initial query to insert the item in the db 101 | const [result] = await pool.query(totalString, newValues); 102 | 103 | //subsequent query to get the information of the item we just inserted 104 | const Member = this.getMember(randomuuid); 105 | 106 | return Member; 107 | } 108 | 109 | async getMember(id: string) { 110 | const [data] = await pool.query(`SELECT * FROM Member WHERE uuid = ?`, [ 111 | id, 112 | ]); 113 | 114 | const memberInfo = (data as RowDataPacket[])[0]; 115 | 116 | const Member = parseDataToMemberType(memberInfo); 117 | return Member; 118 | } 119 | 120 | async getMemberTags(id: string) { 121 | const [data] = await pool.query( 122 | `SELECT membership_group FROM MemberGroup WHERE person_id = ?`, 123 | [id] 124 | ); 125 | 126 | const castedValue = data as RowDataPacket[]; 127 | const Tags = castedValue.map((element) => 128 | parseTagType({ 129 | membership_group: element.membership_group, 130 | }) 131 | ); 132 | 133 | return Tags; 134 | } 135 | 136 | async updateMemberPreferences(id: string) { 137 | await pool.query( 138 | `UPDATE Member SET receive_not_present_email = NOT receive_not_present_email WHERE uuid = ?`, 139 | [id] 140 | ); 141 | 142 | return this.getMember(id); 143 | } 144 | } 145 | 146 | export default MembersController; 147 | -------------------------------------------------------------------------------- /tests/attendanceChange.test.ts: -------------------------------------------------------------------------------- 1 | import supertest from "supertest"; 2 | import { createServer } from "../src/utils"; 3 | import AttendanceController from "../src/controllers/attendanceController"; 4 | 5 | const app = createServer(); 6 | 7 | const mockAttendanceChange = { 8 | id: "howr-youd-oing-2day", 9 | request_type: "absent", 10 | reason: "just suz", 11 | time_submitted: "10:30", 12 | memberID: "idki-mout-ofid-eas1", 13 | eventID: "ihop-euli-kemy-ids1", 14 | }; 15 | 16 | describe("Attendance-Changes tests", () => { 17 | afterEach(() => { 18 | jest.resetAllMocks(); 19 | jest.clearAllMocks(); 20 | }); 21 | 22 | describe("Get Attendance Change Route", () => { 23 | it("should return the mockAttendance when no errors", async () => { 24 | const attendanceControllerGetAllMembers = jest 25 | .spyOn(AttendanceController.prototype, "getAllAttendanceChanges") 26 | // @ts-ignore 27 | .mockReturnValue(mockAttendanceChange); 28 | 29 | const { statusCode, body } = await supertest(app).get( 30 | "/attendance-changes" 31 | ); 32 | expect(statusCode).toBe(200); 33 | 34 | expect(body).toEqual(mockAttendanceChange); 35 | 36 | expect(attendanceControllerGetAllMembers).toHaveBeenCalledTimes(1); 37 | }); 38 | 39 | it("should return multiple AttendanceChanges", async () => { 40 | const attendanceControllerGetAllMembers = jest 41 | .spyOn(AttendanceController.prototype, "getAllAttendanceChanges") 42 | // @ts-ignore 43 | .mockReturnValue([mockAttendanceChange, mockAttendanceChange]); 44 | const { statusCode, body } = await supertest(app).get( 45 | "/attendance-changes" 46 | ); 47 | 48 | expect(statusCode).toBe(200); 49 | 50 | expect(body).toEqual([mockAttendanceChange, mockAttendanceChange]); 51 | 52 | expect(attendanceControllerGetAllMembers).toHaveBeenCalledTimes(1); 53 | }); 54 | 55 | it("should error invalid parameters", async () => { 56 | const attendanceControllerGetAllMembers = jest 57 | .spyOn(AttendanceController.prototype, "getSpecificAttendanceChange") 58 | // @ts-ignore 59 | .mockReturnValue([mockAttendanceChange, mockAttendanceChange]); 60 | const { statusCode, body } = await supertest(app) 61 | .get("/attendance-changes") 62 | .query({ somethingsomething: "idk" }); 63 | 64 | expect(statusCode).toBe(405); 65 | expect(attendanceControllerGetAllMembers).not.toHaveBeenCalled(); 66 | }); 67 | 68 | it("should accept valid parameters and delegate to the proper function", async () => { 69 | const attendanceControllerGetAllMembers = jest 70 | .spyOn(AttendanceController.prototype, "getSpecificAttendanceChange") 71 | // @ts-ignore 72 | .mockReturnValue(mockAttendanceChange); 73 | 74 | const { statusCode, body } = await supertest(app) 75 | .get("/attendance-changes") 76 | .query({ memberID: "areu-keep-ingt-rack" }); 77 | 78 | expect(statusCode).toBe(200); 79 | 80 | expect(body).toEqual(mockAttendanceChange); 81 | 82 | expect(attendanceControllerGetAllMembers).toHaveBeenCalledTimes(1); 83 | }); 84 | 85 | it("should not throw 404 erorrs when there is no attendance changes returned from a member", async () => { 86 | const attendanceControllerGetAllMembers = jest 87 | .spyOn(AttendanceController.prototype, "getSpecificAttendanceChange") 88 | // @ts-ignore 89 | .mockReturnValue([]); 90 | 91 | const { statusCode, body } = await supertest(app) 92 | .get("/attendance-changes") 93 | .query({ memberID: "areu-keep-ingt-rack" }); 94 | 95 | expect(statusCode).toBe(200); 96 | expect(body).toEqual([]); 97 | }); 98 | 99 | it("should throw 500 errors when there is a databse error", async () => { 100 | const attendanceControllerGetAllMembers = jest 101 | .spyOn(AttendanceController.prototype, "getAllAttendanceChanges") 102 | // @ts-ignore 103 | .mockRejectedValueOnce("Rejecting"); 104 | 105 | const { statusCode } = await supertest(app).get("/attendance-changes"); 106 | 107 | expect(statusCode).toBe(500); 108 | }); 109 | }); 110 | 111 | describe("Post Attendance Change Route", () => { 112 | const newAttendanceChange = { 113 | request_type: "absent", 114 | reason: "just suz", 115 | time_submitted: "2020-01-01T00:00:00Z", 116 | memberID: "youh-avep-utth-isto", 117 | eventID: "geth-eril-lbeh-appy", 118 | }; 119 | 120 | // request_type: z.enum(["absent", "arrive late", "leave early"]), 121 | // reason: z.string(), 122 | // time_submitted: z.string().datetime(), 123 | // arrive_time: z.string().datetime().optional(), 124 | // leave_time: z.string().datetime().optional(), 125 | // memberID: z.string(), 126 | // eventID: z.string() 127 | const mockAttendanceChangeCreate = jest 128 | .spyOn(AttendanceController.prototype, "postAttendanceChange") 129 | // @ts-ignore 130 | .mockReturnValue(newAttendanceChange); 131 | 132 | it("should throw an error when we add an unecessary field", async () => { 133 | const invalidJson = { 134 | request_type: "absent", 135 | reason: "just suz", 136 | time_submitted: "2020-01-01T00:00:00Z", 137 | memberID: "hope-this-data-good", 138 | eventID: "andv-erya-ccur-ate1", 139 | unecessaryfield: "what-happ-ened-here", 140 | }; 141 | const { statusCode } = await supertest(app) 142 | .post("/attendance-changes") 143 | .send(invalidJson); 144 | 145 | expect(statusCode).toBe(405); 146 | }); 147 | 148 | it("should throw an error when we are missing a field", async () => { 149 | const invalidJson = { 150 | request_type: "absent", 151 | reason: "just suz", 152 | time_submitted: "2020-01-01T00:00:00Z", 153 | eventID: "test-from-seank-star", 154 | }; 155 | 156 | const { statusCode } = await supertest(app) 157 | .post("/attendance-changes") 158 | .send(invalidJson); 159 | 160 | expect(statusCode).toBe(405); 161 | }); 162 | 163 | it("should throw an error when we have the wrong type for a field", async () => { 164 | const invalidJson = { 165 | request_type: "absent", 166 | reason: "just suz", 167 | time_submitted: "2020-01-01T00:00:00Z", 168 | eventID: 123456789, 169 | memberID: "test-ingi-sapa-in12", 170 | }; 171 | 172 | const { statusCode } = await supertest(app) 173 | .post("/attendance-changes") 174 | .send(invalidJson); 175 | 176 | expect(statusCode).toBe(405); 177 | }); 178 | 179 | it("should go through function and return 200 status when passing a valid object", async () => { 180 | const { statusCode } = await supertest(app) 181 | .post("/attendance-changes") 182 | .send(newAttendanceChange); 183 | 184 | expect(statusCode).toBe(200); 185 | }); 186 | }); 187 | 188 | describe("Get specific Attendance Change Route", () => { 189 | it("should return the same mockAttendance Change", async () => { 190 | const attendanceChangeControllerGetMember = jest 191 | .spyOn(AttendanceController.prototype, "getAttendanceChange") 192 | // @ts-ignore 193 | .mockReturnValue(mockAttendanceChange); 194 | 195 | const { statusCode, body } = await supertest(app).get( 196 | "/attendance-changes/1" 197 | ); 198 | expect(statusCode).toBe(200); 199 | 200 | expect(body).toEqual(mockAttendanceChange); 201 | 202 | expect(attendanceChangeControllerGetMember).toHaveBeenCalledTimes(1); 203 | }); 204 | 205 | it("should return 500 status when there is a db error", async () => { 206 | const attendanceChangeControllerGetMember = jest 207 | .spyOn(AttendanceController.prototype, "getAttendanceChange") 208 | // @ts-ignore 209 | .mockRejectedValueOnce("Rejecting"); 210 | 211 | const { statusCode } = await supertest(app).get("/attendance-changes/1"); 212 | expect(statusCode).toBe(500); 213 | 214 | expect(attendanceChangeControllerGetMember).toHaveBeenCalledTimes(1); 215 | }); 216 | }); 217 | }); 218 | -------------------------------------------------------------------------------- /tests/members.test.ts: -------------------------------------------------------------------------------- 1 | // interestingly this can actually connect to my db and return proper values, for now just using mocks 2 | // but in the future if we want to test actual functionality and logic with routes? 3 | import supertest from "supertest"; 4 | import { createServer } from "../src/utils"; 5 | import MembersController from "../src/controllers/memberController"; 6 | 7 | const app = createServer(); 8 | 9 | const mockMember = { 10 | id: "some-uuid-stri-ngv4", 11 | nuid: 123456789, 12 | first_name: "mock", 13 | last_name: "member", 14 | email: "email@adress.com", 15 | active_member: true, 16 | can_vote: true, 17 | receive_email_notifs: true, 18 | include_in_quorum: true, 19 | can_log_in: true, 20 | }; 21 | 22 | describe("Member Route tests", () => { 23 | afterEach(() => { 24 | jest.resetAllMocks(); 25 | jest.clearAllMocks(); 26 | }); 27 | 28 | describe("Get Members Route", () => { 29 | it("should return a 200 status, and the items from the getAllMembers Controller", async () => { 30 | const mockMembersController = jest 31 | .spyOn(MembersController.prototype, "getAllMembers") 32 | //@ts-ignore 33 | .mockReturnValueOnce(mockMember); 34 | 35 | const { statusCode, body } = await supertest(app).get("/members"); 36 | expect(statusCode).toBe(200); 37 | 38 | expect(body).toEqual(mockMember); 39 | 40 | expect(mockMembersController).toHaveBeenCalledTimes(1); 41 | }); 42 | 43 | it("should return all members, when there is an array of members", async () => { 44 | const multipleMembers = [mockMember, mockMember]; 45 | const mockMembersController = jest 46 | .spyOn(MembersController.prototype, "getAllMembers") 47 | //@ts-ignore 48 | .mockReturnValueOnce(multipleMembers); 49 | 50 | const { statusCode, body } = await supertest(app).get("/members"); 51 | expect(statusCode).toBe(200); 52 | 53 | expect(body).toEqual(multipleMembers); 54 | expect(mockMembersController).toHaveBeenCalledTimes(1); 55 | }); 56 | 57 | it("should return 200 when passing in a correct Query Parameter", async () => { 58 | const mockMembersControllerAllMembers = jest 59 | .spyOn(MembersController.prototype, "getAllMembers") 60 | //@ts-ignore 61 | .mockReturnValueOnce(mockMember); 62 | 63 | const mockMembersControllerGroupMembers = jest 64 | .spyOn(MembersController.prototype, "getSpecificGroup") 65 | //@ts-ignore 66 | .mockReturnValueOnce(mockMember); 67 | 68 | const { statusCode } = await supertest(app) 69 | .get("/members") 70 | .query({ group: "Sandbox" }); 71 | 72 | expect(statusCode).toBe(200); 73 | 74 | //should go to correct query parameter function 75 | expect(mockMembersControllerGroupMembers).toHaveBeenCalledTimes(1); 76 | expect(mockMembersControllerAllMembers).toHaveBeenCalledTimes(0); 77 | }); 78 | 79 | it("should return a 400 status code and an error, when passing in invalid query Params", async () => { 80 | const mockMembersControllerGroupMembers = jest 81 | .spyOn(MembersController.prototype, "getSpecificGroup") 82 | //@ts-ignore 83 | .mockReturnValueOnce(mockMember); 84 | 85 | const { statusCode } = await supertest(app) 86 | .get("/members") 87 | .query({ notAValidParam: "Sandbox" }); 88 | 89 | expect(statusCode).toBe(405); 90 | 91 | //Controller should not be called with the invalid params: 92 | expect(mockMembersControllerGroupMembers).toHaveBeenCalledTimes(0); 93 | }); 94 | 95 | it("should return a 404 error when there are no members", async () => { 96 | const mockMembersController = jest 97 | .spyOn(MembersController.prototype, "getAllMembers") 98 | //@ts-ignore 99 | .mockReturnValueOnce(null); 100 | 101 | const { statusCode } = await supertest(app).get("/members"); 102 | 103 | expect(statusCode).toBe(404); 104 | }); 105 | 106 | it("should return a 500 when the promise/database fails", async () => { 107 | const mockMembersController = jest 108 | .spyOn(MembersController.prototype, "getAllMembers") 109 | //@ts-ignore 110 | .mockRejectedValueOnce("Rejecting"); 111 | 112 | const { statusCode } = await supertest(app).get("/members"); 113 | 114 | expect(statusCode).toBe(500); 115 | }); 116 | }); 117 | 118 | describe("Post Route", () => { 119 | const mockMembersControllerCreateMember = jest 120 | .spyOn(MembersController.prototype, "createMember") 121 | // @ts-ignore 122 | .mockReturnValue(mockMember); 123 | 124 | it("should throw an error when we are missing required elements", async () => { 125 | const invalidJson = { 126 | nuid: 123456789, 127 | first_name: "another", 128 | last_name: "mockMember", 129 | email: "something", 130 | active_member: false, 131 | can_vote: false, 132 | include_in_quorum: false, 133 | }; 134 | 135 | const { statusCode } = await supertest(app) 136 | .post("/members") 137 | .send(invalidJson); 138 | expect(statusCode).toBe(404); 139 | 140 | expect(mockMembersControllerCreateMember).not.toHaveBeenCalled(); 141 | }); 142 | 143 | it("should throw an error when we pass in incorrect types for the elements", async () => { 144 | const invalidJson = { 145 | nuid: 123456789, 146 | first_name: "another", 147 | last_name: "mockMember", 148 | email: "something", 149 | active_member: false, 150 | receive_email_notifs: true, 151 | include_in_quorum: "string", 152 | can_log_in: "string", 153 | }; 154 | 155 | const { statusCode } = await supertest(app) 156 | .post("/members") 157 | .send(invalidJson); 158 | expect(statusCode).toBe(404); 159 | 160 | expect(mockMembersControllerCreateMember).not.toHaveBeenCalled(); 161 | }); 162 | 163 | it("should throw an error when we have additional props", async () => { 164 | const invalidJson = { 165 | nuid: 123456789, 166 | first_name: "another", 167 | last_name: "mockMember", 168 | email: "something", 169 | active_member: false, 170 | receive_email_notifs: true, 171 | include_in_quorum: true, 172 | can_log_in: true, 173 | unecessaryEntry: "something", 174 | }; 175 | 176 | const { statusCode } = await supertest(app) 177 | .post("/members") 178 | .send(invalidJson); 179 | 180 | expect(statusCode).toBe(404); 181 | 182 | expect(mockMembersControllerCreateMember).not.toHaveBeenCalled(); 183 | }); 184 | 185 | it("should correctly return the expected created member", async () => { 186 | const newMember = { 187 | nuid: 123456789, 188 | first_name: "another", 189 | last_name: "mockMember", 190 | email: "something", 191 | active_member: true, 192 | can_vote: true, 193 | include_in_quorum: true, 194 | receive_email_notifs: true, 195 | can_log_in: true, 196 | }; 197 | 198 | const mockMembersControllerGetMember = jest 199 | .spyOn(MembersController.prototype, "getMember") 200 | //@ts-ignore 201 | .mockReturnValueOnce(newMember); 202 | 203 | const { statusCode } = await supertest(app) 204 | .post("/members") 205 | .send(newMember); 206 | 207 | expect(statusCode).toBe(200); 208 | }); 209 | }); 210 | 211 | describe("Get specific Member", () => { 212 | it("should return a 200 status code, and the member from db when there is an availableMember", async () => { 213 | const mockMembersController = jest 214 | .spyOn(MembersController.prototype, "getMember") 215 | //@ts-ignore 216 | .mockReturnValueOnce(mockMember); 217 | 218 | const { statusCode, body } = await supertest(app).get("/members/1"); 219 | expect(body).toEqual(mockMember); 220 | 221 | expect(statusCode).toBe(200); 222 | 223 | expect(mockMembersController).toHaveBeenCalledTimes(1); 224 | }); 225 | 226 | it("should return invalid status when ther is no member that matches", async () => { 227 | const mockMembersController = jest 228 | .spyOn(MembersController.prototype, "getMember") 229 | //@ts-ignore 230 | .mockReturnValueOnce(null); 231 | 232 | const { statusCode } = await supertest(app).get("/members/1"); 233 | 234 | expect(statusCode).toBe(404); 235 | 236 | expect(mockMembersController).toHaveBeenCalledTimes(1); 237 | }); 238 | 239 | it("should return 500 error when the database fails", async () => { 240 | const mockMembersController = jest 241 | .spyOn(MembersController.prototype, "getMember") 242 | //@ts-ignore 243 | .mockRejectedValueOnce("Rejecting"); 244 | 245 | const { statusCode } = await supertest(app).get("/members/1"); 246 | 247 | expect(statusCode).toBe(500); 248 | 249 | expect(mockMembersController).toHaveBeenCalledTimes(1); 250 | }); 251 | }); 252 | }); 253 | --------------------------------------------------------------------------------