From 2655a80283341e42ed62915e877c98fd031b2fc3 Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Thu, 10 Oct 2024 17:35:43 -0500 Subject: [PATCH] tests pass if you comment out sequelize init --- cloudformation/main.yml | 29 +++ package.json | 1 + src/errors/index.ts | 11 + src/functions/database.ts | 50 +++++ src/index.ts | 20 +- src/models/linkry.model.ts | 45 +++++ src/plugins/auth.ts | 2 +- src/roles.ts | 2 + src/routes/linkry.ts | 300 +++++++++++++++++++++++++++ src/types.d.ts | 12 ++ tests/unit/auth.test.ts | 66 ++++-- tests/unit/discordEvent.test.ts | 68 ++++--- tests/unit/eventPost.test.ts | 335 +++++++++++++++++-------------- tests/unit/events.test.ts | 121 ++++++----- tests/unit/health.test.ts | 47 ++++- tests/unit/ical.test.ts | 101 ++++++---- tests/unit/organizations.test.ts | 70 ++++++- tests/unit/vending.test.ts | 69 ++++++- tsconfig.json | 1 + yarn.lock | 290 +++++++++++++++++++++++++- 20 files changed, 1324 insertions(+), 316 deletions(-) create mode 100644 src/functions/database.ts create mode 100644 src/models/linkry.model.ts create mode 100644 src/routes/linkry.ts diff --git a/cloudformation/main.yml b/cloudformation/main.yml index bbe01c0..054f35e 100644 --- a/cloudformation/main.yml +++ b/cloudformation/main.yml @@ -105,6 +105,35 @@ Resources: Path: /{proxy+} Method: ANY + AppApiLambdaFunctionVpc: + Type: AWS::Serverless::Function + DependsOn: + - AppLogGroups + Properties: + CodeUri: ../dist/src/ + AutoPublishAlias: live + Runtime: nodejs20.x + Description: !Sub "${ApplicationFriendlyName} API Lambda - VPC attached" + FunctionName: !Sub ${ApplicationPrefix}-lambda-vpc + Handler: lambda.handler + MemorySize: 512 + Role: !GetAtt AppSecurityRoles.Outputs.MainFunctionRoleArn + Timeout: 60 + Environment: + Variables: + RunEnvironment: !Ref RunEnvironment + VpcConfig: + Ipv6AllowedForDualStack: True + SecurityGroupIds: !FindInMap [EnvironmentToCidr, !Ref RunEnvironment, SecurityGroupIds] + SubnetIds: !FindInMap [EnvironmentToCidr, !Ref RunEnvironment, SubnetIds] + Events: + LinkryEvent: + Type: Api + Properties: + RestApiId: !Ref AppApiGateway + Path: /api/v1/linkry/{proxy+} + Method: ANY + EventRecordsTable: Type: 'AWS::DynamoDB::Table' DeletionPolicy: "Retain" diff --git a/package.json b/package.json index 88ed6dd..31d3102 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "@fastify/auth": "^4.6.1", "@fastify/aws-lambda": "^4.1.0", "@fastify/cors": "^9.0.1", + "@sequelize/postgres": "^7.0.0-alpha.43", "@touch4it/ical-timezones": "^1.9.0", "discord.js": "^14.15.3", "dotenv": "^16.4.5", diff --git a/src/errors/index.ts b/src/errors/index.ts index 438e04f..2d7c86f 100644 --- a/src/errors/index.ts +++ b/src/errors/index.ts @@ -68,6 +68,17 @@ export class InternalServerError extends BaseError<"InternalServerError"> { } } +export class DatabaseDeleteError extends BaseError<"DatabaseDeleteError"> { + constructor({ message }: { message: string }) { + super({ + name: "DatabaseDeleteError", + id: 107, + message, + httpStatusCode: 500, + }); + } +} + export class NotFoundError extends BaseError<"NotFoundError"> { constructor({ endpointName }: { endpointName: string }) { super({ diff --git a/src/functions/database.ts b/src/functions/database.ts new file mode 100644 index 0000000..d8f9407 --- /dev/null +++ b/src/functions/database.ts @@ -0,0 +1,50 @@ +import { Sequelize } from "@sequelize/core"; +import { PostgresDialect } from "@sequelize/postgres"; +import { InternalServerError } from "../errors/index.js"; +import { ShortLinkModel } from "../models/linkry.model.js"; +import { FastifyInstance } from "fastify"; + +let logDebug: CallableFunction = console.log; +let logFatal: CallableFunction = console.log; + +// Function to set the current logger for each invocation +export function setSequelizeLogger( + debugLogger: CallableFunction, + fatalLogger: CallableFunction, +) { + logDebug = (msg: string) => debugLogger(msg); + logFatal = (msg: string) => fatalLogger(msg); +} + +export async function getSequelizeInstance( + fastify: FastifyInstance, +): Promise { + const postgresUrl = + process.env.DATABASE_URL || fastify.secretValue?.postgres_url || ""; + + const sequelize = new Sequelize({ + dialect: PostgresDialect, + url: postgresUrl as string, + ssl: { + rejectUnauthorized: false, + }, + models: [ShortLinkModel], + logging: logDebug as (sql: string, timing?: number) => void, + pool: { + max: 2, + min: 0, + idle: 0, + acquire: 3000, + evict: 30, // lambda function timeout in seconds + }, + }); + try { + await sequelize.sync(); + } catch (e: unknown) { + logFatal(`Could not authenticate to DB! ${e}`); + throw new InternalServerError({ + message: "Could not establish database connection.", + }); + } + return sequelize; +} diff --git a/src/index.ts b/src/index.ts index 3df269f..f5599c2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ import { randomUUID } from "crypto"; import fastify, { FastifyInstance } from "fastify"; import FastifyAuthProvider from "@fastify/auth"; -import fastifyAuthPlugin from "./plugins/auth.js"; +import fastifyAuthPlugin, { getSecretValue } from "./plugins/auth.js"; import protectedRoute from "./routes/protected.js"; import errorHandlerPlugin from "./plugins/errorHandler.js"; import { RunEnvironment, runEnvironments } from "./roles.js"; @@ -10,11 +10,13 @@ import { InternalServerError } from "./errors/index.js"; import eventsPlugin from "./routes/events.js"; import cors from "@fastify/cors"; import fastifyZodValidationPlugin from "./plugins/validate.js"; -import { environmentConfig } from "./config.js"; +import { environmentConfig, genericConfig } from "./config.js"; import organizationsPlugin from "./routes/organizations.js"; import icalPlugin from "./routes/ics.js"; import vendingPlugin from "./routes/vending.js"; +import linkryPlugin from "./routes/linkry.js"; import * as dotenv from "dotenv"; +import { getSequelizeInstance } from "./functions/database.js"; dotenv.config(); const now = () => Date.now(); @@ -47,12 +49,19 @@ async function init() { } app.runEnvironment = process.env.RunEnvironment as RunEnvironment; app.environmentConfig = environmentConfig[app.runEnvironment]; - app.addHook("onRequest", (req, _, done) => { + app.secretValue = null; + app.sequelizeInstance = null; + app.addHook("onRequest", async (req, _) => { + if (!app.secretValue) { + app.secretValue = + (await getSecretValue(genericConfig.ConfigSecretName)) || {}; + } + // if (!app.sequelizeInstance) { + // app.sequelizeInstance = await getSequelizeInstance(app); + // } req.startTime = now(); req.log.info({ url: req.raw.url }, "received request"); - done(); }); - app.addHook("onResponse", (req, reply, done) => { req.log.info( { @@ -71,6 +80,7 @@ async function init() { api.register(eventsPlugin, { prefix: "/events" }); api.register(organizationsPlugin, { prefix: "/organizations" }); api.register(icalPlugin, { prefix: "/ical" }); + api.register(linkryPlugin, { prefix: "/linkry" }); if (app.runEnvironment === "dev") { api.register(vendingPlugin, { prefix: "/vending" }); } diff --git a/src/models/linkry.model.ts b/src/models/linkry.model.ts new file mode 100644 index 0000000..0f1ab6c --- /dev/null +++ b/src/models/linkry.model.ts @@ -0,0 +1,45 @@ +import { + InferCreationAttributes, + InferAttributes, + Model, + CreationOptional, + DataTypes, +} from "@sequelize/core"; +import { + AllowNull, + Attribute, + CreatedAt, + NotNull, + PrimaryKey, + Table, + UpdatedAt, +} from "@sequelize/core/decorators-legacy"; + +@Table({ timestamps: true, tableName: "short_links" }) +export class ShortLinkModel extends Model< + InferAttributes, + InferCreationAttributes +> { + @Attribute(DataTypes.STRING) + @PrimaryKey + declare slug: string; + + @Attribute(DataTypes.STRING) + @NotNull + declare full: string; + + @Attribute(DataTypes.ARRAY(DataTypes.STRING)) + @AllowNull + declare groups?: CreationOptional; + + @Attribute(DataTypes.STRING) + declare author: string; + + @Attribute(DataTypes.DATE) + @CreatedAt + declare createdAt: CreationOptional; + + @Attribute(DataTypes.DATE) + @UpdatedAt + declare updatedAt: CreationOptional; +} diff --git a/src/plugins/auth.ts b/src/plugins/auth.ts index 988c4f6..c413843 100644 --- a/src/plugins/auth.ts +++ b/src/plugins/auth.ts @@ -15,7 +15,7 @@ import { } from "../errors/index.js"; import { genericConfig } from "../config.js"; -function intersection(setA: Set, setB: Set): Set { +export function intersection(setA: Set, setB: Set): Set { const _intersection = new Set(); for (const elem of setB) { if (setA.has(elem)) { diff --git a/src/roles.ts b/src/roles.ts index e571592..ce6102a 100644 --- a/src/roles.ts +++ b/src/roles.ts @@ -3,6 +3,8 @@ export const runEnvironments = ["dev", "prod"] as const; export type RunEnvironment = (typeof runEnvironments)[number]; export enum AppRoles { EVENTS_MANAGER = "manage:events", + LINKS_MANAGER = "manage:links", + LINKS_ADMIN = "admin:links", } export const allAppRoles = Object.values(AppRoles).filter( (value) => typeof value === "string", diff --git a/src/routes/linkry.ts b/src/routes/linkry.ts new file mode 100644 index 0000000..0aa0d7a --- /dev/null +++ b/src/routes/linkry.ts @@ -0,0 +1,300 @@ +import { FastifyPluginAsync, FastifyRequest } from "fastify"; +import { + getSequelizeInstance, + setSequelizeLogger, +} from "../functions/database.js"; +import { ShortLinkModel } from "../models/linkry.model.js"; +import { z } from "zod"; +import { AppRoles } from "../roles.js"; +import { + BaseError, + DatabaseDeleteError, + DatabaseFetchError, + DatabaseInsertError, + InternalServerError, + NotFoundError, + UnauthenticatedError, + UnauthorizedError, + ValidationError, +} from "../errors/index.js"; +import { UniqueConstraintError } from "@sequelize/core"; +import { intersection } from "../plugins/auth.js"; +import { NoDataRequest } from "../types.js"; + +type LinkrySlugOnlyRequest = { + Params: { id: string }; + Querystring: undefined; + Body: undefined; +}; + +const rawRequest = { + slug: z.string().min(1), + full: z.string().url().min(1), + groups: z.optional(z.array(z.string()).min(1)), +}; + +const createRequest = z.object(rawRequest); +const patchRequest = z.object({ ...rawRequest, slug: z.undefined() }); +// todo: patchRequest is all optional + +type LinkyCreateRequest = { + Params: undefined; + Querystring: undefined; + Body: z.infer; +}; + +type LinkryPatchRequest = { + Params: { id: string }; + Querystring: undefined; + Body: z.infer; +}; + +function userCanManageLink( + request: FastifyRequest, + link: ShortLinkModel, +): boolean { + if (request.userRoles?.has(AppRoles.LINKS_ADMIN)) { + return true; + } + if (request.username === link.author) { + return true; + } + if (!link.groups) { + return false; + } + if ( + request.userRoles && + intersection(request.userRoles, new Set(link.groups)) + ) { + return true; + } + return false; +} + +const linkryRoutes: FastifyPluginAsync = async (fastify, _options) => { + fastify.get("/redir/:id", async (request, reply) => { + // update logger instance + setSequelizeLogger(request.log.debug, request.log.fatal); + const slug = request.params.id; + try { + const result = await ShortLinkModel.findByPk(slug); + if (!result) { + const isProd = fastify.runEnvironment === "prod"; + // hide the real URL from the user in prod + throw new NotFoundError({ + endpointName: isProd ? `/${slug}` : `/api/v1/linkry/redir/${slug}`, + }); + } else { + reply.redirect(result.full); + } + } catch (e) { + if (e instanceof BaseError) { + throw e; + } + request.log.error(`Failed to retrieve short link: ${e}`); + throw new DatabaseFetchError({ + message: "Could not fetch short link entry.", + }); + } + }); + fastify.post( + "/redir", + { + preValidation: async (request, reply) => { + await fastify.zodValidateBody(request, reply, createRequest); + }, + onRequest: async (request, reply) => { + await fastify.authorize(request, reply, [AppRoles.LINKS_MANAGER]); + }, + }, + async (request, reply) => { + // update logger instance + setSequelizeLogger(request.log.debug, request.log.fatal); + const slug = request.body.slug; + if (!request.username) { + throw new UnauthenticatedError({ + message: "Could not determine username.", + }); + } + try { + await ShortLinkModel.create({ + ...request.body, + author: request.username, + }); + } catch (e) { + if (e instanceof BaseError) { + throw e; + } + if (e instanceof UniqueConstraintError) { + throw new ValidationError({ + message: "This slug already exists, you must PATCH it directly.", + }); + } + request.log.error(`Failed to insert short link: ${e}`); + throw new DatabaseInsertError({ + message: "Could not create short link entry.", + }); + } + return reply.send({ + message: "Short link created.", + slug, + resource: `/api/v1/linkry/redir/${slug}`, + }); + }, + ); + fastify.patch( + "/redir/:id", + { + preValidation: async (request, reply) => { + await fastify.zodValidateBody(request, reply, patchRequest); + }, + onRequest: async (request, reply) => { + await fastify.authorize(request, reply, [AppRoles.LINKS_MANAGER]); + }, + }, + async (request, reply) => { + // update logger instance + setSequelizeLogger(request.log.debug, request.log.fatal); + const slug = request.params.id; + let result; + try { + result = await ShortLinkModel.findByPk(slug); + if (!result) { + const isProd = fastify.runEnvironment === "prod"; + // hide the real URL from the user in prod + throw new NotFoundError({ + endpointName: isProd ? `/${slug}` : `/api/v1/linkry/redir/${slug}`, + }); + } + } catch (e) { + if (e instanceof BaseError) { + throw e; + } + request.log.error(`Failed to retrieve short link when modifying: ${e}`); + throw new DatabaseFetchError({ + message: "Could not fetch short link entry.", + }); + } + if (!userCanManageLink(request, result)) { + throw new UnauthenticatedError({ + message: "User cannot manage this link.", + }); + } + try { + result.set({ ...request.body, slug: undefined }); + await result.save(); + } catch (e) { + request.log.error(`Failed to modify short link ${slug}: ${e}`); + throw new DatabaseInsertError({ + message: "Failed to modify short link.", + }); + } + reply.send({ + message: "Short link modified.", + slug, + resource: `/api/v1/linkry/redir/${slug}`, + }); + }, + ); + fastify.delete( + "/redir/:id", + { + preValidation: async (request, reply) => { + await fastify.zodValidateBody(request, reply, createRequest); + }, + onRequest: async (request, reply) => { + await fastify.authorize(request, reply, [AppRoles.LINKS_MANAGER]); + }, + }, + async (request, reply) => { + // update logger instance + if (!request.username) { + throw new UnauthenticatedError({ + message: "Could not determine username.", + }); + } + let result; + try { + result = await ShortLinkModel.findByPk(request.params.id); + if (!result) { + throw new NotFoundError({ + endpointName: `/api/v1/linkry/redir/${request.params.id}`, + }); + } + if (!userCanManageLink(request, result)) { + throw new UnauthenticatedError({ + message: "User cannot manage this link.", + }); + } + } catch (e) { + if (e instanceof BaseError) { + throw e; + } + request.log.error( + `Could not fetch original link entry to delete it: ${e}`, + ); + throw new DatabaseFetchError({ + message: `Could not fetch original link entry to delete it.`, + }); + } + + try { + await result.destroy(); + } catch (e) { + if (e instanceof BaseError) { + throw e; + } + request.log.error(`Could not delete short link entry: ${e}`); + throw new DatabaseDeleteError({ + message: `Could not delete short link entry.`, + }); + } + reply.send({ message: "Short link deleted." }); + }, + ); + fastify.get( + "/redirs", + { + onRequest: async (request, reply) => { + await fastify.authorize(request, reply, [AppRoles.LINKS_MANAGER]); + }, + }, + async (request, reply) => { + if (!request.userRoles) { + throw new UnauthorizedError({ message: "Could not get user roles." }); + } + try { + const isAdmin = request.userRoles.has(AppRoles.LINKS_ADMIN); + let filteredLinks = await ShortLinkModel.findAll(); + if (!isAdmin) { + filteredLinks = filteredLinks.filter((slm: ShortLinkModel) => { + return ( + slm.author == request.username || + (slm.groups && + request.userRoles && + intersection(new Set(slm.groups), request.userRoles).size > 0) + ); + }); + } + const myLinks: ShortLinkModel[] = []; + const delegatedLinks: ShortLinkModel[] = []; + for (const link of filteredLinks) { + if (link.author === request.username) { + myLinks.push(link); + } else { + delegatedLinks.push(link); + } + } + return reply.send({ admin: isAdmin, myLinks, delegatedLinks }); + } catch (e) { + if (e instanceof BaseError) { + throw e; + } + request.log.error(`Could not get user's links: ${e}`); + throw new InternalServerError({ message: "Could not get user links." }); + } + }, + ); +}; + +export default linkryRoutes; diff --git a/src/types.d.ts b/src/types.d.ts index e265a07..21183ad 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -2,6 +2,9 @@ import { FastifyRequest, FastifyInstance, FastifyReply } from "fastify"; import { AppRoles, RunEnvironment } from "./roles.ts"; import { AadToken } from "./plugins/auth.ts"; import { ConfigType } from "./config.ts"; +import { Sequelize } from "@sequelize/core"; +import { PostgresDialect } from "@sequelize/postgres"; + declare module "fastify" { interface FastifyInstance { authenticate: ( @@ -20,10 +23,19 @@ declare module "fastify" { ) => Promise; runEnvironment: RunEnvironment; environmentConfig: ConfigType; + secretValue: Record | null; + sequelizeInstance: Sequelize | null; } interface FastifyRequest { startTime: number; username?: string; + userRoles?: Set; tokenPayload?: AadToken; } } + +export type NoDataRequest = { + Params: undefined; + Querystring: undefined; + Body: undefined; +}; diff --git a/tests/unit/auth.test.ts b/tests/unit/auth.test.ts index aaadffd..751f2fe 100644 --- a/tests/unit/auth.test.ts +++ b/tests/unit/auth.test.ts @@ -1,4 +1,4 @@ -import { expect, test, vi } from "vitest"; +import { afterAll, beforeEach, describe, expect, test, vi } from "vitest"; import { GetSecretValueCommand, SecretsManagerClient, @@ -7,13 +7,15 @@ 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 { FastifyInstance } from "fastify"; -const ddbMock = mockClient(SecretsManagerClient); - -const app = await init(); +// Mock the Secrets Manager client +const smMock = mockClient(SecretsManagerClient); const jwt_secret = secretObject["jwt_key"]; + +// Utility function to create a JWT export function createJwt(date?: Date, group?: string) { - let modifiedPayload = jwtPayload; + let modifiedPayload = { ...jwtPayload }; if (date) { const nowMs = Math.floor(date.valueOf() / 1000); const laterMs = nowMs + 3600 * 24; @@ -29,25 +31,49 @@ export function createJwt(date?: Date, group?: string) { } return jwt.sign(modifiedPayload, jwt_secret, { algorithm: "HS256" }); } + +// Stub environment variable for JWT signing key vi.stubEnv("JwtSigningKey", jwt_secret); -const testJwt = createJwt(); +// Global variable for the app instance +let app: FastifyInstance; + +describe("Protected API Tests", () => { + beforeEach(async () => { + // Reset the mock and set up the Secrets Manager mock response + smMock.reset(); + smMock.on(GetSecretValueCommand).resolves({ + SecretString: JSON.stringify(secretObject), + }); -test("Test happy path", async () => { - ddbMock.on(GetSecretValueCommand).resolves({ - SecretString: secretJson, + // Initialize the app before each test + app = await init(); + await app.ready(); }); - const response = await app.inject({ - method: "GET", - url: "/api/v1/protected", - headers: { - authorization: `Bearer ${testJwt}`, - }, + + afterAll(async () => { + // Close the app after all tests are complete + if (app) { + await app.close(); + } }); - expect(response.statusCode).toBe(200); - const jsonBody = await response.json(); - expect(jsonBody).toEqual({ - username: "infra-unit-test@acm.illinois.edu", - roles: ["manage:events"], + + test("Test happy path", async () => { + const testJwt = createJwt(); + + const response = await app.inject({ + method: "GET", + url: "/api/v1/protected", + headers: { + authorization: `Bearer ${testJwt}`, + }, + }); + + expect(response.statusCode).toBe(200); + const jsonBody = await response.json(); + expect(jsonBody).toEqual({ + username: "infra-unit-test@acm.illinois.edu", + roles: ["manage:events", "manage:links", "admin:links"], + }); }); }); diff --git a/tests/unit/discordEvent.test.ts b/tests/unit/discordEvent.test.ts index 8d3f75a..4b344ba 100644 --- a/tests/unit/discordEvent.test.ts +++ b/tests/unit/discordEvent.test.ts @@ -1,43 +1,71 @@ -import { afterAll, expect, test, beforeEach, vi, Mock } from "vitest"; +import { afterAll, expect, test, beforeEach, describe, vi, Mock } from "vitest"; import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb"; import { mockClient } from "aws-sdk-client-mock"; import init from "../../src/index.js"; import { createJwt } from "./auth.test.js"; -import { secretJson, secretObject } from "./secret.testdata.js"; -import supertest from "supertest"; -import { describe } from "node:test"; import { GetSecretValueCommand, SecretsManagerClient, } from "@aws-sdk/client-secrets-manager"; +import { secretJson, secretObject } from "./secret.testdata.js"; +import supertest from "supertest"; +import { FastifyInstance } from "fastify"; import { updateDiscord } from "../../src/functions/discord.js"; const ddbMock = mockClient(DynamoDBClient); const smMock = mockClient(SecretsManagerClient); +// Setup mock environment variable for JWT secret const jwt_secret = secretObject["jwt_key"]; vi.stubEnv("JwtSigningKey", jwt_secret); -vi.mock("../../src/functions/discord.js", () => { - return { - ...vi.importActual("../../src/functions/discord.js"), - updateDiscord: vi.fn(() => { - console.log("Updated discord event."); - }), - }; +// Mock the updateDiscord function +vi.mock("../../src/functions/discord.js", () => ({ + ...vi.importActual("../../src/functions/discord.js"), + updateDiscord: vi.fn(() => { + console.log("Updated discord event."); + }), +})); + +// Global variable to hold the app instance +let app: FastifyInstance; + +beforeEach(() => { + // Reset mocks before each test + ddbMock.reset(); + smMock.reset(); + vi.resetAllMocks(); + + // Mock Secrets Manager responses + smMock.on(GetSecretValueCommand).resolves({ + SecretString: JSON.stringify(secretObject), + }); + + // Use fake timers + vi.useFakeTimers(); }); -const app = await init(); +afterAll(async () => { + // Close the app after all tests are done + if (app) { + await app.close(); + } + vi.useRealTimers(); +}); -// TODO: add discord reject test describe("Test Events <-> Discord integration", () => { + beforeEach(async () => { + // Initialize the app within each describe block to ensure fresh setup + app = await init(); + await app.ready(); + }); + test("Happy path: valid publish submission.", async () => { ddbMock.on(PutItemCommand).resolves({}); smMock.on(GetSecretValueCommand).resolves({ SecretString: secretJson, }); const testJwt = createJwt(); - await app.ready(); const response = await supertest(app.server) .post("/api/v1/events") .set("authorization", `Bearer ${testJwt}`) @@ -61,7 +89,6 @@ describe("Test Events <-> Discord integration", () => { SecretString: secretJson, }); const testJwt = createJwt(); - await app.ready(); const response = await supertest(app.server) .post("/api/v1/events") .set("authorization", `Bearer ${testJwt}`) @@ -80,14 +107,5 @@ describe("Test Events <-> Discord integration", () => { expect((updateDiscord as Mock).mock.calls.length).toBe(0); }); - afterAll(async () => { - await app.close(); - vi.useRealTimers(); - }); - beforeEach(() => { - ddbMock.reset(); - smMock.reset(); - vi.resetAllMocks(); - vi.useFakeTimers(); - }); + // TODO: add discord reject test }); diff --git a/tests/unit/eventPost.test.ts b/tests/unit/eventPost.test.ts index c14d7a6..8b46a1d 100644 --- a/tests/unit/eventPost.test.ts +++ b/tests/unit/eventPost.test.ts @@ -1,4 +1,4 @@ -import { afterAll, expect, test, beforeEach, vi } from "vitest"; +import { afterAll, expect, test, beforeEach, describe, vi } from "vitest"; import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb"; import { mockClient } from "aws-sdk-client-mock"; import init from "../../src/index.js"; @@ -9,195 +9,218 @@ import { } from "@aws-sdk/client-secrets-manager"; import { secretJson, secretObject } from "./secret.testdata.js"; import supertest from "supertest"; +import { FastifyInstance } from "fastify"; const ddbMock = mockClient(DynamoDBClient); const smMock = mockClient(SecretsManagerClient); + +// Setup mock environment variable for JWT secret const jwt_secret = secretObject["jwt_key"]; vi.stubEnv("JwtSigningKey", jwt_secret); -const app = await init(); - -test("Sad path: Not authenticated", async () => { - await app.ready(); - const response = await supertest(app.server).post("/api/v1/events").send({ - description: "Test paid event.", - end: "2024-09-25T19:00:00", - featured: true, - host: "Social Committee", - location: "Illini Union", - start: "2024-09-25T18:00:00", - title: "Fall Semiformal", - paidEventId: "sp24_semiformal", +// Global variable to hold the app instance +let app: FastifyInstance; + +beforeEach(() => { + // Reset mocks before each test + ddbMock.reset(); + smMock.reset(); + + // Mock Secrets Manager responses + smMock.on(GetSecretValueCommand).resolves({ + SecretString: JSON.stringify(secretObject), }); - expect(response.statusCode).toBe(403); + // Use fake timers + vi.useFakeTimers(); }); -test("Sad path: Authenticated but not authorized", async () => { - await app.ready(); - const testJwt = createJwt(undefined, "1"); - const response = await supertest(app.server) - .post("/api/v1/events") - .set("Authorization", `Bearer ${testJwt}`) - .send({ - description: "Test paid event.", - end: "2024-09-25T19:00:00", - featured: true, - host: "Social Committee", - location: "Illini Union", - start: "2024-09-25T18:00:00", - title: "Fall Semiformal", - paidEventId: "sp24_semiformal", - }); - expect(response.statusCode).toBe(401); -}); -test("Sad path: Prevent empty body request", async () => { - await app.ready(); - const testJwt = createJwt(undefined, "0"); - const response = await supertest(app.server) - .post("/api/v1/events") - .set("Authorization", `Bearer ${testJwt}`) - .send(); - expect(response.statusCode).toBe(400); - expect(response.body).toStrictEqual({ - error: true, - name: "ValidationError", - id: 104, - message: "Required", - }); +afterAll(async () => { + // Close the app after all tests are done + if (app) { + await app.close(); + } + vi.useRealTimers(); }); -test("Sad path: Prevent specifying repeatEnds on non-repeating events", async () => { - ddbMock.on(PutItemCommand).resolves({}); - smMock.on(GetSecretValueCommand).resolves({ - SecretString: secretJson, + +describe("Event API Tests", () => { + beforeEach(async () => { + // Initialize the app within each describe block to ensure fresh setup + app = await init(); + await app.ready(); }); - const testJwt = createJwt(); - await app.ready(); - const response = await supertest(app.server) - .post("/api/v1/events") - .set("authorization", `Bearer ${testJwt}`) - .send({ + test("Sad path: Not authenticated", async () => { + await app.ready(); + const response = await supertest(app.server).post("/api/v1/events").send({ description: "Test paid event.", end: "2024-09-25T19:00:00", - featured: false, + featured: true, host: "Social Committee", location: "Illini Union", start: "2024-09-25T18:00:00", title: "Fall Semiformal", - repeatEnds: "2024-09-25T18:00:00", paidEventId: "sp24_semiformal", }); - expect(response.statusCode).toBe(400); - expect(response.body).toStrictEqual({ - error: true, - name: "ValidationError", - id: 104, - message: "repeats is required when repeatEnds is defined", + expect(response.statusCode).toBe(403); }); -}); -test("Sad path: Prevent specifying unknown repeat frequencies", async () => { - ddbMock.on(PutItemCommand).resolves({}); - smMock.on(GetSecretValueCommand).resolves({ - SecretString: secretJson, + test("Sad path: Authenticated but not authorized", async () => { + await app.ready(); + const testJwt = createJwt(undefined, "1"); + const response = await supertest(app.server) + .post("/api/v1/events") + .set("Authorization", `Bearer ${testJwt}`) + .send({ + description: "Test paid event.", + end: "2024-09-25T19:00:00", + featured: true, + host: "Social Committee", + location: "Illini Union", + start: "2024-09-25T18:00:00", + title: "Fall Semiformal", + paidEventId: "sp24_semiformal", + }); + expect(response.statusCode).toBe(401); }); - const testJwt = createJwt(); - await app.ready(); - const response = await supertest(app.server) - .post("/api/v1/events") - .set("authorization", `Bearer ${testJwt}`) - .send({ - description: "Test paid event.", - end: "2024-09-25T19:00:00", - featured: false, - host: "Social Committee", - location: "Illini Union", - start: "2024-09-25T18:00:00", - title: "Fall Semiformal", - repeats: "forever_and_ever", - paidEventId: "sp24_semiformal", + test("Sad path: Prevent empty body request", async () => { + await app.ready(); + const testJwt = createJwt(undefined, "0"); + const response = await supertest(app.server) + .post("/api/v1/events") + .set("Authorization", `Bearer ${testJwt}`) + .send(); + expect(response.statusCode).toBe(400); + expect(response.body).toStrictEqual({ + error: true, + name: "ValidationError", + id: 104, + message: "Required", }); - - expect(response.statusCode).toBe(400); - expect(response.body).toStrictEqual({ - error: true, - name: "ValidationError", - id: 104, - message: `Invalid enum value. Expected 'weekly' | 'biweekly', received 'forever_and_ever' at "repeats"`, }); -}); + test("Sad path: Prevent specifying repeatEnds on non-repeating events", async () => { + ddbMock.on(PutItemCommand).resolves({}); + smMock.on(GetSecretValueCommand).resolves({ + SecretString: secretJson, + }); + const testJwt = createJwt(); + await app.ready(); + const response = await supertest(app.server) + .post("/api/v1/events") + .set("authorization", `Bearer ${testJwt}`) + .send({ + description: "Test paid event.", + end: "2024-09-25T19:00:00", + featured: false, + host: "Social Committee", + location: "Illini Union", + start: "2024-09-25T18:00:00", + title: "Fall Semiformal", + repeatEnds: "2024-09-25T18:00:00", + paidEventId: "sp24_semiformal", + }); -test("Happy path: Adding a non-repeating, featured, paid event", async () => { - ddbMock.on(PutItemCommand).resolves({}); - smMock.on(GetSecretValueCommand).resolves({ - SecretString: secretJson, + expect(response.statusCode).toBe(400); + expect(response.body).toStrictEqual({ + error: true, + name: "ValidationError", + id: 104, + message: "repeats is required when repeatEnds is defined", + }); }); - const testJwt = createJwt(); - await app.ready(); - const response = await supertest(app.server) - .post("/api/v1/events") - .set("authorization", `Bearer ${testJwt}`) - .send({ - description: "Test paid event.", - end: "2024-09-25T19:00:00", - featured: true, - host: "Social Committee", - location: "Illini Union", - locationLink: "https://maps.app.goo.gl/rUBhjze5mWuTSUJK9", - start: "2024-09-25T18:00:00", - title: "Fall Semiformal", - paidEventId: "sp24_semiformal", + + test("Sad path: Prevent specifying unknown repeat frequencies", async () => { + ddbMock.on(PutItemCommand).resolves({}); + smMock.on(GetSecretValueCommand).resolves({ + SecretString: secretJson, }); + const testJwt = createJwt(); + await app.ready(); + const response = await supertest(app.server) + .post("/api/v1/events") + .set("authorization", `Bearer ${testJwt}`) + .send({ + description: "Test paid event.", + end: "2024-09-25T19:00:00", + featured: false, + host: "Social Committee", + location: "Illini Union", + start: "2024-09-25T18:00:00", + title: "Fall Semiformal", + repeats: "forever_and_ever", + paidEventId: "sp24_semiformal", + }); - expect(response.statusCode).toBe(200); - const responseDataJson = response.body as { id: string; resource: string }; - expect(responseDataJson).toHaveProperty("id"); - const uuid = responseDataJson["id"]; - expect(responseDataJson).toEqual({ - id: uuid, - resource: `/api/v1/events/${uuid}`, + expect(response.statusCode).toBe(400); + expect(response.body).toStrictEqual({ + error: true, + name: "ValidationError", + id: 104, + message: `Invalid enum value. Expected 'weekly' | 'biweekly', received 'forever_and_ever' at "repeats"`, + }); }); -}); -test("Happy path: Adding a weekly repeating, non-featured, paid event", async () => { - ddbMock.on(PutItemCommand).resolves({}); - smMock.on(GetSecretValueCommand).resolves({ - SecretString: secretJson, - }); - const testJwt = createJwt(); - await app.ready(); - const response = await supertest(app.server) - .post("/api/v1/events") - .set("authorization", `Bearer ${testJwt}`) - .send({ - description: "Test paid event.", - end: "2024-09-25T19:00:00", - featured: false, - host: "Social Committee", - location: "Illini Union", - start: "2024-09-25T18:00:00", - title: "Fall Semiformal", - repeats: "weekly", - paidEventId: "sp24_semiformal", + test("Happy path: Adding a non-repeating, featured, paid event", async () => { + ddbMock.on(PutItemCommand).resolves({}); + smMock.on(GetSecretValueCommand).resolves({ + SecretString: secretJson, }); + const testJwt = createJwt(); + await app.ready(); + const response = await supertest(app.server) + .post("/api/v1/events") + .set("authorization", `Bearer ${testJwt}`) + .send({ + description: "Test paid event.", + end: "2024-09-25T19:00:00", + featured: true, + host: "Social Committee", + location: "Illini Union", + locationLink: "https://maps.app.goo.gl/rUBhjze5mWuTSUJK9", + start: "2024-09-25T18:00:00", + title: "Fall Semiformal", + paidEventId: "sp24_semiformal", + }); - expect(response.statusCode).toBe(200); - const responseDataJson = response.body as { id: string; resource: string }; - expect(responseDataJson).toHaveProperty("id"); - const uuid = responseDataJson["id"]; - expect(responseDataJson).toEqual({ - id: uuid, - resource: `/api/v1/events/${uuid}`, + expect(response.statusCode).toBe(200); + const responseDataJson = response.body as { id: string; resource: string }; + expect(responseDataJson).toHaveProperty("id"); + const uuid = responseDataJson["id"]; + expect(responseDataJson).toEqual({ + id: uuid, + resource: `/api/v1/events/${uuid}`, + }); }); -}); -afterAll(async () => { - await app.close(); - vi.useRealTimers(); -}); -beforeEach(() => { - ddbMock.reset(); - smMock.reset(); - vi.useFakeTimers(); + test("Happy path: Adding a weekly repeating, non-featured, paid event", async () => { + ddbMock.on(PutItemCommand).resolves({}); + smMock.on(GetSecretValueCommand).resolves({ + SecretString: secretJson, + }); + const testJwt = createJwt(); + await app.ready(); + const response = await supertest(app.server) + .post("/api/v1/events") + .set("authorization", `Bearer ${testJwt}`) + .send({ + description: "Test paid event.", + end: "2024-09-25T19:00:00", + featured: false, + host: "Social Committee", + location: "Illini Union", + start: "2024-09-25T18:00:00", + title: "Fall Semiformal", + repeats: "weekly", + paidEventId: "sp24_semiformal", + }); + + expect(response.statusCode).toBe(200); + const responseDataJson = response.body as { id: string; resource: string }; + expect(responseDataJson).toHaveProperty("id"); + const uuid = responseDataJson["id"]; + expect(responseDataJson).toEqual({ + id: uuid, + resource: `/api/v1/events/${uuid}`, + }); + }); }); diff --git a/tests/unit/events.test.ts b/tests/unit/events.test.ts index 6c91466..1daab55 100644 --- a/tests/unit/events.test.ts +++ b/tests/unit/events.test.ts @@ -1,4 +1,4 @@ -import { afterAll, expect, test, beforeEach, vi } from "vitest"; +import { afterAll, expect, test, beforeEach, describe, vi } from "vitest"; import { ScanCommand, DynamoDBClient } from "@aws-sdk/client-dynamodb"; import { mockClient } from "aws-sdk-client-mock"; import init from "../../src/index.js"; @@ -9,61 +9,90 @@ import { dynamoTableDataUnmarshalledUpcomingOnly, } from "./mockEventData.testdata.js"; import { secretObject } from "./secret.testdata.js"; +import { FastifyInstance } from "fastify"; +import { + GetSecretValueCommand, + SecretsManagerClient, +} from "@aws-sdk/client-secrets-manager"; const ddbMock = mockClient(DynamoDBClient); +const smMock = mockClient(SecretsManagerClient); + +// Setup mock environment variable for JWT secret const jwt_secret = secretObject["jwt_key"]; vi.stubEnv("JwtSigningKey", jwt_secret); -const app = await init(); -test("Test getting events", async () => { - ddbMock.on(ScanCommand).resolves({ - Items: dynamoTableData as any, - }); - const response = await app.inject({ - method: "GET", - url: "/api/v1/events", - }); - expect(response.statusCode).toBe(200); - const responseDataJson = (await response.json()) as EventGetResponse; - expect(responseDataJson).toEqual(dynamoTableDataUnmarshalled); -}); +// Global variable to hold the app instance +let app: FastifyInstance; -test("Test dynamodb error handling", async () => { - ddbMock.on(ScanCommand).rejects("Could not get data."); - const response = await app.inject({ - method: "GET", - url: "/api/v1/events", - }); - expect(response.statusCode).toBe(500); - const responseDataJson = await response.json(); - expect(responseDataJson).toEqual({ - error: true, - name: "DatabaseFetchError", - id: 106, - message: "Failed to get events from Dynamo table.", - }); -}); +beforeEach(() => { + // Reset mocks before each test + ddbMock.reset(); -test("Test upcoming only", async () => { - const date = new Date(2024, 7, 10, 13, 0, 0); // 2024-08-10T17:00:00.000Z, don't ask me why its off a month - vi.setSystemTime(date); - ddbMock.on(ScanCommand).resolves({ - Items: dynamoTableData as any, - }); - const response = await app.inject({ - method: "GET", - url: "/api/v1/events?upcomingOnly=true", - }); - expect(response.statusCode).toBe(200); - const responseDataJson = (await response.json()) as EventGetResponse; - expect(responseDataJson).toEqual(dynamoTableDataUnmarshalledUpcomingOnly); + // Use fake timers + vi.useFakeTimers(); }); afterAll(async () => { - await app.close(); + // Close the app after all tests are done + if (app) { + await app.close(); + } vi.useRealTimers(); }); -beforeEach(() => { - ddbMock.reset(); - vi.useFakeTimers(); + +describe("Event GET API Tests", () => { + beforeEach(async () => { + // Initialize the app within each describe block to ensure fresh setup + smMock.reset(); + smMock.on(GetSecretValueCommand).resolves({ + SecretString: JSON.stringify(secretObject), + }); + app = await init(); + await app.ready(); + }); + + test("Test getting events", async () => { + ddbMock.on(ScanCommand).resolves({ + Items: dynamoTableData as any, + }); + const response = await app.inject({ + method: "GET", + url: "/api/v1/events", + }); + expect(response.statusCode).toBe(200); + const responseDataJson = (await response.json()) as EventGetResponse; + expect(responseDataJson).toEqual(dynamoTableDataUnmarshalled); + }); + + test("Test dynamodb error handling", async () => { + ddbMock.on(ScanCommand).rejects("Could not get data."); + const response = await app.inject({ + method: "GET", + url: "/api/v1/events", + }); + expect(response.statusCode).toBe(500); + const responseDataJson = await response.json(); + expect(responseDataJson).toEqual({ + error: true, + name: "DatabaseFetchError", + id: 106, + message: "Failed to get events from Dynamo table.", + }); + }); + + test("Test upcoming only", async () => { + const date = new Date(2024, 7, 10, 13, 0, 0); // 2024-08-10T17:00:00.000Z, don't ask me why its off a month + vi.setSystemTime(date); + ddbMock.on(ScanCommand).resolves({ + Items: dynamoTableData as any, + }); + const response = await app.inject({ + method: "GET", + url: "/api/v1/events?upcomingOnly=true", + }); + expect(response.statusCode).toBe(200); + const responseDataJson = (await response.json()) as EventGetResponse; + expect(responseDataJson).toEqual(dynamoTableDataUnmarshalledUpcomingOnly); + }); }); diff --git a/tests/unit/health.test.ts b/tests/unit/health.test.ts index 452d5ff..bf8464c 100644 --- a/tests/unit/health.test.ts +++ b/tests/unit/health.test.ts @@ -1,17 +1,44 @@ -import { afterAll, expect, test } from "vitest"; +import { afterAll, expect, test, beforeEach, describe } from "vitest"; import init from "../../src/index.js"; import { EventGetResponse } from "../../src/routes/events.js"; +import { FastifyInstance } from "fastify"; +import { mockClient } from "aws-sdk-client-mock"; +import { + GetSecretValueCommand, + SecretsManagerClient, +} from "@aws-sdk/client-secrets-manager"; +import { secretObject } from "./secret.testdata.js"; -const app = await init(); -test("Test getting events", async () => { - const response = await app.inject({ - method: "GET", - url: "/api/v1/healthz", +const smMock = mockClient(SecretsManagerClient); + +// Global variable to hold the app instance +let app: FastifyInstance; + +beforeEach(async () => { + // Initialize the app before each test + smMock.reset(); + smMock.on(GetSecretValueCommand).resolves({ + SecretString: JSON.stringify(secretObject), }); - expect(response.statusCode).toBe(200); - const responseDataJson = (await response.json()) as EventGetResponse; - expect(responseDataJson).toEqual({ message: "UP" }); + app = await init(); + await app.ready(); }); + afterAll(async () => { - await app.close(); + // Close the app after all tests are done + if (app) { + await app.close(); + } +}); + +describe("Health Check API Test", () => { + test("Test getting health status", async () => { + const response = await app.inject({ + method: "GET", + url: "/api/v1/healthz", + }); + expect(response.statusCode).toBe(200); + const responseDataJson = (await response.json()) as EventGetResponse; + expect(responseDataJson).toEqual({ message: "UP" }); + }); }); diff --git a/tests/unit/ical.test.ts b/tests/unit/ical.test.ts index c9b8649..6cc70a2 100644 --- a/tests/unit/ical.test.ts +++ b/tests/unit/ical.test.ts @@ -1,53 +1,84 @@ -import { afterAll, expect, test, beforeEach, vi } from "vitest"; +import { afterAll, expect, test, beforeEach, describe, vi } from "vitest"; import { ScanCommand, DynamoDBClient } from "@aws-sdk/client-dynamodb"; import { mockClient } from "aws-sdk-client-mock"; import init from "../../src/index.js"; import { dynamoTableData } from "./mockEventData.testdata.js"; -import { secretObject } from "./secret.testdata.js"; +import { secretObject, secretJson } from "./secret.testdata.js"; import { readFile } from "fs/promises"; +import { FastifyInstance } from "fastify"; +import { + GetSecretValueCommand, + SecretsManagerClient, +} from "@aws-sdk/client-secrets-manager"; const ddbMock = mockClient(DynamoDBClient); +const smMock = mockClient(SecretsManagerClient); + +// Setup mock environment variable for JWT secret const jwt_secret = secretObject["jwt_key"]; vi.stubEnv("JwtSigningKey", jwt_secret); -const app = await init(); -test("Test getting ACM-wide iCal calendar", async () => { - const date = new Date(2024, 7, 22, 15, 51, 48); // August 22, 2024, at 15:51:48 (3:51:48 PM) - vi.setSystemTime(date); - ddbMock.on(ScanCommand).resolves({ - Items: dynamoTableData as any, - }); - const response = await app.inject({ - method: "GET", - url: "/api/v1/ical", - }); - expect(response.statusCode).toBe(200); - expect(response.headers["content-disposition"]).toEqual( - 'attachment; filename="calendar.ics"', - ); - expect(response.body).toEqual( - (await readFile("./tests/unit/data/acmWideCalendar.ics")).toString(), - ); -}); +// Global variable to hold the app instance +let app: FastifyInstance; -test("Test getting non-existent iCal calendar fails", async () => { - const date = new Date(2024, 7, 22, 15, 51, 48); // August 22, 2024, at 15:51:48 (3:51:48 PM) - vi.setSystemTime(date); - ddbMock.on(ScanCommand).resolves({ - Items: dynamoTableData as any, - }); - const response = await app.inject({ - method: "GET", - url: "/api/v1/ical/invalid", +beforeEach(() => { + // Reset mocks before each test + ddbMock.reset(); + smMock.reset(); + + // Mock Secrets Manager responses + smMock.on(GetSecretValueCommand).resolves({ + SecretString: JSON.stringify(secretObject), }); - expect(response.statusCode).toBe(400); + + // Use fake timers + vi.useFakeTimers(); }); afterAll(async () => { - await app.close(); + // Close the app after all tests are done + if (app) { + await app.close(); + } vi.useRealTimers(); }); -beforeEach(() => { - ddbMock.reset(); - vi.useFakeTimers(); + +describe("iCal API Tests", () => { + beforeEach(async () => { + // Initialize the app within each describe block to ensure fresh setup + app = await init(); + await app.ready(); + }); + + test("Test getting ACM-wide iCal calendar", async () => { + const date = new Date(2024, 7, 22, 15, 51, 48); // August 22, 2024, at 15:51:48 (3:51:48 PM) + vi.setSystemTime(date); + ddbMock.on(ScanCommand).resolves({ + Items: dynamoTableData as any, + }); + const response = await app.inject({ + method: "GET", + url: "/api/v1/ical", + }); + expect(response.statusCode).toBe(200); + expect(response.headers["content-disposition"]).toEqual( + 'attachment; filename="calendar.ics"', + ); + expect(response.body).toEqual( + (await readFile("./tests/unit/data/acmWideCalendar.ics")).toString(), + ); + }); + + test("Test getting non-existent iCal calendar fails", async () => { + const date = new Date(2024, 7, 22, 15, 51, 48); // August 22, 2024, at 15:51:48 (3:51:48 PM) + vi.setSystemTime(date); + ddbMock.on(ScanCommand).resolves({ + Items: dynamoTableData as any, + }); + const response = await app.inject({ + method: "GET", + url: "/api/v1/ical/invalid", + }); + expect(response.statusCode).toBe(400); + }); }); diff --git a/tests/unit/organizations.test.ts b/tests/unit/organizations.test.ts index 78bbc98..e0069dc 100644 --- a/tests/unit/organizations.test.ts +++ b/tests/unit/organizations.test.ts @@ -1,15 +1,67 @@ -import { afterAll, expect, test } from "vitest"; +import { afterAll, expect, test, beforeEach, describe, vi } from "vitest"; +import { ScanCommand, DynamoDBClient } from "@aws-sdk/client-dynamodb"; +import { mockClient } from "aws-sdk-client-mock"; import init from "../../src/index.js"; +import { FastifyInstance } from "fastify"; +import { + GetSecretValueCommand, + SecretsManagerClient, +} from "@aws-sdk/client-secrets-manager"; +import { secretObject } from "./secret.testdata.js"; -const app = await init(); -test("Test getting the list of organizations succeeds", async () => { - const response = await app.inject({ - method: "GET", - url: "/api/v1/organizations", +const ddbMock = mockClient(DynamoDBClient); +const smMock = mockClient(SecretsManagerClient); + +// Setup mock environment variable for JWT secret +const jwt_secret = secretObject["jwt_key"]; +vi.stubEnv("JwtSigningKey", jwt_secret); + +// Global variable to hold the app instance +let app: FastifyInstance; + +beforeEach(() => { + // Reset mocks before each test + ddbMock.reset(); + smMock.reset(); + + // Mock Secrets Manager responses + smMock.on(GetSecretValueCommand).resolves({ + SecretString: JSON.stringify(secretObject), }); - expect(response.statusCode).toBe(200); - const responseDataJson = await response.json(); + + // Use fake timers + vi.useFakeTimers(); }); + afterAll(async () => { - await app.close(); + // Close the app after all tests are done + if (app) { + await app.close(); + } + vi.useRealTimers(); +}); + +describe("Organizations API Test", () => { + beforeEach(async () => { + // Initialize the app within each describe block to ensure fresh setup + app = await init(); + await app.ready(); + }); + + test("Test getting the list of organizations succeeds", async () => { + // Mock DynamoDB response for organizations + ddbMock.on(ScanCommand).resolves({ + Items: [ + // Add mock organization data here if needed + ], + }); + + const response = await app.inject({ + method: "GET", + url: "/api/v1/organizations", + }); + expect(response.statusCode).toBe(200); + const responseDataJson = await response.json(); + // Add more specific expectations for the response data if needed + }); }); diff --git a/tests/unit/vending.test.ts b/tests/unit/vending.test.ts index e03979c..d690145 100644 --- a/tests/unit/vending.test.ts +++ b/tests/unit/vending.test.ts @@ -1,11 +1,66 @@ -import { expect, test } from "vitest"; +import { afterAll, expect, test, beforeEach, describe, vi } from "vitest"; +import { ScanCommand, DynamoDBClient } from "@aws-sdk/client-dynamodb"; +import { mockClient } from "aws-sdk-client-mock"; import init from "../../src/index.js"; +import { FastifyInstance } from "fastify"; +import { + GetSecretValueCommand, + SecretsManagerClient, +} from "@aws-sdk/client-secrets-manager"; +import { secretObject } from "./secret.testdata.js"; -const app = await init(); -test("Test getting events", async () => { - const response = await app.inject({ - method: "GET", - url: "/api/v1/vending/items", +const ddbMock = mockClient(DynamoDBClient); +const smMock = mockClient(SecretsManagerClient); + +// Setup mock environment variable for JWT secret +const jwt_secret = secretObject["jwt_key"]; +vi.stubEnv("JwtSigningKey", jwt_secret); + +// Global variable to hold the app instance +let app: FastifyInstance; + +beforeEach(() => { + // Reset mocks before each test + ddbMock.reset(); + smMock.reset(); + + // Mock Secrets Manager responses + smMock.on(GetSecretValueCommand).resolves({ + SecretString: JSON.stringify(secretObject), + }); + + // Use fake timers + vi.useFakeTimers(); +}); + +afterAll(async () => { + // Close the app after all tests are done + if (app) { + await app.close(); + } + vi.useRealTimers(); +}); + +describe("Vending Items API Test", () => { + beforeEach(async () => { + // Initialize the app within each describe block to ensure fresh setup + app = await init(); + await app.ready(); + }); + + test("Test getting vending items", async () => { + // Mock DynamoDB response for vending items + ddbMock.on(ScanCommand).resolves({ + Items: [ + // Add mock vending item data here if needed + ], + }); + + const response = await app.inject({ + method: "GET", + url: "/api/v1/vending/items", + }); + expect(response.statusCode).toBe(200); + // Add more specific expectations for the response data if needed }); - expect(response.statusCode).toBe(200); }); diff --git a/tsconfig.json b/tsconfig.json index cb903f2..cdd1c7d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "compilerOptions": { "module": "Node16", "outDir": "dist", + "experimentalDecorators": true }, "ts-node": { "esm": true diff --git a/yarn.lock b/yarn.lock index f2fbe03..2e4de09 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1162,6 +1162,54 @@ resolved "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz" integrity sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ== +"@sequelize/core@7.0.0-alpha.43": + version "7.0.0-alpha.43" + resolved "https://registry.yarnpkg.com/@sequelize/core/-/core-7.0.0-alpha.43.tgz#c07dffb55b5c23f392b2735795534e384951380f" + integrity sha512-j3lZvUQPDNEb01kroSazprTHcQ1Y1t+LW6miKhmQ8O4+i7HY1WerO+co6TBcjunssTjJi+VyYXYlLQ0emc2k3w== + dependencies: + "@sequelize/utils" "7.0.0-alpha.43" + "@types/debug" "^4.1.12" + "@types/validator" "^13.11.9" + ansis "^3.2.0" + bnf-parser "^3.1.6" + dayjs "^1.11.10" + debug "^4.3.4" + dottie "^2.0.6" + fast-glob "^3.3.2" + inflection "^3.0.0" + lodash "^4.17.21" + retry-as-promised "^7.0.4" + semver "^7.3" + sequelize-pool "^8.0.0" + toposort-class "^1.0.1" + type-fest "^4.14.0" + uuid "^10.0.0" + validator "^13.11.0" + +"@sequelize/postgres@^7.0.0-alpha.43": + version "7.0.0-alpha.43" + resolved "https://registry.yarnpkg.com/@sequelize/postgres/-/postgres-7.0.0-alpha.43.tgz#5cf107420d3944c42e85d8e28752dcde597cff23" + integrity sha512-YAkLUMQCurVpgLF5qCKvtuvMYbwU6mgXNdNe/b1UEP+iW4AB06I+MMwfB7WtUak0X7IGld9YjXPOUz4HhDOTFg== + dependencies: + "@sequelize/core" "7.0.0-alpha.43" + "@sequelize/utils" "7.0.0-alpha.43" + "@types/pg" "^8.11.4" + lodash "^4.17.21" + pg "^8.11.3" + pg-hstore "^2.3.4" + pg-types "^4.0.2" + postgres-array "^3.0.2" + semver "^7.6.0" + wkx "^0.5.0" + +"@sequelize/utils@7.0.0-alpha.43": + version "7.0.0-alpha.43" + resolved "https://registry.yarnpkg.com/@sequelize/utils/-/utils-7.0.0-alpha.43.tgz#9b6e9edc79d0518258b790fcf5ce7f6a885e9f06" + integrity sha512-ex1iVNuqc7db1Il8VGkF4DWDkn7OpEsB4FGvS3OWz8g7blqvlO6T3fP4c+qmOxOrmdzDPWayh43U3xr/F4q3qQ== + dependencies: + "@types/lodash" "^4.17.0" + lodash "^4.17.21" + "@sinonjs/commons@^2.0.0": version "2.0.0" resolved "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz" @@ -1634,6 +1682,13 @@ resolved "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz" integrity sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q== +"@types/debug@^4.1.12": + version "4.1.12" + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" + integrity sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ== + dependencies: + "@types/ms" "*" + "@types/estree@1.0.5", "@types/estree@^1.0.0": version "1.0.5" resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz" @@ -1676,6 +1731,11 @@ dependencies: "@types/node" "*" +"@types/lodash@^4.17.0": + version "4.17.10" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.10.tgz#64f3edf656af2fe59e7278b73d3e62404144a6e6" + integrity sha512-YpS0zzoduEhuOWjAotS6A5AVCva7X4lVlYLF0FYHAY9sdraBfnatttHItlWeZdGhuEkf+OzMNg2ZYAx8t+52uQ== + "@types/methods@^1.1.4": version "1.1.4" resolved "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz" @@ -1686,6 +1746,11 @@ resolved "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz" integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== +"@types/ms@*": + version "0.7.34" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433" + integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g== + "@types/node@*", "@types/node@^22.1.0": version "22.1.0" resolved "https://registry.npmjs.org/@types/node/-/node-22.1.0.tgz" @@ -1693,6 +1758,15 @@ dependencies: undici-types "~6.13.0" +"@types/pg@^8.11.4": + version "8.11.10" + resolved "https://registry.yarnpkg.com/@types/pg/-/pg-8.11.10.tgz#b8fb2b2b759d452fe3ec182beadd382563b63291" + integrity sha512-LczQUW4dbOQzsH2RQ5qoeJ6qJPdrcM/DcMLoqWQkMLMsq83J5lAX3LXjdkWdpscFy67JSOWDnh7Ny/sPFykmkg== + dependencies: + "@types/node" "*" + pg-protocol "*" + pg-types "^4.0.1" + "@types/qs@*": version "6.9.15" resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz" @@ -1750,6 +1824,11 @@ "@types/methods" "^1.1.4" "@types/superagent" "^8.1.0" +"@types/validator@^13.11.9": + version "13.12.2" + resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.12.2.tgz#760329e756e18a4aab82fc502b51ebdfebbe49f5" + integrity sha512-6SlHBzUW8Jhf3liqrGGXyTJSIFe4nqlJ5A5KaMZ2l/vbM3Wh3KSybots/wfWVzNLK4D1NZluDlSQIbIEPx6oyA== + "@types/ws@^8.5.10": version "8.5.12" resolved "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz" @@ -2026,6 +2105,11 @@ ansi-styles@^6.0.0, ansi-styles@^6.2.1: resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz" integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== +ansis@^3.2.0: + version "3.3.2" + resolved "https://registry.yarnpkg.com/ansis/-/ansis-3.3.2.tgz#15adc36fea112da95c74d309706e593618accac3" + integrity sha512-cFthbBlt+Oi0i9Pv/j6YdVWJh54CtjGACaMPCIrEV4Ha7HWsIjXDwseYV79TIL0B4+KfSwD5S70PeQDkPUd1rA== + argparse@^1.0.7: version "1.0.10" resolved "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz" @@ -2244,6 +2328,11 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" +bnf-parser@^3.1.6: + version "3.1.6" + resolved "https://registry.yarnpkg.com/bnf-parser/-/bnf-parser-3.1.6.tgz#a36df2ba38ad63576a5864747d799cb716e760aa" + integrity sha512-3x0ECh6CghmcAYnY6uiVAOfl263XkWffDq5fQS20ac3k0U7TE5rTWNXTnOTckgmfZc94iharwqCyoV8OAYxYoA== + bowser@^2.11.0: version "2.11.0" resolved "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz" @@ -2531,6 +2620,11 @@ data-view-byte-offset@^1.0.0: es-errors "^1.3.0" is-data-view "^1.0.1" +dayjs@^1.11.10: + version "1.11.13" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.13.tgz#92430b0139055c3ebb60150aa13e860a4b5a366c" + integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg== + debug@^3.2.7: version "3.2.7" resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" @@ -2640,6 +2734,11 @@ dotenv@^16.4.5: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== +dottie@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/dottie/-/dottie-2.0.6.tgz#34564ebfc6ec5e5772272d466424ad5b696484d4" + integrity sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA== + each-parallel-async@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/each-parallel-async/-/each-parallel-async-1.0.0.tgz" @@ -3725,6 +3824,11 @@ imurmurhash@^0.1.4: resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== +inflection@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/inflection/-/inflection-3.0.0.tgz#6a956fa90d72a27d22e6b32ec1064877593ee23b" + integrity sha512-1zEJU1l19SgJlmwqsEyFTbScw/tkMHFenUo//Y0i+XEP83gDFdMvPizAD/WGcE+l1ku12PcTVHQhO6g5E0UCMw== + inflight@^1.0.4: version "1.0.6" resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" @@ -4523,6 +4627,11 @@ obliterator@^2.0.1: resolved "https://registry.npmjs.org/obliterator/-/obliterator-2.0.4.tgz" integrity sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ== +obuf@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" + integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== + on-exit-leak-free@^2.1.0: version "2.1.2" resolved "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz" @@ -4661,6 +4770,87 @@ performance-now@^2.1.0: resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz" integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== +pg-cloudflare@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz#e6d5833015b170e23ae819e8c5d7eaedb472ca98" + integrity sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q== + +pg-connection-string@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.7.0.tgz#f1d3489e427c62ece022dba98d5262efcb168b37" + integrity sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA== + +pg-hstore@^2.3.4: + version "2.3.4" + resolved "https://registry.yarnpkg.com/pg-hstore/-/pg-hstore-2.3.4.tgz#4425e3e2a3e15d2a334c35581186c27cf2e9b8dd" + integrity sha512-N3SGs/Rf+xA1M2/n0JBiXFDVMzdekwLZLAO0g7mpDY9ouX+fDI7jS6kTq3JujmYbtNSJ53TJ0q4G98KVZSM4EA== + dependencies: + underscore "^1.13.1" + +pg-int8@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" + integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== + +pg-numeric@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pg-numeric/-/pg-numeric-1.0.2.tgz#816d9a44026086ae8ae74839acd6a09b0636aa3a" + integrity sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw== + +pg-pool@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.7.0.tgz#d4d3c7ad640f8c6a2245adc369bafde4ebb8cbec" + integrity sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g== + +pg-protocol@*, pg-protocol@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.7.0.tgz#ec037c87c20515372692edac8b63cf4405448a93" + integrity sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ== + +pg-types@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3" + integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA== + dependencies: + pg-int8 "1.0.1" + postgres-array "~2.0.0" + postgres-bytea "~1.0.0" + postgres-date "~1.0.4" + postgres-interval "^1.1.0" + +pg-types@^4.0.1, pg-types@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-4.0.2.tgz#399209a57c326f162461faa870145bb0f918b76d" + integrity sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng== + dependencies: + pg-int8 "1.0.1" + pg-numeric "1.0.2" + postgres-array "~3.0.1" + postgres-bytea "~3.0.0" + postgres-date "~2.1.0" + postgres-interval "^3.0.0" + postgres-range "^1.1.1" + +pg@^8.11.3: + version "8.13.0" + resolved "https://registry.yarnpkg.com/pg/-/pg-8.13.0.tgz#e3d245342eb0158112553fcc1890a60720ae2a3d" + integrity sha512-34wkUTh3SxTClfoHB3pQ7bIMvw9dpFU1audQQeZG837fmHfHpr14n/AELVDoOYVDW2h5RDWU78tFjkD+erSBsw== + dependencies: + pg-connection-string "^2.7.0" + pg-pool "^3.7.0" + pg-protocol "^1.7.0" + pg-types "^2.1.0" + pgpass "1.x" + optionalDependencies: + pg-cloudflare "^1.1.1" + +pgpass@1.x: + version "1.0.5" + resolved "https://registry.yarnpkg.com/pgpass/-/pgpass-1.0.5.tgz#9b873e4a564bb10fa7a7dbd55312728d422a223d" + integrity sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug== + dependencies: + split2 "^4.1.0" + picocolors@^1.0.0, picocolors@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz" @@ -4720,6 +4910,55 @@ postcss@^8.4.40: picocolors "^1.0.1" source-map-js "^1.2.0" +postgres-array@^3.0.2, postgres-array@~3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-3.0.2.tgz#68d6182cb0f7f152a7e60dc6a6889ed74b0a5f98" + integrity sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog== + +postgres-array@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" + integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA== + +postgres-bytea@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-1.0.0.tgz#027b533c0aa890e26d172d47cf9ccecc521acd35" + integrity sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w== + +postgres-bytea@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-3.0.0.tgz#9048dc461ac7ba70a6a42d109221619ecd1cb089" + integrity sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw== + dependencies: + obuf "~1.1.2" + +postgres-date@~1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.7.tgz#51bc086006005e5061c591cee727f2531bf641a8" + integrity sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q== + +postgres-date@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-2.1.0.tgz#b85d3c1fb6fb3c6c8db1e9942a13a3bf625189d0" + integrity sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA== + +postgres-interval@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-1.2.0.tgz#b460c82cb1587507788819a06aa0fffdb3544695" + integrity sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ== + dependencies: + xtend "^4.0.0" + +postgres-interval@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-3.0.0.tgz#baf7a8b3ebab19b7f38f07566c7aab0962f0c86a" + integrity sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw== + +postgres-range@^1.1.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/postgres-range/-/postgres-range-1.1.4.tgz#a59c5f9520909bcec5e63e8cf913a92e4c952863" + integrity sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w== + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" @@ -4914,6 +5153,11 @@ ret@~0.4.0: resolved "https://registry.npmjs.org/ret/-/ret-0.4.3.tgz" integrity sha512-0f4Memo5QP7WQyUEAYUO3esD/XjOc3Zjjg5CPsAq1p8sIu0XPeMbHJemKA0BO7tV0X7+A0FoEpbmHXWxPyD3wQ== +retry-as-promised@^7.0.4: + version "7.0.4" + resolved "https://registry.yarnpkg.com/retry-as-promised/-/retry-as-promised-7.0.4.tgz#9df73adaeea08cb2948b9d34990549dc13d800a2" + integrity sha512-XgmCoxKWkDofwH8WddD0w85ZfqYz+ZHlr5yo+3YUCfycWawU56T5ckWXsScsj5B8tqUcIG67DxXByo3VUgiAdA== + reusify@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" @@ -5045,11 +5289,16 @@ semver@^6.1.2, semver@^6.3.1: resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.5.4, semver@^7.6.0: +semver@^7.3, semver@^7.5.4, semver@^7.6.0: version "7.6.3" resolved "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz" integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== +sequelize-pool@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/sequelize-pool/-/sequelize-pool-8.0.0.tgz#4bd3a62b9ab8ec336190e2cbce81aa769d7365ee" + integrity sha512-xY04c5ctp6lKE4ZoSVo7YJHN5FHVhoYMoH2Z8jWIJG26c9kJSkIjql5HgD9XG5cwJbYMF0Ko2Fhgws20HC90Ug== + set-cookie-parser@^2.4.1: version "2.7.0" resolved "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.0.tgz" @@ -5194,7 +5443,7 @@ source-map-js@^1.2.0: resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz" integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== -split2@^4.0.0: +split2@^4.0.0, split2@^4.1.0: version "4.2.0" resolved "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz" integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== @@ -5478,6 +5727,11 @@ toad-cache@^3.3.0: resolved "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz" integrity sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw== +toposort-class@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toposort-class/-/toposort-class-1.0.1.tgz#7ffd1f78c8be28c3ba45cd4e1a3f5ee193bd9988" + integrity sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg== + totalist@^3.0.0: version "3.0.1" resolved "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz" @@ -5587,6 +5841,11 @@ type-fest@^0.8.1: resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type-fest@^4.14.0: + version "4.26.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.26.1.tgz#a4a17fa314f976dd3e6d6675ef6c775c16d7955e" + integrity sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg== + typed-array-buffer@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz" @@ -5646,6 +5905,11 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +underscore@^1.13.1: + version "1.13.7" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.7.tgz#970e33963af9a7dda228f17ebe8399e5fbe63a10" + integrity sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g== + undici-types@~6.13.0: version "6.13.0" resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz" @@ -5668,6 +5932,11 @@ uuid-random@^1.3.2: resolved "https://registry.npmjs.org/uuid-random/-/uuid-random-1.3.2.tgz" integrity sha512-UOzej0Le/UgkbWEO8flm+0y+G+ljUon1QWTEZOq1rnMAsxo2+SckbiZdKzAHHlVh6gJqI1TjC/xwgR50MuCrBQ== +uuid@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294" + integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ== + uuid@^3.3.2: version "3.4.0" resolved "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz" @@ -5683,6 +5952,11 @@ v8-compile-cache@^2.0.3: resolved "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz" integrity sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw== +validator@^13.11.0: + version "13.12.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-13.12.0.tgz#7d78e76ba85504da3fee4fd1922b385914d4b35f" + integrity sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg== + verror@1.10.0: version "1.10.0" resolved "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz" @@ -5783,6 +6057,13 @@ why-is-node-running@^2.3.0: siginfo "^2.0.0" stackback "0.0.2" +wkx@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/wkx/-/wkx-0.5.0.tgz#c6c37019acf40e517cc6b94657a25a3d4aa33e8c" + integrity sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg== + dependencies: + "@types/node" "*" + word-wrap@^1.2.5, word-wrap@~1.2.3: version "1.2.5" resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz" @@ -5814,6 +6095,11 @@ ws@^8.16.0: resolved "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz" integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== +xtend@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + yallist@^2.1.2: version "2.1.2" resolved "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz"