diff --git a/apps/api/__tests__/lib/s3.test.ts b/apps/api/__tests__/lib/s3.test.ts index 4321643..fd8a673 100644 --- a/apps/api/__tests__/lib/s3.test.ts +++ b/apps/api/__tests__/lib/s3.test.ts @@ -1,58 +1,60 @@ -import { getOne, uploadOne, deleteOne } from "../../lib/s3"; - -test("getOnePresigned does not return null", async () => { - let url = null; - url = await getOne( - "dev/profile-images/users/1877385915/1-intro-photo-final.jpg" - ); - console.log(url); - - expect(url).not.toBe(null); -}); - -test("getting non-existent s3 object throws error", async () => { - await expect( - getOne("dev/code-modules/users/001/001/somenonexistentobject.tsx") - ).rejects.toThrow(); -}); - -test("upload test.txt module to s3", async () => { - const url = "dev/code-modules/users/001/004/test.txt"; - await uploadOne(url, Buffer.from("Hello world!")); - - // Try getting the newly added data - let textData = null; - try { - const presigned = await getOne(url); - const response = await fetch(presigned); - - textData = await response.text(); - } catch (error) { - console.log(`Something went wrong: ${error}`); - } - - expect(textData).toBe("Hello world!"); -}); - -test("deleting s3 objects", async () => { - // Upload new test module - const url = "dev/code-modules/users/001/005/test.txt"; - await uploadOne(url, Buffer.from("Hello world!")); - - // Try getting the newly added data - let textData = null; - try { - const presigned = await getOne(url); - const response = await fetch(presigned); - - textData = await response.text(); - } catch (error) { - console.log(`Something went wrong: ${error}`); - } - - // Delete newly added data - await deleteOne(url); - - // Expect that the deleted url no longer exists - await expect(getOne(url)).rejects.toThrow(); +import { getOne, uploadOne, deleteOne } from "../../src/lib/s3"; + +describe("S3 Module", () => { + it("returns a valid presigned URL for an existing S3 object", async () => { + let url = null; + url = await getOne( + "dev/profile-images/users/1877385915/1-intro-photo-final.jpg" + ); + console.log(url); + + expect(url).not.toBe(null); + }); + + it("throws an error when trying to get a non-existent S3 object", async () => { + await expect( + getOne("dev/code-modules/users/001/001/somenonexistentobject.tsx") + ).rejects.toThrow(); + }); + + it("successfully uploads and retrieves text data from S3", async () => { + const url = "dev/code-modules/users/001/004/test.txt"; + await uploadOne(url, Buffer.from("Hello world!")); + + // Try getting the newly added data + let textData = null; + try { + const presigned = await getOne(url); + const response = await fetch(presigned); + + textData = await response.text(); + } catch (error) { + console.log(`Something went wrong: ${error}`); + } + + expect(textData).toBe("Hello world!"); + }); + + it("successfully deletes an S3 object and ensures it no longer exists", async () => { + // Upload new test module + const url = "dev/code-modules/users/001/005/test.txt"; + await uploadOne(url, Buffer.from("Hello world!")); + + // Try getting the newly added data + let textData = null; + try { + const presigned = await getOne(url); + const response = await fetch(presigned); + + textData = await response.text(); + } catch (error) { + console.log(`Something went wrong: ${error}`); + } + + // Delete newly added data + await deleteOne(url); + + // Expect that the deleted url no longer exists + await expect(getOne(url)).rejects.toThrow(); + }); }); diff --git a/apps/api/__tests__/routes/auth.test.ts b/apps/api/__tests__/routes/auth.test.ts new file mode 100644 index 0000000..1fb6278 --- /dev/null +++ b/apps/api/__tests__/routes/auth.test.ts @@ -0,0 +1,183 @@ +import request from "supertest"; +import { app } from "../../src/app"; +import bcrypt from "bcrypt"; +import jwt from "jsonwebtoken"; +import { PrismaClient } from "@prisma/client"; + +const prisma = new PrismaClient(); + +beforeAll(async () => { + await prisma.$connect(); +}); + +afterAll(async () => { + await prisma.$disconnect(); +}); + +jest.mock("@prisma/client", () => { + const mPrismaClient = { + user: { + findUnique: jest.fn(), + findFirst: jest.fn(), + create: jest.fn(), + }, + $connect: jest.fn(), + $disconnect: jest.fn(), + }; + return { + PrismaClient: jest.fn(() => mPrismaClient), + }; +}); + +jest.mock("bcrypt", () => ({ + compare: jest.fn(), + genSalt: jest.fn(), + hash: jest.fn(), +})); + +jest.mock("jsonwebtoken", () => ({ + sign: jest.fn(), +})); + +describe("POST /api/auth/login", () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it("should return 400 if username or password is missing", async () => { + const response = await request(app).post("/api/auth/login").send({}); + expect(response.status).toBe(400); + expect(response.body.error).toBe("Username and password are required"); + }); + + it("should return 401 if the user is not found", async () => { + (prisma.user.findUnique as jest.Mock).mockResolvedValue(null); + + const response = await request(app) + .post("/api/auth/login") + .send({ username: "testuser", password: "password123" }); + + expect(response.status).toBe(401); + expect(response.body.error).toBe("Invalid credentials"); + }); + + it("should return 401 if the password is invalid", async () => { + (prisma.user.findUnique as jest.Mock).mockResolvedValue({ + id: 1, + username: "testuser", + password: "hashedPassword", + }); + + (bcrypt.compare as jest.Mock).mockResolvedValue(false); + + const response = await request(app) + .post("/api/auth/login") + .send({ username: "testuser", password: "password123" }); + + expect(response.status).toBe(401); + expect(response.body.error).toBe("Invalid credentials"); + }); + + it("should return 200 and set a token if the login is successful", async () => { + (prisma.user.findUnique as jest.Mock).mockResolvedValue({ + id: 1, + username: "testuser", + password: "hashedPassword", + }); + + (bcrypt.compare as jest.Mock).mockResolvedValue(true); + (jwt.sign as jest.Mock).mockReturnValue("mockToken"); + + const response = await request(app) + .post("/api/auth/login") + .send({ username: "testuser", password: "password123" }); + + expect(response.status).toBe(200); + expect(response.body.message).toBe("Login successful"); + expect(response.headers["set-cookie"]).toBeDefined(); + }); +}); + +describe("POST /api/auth/signup", () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it("should return 400 if required fields are missing", async () => { + const response = await request(app).post("/api/auth/signup").send({}); + expect(response.status).toBe(400); + expect(response.body.error).toBe("All fields are required"); + }); + + it("should return 400 if email is already in use", async () => { + (prisma.user.findFirst as jest.Mock).mockResolvedValue({ + email: "test@example.com", + }); + + const response = await request(app).post("/api/auth/signup").send({ + email: "test@example.com", + username: "testuser", + password: "password123", + }); + + expect(response.status).toBe(400); + expect(response.body.error).toBe("Email already in use"); + }); + + it("should return 400 if username is already taken", async () => { + (prisma.user.findFirst as jest.Mock).mockResolvedValue({ + username: "testuser", + }); + + const response = await request(app).post("/api/auth/signup").send({ + email: "unique@example.com", + username: "testuser", + password: "password123", + }); + + expect(response.status).toBe(400); + expect(response.body.error).toBe("Username already taken"); + }); + + it("should return 201 and create a new user if data is valid", async () => { + (prisma.user.findFirst as jest.Mock).mockResolvedValue(null); + (bcrypt.genSalt as jest.Mock).mockResolvedValue("mockSalt"); + (bcrypt.hash as jest.Mock).mockResolvedValue("mockHashedPassword"); + (prisma.user.create as jest.Mock).mockResolvedValue({ + id: 1, + email: "newuser@example.com", + username: "newuser", + password: "mockHashedPassword", + salt: "mockSalt", + }); + + const response = await request(app).post("/api/auth/signup").send({ + email: "newuser@example.com", + username: "newuser", + password: "password123", + }); + + expect(response.status).toBe(201); + expect(response.body.message).toBe("User created successfully"); + expect(response.body.user).toEqual({ + id: 1, + email: "newuser@example.com", + username: "newuser", + }); + }); + + it("should return 500 if there is a server error", async () => { + (prisma.user.findFirst as jest.Mock).mockImplementation(() => { + throw new Error("Database error"); + }); + + const response = await request(app).post("/api/auth/signup").send({ + email: "newuser@example.com", + username: "newuser", + password: "password123", + }); + + expect(response.status).toBe(500); + expect(response.body.error).toBe("Internal server error"); + }); +}); diff --git a/apps/api/package.json b/apps/api/package.json index 2fc69c0..7697080 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -6,10 +6,11 @@ "start": "node dist/index.js", "build": "tsc", "dev": "nodemon src/index.ts", - "test": "jest --coverage=true" + "test": "jest --coverage --detectOpenHandles" }, "dependencies": { - + "@aws-sdk/client-s3": "^3.717.0", + "@aws-sdk/s3-request-presigner": "^3.717.0", "@prisma/client": "^5.10.2", "@types/bcrypt": "^5.0.2", "@types/cookie-parser": "^1.4.8", @@ -20,17 +21,17 @@ "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.18.3", - "jsonwebtoken": "^9.0.2", - "@aws-sdk/client-s3": "^3.717.0", - "@aws-sdk/s3-request-presigner": "^3.717.0" + "jsonwebtoken": "^9.0.2" }, "devDependencies": { "@types/express": "^5.0.0", "@types/jest": "^29.5.14", "@types/node": "^20", + "@types/supertest": "^6.0.2", "jest": "^29.7.0", "nodemon": "^3.1.0", "prisma": "^5.10.2", + "supertest": "^7.0.0", "ts-jest": "^29.1.2", "ts-node": "^10.9.2", "typescript": "^5.7.2" diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts new file mode 100644 index 0000000..b450a1d --- /dev/null +++ b/apps/api/src/app.ts @@ -0,0 +1,19 @@ +import express from "express"; +import cors from "cors"; + +import dotenv from "dotenv"; +import cookieParser from "cookie-parser"; + +import { authRouter } from "./routes"; + +dotenv.config(); + +const app = express(); + +app.use(cors()); +app.use(express.json()); +app.use(cookieParser()); + +app.use("/api/auth", authRouter); + +export { app }; diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 4e53081..e28da5f 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -1,130 +1,9 @@ -const cors = require("cors"); -import express, { Request, Response, RequestHandler } from "express"; -import { PrismaClient } from "@prisma/client"; -import bcrypt from "bcrypt"; -import dotenv from "dotenv"; -import jwt from "jsonwebtoken"; -import cookieParser from 'cookie-parser'; +import { app } from "./app"; -dotenv.config(); - -const prisma = new PrismaClient(); -const app = express(); const PORT = process.env.PORT || 5000; -app.use(cors()); -app.use(express.json()); -app.use(cookieParser()); - -const loginHandler: RequestHandler = async (req: Request, res: Response) => { - try { - const { username, password } = req.body; - - if (!username || !password) { - res.status(400).json({ - error: "Username and password are required", - }); - return; - } - - const user = await prisma.user.findUnique({ - where: { username } - }); - - if (!user) { - res.status(401).json({ error: "Invalid credentials" }); - return; - } - - const isValidPassword = await bcrypt.compare(password, user.password); - - if (!isValidPassword) { - res.status(401).json({ error: "Invalid credentials" }); - return; - } - - const token = jwt.sign( - { userId: user.id, username: user.username }, - process.env.JWT_SECRET!, - { expiresIn: "24h" } - ); - - res.cookie('token', token, { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - sameSite: 'none', - maxAge: 24 * 60 * 60 * 1000 - }); - - res.json({ - message: 'Login successful' - }); - - } catch (error) { - console.error('Login error:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}; - -const signupHandler: RequestHandler = async (req: Request, res: Response) => { - try { - const { email, username, password } = req.body; - - if (!email || !username || !password) { - res.status(400).json({ error: 'All fields are required' }); - return; - } - - const existingUser = await prisma.user.findFirst({ - where: { - OR: [ - { email }, - { username } - ] - } - }); - - if (existingUser) { - res.status(400).json({ - error: existingUser.email === email - ? 'Email already in use' - : 'Username already taken' - }); - return; - } - - const salt = await bcrypt.genSalt(10); - const hashedPassword = await bcrypt.hash(password, salt); - - const user = await prisma.user.create({ - data: { - email, - username, - password: hashedPassword, - salt - } - }); - - res.status(201).json({ - message: 'User created successfully', - user: { - id: user.id, - username: user.username, - email: user.email - } - }); - - } catch (error) { - console.error('Signup error:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}; - -app.post("/login", loginHandler); -app.post("/signup", signupHandler); - app.listen(PORT, () => { console.log( `server on port ${PORT} good job for not crashing everything - justin` ); -}); \ No newline at end of file +}); diff --git a/apps/api/lib/s3.ts b/apps/api/src/lib/s3.ts similarity index 100% rename from apps/api/lib/s3.ts rename to apps/api/src/lib/s3.ts diff --git a/apps/api/src/routes/authRouter.ts b/apps/api/src/routes/authRouter.ts new file mode 100644 index 0000000..8549412 --- /dev/null +++ b/apps/api/src/routes/authRouter.ts @@ -0,0 +1,110 @@ +import { Router, Request, Response } from "express"; +import bcrypt from "bcrypt"; +import jwt from "jsonwebtoken"; +import { PrismaClient } from "@prisma/client"; + +const prisma = new PrismaClient(); + +const authRouter = Router(); + +authRouter.post("/login", async (req: Request, res: Response) => { + try { + const { username, password } = req.body; + + if (!username || !password) { + res.status(400).json({ + error: "Username and password are required", + }); + return; + } + + const user = await prisma.user.findUnique({ + where: { username }, + }); + + if (!user) { + res.status(401).json({ error: "Invalid credentials" }); + return; + } + + const isValidPassword = await bcrypt.compare(password, user.password); + + if (!isValidPassword) { + res.status(401).json({ error: "Invalid credentials" }); + return; + } + + const token = jwt.sign( + { userId: user.id, username: user.username }, + process.env.JWT_SECRET!, + { expiresIn: "24h" } + ); + + res.cookie("token", token, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "none", + maxAge: 24 * 60 * 60 * 1000, + }); + + res.json({ + message: "Login successful", + }); + } catch (error) { + console.error("Login error:", error); + res.status(500).json({ error: "Internal server error" }); + } +}); + +authRouter.post("/signup", async (req: Request, res: Response) => { + try { + const { email, username, password } = req.body; + + if (!email || !username || !password) { + res.status(400).json({ error: "All fields are required" }); + return; + } + + const existingUser = await prisma.user.findFirst({ + where: { + OR: [{ email }, { username }], + }, + }); + + if (existingUser) { + res.status(400).json({ + error: + existingUser.email === email + ? "Email already in use" + : "Username already taken", + }); + return; + } + + const salt = await bcrypt.genSalt(10); + const hashedPassword = await bcrypt.hash(password, salt); + + const user = await prisma.user.create({ + data: { + email, + username, + password: hashedPassword, + salt, + }, + }); + + res.status(201).json({ + message: "User created successfully", + user: { + id: user.id, + username: user.username, + email: user.email, + }, + }); + } catch (error) { + console.error("Signup error:", error); + res.status(500).json({ error: "Internal server error" }); + } +}); + +export default authRouter; diff --git a/apps/api/src/routes/componentRouter.ts b/apps/api/src/routes/componentRouter.ts new file mode 100644 index 0000000..e69de29 diff --git a/apps/api/src/routes/index.ts b/apps/api/src/routes/index.ts new file mode 100644 index 0000000..5891802 --- /dev/null +++ b/apps/api/src/routes/index.ts @@ -0,0 +1 @@ +export { default as authRouter } from "./authRouter"; \ No newline at end of file diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json index 0b60f4a..1b4f4b3 100644 --- a/apps/api/tsconfig.json +++ b/apps/api/tsconfig.json @@ -9,6 +9,6 @@ "skipLibCheck": true, "baseUrl": "." // Base directory for resolving non-relative imports }, - "include": ["src", "lib", "__tests__"], + "include": ["src", "src/lib", "__tests__"], "exclude": ["node_modules", "dist"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c73bf09..f79cc60 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -69,6 +69,9 @@ importers: '@types/node': specifier: ^20 version: 20.17.10 + '@types/supertest': + specifier: ^6.0.2 + version: 6.0.2 jest: specifier: ^29.7.0 version: 29.7.0(@types/node@20.17.10)(ts-node@10.9.2(@types/node@20.17.10)(typescript@5.7.2)) @@ -78,6 +81,9 @@ importers: prisma: specifier: ^5.10.2 version: 5.22.0 + supertest: + specifier: ^7.0.0 + version: 7.0.0 ts-jest: specifier: ^29.1.2 version: 29.2.5(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(jest@29.7.0(@types/node@20.17.10)(ts-node@10.9.2(@types/node@20.17.10)(typescript@5.7.2)))(typescript@5.7.2) @@ -1144,6 +1150,9 @@ packages: peerDependencies: '@types/express': '*' + '@types/cookiejar@2.1.5': + resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} + '@types/cors@2.8.17': resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} @@ -1189,6 +1198,9 @@ packages: '@types/jsonwebtoken@9.0.7': resolution: {integrity: sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==} + '@types/methods@1.1.4': + resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} + '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} @@ -1218,6 +1230,12 @@ packages: '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + '@types/superagent@8.1.9': + resolution: {integrity: sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==} + + '@types/supertest@6.0.2': + resolution: {integrity: sha512-137ypx2lk/wTQbW6An6safu9hXmajAifU/s7szAHLN/FeIm5w7yR0Wkl9fdJMRSHwOn4HLAI0DaB2TOORuhPDg==} + '@types/yargs-parser@21.0.3': resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} @@ -1487,12 +1505,18 @@ packages: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} + asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + ast-types-flow@0.0.8: resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -1678,6 +1702,10 @@ packages: resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} engines: {node: '>=12.5.0'} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -1685,6 +1713,9 @@ packages: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} + component-emitter@1.3.1: + resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -1717,6 +1748,9 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} + cookiejar@2.1.4: + resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} + cors@2.8.5: resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} engines: {node: '>= 0.10'} @@ -1804,6 +1838,10 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + delegates@1.0.0: resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} @@ -1823,6 +1861,9 @@ packages: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} + dezalgo@1.0.4: + resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} + didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} @@ -2110,6 +2151,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + fast-uri@3.0.5: resolution: {integrity: sha512-5JnBCWpFlMo0a3ciDy/JckMzzv1U9coZrIhedq+HXxxUfDTAiS0LA8OKVao4G9BxmCVck/jtA5r3KAtRWEyD8Q==} @@ -2160,6 +2204,13 @@ packages: resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} engines: {node: '>=14'} + form-data@4.0.1: + resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} + engines: {node: '>= 6'} + + formidable@3.5.2: + resolution: {integrity: sha512-Jqc1btCy3QzRbJaICGwKcBfGWuLADRerLzDqi2NwSt/UkXLsHJw2TVResiaoBufHVHy9aSgClOHCeJsSsFLTbg==} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -2297,6 +2348,10 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hexoid@2.0.0: + resolution: {integrity: sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==} + engines: {node: '>=8'} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -2845,6 +2900,11 @@ packages: engines: {node: '>=4'} hasBin: true + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -3518,6 +3578,14 @@ packages: engines: {node: '>=16 || 14 >=14.17'} hasBin: true + superagent@9.0.2: + resolution: {integrity: sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w==} + engines: {node: '>=14.18.0'} + + supertest@7.0.0: + resolution: {integrity: sha512-qlsr7fIC0lSddmA3tzojvzubYxvlGtzumcdHgPwbFWMISQwL22MhM2Y3LNt+6w9Yyx7559VW5ab70dgphm8qQA==} + engines: {node: '>=14.18.0'} + supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} @@ -5443,6 +5511,8 @@ snapshots: dependencies: '@types/express': 5.0.0 + '@types/cookiejar@2.1.5': {} + '@types/cors@2.8.17': dependencies: '@types/node': 20.17.10 @@ -5502,6 +5572,8 @@ snapshots: dependencies: '@types/node': 20.17.10 + '@types/methods@1.1.4': {} + '@types/mime@1.3.5': {} '@types/node@20.17.10': @@ -5533,6 +5605,18 @@ snapshots: '@types/stack-utils@2.0.3': {} + '@types/superagent@8.1.9': + dependencies: + '@types/cookiejar': 2.1.5 + '@types/methods': 1.1.4 + '@types/node': 20.17.10 + form-data: 4.0.1 + + '@types/supertest@6.0.2': + dependencies: + '@types/methods': 1.1.4 + '@types/superagent': 8.1.9 + '@types/yargs-parser@21.0.3': {} '@types/yargs@17.0.33': @@ -5859,10 +5943,14 @@ snapshots: get-intrinsic: 1.2.6 is-array-buffer: 3.0.5 + asap@2.0.6: {} + ast-types-flow@0.0.8: {} async@3.2.6: {} + asynckit@0.4.0: {} + available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.0.0 @@ -6083,10 +6171,16 @@ snapshots: color-string: 1.9.1 optional: true + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + commander@2.20.3: {} commander@4.1.1: {} + component-emitter@1.3.1: {} + concat-map@0.0.1: {} console-control-strings@1.1.0: {} @@ -6110,6 +6204,8 @@ snapshots: cookie@0.7.2: {} + cookiejar@2.1.4: {} + cors@2.8.5: dependencies: object-assign: 4.1.1 @@ -6194,6 +6290,8 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + delayed-stream@1.0.0: {} + delegates@1.0.0: {} depd@2.0.0: {} @@ -6204,6 +6302,11 @@ snapshots: detect-newline@3.1.0: {} + dezalgo@1.0.4: + dependencies: + asap: 2.0.6 + wrappy: 1.0.2 + didyoumean@1.2.2: {} diff-sequences@29.6.3: {} @@ -6650,6 +6753,8 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-safe-stringify@2.1.1: {} + fast-uri@3.0.5: {} fast-xml-parser@4.4.1: @@ -6714,6 +6819,18 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + form-data@4.0.1: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + + formidable@3.5.2: + dependencies: + dezalgo: 1.0.4 + hexoid: 2.0.0 + once: 1.4.0 + forwarded@0.2.0: {} fresh@0.5.2: {} @@ -6851,6 +6968,8 @@ snapshots: dependencies: function-bind: 1.1.2 + hexoid@2.0.0: {} + html-escaper@2.0.2: {} http-errors@2.0.0: @@ -7572,6 +7691,8 @@ snapshots: mime@1.6.0: {} + mime@2.6.0: {} + mimic-fn@2.1.0: {} mini-css-extract-plugin@2.9.2(webpack@5.97.1): @@ -8301,6 +8422,27 @@ snapshots: pirates: 4.0.6 ts-interface-checker: 0.1.13 + superagent@9.0.2: + dependencies: + component-emitter: 1.3.1 + cookiejar: 2.1.4 + debug: 4.4.0(supports-color@5.5.0) + fast-safe-stringify: 2.1.1 + form-data: 4.0.1 + formidable: 3.5.2 + methods: 1.1.2 + mime: 2.6.0 + qs: 6.13.0 + transitivePeerDependencies: + - supports-color + + supertest@7.0.0: + dependencies: + methods: 1.1.2 + superagent: 9.0.2 + transitivePeerDependencies: + - supports-color + supports-color@5.5.0: dependencies: has-flag: 3.0.0