From f48451c37f5c2dbca377f4e138065c295cb25211 Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Tue, 12 Nov 2024 10:51:38 -0600 Subject: [PATCH 1/4] merch broken but ticketing works? --- src/config.ts | 4 + src/errors/index.ts | 22 ++++ src/functions/validation.ts | 7 ++ src/index.ts | 2 + src/plugins/errorHandler.ts | 1 + src/roles.ts | 1 + src/routes/tickets.ts | 210 ++++++++++++++++++++++++++++++++++++ 7 files changed, 247 insertions(+) create mode 100644 src/functions/validation.ts create mode 100644 src/routes/tickets.ts diff --git a/src/config.ts b/src/config.ts index 2ca4edf8..8bd25a05 100644 --- a/src/config.ts +++ b/src/config.ts @@ -21,6 +21,8 @@ type GenericConfigType = { ConfigSecretName: string; UpcomingEventThresholdSeconds: number; AwsRegion: string; + MerchStorePurchasesTableName: string; + TicketPurchasesTableName: string; }; type EnvironmentConfigType = { @@ -32,6 +34,8 @@ const genericConfig: GenericConfigType = { ConfigSecretName: "infra-core-api-config", UpcomingEventThresholdSeconds: 1800, // 30 mins AwsRegion: process.env.AWS_REGION || "us-east-1", + MerchStorePurchasesTableName: "infra-merchstore-purchase-history", + TicketPurchasesTableName: "infra-events-tickets", } as const; const environmentConfig: EnvironmentConfigType = { diff --git a/src/errors/index.ts b/src/errors/index.ts index 438e04f4..7e05339f 100644 --- a/src/errors/index.ts +++ b/src/errors/index.ts @@ -122,3 +122,25 @@ export class DiscordEventError extends BaseError<"DiscordEventError"> { }); } } + +export class TicketNotFoundError extends BaseError<"TicketNotFoundError"> { + constructor({ message }: { message?: string }) { + super({ + name: "TicketNotFoundError", + id: 108, + message: message || "Could not find the ticket presented.", + httpStatusCode: 404, + }); + } +} + +export class TicketNotValidError extends BaseError<"TicketNotValidError"> { + constructor({ message }: { message?: string }) { + super({ + name: "TicketNotValidError", + id: 109, + message: message || "Ticket presented was found but is not valid.", + httpStatusCode: 400, + }); + } +} diff --git a/src/functions/validation.ts b/src/functions/validation.ts new file mode 100644 index 00000000..b9f241f8 --- /dev/null +++ b/src/functions/validation.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +export function validateEmail(email: string): boolean { + const emailSchema = z.string().email(); + const result = emailSchema.safeParse(email); + return result.success; +} diff --git a/src/index.ts b/src/index.ts index 3df269fe..cdab0802 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,6 +15,7 @@ import organizationsPlugin from "./routes/organizations.js"; import icalPlugin from "./routes/ics.js"; import vendingPlugin from "./routes/vending.js"; import * as dotenv from "dotenv"; +import ticketsPlugin from "./routes/tickets.js"; dotenv.config(); const now = () => Date.now(); @@ -71,6 +72,7 @@ async function init() { api.register(eventsPlugin, { prefix: "/events" }); api.register(organizationsPlugin, { prefix: "/organizations" }); api.register(icalPlugin, { prefix: "/ical" }); + api.register(ticketsPlugin, { prefix: "/tickets" }); if (app.runEnvironment === "dev") { api.register(vendingPlugin, { prefix: "/vending" }); } diff --git a/src/plugins/errorHandler.ts b/src/plugins/errorHandler.ts index b83a3f2b..b1d998dc 100644 --- a/src/plugins/errorHandler.ts +++ b/src/plugins/errorHandler.ts @@ -27,6 +27,7 @@ const errorHandlerPlugin = fp(async (fastify) => { finalErr.toString(), ); } else if (err instanceof Error) { + request.log.error(err); request.log.error( { errName: err.name, errMessage: err.message }, "Native unhandled error: response sent to client.", diff --git a/src/roles.ts b/src/roles.ts index e571592d..af077f23 100644 --- a/src/roles.ts +++ b/src/roles.ts @@ -3,6 +3,7 @@ export const runEnvironments = ["dev", "prod"] as const; export type RunEnvironment = (typeof runEnvironments)[number]; export enum AppRoles { EVENTS_MANAGER = "manage:events", + TICKET_SCANNER = "scan:tickets", } export const allAppRoles = Object.values(AppRoles).filter( (value) => typeof value === "string", diff --git a/src/routes/tickets.ts b/src/routes/tickets.ts new file mode 100644 index 00000000..54b49625 --- /dev/null +++ b/src/routes/tickets.ts @@ -0,0 +1,210 @@ +import { FastifyPluginAsync } from "fastify"; +import { z } from "zod"; +import { DynamoDBClient, UpdateItemCommand } from "@aws-sdk/client-dynamodb"; +import { genericConfig } from "../config.js"; +import { + BaseError, + DatabaseFetchError, + TicketNotFoundError, + TicketNotValidError, + UnauthenticatedError, + ValidationError, +} from "../errors/index.js"; +import { unmarshall } from "@aws-sdk/util-dynamodb"; +import { validateEmail } from "../functions/validation.js"; +import { AppRoles } from "../roles.js"; +import { zodToJsonSchema } from "zod-to-json-schema"; + +const postMerchSchema = z.object({ + type: z.literal("merch"), + email: z.string().email(), + stripe_pi: z.string().min(1), +}); + +const postTicketSchema = z.object({ + type: z.literal("ticket"), + ticket_id: z.string().min(1), +}); + +const purchaseSchema = z.object({ + email: z.string().email(), + productId: z.string(), + quantity: z.number().int().positive(), + size: z.string().optional(), +}); + +type PurchaseData = z.infer; + +const responseJsonSchema = zodToJsonSchema( + z.object({ + valid: z.boolean(), + type: z.enum(["merch", "ticket"]), + ticketId: z.string().min(1), + purchaserData: purchaseSchema, + }), +); + +const postSchema = z.union([postMerchSchema, postTicketSchema]); + +type VerifyPostRequest = z.infer; + +const dynamoClient = new DynamoDBClient({ + region: genericConfig.AwsRegion, +}); + +const ticketsPlugin: FastifyPluginAsync = async (fastify, _options) => { + fastify.post<{ Body: VerifyPostRequest }>( + "/checkIn", + { + schema: { + response: { 200: responseJsonSchema }, + }, + preValidation: async (request, reply) => { + await fastify.zodValidateBody(request, reply, postSchema); + }, + onRequest: async (request, reply) => { + await fastify.authorize(request, reply, [AppRoles.TICKET_SCANNER]); + }, + }, + async (request, reply) => { + let command: UpdateItemCommand; + let ticketId: string; + if (!request.username) { + throw new UnauthenticatedError({ message: "Could not find username." }); + } + switch (request.body.type) { + case "merch": + ticketId = request.body.stripe_pi; + command = new UpdateItemCommand({ + TableName: genericConfig.MerchStorePurchasesTableName, + Key: { + stripe_pi: { S: ticketId }, + }, + UpdateExpression: "SET fulfilled = :true_val", + ConditionExpression: "#email = :email_val", // Added # to reference the attribute name + ExpressionAttributeNames: { + "#email": "email", // Define the attribute name + }, + ExpressionAttributeValues: { + ":true_val": { BOOL: true }, + ":email_val": { S: request.body.email }, + }, + ReturnValues: "ALL_OLD", + }); + break; + case "ticket": + ticketId = request.body.ticket_id; + command = new UpdateItemCommand({ + TableName: genericConfig.TicketPurchasesTableName, + Key: { + ticket_id: { S: ticketId }, + }, + UpdateExpression: "SET #used = :trueValue", + ExpressionAttributeNames: { + "#used": "used", + }, + ExpressionAttributeValues: { + ":trueValue": { BOOL: true }, + }, + ReturnValues: "ALL_OLD", + }); + break; + default: + throw new ValidationError({ message: `Unknown verification type!` }); + } + let purchaserData: PurchaseData; + try { + const ticketEntry = await dynamoClient.send(command); + if (!ticketEntry.Attributes) { + throw new DatabaseFetchError({ + message: "Could not find ticket data", + }); + } + const attributes = unmarshall(ticketEntry.Attributes); + if (attributes["refunded"]) { + throw new TicketNotValidError({ + message: "Ticket was already refunded.", + }); + } + if (attributes["used"]) { + throw new TicketNotValidError({ + message: "Ticket has already been used.", + }); + } + if (request.body.type === "ticket") { + const rawData = attributes["ticketholder_netid"]; + const isEmail = validateEmail(attributes["ticketholder_netid"]); + purchaserData = { + email: isEmail ? rawData : `${rawData}@illinois.edu`, + productId: attributes["event_id"], + quantity: 1, + }; + } else { + purchaserData = { + email: attributes["email"], + productId: attributes["item_id"], + quantity: attributes["quantity"], + size: attributes["size"], + }; + } + } catch (e: unknown) { + if (!(e instanceof Error)) { + throw e; + } + request.log.error(e); + if (e instanceof BaseError) { + throw e; + } + if (e.name === "ConditionalCheckFailedException") { + throw new TicketNotFoundError({ + message: "Ticket does not exist", + }); + } + throw new DatabaseFetchError({ + message: "Could not set ticket to used - database operation failed", + }); + } + const response = { + valid: true, + type: request.body.type, + ticketId, + purchaserData, + }; + switch (request.body.type) { + case "merch": + ticketId = request.body.stripe_pi; + command = new UpdateItemCommand({ + TableName: genericConfig.MerchStorePurchasesTableName, + Key: { + stripe_pi: { S: ticketId }, + }, + UpdateExpression: "SET scannerEmail = :scanner_email", + ConditionExpression: "email = :email_val", + ExpressionAttributeValues: { + ":scanner_email": { S: request.username }, + }, + }); + break; + case "ticket": + ticketId = request.body.ticket_id; + command = new UpdateItemCommand({ + TableName: genericConfig.TicketPurchasesTableName, + Key: { + ticket_id: { S: ticketId }, + }, + UpdateExpression: "SET scannerEmail = :scanner_email", + ExpressionAttributeValues: { + ":scanner_email": { S: request.username }, + }, + }); + break; + default: + throw new ValidationError({ message: `Unknown verification type!` }); + } + await dynamoClient.send(command); + reply.send(response); + }, + ); +}; + +export default ticketsPlugin; From 0c48705fd3f2e87bea03be654bb124af308952f2 Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Tue, 12 Nov 2024 12:45:42 -0600 Subject: [PATCH 2/4] fix the merch route --- src/routes/tickets.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/routes/tickets.ts b/src/routes/tickets.ts index 54b49625..72590a59 100644 --- a/src/routes/tickets.ts +++ b/src/routes/tickets.ts @@ -81,9 +81,9 @@ const ticketsPlugin: FastifyPluginAsync = async (fastify, _options) => { stripe_pi: { S: ticketId }, }, UpdateExpression: "SET fulfilled = :true_val", - ConditionExpression: "#email = :email_val", // Added # to reference the attribute name + ConditionExpression: "#email = :email_val", ExpressionAttributeNames: { - "#email": "email", // Define the attribute name + "#email": "email", }, ExpressionAttributeValues: { ":true_val": { BOOL: true }, @@ -126,7 +126,7 @@ const ticketsPlugin: FastifyPluginAsync = async (fastify, _options) => { message: "Ticket was already refunded.", }); } - if (attributes["used"]) { + if (attributes["used"] || attributes["fulfilled"]) { throw new TicketNotValidError({ message: "Ticket has already been used.", }); @@ -182,6 +182,7 @@ const ticketsPlugin: FastifyPluginAsync = async (fastify, _options) => { ConditionExpression: "email = :email_val", ExpressionAttributeValues: { ":scanner_email": { S: request.username }, + ":email_val": { S: request.body.email }, }, }); break; From e0a4fa56e52bc07459de96c8491f3550d64580f4 Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Tue, 12 Nov 2024 14:33:57 -0600 Subject: [PATCH 3/4] add unit tests --- src/routes/tickets.ts | 12 +- tests/unit/auth.test.ts | 3 +- tests/unit/mockMerchPurchases.testdata.ts | 121 +++++++++++ tests/unit/mockTickets.testdata.ts | 84 +++++++ tests/unit/tickets.test.ts | 254 ++++++++++++++++++++++ 5 files changed, 467 insertions(+), 7 deletions(-) create mode 100644 tests/unit/mockMerchPurchases.testdata.ts create mode 100644 tests/unit/mockTickets.testdata.ts create mode 100644 tests/unit/tickets.test.ts diff --git a/src/routes/tickets.ts b/src/routes/tickets.ts index 72590a59..5b9f5cf7 100644 --- a/src/routes/tickets.ts +++ b/src/routes/tickets.ts @@ -18,12 +18,12 @@ import { zodToJsonSchema } from "zod-to-json-schema"; const postMerchSchema = z.object({ type: z.literal("merch"), email: z.string().email(), - stripe_pi: z.string().min(1), + stripePi: z.string().min(1), }); const postTicketSchema = z.object({ type: z.literal("ticket"), - ticket_id: z.string().min(1), + ticketId: z.string().min(1), }); const purchaseSchema = z.object({ @@ -74,7 +74,7 @@ const ticketsPlugin: FastifyPluginAsync = async (fastify, _options) => { } switch (request.body.type) { case "merch": - ticketId = request.body.stripe_pi; + ticketId = request.body.stripePi; command = new UpdateItemCommand({ TableName: genericConfig.MerchStorePurchasesTableName, Key: { @@ -93,7 +93,7 @@ const ticketsPlugin: FastifyPluginAsync = async (fastify, _options) => { }); break; case "ticket": - ticketId = request.body.ticket_id; + ticketId = request.body.ticketId; command = new UpdateItemCommand({ TableName: genericConfig.TicketPurchasesTableName, Key: { @@ -172,7 +172,7 @@ const ticketsPlugin: FastifyPluginAsync = async (fastify, _options) => { }; switch (request.body.type) { case "merch": - ticketId = request.body.stripe_pi; + ticketId = request.body.stripePi; command = new UpdateItemCommand({ TableName: genericConfig.MerchStorePurchasesTableName, Key: { @@ -187,7 +187,7 @@ const ticketsPlugin: FastifyPluginAsync = async (fastify, _options) => { }); break; case "ticket": - ticketId = request.body.ticket_id; + ticketId = request.body.ticketId; command = new UpdateItemCommand({ TableName: genericConfig.TicketPurchasesTableName, Key: { diff --git a/tests/unit/auth.test.ts b/tests/unit/auth.test.ts index aaadffdb..42d2c9af 100644 --- a/tests/unit/auth.test.ts +++ b/tests/unit/auth.test.ts @@ -7,6 +7,7 @@ import { mockClient } from "aws-sdk-client-mock"; import init from "../../src/index.js"; import { secretJson, secretObject, jwtPayload } from "./secret.testdata.js"; import jwt from "jsonwebtoken"; +import { allAppRoles } from "../../src/roles.js"; const ddbMock = mockClient(SecretsManagerClient); @@ -48,6 +49,6 @@ test("Test happy path", async () => { const jsonBody = await response.json(); expect(jsonBody).toEqual({ username: "infra-unit-test@acm.illinois.edu", - roles: ["manage:events"], + roles: allAppRoles, }); }); diff --git a/tests/unit/mockMerchPurchases.testdata.ts b/tests/unit/mockMerchPurchases.testdata.ts new file mode 100644 index 00000000..c06c77b4 --- /dev/null +++ b/tests/unit/mockMerchPurchases.testdata.ts @@ -0,0 +1,121 @@ +import { unmarshall } from "@aws-sdk/util-dynamodb"; +const fulfilledMerchItem1 = { + stripe_pi: { + S: "pi_3Q5GewDiGOXU9RuS16txRR5D", + }, + email: { + S: "testing0@illinois.edu", + }, + fulfilled: { + BOOL: true, + }, + item_id: { + S: "sigpwny_fallctf_2022_shirt", + }, + quantity: { + N: "1", + }, + refunded: { + BOOL: false, + }, + scannerEmail: { + S: "dsingh14@illinois.edu", + }, + size: { + S: "M", + }, +}; + +const unfulfilledMerchItem1 = { + stripe_pi: { + S: "pi_8J4NrYdA3S7cW8Ty92FnGJ6L", + }, + email: { + S: "testing1@illinois.edu", + }, + fulfilled: { + BOOL: false, + }, + item_id: { + S: "2024_fa_barcrawl", + }, + quantity: { + N: "3", + }, + refunded: { + BOOL: false, + }, + size: { + S: "L", + }, +}; + +const refundedMerchItem = { + stripe_pi: { + S: "pi_6T9QvUwR2IOj4CyF35DsXK7P", + }, + email: { + S: "testing2@illinois.edu", + }, + fulfilled: { + BOOL: false, + }, + item_id: { + S: "2024_fa_barcrawl", + }, + quantity: { + N: "3", + }, + refunded: { + BOOL: true, + }, + size: { + S: "L", + }, +}; + +const fulfilledMerchItem2 = { + stripe_pi: { + S: "pi_5L8SwOdN9PXu6RyV83FgQK1C", + }, + email: { + S: "testing2@illinois.edu", + }, + fulfilled: { + BOOL: true, + }, + item_id: { + S: "2024_fa_barcrawl", + }, + quantity: { + N: "1", + }, + refunded: { + BOOL: false, + }, + size: { + S: "XS", + }, +}; + +const dynamoTableData = [ + fulfilledMerchItem1, + unfulfilledMerchItem1, + refundedMerchItem, + fulfilledMerchItem2, +]; + +const dynamoTableDataUnmarshalled = dynamoTableData.map((x: any) => { + const temp = unmarshall(x); + delete temp.createdBy; + return temp; +}); + +export { + dynamoTableData, + fulfilledMerchItem1, + unfulfilledMerchItem1, + refundedMerchItem, + fulfilledMerchItem2, + dynamoTableDataUnmarshalled, +}; diff --git a/tests/unit/mockTickets.testdata.ts b/tests/unit/mockTickets.testdata.ts new file mode 100644 index 00000000..cbbb501c --- /dev/null +++ b/tests/unit/mockTickets.testdata.ts @@ -0,0 +1,84 @@ +import { unmarshall } from "@aws-sdk/util-dynamodb"; +const fulfilledTicket1 = { + ticket_id: { + S: "975b4470cf37d7cf20fd404a711513fd1d1e68259ded27f10727d1384961843d", + }, + event_id: { + S: "fa23_barcrawl", + }, + payment_method: { + S: "stripe_autocreate", + }, + purchase_time: { + N: "1702347952", + }, + scannerEmail: { + S: "dsingh14@illinois.edu", + }, + ticketholder_netid: { + S: "dsingh14", + }, + used: { + BOOL: true, + }, +}; +const unfulfilledTicket1 = { + ticket_id: { + S: "9d98e1e3c2138c93dd5a284239eddfa9c3037a0862972cd0f51ee1b54257a37e", + }, + event_id: { + S: "fa23_barcrawl", + }, + payment_method: { + S: "stripe_autocreate", + }, + purchase_time: { + N: "1702347952", + }, + ticketholder_netid: { + S: "dsingh14", + }, + used: { + BOOL: false, + }, +}; +const unfulfilledTicket1WithEmail = { + ticket_id: { + S: "444ce3c3befe4a1f6b0ba940b8ff7dd91dda74e1a37ca8f5f16c8422a829d7f7", + }, + event_id: { + S: "fa22_barcrawl", + }, + payment_method: { + S: "stripe_autocreate", + }, + purchase_time: { + N: "1704347923", + }, + ticketholder_netid: { + S: "testinguser1@illinois.edu", + }, + used: { + BOOL: false, + }, +}; + +const dynamoTableData = [ + fulfilledTicket1, + unfulfilledTicket1, + unfulfilledTicket1WithEmail, +]; + +const dynamoTableDataUnmarshalled = dynamoTableData.map((x: any) => { + const temp = unmarshall(x); + delete temp.createdBy; + return temp; +}); + +export { + dynamoTableData, + fulfilledTicket1, + unfulfilledTicket1, + unfulfilledTicket1WithEmail, + dynamoTableDataUnmarshalled, +}; diff --git a/tests/unit/tickets.test.ts b/tests/unit/tickets.test.ts new file mode 100644 index 00000000..3db0a657 --- /dev/null +++ b/tests/unit/tickets.test.ts @@ -0,0 +1,254 @@ +import { afterAll, expect, test, beforeEach, vi, describe } from "vitest"; +import { DynamoDBClient, UpdateItemCommand } from "@aws-sdk/client-dynamodb"; +import { mockClient } from "aws-sdk-client-mock"; +import init from "../../src/index.js"; +import { EventGetResponse } from "../../src/routes/events.js"; +import { secretObject } from "./secret.testdata.js"; +import { + fulfilledMerchItem1, + refundedMerchItem, + unfulfilledMerchItem1, +} from "./mockMerchPurchases.testdata.js"; +import { createJwt } from "./auth.test.js"; +import supertest from "supertest"; +import { + unfulfilledTicket1, + unfulfilledTicket1WithEmail, +} from "./mockTickets.testdata.js"; + +const ddbMock = mockClient(DynamoDBClient); +const jwt_secret = secretObject["jwt_key"]; +vi.stubEnv("JwtSigningKey", jwt_secret); + +const app = await init(); + +describe("Test ticket purchase verification", async () => { + test("Happy path: fulfill an unfulfilled item", async () => { + ddbMock + .on(UpdateItemCommand) + .resolvesOnce({ Attributes: unfulfilledTicket1 }) + .resolvesOnce({}); + + const testJwt = createJwt(); + await app.ready(); + const response = await supertest(app.server) + .post("/api/v1/tickets/checkIn") + .set("authorization", `Bearer ${testJwt}`) + .send({ + type: "ticket", + ticketId: + "9d98e1e3c2138c93dd5a284239eddfa9c3037a0862972cd0f51ee1b54257a37e", + }); + const responseDataJson = response.body as EventGetResponse; + expect(response.statusCode).toEqual(200); + expect(responseDataJson).toEqual({ + valid: true, + type: "ticket", + ticketId: + "9d98e1e3c2138c93dd5a284239eddfa9c3037a0862972cd0f51ee1b54257a37e", + purchaserData: { + email: `${unfulfilledTicket1["ticketholder_netid"]["S"]}@illinois.edu`, + productId: unfulfilledTicket1["event_id"]["S"], + quantity: 1, + }, + }); + }); + test("Happy path: fulfill an unfulfilled item parses NetId versus email correctly", async () => { + ddbMock + .on(UpdateItemCommand) + .resolvesOnce({ Attributes: unfulfilledTicket1WithEmail }) + .resolvesOnce({}); + + const testJwt = createJwt(); + await app.ready(); + const response = await supertest(app.server) + .post("/api/v1/tickets/checkIn") + .set("authorization", `Bearer ${testJwt}`) + .send({ + type: "ticket", + ticketId: + "9d98e1e3c2138c93dd5a284239eddfa9c3037a0862972cd0f51ee1b54257a37e", + }); + const responseDataJson = response.body as EventGetResponse; + expect(response.statusCode).toEqual(200); + expect(responseDataJson).toEqual({ + valid: true, + type: "ticket", + ticketId: + "9d98e1e3c2138c93dd5a284239eddfa9c3037a0862972cd0f51ee1b54257a37e", + purchaserData: { + email: unfulfilledTicket1WithEmail["ticketholder_netid"]["S"], + productId: unfulfilledTicket1WithEmail["event_id"]["S"], + quantity: 1, + }, + }); + }); + test("Sad path: merch info with ticket type should fail", async () => { + ddbMock + .on(UpdateItemCommand) + .resolvesOnce({ Attributes: refundedMerchItem }) + .resolvesOnce({}); + + const testJwt = createJwt(); + await app.ready(); + const response = await supertest(app.server) + .post("/api/v1/tickets/checkIn") + .set("authorization", `Bearer ${testJwt}`) + .send({ + type: "ticket", + email: "testing2@illinois.edu", + stripePi: "pi_6T9QvUwR2IOj4CyF35DsXK7P", + }); + const responseDataJson = response.body as EventGetResponse; + expect(response.statusCode).toEqual(400); + expect(responseDataJson).toEqual({ + error: true, + id: 104, + message: + 'Invalid literal value, expected "merch" at "type", or Required at "ticketId"', + name: "ValidationError", + }); + }); + test("Sad path: fulfilling an already-fulfilled ticket item fails", async () => { + ddbMock + .on(UpdateItemCommand) + .resolvesOnce({ Attributes: fulfilledMerchItem1 }) + .resolvesOnce({}); + + const testJwt = createJwt(); + await app.ready(); + const response = await supertest(app.server) + .post("/api/v1/tickets/checkIn") + .set("authorization", `Bearer ${testJwt}`) + .send({ + type: "ticket", + ticketId: + "975b4470cf37d7cf20fd404a711513fd1d1e68259ded27f10727d1384961843d", + }); + const responseDataJson = response.body as EventGetResponse; + expect(response.statusCode).toEqual(400); + expect(responseDataJson).toEqual({ + error: true, + id: 109, + message: "Ticket has already been used.", + name: "TicketNotValidError", + }); + }); +}); + +describe("Test merch purchase verification", async () => { + test("Happy path: fulfill an unfulfilled item", async () => { + ddbMock + .on(UpdateItemCommand) + .resolvesOnce({ Attributes: unfulfilledMerchItem1 }) + .resolvesOnce({}); + + const testJwt = createJwt(); + await app.ready(); + const response = await supertest(app.server) + .post("/api/v1/tickets/checkIn") + .set("authorization", `Bearer ${testJwt}`) + .send({ + type: "merch", + email: "testing1@illinois.edu", + stripePi: "pi_8J4NrYdA3S7cW8Ty92FnGJ6L", + }); + const responseDataJson = response.body as EventGetResponse; + expect(response.statusCode).toEqual(200); + expect(responseDataJson).toEqual({ + valid: true, + type: "merch", + ticketId: "pi_8J4NrYdA3S7cW8Ty92FnGJ6L", + purchaserData: { + email: unfulfilledMerchItem1["email"]["S"], + productId: unfulfilledMerchItem1["item_id"]["S"], + quantity: parseInt(unfulfilledMerchItem1["quantity"]["N"], 10), + size: unfulfilledMerchItem1["size"]["S"], + }, + }); + }); + test("Sad path: ticket info with merch type should fail", async () => { + ddbMock + .on(UpdateItemCommand) + .resolvesOnce({ Attributes: refundedMerchItem }) + .resolvesOnce({}); + + const testJwt = createJwt(); + await app.ready(); + const response = await supertest(app.server) + .post("/api/v1/tickets/checkIn") + .set("authorization", `Bearer ${testJwt}`) + .send({ + type: "merch", + ticketId: + "975b4470cf37d7cf20fd404a711513fd1d1e68259ded27f10727d1384961843d", + }); + const responseDataJson = response.body as EventGetResponse; + expect(response.statusCode).toEqual(400); + expect(responseDataJson).toEqual({ + error: true, + id: 104, + message: `Required at "email"; Required at "stripePi", or Invalid literal value, expected "ticket" at "type"`, + name: "ValidationError", + }); + }); + test("Sad path: fulfilling a refunded merch item fails", async () => { + ddbMock + .on(UpdateItemCommand) + .resolvesOnce({ Attributes: refundedMerchItem }) + .resolvesOnce({}); + + const testJwt = createJwt(); + await app.ready(); + const response = await supertest(app.server) + .post("/api/v1/tickets/checkIn") + .set("authorization", `Bearer ${testJwt}`) + .send({ + type: "merch", + email: "testing2@illinois.edu", + stripePi: "pi_6T9QvUwR2IOj4CyF35DsXK7P", + }); + const responseDataJson = response.body as EventGetResponse; + expect(response.statusCode).toEqual(400); + expect(responseDataJson).toEqual({ + error: true, + id: 109, + message: "Ticket was already refunded.", + name: "TicketNotValidError", + }); + }); + test("Sad path: fulfilling an already-fulfilled merch item fails", async () => { + ddbMock + .on(UpdateItemCommand) + .resolvesOnce({ Attributes: fulfilledMerchItem1 }) + .resolvesOnce({}); + + const testJwt = createJwt(); + await app.ready(); + const response = await supertest(app.server) + .post("/api/v1/tickets/checkIn") + .set("authorization", `Bearer ${testJwt}`) + .send({ + type: "merch", + email: "testing0@illinois.edu", + stripePi: "pi_3Q5GewDiGOXU9RuS16txRR5D", + }); + const responseDataJson = response.body as EventGetResponse; + expect(response.statusCode).toEqual(400); + expect(responseDataJson).toEqual({ + error: true, + id: 109, + message: "Ticket has already been used.", + name: "TicketNotValidError", + }); + }); +}); + +afterAll(async () => { + await app.close(); + vi.useRealTimers(); +}); +beforeEach(() => { + ddbMock.reset(); + vi.useFakeTimers(); +}); From 30acfa31c78a203c7b55ac5b92b179ff8aeb38d4 Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Tue, 12 Nov 2024 14:34:30 -0600 Subject: [PATCH 4/4] fix test --- tests/unit/organizations.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/organizations.test.ts b/tests/unit/organizations.test.ts index 78bbc985..81119c6b 100644 --- a/tests/unit/organizations.test.ts +++ b/tests/unit/organizations.test.ts @@ -8,7 +8,7 @@ test("Test getting the list of organizations succeeds", async () => { url: "/api/v1/organizations", }); expect(response.statusCode).toBe(200); - const responseDataJson = await response.json(); + await response.json(); }); afterAll(async () => { await app.close();