From 110951b10b732e72082076b9f06d95a17da25898 Mon Sep 17 00:00:00 2001 From: Ilyas Karim Date: Wed, 13 Nov 2024 04:59:59 +0500 Subject: [PATCH 1/6] Create users.json --- tests/test-data/users.json | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 tests/test-data/users.json diff --git a/tests/test-data/users.json b/tests/test-data/users.json new file mode 100644 index 0000000..e252aff --- /dev/null +++ b/tests/test-data/users.json @@ -0,0 +1,32 @@ +[ + { + "id": 1, + "username": "user_1", + "email": "user_1@example.com", + "created_at": "2023-12-31T23:58:33.390632" + }, + { + "id": 2, + "username": "user_2", + "email": "user_2@example.com", + "created_at": "2024-04-06T23:58:33.390632" + }, + { + "id": 3, + "username": "user_3", + "email": "user_3@example.com", + "created_at": "2023-12-07T23:58:33.390632" + }, + { + "id": 4, + "username": "user_4", + "email": "user_4@example.com", + "created_at": "2024-02-29T23:58:33.390632" + }, + { + "id": 5, + "username": "user_5", + "email": "user_5@example.com", + "created_at": "2024-09-27T23:58:33.390632" + } +] From 65456a41433ec54515aa9570dfdc49dd88355e7b Mon Sep 17 00:00:00 2001 From: Ilyas Karim Date: Wed, 13 Nov 2024 05:20:00 +0500 Subject: [PATCH 2/6] Apis setup --- prisma/schema.prisma | 38 +++++---- src/v1/routes.ts | 73 +++++++++-------- src/v1/transaction.ts | 82 ++++++++++++++++++-- src/v1/user.ts | 25 +++++- src/v1/validations/coin.validation.ts | 28 +++++++ src/v1/validations/transaction.validation.ts | 34 ++++++++ src/v1/{ => validations}/user.validation.ts | 5 ++ src/v1/wallet.ts | 1 - 8 files changed, 227 insertions(+), 59 deletions(-) create mode 100644 src/v1/validations/coin.validation.ts create mode 100644 src/v1/validations/transaction.validation.ts rename src/v1/{ => validations}/user.validation.ts (79%) delete mode 100644 src/v1/wallet.ts diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 904bb12..fee7bd7 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -12,30 +12,40 @@ model User { username String @unique email String @unique application_user_id String @unique - Wallet Wallet[] Transaction Transaction[] created_at DateTime @default(now()) updated_at DateTime @updatedAt } - -model Wallet { - id Int @id @default(autoincrement()) - user_id Int - user User @relation(fields: [user_id], references: [id]) - balance Int - Transaction Transaction[] - created_at DateTime @default(now()) - updated_at DateTime @updatedAt -} - model Transaction { id Int @id @default(autoincrement()) transaction_type String user_id Int user User @relation(fields: [user_id], references: [id]) - wallet_id Int - wallet Wallet @relation(fields: [wallet_id], references: [id]) amount Int + coin_id Int + coin Coin @relation(fields: [coin_id], references: [id]) created_at DateTime @default(now()) updated_at DateTime @updatedAt } + + +model Coin { + id Int @id @default(autoincrement()) + name String + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + Transactions Transaction[] + CoinRatesOne CoinRate[] @relation("coin_one") + CoinRatesTwo CoinRate[] @relation("coin_two") +} + +model CoinRate { + id Int @id @default(autoincrement()) + coin_one_id Int + coin_one Coin @relation("coin_one", fields: [coin_one_id], references: [id]) + coin_two_id Int + coin_two Coin @relation("coin_two", fields: [coin_two_id], references: [id]) + rate Float + created_at DateTime @default(now()) + updated_at DateTime @updatedAt +} diff --git a/src/v1/routes.ts b/src/v1/routes.ts index d22f501..b6268c5 100644 --- a/src/v1/routes.ts +++ b/src/v1/routes.ts @@ -1,46 +1,45 @@ import { middlewareValidateYupSchemaAgainstReqBody, validateUserExistsSentThroughReqBody } from "../utils/middlewares"; -import { createUserSchema, updateUserSchema } from "./user.validation"; +import { createUserSchema, updateUserSchema } from "./validations/user.validation"; import { createUser, getUserBalance, updateUser } from "./user"; import express from "express"; +import { + createTransactionSchema, + deleteTransactionSchema, + getTransactionSchema, + getTransactionsSchema, + updateTransactionSchema, +} from "./validations/transaction.validation"; +import { deleteTransaction, getTransaction, getTransactions } from "./transaction"; +import { updateTransaction } from "./transaction"; +import { createTransaction } from "./transaction"; const v1Routes = express.Router(); -v1Routes.get("/createTransaction", function (req, res) { - res.status(200).json({ - message: "createTransaction", - }); -}); -v1Routes.get("/updateTransaction", function (req, res) { - res.status(200).json({ - message: "updateTransaction", - }); -}); -v1Routes.delete("/deleteTransaction", function (req, res) { - res.status(200).json({ - message: "deleteTransaction", - }); -}); -v1Routes.get("/getTransaction", function (req, res) { - res.status(200).json({ - message: "getTransaction", - }); -}); -v1Routes.get("/getTransactions", function (req, res) { - res.status(200).json({ - message: "getTransactions", - }); -}); -v1Routes.get("/getTransactionsByUser", function (req, res) { - res.status(200).json({ - message: "getTransactionsByUser", - }); -}); - -v1Routes.post("/createWallet", function (req, res) { - res.status(200).json({ - message: "createWallet", - }); -}); +v1Routes.post( + "/createTransaction", + middlewareValidateYupSchemaAgainstReqBody(createTransactionSchema), + createTransaction, +); +v1Routes.put( + "/updateTransaction", + middlewareValidateYupSchemaAgainstReqBody(updateTransactionSchema), + updateTransaction, +); +v1Routes.delete( + "/deleteTransaction", + middlewareValidateYupSchemaAgainstReqBody(deleteTransactionSchema), + deleteTransaction, +); +v1Routes.get( + "/getTransaction", + middlewareValidateYupSchemaAgainstReqBody(getTransactionSchema), + getTransaction, +); +v1Routes.get( + "/getTransactions", + middlewareValidateYupSchemaAgainstReqBody(getTransactionsSchema), + getTransactions, +); v1Routes.post("/createUser", middlewareValidateYupSchemaAgainstReqBody(createUserSchema), createUser); v1Routes.put( diff --git a/src/v1/transaction.ts b/src/v1/transaction.ts index 7da3afa..a7efe8c 100644 --- a/src/v1/transaction.ts +++ b/src/v1/transaction.ts @@ -1,6 +1,76 @@ -export const createTransaction = () => {}; -export const updateTransaction = () => {}; -export const deleteTransaction = () => {}; -export const getTransaction = () => {}; -export const getTransactions = () => {}; -export const getTransactionsByUser = () => {}; +import prisma from "../database/prisma"; +import { generateError } from "../utils/errors"; +import { Request, Response } from "express"; + +export const createTransaction = async (req: Request, res: Response) => { + try { + const transaction = await prisma.transaction.create({ + data: req.body.input, + }); + res.status(200).json({ + message: "Transaction created successfully", + transaction: transaction, + }); + } catch (e) { + generateError(res, e); + } +}; +export const updateTransaction = async (req: Request, res: Response) => { + try { + const transaction = await prisma.transaction.update({ + where: { id: req.body.input.id }, + data: req.body.input, + }); + res.status(200).json({ + message: "Transaction updated successfully", + transaction: transaction, + }); + } catch (e) { + generateError(res, e); + } +}; +export const deleteTransaction = async (req: Request, res: Response) => { + try { + const transaction = await prisma.transaction.delete({ + where: { id: req.body.input.id }, + }); + res.status(200).json({ + message: "Transaction deleted successfully", + transaction: transaction, + }); + } catch (e) { + generateError(res, e); + } +}; +export const getTransaction = async (req: Request, res: Response) => { + try { + const transaction = await prisma.transaction.findUnique({ + where: { id: req.body.input.id }, + }); + res.status(200).json({ + message: "Transaction retrieved successfully", + transaction: transaction, + }); + } catch (e) { + generateError(res, e); + } +}; +export const getTransactions = async (req: Request, res: Response) => { + try { + const { user_id } = req.body.input; + let transactions; + if (user_id) { + transactions = await prisma.transaction.findMany({ + where: { user_id: user_id }, + }); + } else { + transactions = await prisma.transaction.findMany(); + } + res.status(200).json({ + message: "Transactions retrieved successfully", + transactions: transactions, + }); + } catch (e) { + generateError(res, e); + } +}; \ No newline at end of file diff --git a/src/v1/user.ts b/src/v1/user.ts index d86e6e9..cefe07f 100644 --- a/src/v1/user.ts +++ b/src/v1/user.ts @@ -38,8 +38,31 @@ export const updateUser = async (req: Request, res: Response) => { } }; -export const getUserBalance = (req: Request, res: Response) => { +export const getUserBalance = async (req: Request, res: Response) => { + const { coin_id } = req.body.input; + const user = req.CurrentRequestUser; + const transactions = await prisma.transaction.findMany({ + where: { + user_id: user.id, + coin_id: coin_id, + }, + }); + const coin = await prisma.coin.findUnique({ + where: { + id: coin_id, + }, + }); + + if (!coin) { + res.status(404).json({ + message: "Coin not found", + }); + } + + const balance = transactions.reduce((acc, transaction) => acc + transaction.amount, 0); res.status(200).json({ message: "getUserBalance", + balance: balance, + coin: coin, }); }; diff --git a/src/v1/validations/coin.validation.ts b/src/v1/validations/coin.validation.ts new file mode 100644 index 0000000..1ed99c8 --- /dev/null +++ b/src/v1/validations/coin.validation.ts @@ -0,0 +1,28 @@ +import { InferType, number, object, string } from "yup"; + +export const createCoinSchema = object({ + name: string().required("Name is required"), +}); +export type CreateCoinSchema = InferType; + +export const updateCoinSchema = object({ + id: number().required("ID is required"), + name: string().required("Name is required"), +}); +export type UpdateCoinSchema = InferType; + + +export const createCoinRateSchema = object({ + coin_one_id: number().required("Coin One ID is required"), + coin_two_id: number().required("Coin Two ID is required"), + rate: number().required("Rate is required"), +}); +export type CreateCoinRateSchema = InferType; + +export const updateCoinRateSchema = object({ + id: number().required("ID is required"), + coin_one_id: number().required("Coin One ID is required"), + coin_two_id: number().required("Coin Two ID is required"), + rate: number().required("Rate is required"), +}); +export type UpdateCoinRateSchema = InferType; diff --git a/src/v1/validations/transaction.validation.ts b/src/v1/validations/transaction.validation.ts new file mode 100644 index 0000000..63ec78b --- /dev/null +++ b/src/v1/validations/transaction.validation.ts @@ -0,0 +1,34 @@ +import { InferType, number, object } from "yup"; + +export const createTransactionSchema = object({ + user_id: number().required("User ID is required"), + amount: number().required("Amount is required"), + coin_id: number().required("Coin ID is required"), +}); +export type CreateTransactionSchema = InferType; + +export const updateTransactionSchema = object({ + id: number().required("ID is required"), + user_id: number().required("User ID is required"), + amount: number().required("Amount is required"), + coin_id: number().required("Coin ID is required"), +}); +export type UpdateTransactionSchema = InferType; + + +export const deleteTransactionSchema = object({ + id: number().required("ID is required"), +}); +export type DeleteTransactionSchema = InferType; + +export const getTransactionsSchema = object({ + user_id: number().optional(), +}); +export type GetTransactionsSchema = InferType; + + +export const getTransactionSchema = object({ + id: number().required("ID is required"), +}); +export type GetTransactionSchema = InferType; + \ No newline at end of file diff --git a/src/v1/user.validation.ts b/src/v1/validations/user.validation.ts similarity index 79% rename from src/v1/user.validation.ts rename to src/v1/validations/user.validation.ts index a853490..ee5396a 100644 --- a/src/v1/user.validation.ts +++ b/src/v1/validations/user.validation.ts @@ -14,3 +14,8 @@ export const updateUserSchema = object({ email: string().email().required("Email is required"), }); export type UpdateUserSchema = InferType; + +export const getUserBalanceSchema = object({ + coin_id: number().required("Coin ID is required"), +}); +export type GetUserBalanceSchema = InferType; diff --git a/src/v1/wallet.ts b/src/v1/wallet.ts deleted file mode 100644 index 56eae8a..0000000 --- a/src/v1/wallet.ts +++ /dev/null @@ -1 +0,0 @@ -export const createWallet = () => {}; From 9e34014a359d2eb3e4ce0ceed10361f5b8ec2e6d Mon Sep 17 00:00:00 2001 From: Ilyas Karim Date: Wed, 13 Nov 2024 06:48:00 +0500 Subject: [PATCH 3/6] Initial APis --- package.json | 2 + .../20231121101343_init/migration.sql | 41 ---------- .../migrations/20241113013717_/migration.sql | 76 +++++++++++++++++++ prisma/migrations/migration_lock.toml | 2 +- prisma/schema.prisma | 57 ++++++++------ src/index.ts | 13 +++- src/v1/coin.ts | 25 ++++++ src/v1/routes.ts | 22 +++--- src/v1/user.ts | 58 ++++++++++++-- src/v1/validations/coin.validation.ts | 8 +- src/v1/validations/transaction.validation.ts | 11 +-- src/v1/validations/user.validation.ts | 3 +- tests/index.test.ts | 63 +++++++++++++-- tests/test-data/data.ts | 64 ++++++++++++++++ tests/test-data/users.json | 32 -------- yarn.lock | 5 ++ 16 files changed, 349 insertions(+), 133 deletions(-) delete mode 100644 prisma/migrations/20231121101343_init/migration.sql create mode 100644 prisma/migrations/20241113013717_/migration.sql create mode 100644 src/v1/coin.ts create mode 100644 tests/test-data/data.ts delete mode 100644 tests/test-data/users.json diff --git a/package.json b/package.json index 0204287..f436dd2 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "scripts": { "start": "node dist/index.js", "build": "tsc", + "sync-prisma": "npx prisma migrate dev && npm run prisma-generate", "prisma-generate": "npx prisma generate", "prisma-studio": "npx prisma studio", "dev": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/index.ts", @@ -50,6 +51,7 @@ "cors": "^2.8.5", "express": "^4.18.2", "lodash.get": "^4.4.2", + "uuid": "^11.0.3", "yup": "^1.3.2" } } diff --git a/prisma/migrations/20231121101343_init/migration.sql b/prisma/migrations/20231121101343_init/migration.sql deleted file mode 100644 index 52cdb73..0000000 --- a/prisma/migrations/20231121101343_init/migration.sql +++ /dev/null @@ -1,41 +0,0 @@ --- CreateTable -CREATE TABLE "User" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - "username" TEXT NOT NULL, - "email" TEXT NOT NULL, - "application_user_id" TEXT NOT NULL, - "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" DATETIME NOT NULL -); - --- CreateTable -CREATE TABLE "Wallet" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - "user_id" INTEGER NOT NULL, - "balance" INTEGER NOT NULL, - "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" DATETIME NOT NULL, - CONSTRAINT "Wallet_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE -); - --- CreateTable -CREATE TABLE "Transaction" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - "transaction_type" TEXT NOT NULL, - "user_id" INTEGER NOT NULL, - "wallet_id" INTEGER NOT NULL, - "amount" INTEGER NOT NULL, - "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" DATETIME NOT NULL, - CONSTRAINT "Transaction_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, - CONSTRAINT "Transaction_wallet_id_fkey" FOREIGN KEY ("wallet_id") REFERENCES "Wallet" ("id") ON DELETE RESTRICT ON UPDATE CASCADE -); - --- CreateIndex -CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); - --- CreateIndex -CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); - --- CreateIndex -CREATE UNIQUE INDEX "User_application_user_id_key" ON "User"("application_user_id"); diff --git a/prisma/migrations/20241113013717_/migration.sql b/prisma/migrations/20241113013717_/migration.sql new file mode 100644 index 0000000..5fb2cfd --- /dev/null +++ b/prisma/migrations/20241113013717_/migration.sql @@ -0,0 +1,76 @@ +-- CreateEnum +CREATE TYPE "TransactionType" AS ENUM ('DEPOSIT', 'WITHDRAWAL'); + +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL, + "username" TEXT NOT NULL, + "email" TEXT NOT NULL, + "application_user_id" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Transaction" ( + "id" TEXT NOT NULL, + "transaction_type" "TransactionType" NOT NULL, + "user_id" TEXT NOT NULL, + "amount" INTEGER NOT NULL, + "coin_id" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Transaction_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Coin" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Coin_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "CoinRate" ( + "id" TEXT NOT NULL, + "coin_one_id" TEXT NOT NULL, + "coin_two_id" TEXT NOT NULL, + "rate" DOUBLE PRECISION NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "CoinRate_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_application_user_id_key" ON "User"("application_user_id"); + +-- CreateIndex +CREATE INDEX "Transaction_user_id_coin_id_idx" ON "Transaction"("user_id", "coin_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "Coin_name_key" ON "Coin"("name"); + +-- AddForeignKey +ALTER TABLE "Transaction" ADD CONSTRAINT "Transaction_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Transaction" ADD CONSTRAINT "Transaction_coin_id_fkey" FOREIGN KEY ("coin_id") REFERENCES "Coin"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CoinRate" ADD CONSTRAINT "CoinRate_coin_one_id_fkey" FOREIGN KEY ("coin_one_id") REFERENCES "Coin"("name") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CoinRate" ADD CONSTRAINT "CoinRate_coin_two_id_fkey" FOREIGN KEY ("coin_two_id") REFERENCES "Coin"("name") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml index e5e5c47..fbffa92 100644 --- a/prisma/migrations/migration_lock.toml +++ b/prisma/migrations/migration_lock.toml @@ -1,3 +1,3 @@ # Please do not edit this file manually # It should be added in your version-control system (i.e. Git) -provider = "sqlite" \ No newline at end of file +provider = "postgresql" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index fee7bd7..76afc52 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -3,12 +3,17 @@ generator client { } datasource db { - provider = "sqlite" + provider = "postgresql" url = env("DATABASE_URL") } +enum TransactionType { + DEPOSIT + WITHDRAWAL +} + model User { - id Int @id @default(autoincrement()) + id String @id @default(uuid()) username String @unique email String @unique application_user_id String @unique @@ -16,36 +21,40 @@ model User { created_at DateTime @default(now()) updated_at DateTime @updatedAt } + model Transaction { - id Int @id @default(autoincrement()) - transaction_type String - user_id Int - user User @relation(fields: [user_id], references: [id]) + id String @id @default(uuid()) + transaction_type TransactionType + user_id String + user User @relation(fields: [user_id], references: [id]) amount Int - coin_id Int - coin Coin @relation(fields: [coin_id], references: [id]) - created_at DateTime @default(now()) - updated_at DateTime @updatedAt -} + coin_id String + coin Coin @relation(fields: [coin_id], references: [id]) + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + @@index([user_id, coin_id]) +} model Coin { - id Int @id @default(autoincrement()) - name String - created_at DateTime @default(now()) - updated_at DateTime @updatedAt + id String @id @default(uuid()) + name String + created_at DateTime @default(now()) + updated_at DateTime @updatedAt Transactions Transaction[] - CoinRatesOne CoinRate[] @relation("coin_one") - CoinRatesTwo CoinRate[] @relation("coin_two") + CoinRatesOne CoinRate[] @relation("coin_one") + CoinRatesTwo CoinRate[] @relation("coin_two") + + @@unique([name]) } model CoinRate { - id Int @id @default(autoincrement()) - coin_one_id Int - coin_one Coin @relation("coin_one", fields: [coin_one_id], references: [id]) - coin_two_id Int - coin_two Coin @relation("coin_two", fields: [coin_two_id], references: [id]) + id String @id @default(uuid()) + coin_one_id String + coin_one Coin @relation("coin_one", fields: [coin_one_id], references: [name]) + coin_two_id String + coin_two Coin @relation("coin_two", fields: [coin_two_id], references: [name]) rate Float - created_at DateTime @default(now()) - updated_at DateTime @updatedAt + created_at DateTime @default(now()) + updated_at DateTime @updatedAt } diff --git a/src/index.ts b/src/index.ts index a0a7879..fe43628 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,8 +2,9 @@ import express, { Request, Response } from "express"; import cors from "cors"; import packageJSON from "./../package.json"; import v1Routes from "./v1/routes"; -import { User } from "@prisma/client"; +import { Prisma, User } from "@prisma/client"; import { requestLogger } from "./utils/middlewares"; +import prisma from "./database/prisma"; declare global { namespace Express { @@ -29,6 +30,16 @@ app.get("/", (req, res) => { }); }); +app.get("/clean", async (req, res) => { + const tables = ["User", "Transaction", "Coin", "CoinRate"]; + for (const table of tables) { + await prisma.$executeRawUnsafe(`TRUNCATE TABLE "${table}" CASCADE;`); + } + res.status(200).json({ + message: "Database cleaned", + }); +}); + app.use("/api/v1", v1Routes); app.use((err: any, req: Request, res: Response, next: Function) => { diff --git a/src/v1/coin.ts b/src/v1/coin.ts new file mode 100644 index 0000000..e560734 --- /dev/null +++ b/src/v1/coin.ts @@ -0,0 +1,25 @@ +import { Response, Request } from "express"; +import prisma from "../database/prisma"; + +export const createCoin = async (req: Request, res: Response) => { + const { input } = req.body; + const coin = await prisma.coin.create({ + data: input, + }); + res.status(200).json({ + message: "Coin created successfully", + coin, + }); +}; + + +export const createCoinRate = async (req: Request, res: Response) => { + const { input } = req.body; + const coinRate = await prisma.coinRate.create({ + data: input, + }); + res.status(200).json({ + message: "Coin rate created successfully", + coinRate, + }); +}; diff --git a/src/v1/routes.ts b/src/v1/routes.ts index b6268c5..e367445 100644 --- a/src/v1/routes.ts +++ b/src/v1/routes.ts @@ -1,6 +1,6 @@ import { middlewareValidateYupSchemaAgainstReqBody, validateUserExistsSentThroughReqBody } from "../utils/middlewares"; import { createUserSchema, updateUserSchema } from "./validations/user.validation"; -import { createUser, getUserBalance, updateUser } from "./user"; +import { createUser, getUserBalance, getUserBalanceByCoin, updateUser } from "./user"; import express from "express"; import { @@ -13,6 +13,8 @@ import { import { deleteTransaction, getTransaction, getTransactions } from "./transaction"; import { updateTransaction } from "./transaction"; import { createTransaction } from "./transaction"; +import { createCoinRateSchema, createCoinSchema } from "./validations/coin.validation"; +import { createCoin, createCoinRate } from "./coin"; const v1Routes = express.Router(); v1Routes.post( @@ -30,16 +32,8 @@ v1Routes.delete( middlewareValidateYupSchemaAgainstReqBody(deleteTransactionSchema), deleteTransaction, ); -v1Routes.get( - "/getTransaction", - middlewareValidateYupSchemaAgainstReqBody(getTransactionSchema), - getTransaction, -); -v1Routes.get( - "/getTransactions", - middlewareValidateYupSchemaAgainstReqBody(getTransactionsSchema), - getTransactions, -); +v1Routes.get("/getTransaction", middlewareValidateYupSchemaAgainstReqBody(getTransactionSchema), getTransaction); +v1Routes.get("/getTransactions", middlewareValidateYupSchemaAgainstReqBody(getTransactionsSchema), getTransactions); v1Routes.post("/createUser", middlewareValidateYupSchemaAgainstReqBody(createUserSchema), createUser); v1Routes.put( @@ -48,5 +42,11 @@ v1Routes.put( middlewareValidateYupSchemaAgainstReqBody(updateUserSchema), updateUser, ); +v1Routes.get("/getUserBalanceByCoin", validateUserExistsSentThroughReqBody("body.input.id"), getUserBalanceByCoin); v1Routes.get("/getUserBalance", validateUserExistsSentThroughReqBody("body.input.id"), getUserBalance); + + +v1Routes.post("/createCoin", middlewareValidateYupSchemaAgainstReqBody(createCoinSchema), createCoin); +v1Routes.post("/createCoinRate", middlewareValidateYupSchemaAgainstReqBody(createCoinRateSchema), createCoinRate); + export default v1Routes; diff --git a/src/v1/user.ts b/src/v1/user.ts index cefe07f..c343864 100644 --- a/src/v1/user.ts +++ b/src/v1/user.ts @@ -5,17 +5,14 @@ import { generateError } from "../utils/errors"; export const createUser = async (req: Request, res: Response) => { try { let response = await prisma.user.create({ - data: { - email: req.body.input.email, - username: req.body.input.username, - application_user_id: req.body.input.application_user_id, - }, + data: req.body.input, }); res.status(200).json({ message: "User created successfully", user: response, }); } catch (e) { + console.log(e); generateError(res, e); } }; @@ -38,13 +35,21 @@ export const updateUser = async (req: Request, res: Response) => { } }; -export const getUserBalance = async (req: Request, res: Response) => { +export const getUserBalanceByCoin = async (req: Request, res: Response) => { const { coin_id } = req.body.input; const user = req.CurrentRequestUser; - const transactions = await prisma.transaction.findMany({ + const deposits = await prisma.transaction.findMany({ where: { user_id: user.id, coin_id: coin_id, + transaction_type: "DEPOSIT", + }, + }); + const withdrawals = await prisma.transaction.findMany({ + where: { + user_id: user.id, + coin_id: coin_id, + transaction_type: "WITHDRAWAL", }, }); const coin = await prisma.coin.findUnique({ @@ -59,10 +64,47 @@ export const getUserBalance = async (req: Request, res: Response) => { }); } - const balance = transactions.reduce((acc, transaction) => acc + transaction.amount, 0); + const balance = + deposits.reduce((acc, transaction) => acc + transaction.amount, 0) - + withdrawals.reduce((acc, transaction) => acc + transaction.amount, 0); res.status(200).json({ message: "getUserBalance", balance: balance, coin: coin, }); }; + +export const getUserBalance = async (req: Request, res: Response) => { + const user = req.CurrentRequestUser; + const userAllTransactions = await prisma.transaction.findMany({ + where: { + user_id: user.id, + }, + }); + + const coins = userAllTransactions.map((transaction) => transaction.coin_id); + const uniqueCoins = [...new Set(coins)]; + const balances: Record = {}; + + for (const coinId of uniqueCoins) { + const coin = await prisma.coin.findUnique({ + where: { + id: coinId, + }, + }); + if (!coin) { + continue; + } + const transactions = userAllTransactions.filter((transaction) => transaction.coin_id === coinId); + const deposits = transactions.filter((transaction) => transaction.transaction_type === "DEPOSIT"); + const withdrawals = transactions.filter((transaction) => transaction.transaction_type === "WITHDRAWAL"); + const balance = + deposits.reduce((acc, transaction) => acc + transaction.amount, 0) - + withdrawals.reduce((acc, transaction) => acc + transaction.amount, 0); + balances[coin.name] = balance; + } + res.status(200).json({ + message: "getUserBalance", + balances: balances, + }); +}; diff --git a/src/v1/validations/coin.validation.ts b/src/v1/validations/coin.validation.ts index 1ed99c8..ff7b8b5 100644 --- a/src/v1/validations/coin.validation.ts +++ b/src/v1/validations/coin.validation.ts @@ -13,16 +13,16 @@ export type UpdateCoinSchema = InferType; export const createCoinRateSchema = object({ - coin_one_id: number().required("Coin One ID is required"), - coin_two_id: number().required("Coin Two ID is required"), + coin_one_id: string().required("Coin One ID is required"), + coin_two_id: string().required("Coin Two ID is required"), rate: number().required("Rate is required"), }); export type CreateCoinRateSchema = InferType; export const updateCoinRateSchema = object({ id: number().required("ID is required"), - coin_one_id: number().required("Coin One ID is required"), - coin_two_id: number().required("Coin Two ID is required"), + coin_one_id: string().required("Coin One ID is required"), + coin_two_id: string().required("Coin Two ID is required"), rate: number().required("Rate is required"), }); export type UpdateCoinRateSchema = InferType; diff --git a/src/v1/validations/transaction.validation.ts b/src/v1/validations/transaction.validation.ts index 63ec78b..d33dc9b 100644 --- a/src/v1/validations/transaction.validation.ts +++ b/src/v1/validations/transaction.validation.ts @@ -1,21 +1,22 @@ -import { InferType, number, object } from "yup"; +import { InferType, number, object, string } from "yup"; export const createTransactionSchema = object({ user_id: number().required("User ID is required"), amount: number().required("Amount is required"), coin_id: number().required("Coin ID is required"), + transaction_type: string().oneOf(["DEPOSIT", "WITHDRAWAL"]).required("Transaction type is required"), }); export type CreateTransactionSchema = InferType; export const updateTransactionSchema = object({ id: number().required("ID is required"), - user_id: number().required("User ID is required"), - amount: number().required("Amount is required"), - coin_id: number().required("Coin ID is required"), + user_id: number().optional(), + amount: number().optional(), + coin_id: number().optional(), + transaction_type: string().oneOf(["DEPOSIT", "WITHDRAWAL"]).optional() }); export type UpdateTransactionSchema = InferType; - export const deleteTransactionSchema = object({ id: number().required("ID is required"), }); diff --git a/src/v1/validations/user.validation.ts b/src/v1/validations/user.validation.ts index ee5396a..0c71dd2 100644 --- a/src/v1/validations/user.validation.ts +++ b/src/v1/validations/user.validation.ts @@ -1,6 +1,7 @@ import { object, string, number, date, InferType } from "yup"; export const createUserSchema = object({ + id: string().required("ID is required"), username: string().required("Username is required"), application_user_id: string().required("Application User ID is required"), email: string().email().required("Email is required"), @@ -8,7 +9,7 @@ export const createUserSchema = object({ export type CreateUserSchema = InferType; export const updateUserSchema = object({ - id: number().required("ID is required"), + id: string().required("ID is required"), username: string().required("Username is required"), application_user_id: string().required("Application User ID is required"), email: string().email().required("Email is required"), diff --git a/tests/index.test.ts b/tests/index.test.ts index bd50db1..8036b65 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -1,8 +1,13 @@ -import {describe, expect, test} from '@jest/globals'; -import { axios } from './test-data/axios'; +import axiosRoot from "axios"; +import { describe, expect, test } from "@jest/globals"; +import { axios } from "./test-data/axios"; +import { users, coins, coinRates } from "./test-data/data"; -describe('First Steps', () => { - test('Expects server is running', async () => { +describe("First Steps", () => { + test("Expects server is running", async () => { + const clean = await axios.get("/clean"); + expect(clean.status).toBe(200); + expect(clean.data.message).toBe("Database cleaned"); const response = await axios.get("/"); const message = response.data.message; const version = response.data.version; @@ -10,4 +15,52 @@ describe('First Steps', () => { expect(message.constructor).toBe(String); expect(version.constructor).toBe(String); }); -}); \ No newline at end of file +}); + +describe("User Routes", () => { + test("Create coins", async () => { + for (const coin of coins) { + const response = await axios.post("/api/v1/createCoin", { + input: coin, + }); + expect(response.status).toBe(200); + const { data } = response; + expect(data.message).toBe("Coin created successfully"); + expect(data.coin.id).toBeDefined(); + expect(data.coin.name).toBe(coin.name); + } + for (const coinRate of coinRates) { + try { + const response = await axios.post("/api/v1/createCoinRate", { + input: coinRate, + }); + expect(response.status).toBe(200); + const { data } = response; + expect(data.message).toBe("Coin rate created successfully"); + expect(data.coinRate.id).toBeDefined(); + } catch (error) { + if (axiosRoot.isAxiosError(error)) { + console.log(error.response?.data); + expect(error.response?.status).toBe(200); + } + } + } + }); + test("Create User", async () => { + for (const user of users) { + try { + const response = await axios.post("/api/v1/createUser", { + input: user, + }); + expect(response.status).toBe(200); + const { data } = response; + expect(data).toBeDefined(); + } catch (error) { + if (axiosRoot.isAxiosError(error)) { + console.log(error.response?.data); + expect(error.response?.status).toBe(200); + } + } + } + }); +}); diff --git a/tests/test-data/data.ts b/tests/test-data/data.ts new file mode 100644 index 0000000..b9f7c7f --- /dev/null +++ b/tests/test-data/data.ts @@ -0,0 +1,64 @@ +import { v4 as uuidv4 } from "uuid"; + +export const users = [ + { + id: "b680a082-b999-4fed-871c-4a194b5710ad", + application_user_id: "11231234567890", + username: "user_1", + email: "user_1@example.com", + }, + { + id: uuidv4(), + username: "user_2", + application_user_id: "131", + email: "user_2@example.com", + }, + { + id: uuidv4(), + username: "user_3", + application_user_id: "167890", + email: "user_3@example.com", + }, + { + id: uuidv4(), + username: "user_4", + application_user_id: "12345123167890", + email: "user_4@example.com", + }, + { + id: uuidv4(), + username: "user_5", + application_user_id: "1231", + email: "user_5@example.com", + }, +]; + +export const coins = [ + { + id: uuidv4(), + name: "UC", + }, + { + id: uuidv4(), + name: "USDT", + }, + { + id: uuidv4(), + name: "USDC", + }, +]; + +export const coinRates = [ + { + id: uuidv4(), + coin_one_id: "UC", + coin_two_id: "USDT", + rate: 1, + }, + { + id: uuidv4(), + coin_one_id: "UC", + coin_two_id: "USDC", + rate: 2, + }, +]; diff --git a/tests/test-data/users.json b/tests/test-data/users.json deleted file mode 100644 index e252aff..0000000 --- a/tests/test-data/users.json +++ /dev/null @@ -1,32 +0,0 @@ -[ - { - "id": 1, - "username": "user_1", - "email": "user_1@example.com", - "created_at": "2023-12-31T23:58:33.390632" - }, - { - "id": 2, - "username": "user_2", - "email": "user_2@example.com", - "created_at": "2024-04-06T23:58:33.390632" - }, - { - "id": 3, - "username": "user_3", - "email": "user_3@example.com", - "created_at": "2023-12-07T23:58:33.390632" - }, - { - "id": 4, - "username": "user_4", - "email": "user_4@example.com", - "created_at": "2024-02-29T23:58:33.390632" - }, - { - "id": 5, - "username": "user_5", - "email": "user_5@example.com", - "created_at": "2024-09-27T23:58:33.390632" - } -] diff --git a/yarn.lock b/yarn.lock index 090dbab..d34b47d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3055,6 +3055,11 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== +uuid@^11.0.3: + version "11.0.3" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.0.3.tgz#248451cac9d1a4a4128033e765d137e2b2c49a3d" + integrity sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg== + v8-compile-cache-lib@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" From f2bc2b1569101bfb27ba714ba9bde5212c855bc9 Mon Sep 17 00:00:00 2001 From: Ilyas Karim Date: Wed, 13 Nov 2024 06:52:12 +0500 Subject: [PATCH 4/6] Test data --- tests/test-data/data.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/test-data/data.ts b/tests/test-data/data.ts index b9f7c7f..a10e132 100644 --- a/tests/test-data/data.ts +++ b/tests/test-data/data.ts @@ -8,25 +8,25 @@ export const users = [ email: "user_1@example.com", }, { - id: uuidv4(), + id: "b680a082-b999-4fed-871c-4a194b5710ae", username: "user_2", application_user_id: "131", email: "user_2@example.com", }, { - id: uuidv4(), + id: "b680a082-b999-4fed-871c-4a194b5710af", username: "user_3", application_user_id: "167890", email: "user_3@example.com", }, { - id: uuidv4(), + id: "b680a082-b999-4fed-871c-4a194b5710b0", username: "user_4", application_user_id: "12345123167890", email: "user_4@example.com", }, { - id: uuidv4(), + id: "b680a082-b999-4fed-871c-4a194b5710b1", username: "user_5", application_user_id: "1231", email: "user_5@example.com", @@ -35,28 +35,28 @@ export const users = [ export const coins = [ { - id: uuidv4(), + id: "b680a082-b999-4fed-871c-4a194b5710b2", name: "UC", }, { - id: uuidv4(), + id: "b680a082-b999-4fed-871c-4a194b5710b3", name: "USDT", }, { - id: uuidv4(), + id: "b680a082-b999-4fed-871c-4a194b5710b4", name: "USDC", }, ]; export const coinRates = [ { - id: uuidv4(), + id: "b680a082-b999-4fed-871c-4a194b5710b5", coin_one_id: "UC", coin_two_id: "USDT", rate: 1, }, { - id: uuidv4(), + id: "b680a082-b999-4fed-871c-4a194b5710b6", coin_one_id: "UC", coin_two_id: "USDC", rate: 2, From 8251ddef8a0aa2c4c63e79c37e5ca5150a0b3cb4 Mon Sep 17 00:00:00 2001 From: Ilyas Karim Date: Wed, 25 Dec 2024 04:39:46 +0500 Subject: [PATCH 5/6] League feature --- .DS_Store | Bin 0 -> 6148 bytes .../migrations/20241224233404_/migration.sql | 53 ++++++ prisma/schema.prisma | 36 ++++ src/index.ts | 1 + src/v1/league.ts | 168 ++++++++++++++++++ src/v1/routes.ts | 45 +++++ src/v1/validations/league.validation.ts | 143 +++++++++++++++ tests/league.test.ts | 114 ++++++++++++ tests/test-data/league-data.ts | 86 +++++++++ 9 files changed, 646 insertions(+) create mode 100644 .DS_Store create mode 100644 prisma/migrations/20241224233404_/migration.sql create mode 100644 src/v1/league.ts create mode 100644 src/v1/validations/league.validation.ts create mode 100644 tests/league.test.ts create mode 100644 tests/test-data/league-data.ts diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 { app.get("/clean", async (req, res) => { const tables = ["User", "Transaction", "Coin", "CoinRate"]; for (const table of tables) { + console.log(`Truncating table ${table}`); await prisma.$executeRawUnsafe(`TRUNCATE TABLE "${table}" CASCADE;`); } res.status(200).json({ diff --git a/src/v1/league.ts b/src/v1/league.ts new file mode 100644 index 0000000..603e35e --- /dev/null +++ b/src/v1/league.ts @@ -0,0 +1,168 @@ +import { Request, Response } from "express"; +import prisma from "../database/prisma"; + +export const createLeague = async (req: Request, res: Response) => { + try { + const { input } = req.body; + + const league = await prisma.league.create({ + data: input, + }); + return res.status(200).json({ + message: "League created successfully", + league, + }); + } catch (error) { + console.error("Error creating league:", error); + return res.status(500).json({ + message: "Error creating league", + error, + }); + } +}; + +export const assignUserToLeague = async (req: Request, res: Response) => { + try { + const { input } = req.body; + const leagueUser = await prisma.leagueUser.create({ + data: input, + }); + return res.status(200).json({ + message: "User assigned to league successfully", + leagueUser, + }); + } catch (error) { + console.error("Error assigning user to league:", error); + return res.status(500).json({ + message: "Error assigning user to league", + error, + }); + } +}; + +export const addUserExperience = async (req: Request, res: Response) => { + try { + const { input } = req.body; + const userExperience = await prisma.userExperience.create({ + data: input, + }); + return res.status(200).json({ + message: "User experience added successfully", + userExperience, + }); + } catch (error) { + console.error("Error adding user experience:", error); + return res.status(500).json({ + message: "Error adding user experience", + error, + }); + } +}; + +export const getLeagueLeaderboard = async (req: Request, res: Response) => { + try { + const { leagueId } = req.params; + const leaderboard = await prisma.userExperience.findMany({ + where: { + user: { + LeagueUser: { + some: { + league_id: leagueId, + }, + }, + }, + }, + orderBy: { + amount: 'desc', + }, + include: { + user: { + select: { + id: true, + username: true, + }, + }, + }, + }); + + return res.status(200).json({ + leaderboard: leaderboard.map(entry => ({ + user_id: entry.user.id, + username: entry.user.username, + experience: entry.amount, + })), + }); + } catch (error) { + console.error("Error getting league leaderboard:", error); + return res.status(500).json({ + message: "Error getting league leaderboard", + error, + }); + } +}; + +export const getUserLeagueStatus = async (req: Request, res: Response) => { + try { + const { userId } = req.params; + + const userStatus = await prisma.leagueUser.findFirst({ + where: { + user_id: userId, + }, + include: { + league: true, + user: { + include: { + UserExperience: { + orderBy: { + created_at: 'desc', + }, + take: 1, + }, + }, + }, + }, + }); + + if (!userStatus) { + return res.status(404).json({ + message: "User league status not found", + }); + } + + // Get user's rank in the league + const leagueUsers = await prisma.userExperience.findMany({ + where: { + user: { + LeagueUser: { + some: { + league_id: userStatus.league_id, + }, + }, + }, + }, + orderBy: { + amount: 'desc', + }, + }); + + const userRank = leagueUsers.findIndex( + (user) => user.user_id === userId + ) + 1; + + return res.status(200).json({ + userStatus: { + league_id: userStatus.league_id, + current_rank: userRank, + experience: userStatus.user.UserExperience[0]?.amount || 0, + league_name: userStatus.league.name, + }, + }); + } catch (error) { + console.error("Error getting user league status:", error); + return res.status(500).json({ + message: "Error getting user league status", + error, + }); + } +}; \ No newline at end of file diff --git a/src/v1/routes.ts b/src/v1/routes.ts index e367445..b7a2afc 100644 --- a/src/v1/routes.ts +++ b/src/v1/routes.ts @@ -1,6 +1,19 @@ import { middlewareValidateYupSchemaAgainstReqBody, validateUserExistsSentThroughReqBody } from "../utils/middlewares"; import { createUserSchema, updateUserSchema } from "./validations/user.validation"; import { createUser, getUserBalance, getUserBalanceByCoin, updateUser } from "./user"; +import { + createLeagueSchema, + assignUserToLeagueSchema, + addUserExperienceSchema, + getUserLeagueStatusSchema +} from "./validations/league.validation"; +import { + createLeague, + assignUserToLeague, + addUserExperience, + getLeagueLeaderboard, + getUserLeagueStatus +} from "./league"; import express from "express"; import { @@ -15,6 +28,7 @@ import { updateTransaction } from "./transaction"; import { createTransaction } from "./transaction"; import { createCoinRateSchema, createCoinSchema } from "./validations/coin.validation"; import { createCoin, createCoinRate } from "./coin"; +import { getLeagueLeaderboardSchema } from "./validations/league.validation"; const v1Routes = express.Router(); v1Routes.post( @@ -49,4 +63,35 @@ v1Routes.get("/getUserBalance", validateUserExistsSentThroughReqBody("body.input v1Routes.post("/createCoin", middlewareValidateYupSchemaAgainstReqBody(createCoinSchema), createCoin); v1Routes.post("/createCoinRate", middlewareValidateYupSchemaAgainstReqBody(createCoinRateSchema), createCoinRate); +// League routes +v1Routes.post( + "/createLeague", + middlewareValidateYupSchemaAgainstReqBody(createLeagueSchema), + createLeague +); + +v1Routes.post( + "/assignUserToLeague", + middlewareValidateYupSchemaAgainstReqBody(assignUserToLeagueSchema), + assignUserToLeague +); + +v1Routes.post( + "/addUserExperience", + middlewareValidateYupSchemaAgainstReqBody(addUserExperienceSchema), + addUserExperience +); + +v1Routes.get( + "/leagueLeaderboard/:leagueId", + middlewareValidateYupSchemaAgainstReqBody(getLeagueLeaderboardSchema), + getLeagueLeaderboard +); + +v1Routes.get( + "/userLeagueStatus/:userId", + middlewareValidateYupSchemaAgainstReqBody(getUserLeagueStatusSchema), + getUserLeagueStatus +); + export default v1Routes; diff --git a/src/v1/validations/league.validation.ts b/src/v1/validations/league.validation.ts new file mode 100644 index 0000000..c6b8839 --- /dev/null +++ b/src/v1/validations/league.validation.ts @@ -0,0 +1,143 @@ +import * as yup from "yup"; +import prisma from "../../database/prisma"; + +// Validation for creating a new league +export const createLeagueSchema = yup.object().shape({ + input: yup.object().shape({ + id: yup.string().uuid(), + name: yup + .string() + .required("League name is required") + .min(3, "League name must be at least 3 characters") + .max(50, "League name cannot exceed 50 characters"), + demotion_rate: yup + .number() + .required("Demotion rate is required") + .min(0, "Demotion rate must be between 0 and 1") + .max(1, "Demotion rate must be between 0 and 1") + .test( + "is-decimal", + "Demotion rate must be a decimal between 0 and 1", + (value) => value !== undefined && value >= 0 && value <= 1 + ), + promotion_rate: yup + .number() + .required("Promotion rate is required") + .min(0, "Promotion rate must be between 0 and 1") + .max(1, "Promotion rate must be between 0 and 1") + .test( + "is-decimal", + "Promotion rate must be a decimal between 0 and 1", + (value) => value !== undefined && value >= 0 && value <= 1 + ), + league_start_date: yup + .date() + .required("League start date is required") + .min(new Date(), "League start date must be in the future"), + league_end_date: yup + .date() + .required("League end date is required") + .min( + yup.ref("league_start_date"), + "League end date must be after start date" + ) + .test( + "is-valid-duration", + "League duration must be at least 1 day", + function (value) { + const start = this.parent.league_start_date; + if (!start || !value) return false; + const duration = value.getTime() - start.getTime(); + return duration >= 24 * 60 * 60 * 1000; // 24 hours in milliseconds + } + ), + }), +}); + +// Validation for assigning a user to a league +export const assignUserToLeagueSchema = yup.object().shape({ + input: yup.object().shape({ + id: yup.string().uuid(), + user_id: yup + .string() + .uuid("Invalid user ID format") + .required("User ID is required"), + league_id: yup + .string() + .uuid("Invalid league ID format") + .required("League ID is required"), + }).test( + "unique-league-assignment", + "User can only be assigned to one league at a time", + async function(value) { + if (!value.user_id || !value.league_id) return false; + + try { + const existingAssignment = await prisma.leagueUser.findFirst({ + where: { + user_id: value.user_id, + league_id: { + not: value.league_id // Allow updating the same league + } + } + }); + return !existingAssignment; + } catch (error) { + return false; + } + } + ), +}); + +// Validation for adding user experience +export const addUserExperienceSchema = yup.object().shape({ + input: yup.object().shape({ + id: yup.string().uuid(), + user_id: yup + .string() + .uuid("Invalid user ID format") + .required("User ID is required"), + amount: yup + .number() + .positive("Experience amount must be positive") + .required("Experience amount is required") + .test( + "is-valid-amount", + "Experience amount must be a reasonable number (0-1000000)", + (value) => value !== undefined && value >= 0 && value <= 1000000 + ), + }).test( + "user-in-league", + "User must be assigned to a league to gain experience", + async function(value) { + if (!value.user_id) return false; + + try { + const userLeague = await prisma.leagueUser.findFirst({ + where: { + user_id: value.user_id + } + }); + return !!userLeague; + } catch (error) { + return false; + } + } + ), +}); + +// Additional validation for getting league leaderboard +export const getLeagueLeaderboardSchema = yup.object().shape({ + leagueId: yup + .string() + .uuid("Invalid league ID format") + .required("League ID is required"), +}); + +// Additional validation for getting user league status +export const getUserLeagueStatusSchema = yup.object().shape({ + userId: yup + .string() + .uuid("Invalid user ID format") + .required("User ID is required"), +}); \ No newline at end of file diff --git a/tests/league.test.ts b/tests/league.test.ts new file mode 100644 index 0000000..26515c9 --- /dev/null +++ b/tests/league.test.ts @@ -0,0 +1,114 @@ +import axiosRoot from "axios"; +import { describe, expect, test } from "@jest/globals"; +import { axios } from "./test-data/axios"; +import { leagues, leagueUsers, userExperiences } from "./test-data/league-data"; + +describe("League Routes", () => { + test("Create Leagues", async () => { + for (const league of leagues) { + try { + console.log(league, "Asdfasdf"); + const response = await axios.post("/api/v1/createLeague", { + input: league, + }); + expect(response.status).toBe(200); + const { data } = response; + expect(data.message).toBe("League created successfully"); + expect(data.league.id).toBeDefined(); + expect(data.league.name).toBe(league.name); + expect(data.league.demotion_rate).toBe(league.demotion_rate); + expect(data.league.promotion_rate).toBe(league.promotion_rate); + } catch (error) { + if (axiosRoot.isAxiosError(error)) { + console.log(error.response?.data); + expect(error.response?.status).toBe(200); + } + } + } + }); + +// test("Assign Users to Leagues", async () => { +// for (const leagueUser of leagueUsers) { +// try { +// const response = await axios.post("/api/v1/assignUserToLeague", { +// input: leagueUser, +// }); +// expect(response.status).toBe(200); +// const { data } = response; +// expect(data.message).toBe("User assigned to league successfully"); +// expect(data.leagueUser.id).toBeDefined(); +// expect(data.leagueUser.user_id).toBe(leagueUser.user_id); +// expect(data.leagueUser.league_id).toBe(leagueUser.league_id); +// } catch (error) { +// if (axiosRoot.isAxiosError(error)) { +// console.log(error.response?.data); +// expect(error.response?.status).toBe(200); +// } +// } +// } +// }); + +// test("Add User Experience", async () => { +// for (const experience of userExperiences) { +// try { +// const response = await axios.post("/api/v1/addUserExperience", { +// input: experience, +// }); +// expect(response.status).toBe(200); +// const { data } = response; +// expect(data.message).toBe("User experience added successfully"); +// expect(data.userExperience.id).toBeDefined(); +// expect(data.userExperience.user_id).toBe(experience.user_id); +// expect(data.userExperience.amount).toBe(experience.amount); +// } catch (error) { +// if (axiosRoot.isAxiosError(error)) { +// console.log(error.response?.data); +// expect(error.response?.status).toBe(200); +// } +// } +// } +// }); + +// test("Get League Leaderboard", async () => { +// for (const league of leagues) { +// try { +// const response = await axios.get(`/api/v1/leagueLeaderboard/${league.id}`); +// expect(response.status).toBe(200); +// const { data } = response; +// expect(data.leaderboard).toBeDefined(); +// expect(Array.isArray(data.leaderboard)).toBe(true); + +// // Verify leaderboard is sorted by experience (highest first) +// const sortedCorrectly = data.leaderboard.every((user: any, index: number, array: any[]) => { +// if (index === 0) return true; +// return user.experience <= array[index - 1].experience; +// }); +// expect(sortedCorrectly).toBe(true); +// } catch (error) { +// if (axiosRoot.isAxiosError(error)) { +// console.log(error.response?.data); +// expect(error.response?.status).toBe(200); +// } +// } +// } +// }); + +// test("Get User League Status", async () => { +// for (const leagueUser of leagueUsers) { +// try { +// const response = await axios.get(`/api/v1/userLeagueStatus/${leagueUser.user_id}`); +// expect(response.status).toBe(200); +// const { data } = response; +// expect(data.userStatus).toBeDefined(); +// expect(data.userStatus.league_id).toBe(leagueUser.league_id); +// expect(data.userStatus.current_rank).toBeDefined(); +// expect(data.userStatus.experience).toBeDefined(); +// } catch (error) { +// if (axiosRoot.isAxiosError(error)) { +// console.log(error.response?.data); +// expect(error.response?.status).toBe(200); +// } +// } +// } +// }); +}); \ No newline at end of file diff --git a/tests/test-data/league-data.ts b/tests/test-data/league-data.ts new file mode 100644 index 0000000..ee94655 --- /dev/null +++ b/tests/test-data/league-data.ts @@ -0,0 +1,86 @@ +import { v4 as uuidv4 } from "uuid"; + +export const leagues = [ + { + id: "b680a082-b999-4fed-871c-4a194b5710c1", + name: "Bronze League", + demotion_rate: 0.2, // Bottom 20% get demoted + promotion_rate: 0.1, // Top 10% get promoted + league_start_date: new Date("2024-03-01T00:00:00Z"), + league_end_date: new Date("2024-03-31T23:59:59Z"), + }, + { + id: "b680a082-b999-4fed-871c-4a194b5710c2", + name: "Silver League", + demotion_rate: 0.15, + promotion_rate: 0.15, + league_start_date: new Date("2024-03-01T00:00:00Z"), + league_end_date: new Date("2024-03-31T23:59:59Z"), + }, + { + id: "b680a082-b999-4fed-871c-4a194b5710c3", + name: "Gold League", + demotion_rate: 0.1, + promotion_rate: 0.2, + league_start_date: new Date("2024-03-01T00:00:00Z"), + league_end_date: new Date("2024-03-31T23:59:59Z"), + } +]; + +// League user assignments - connecting users to leagues +export const leagueUsers = [ + { + id: "b680a082-b999-4fed-871c-4a194b5710d1", + user_id: "b680a082-b999-4fed-871c-4a194b5710ad", // user_1 + league_id: "b680a082-b999-4fed-871c-4a194b5710c1", // Bronze League + }, + { + id: "b680a082-b999-4fed-871c-4a194b5710d2", + user_id: "b680a082-b999-4fed-871c-4a194b5710ae", // user_2 + league_id: "b680a082-b999-4fed-871c-4a194b5710c2", // Silver League + }, + { + id: "b680a082-b999-4fed-871c-4a194b5710d3", + user_id: "b680a082-b999-4fed-871c-4a194b5710af", // user_3 + league_id: "b680a082-b999-4fed-871c-4a194b5710c2", // Silver League + }, + { + id: "b680a082-b999-4fed-871c-4a194b5710d4", + user_id: "b680a082-b999-4fed-871c-4a194b5710b0", // user_4 + league_id: "b680a082-b999-4fed-871c-4a194b5710c3", // Gold League + }, + { + id: "b680a082-b999-4fed-871c-4a194b5710d5", + user_id: "b680a082-b999-4fed-871c-4a194b5710b1", // user_5 + league_id: "b680a082-b999-4fed-871c-4a194b5710c1", // Bronze League + } +]; + +// User experience data for testing leaderboard rankings +export const userExperiences = [ + { + id: "b680a082-b999-4fed-871c-4a194b5710e1", + user_id: "b680a082-b999-4fed-871c-4a194b5710ad", // user_1 + amount: 1000.0, + }, + { + id: "b680a082-b999-4fed-871c-4a194b5710e2", + user_id: "b680a082-b999-4fed-871c-4a194b5710ae", // user_2 + amount: 2500.0, + }, + { + id: "b680a082-b999-4fed-871c-4a194b5710e3", + user_id: "b680a082-b999-4fed-871c-4a194b5710af", // user_3 + amount: 2200.0, + }, + { + id: "b680a082-b999-4fed-871c-4a194b5710e4", + user_id: "b680a082-b999-4fed-871c-4a194b5710b0", // user_4 + amount: 5000.0, + }, + { + id: "b680a082-b999-4fed-871c-4a194b5710e5", + user_id: "b680a082-b999-4fed-871c-4a194b5710b1", // user_5 + amount: 800.0, + } +]; \ No newline at end of file From 64ca183fa223a9a76e5dced4e318b9ec2fec3a47 Mon Sep 17 00:00:00 2001 From: Ilyas Karim Date: Wed, 25 Dec 2024 04:50:22 +0500 Subject: [PATCH 6/6] Writing league tests --- src/utils/middlewares.ts | 1 + src/v1/validations/league.validation.ts | 217 ++++++++++-------------- tests/league.test.ts | 162 +++++++++--------- 3 files changed, 177 insertions(+), 203 deletions(-) diff --git a/src/utils/middlewares.ts b/src/utils/middlewares.ts index d3c9f21..2f99f2f 100644 --- a/src/utils/middlewares.ts +++ b/src/utils/middlewares.ts @@ -21,6 +21,7 @@ export const validateYupSchemaAgainstAnObject = async function (schema: any, obj export const middlewareValidateYupSchemaAgainstReqBody = function (schema: any) { return async function (req: any, res: any, next: any) { + console.log("validation middleware", req.body); let validate = await validateYupSchemaAgainstAnObject(schema, req.body.input); if (validate.length > 0) { return res.status(400).json({ diff --git a/src/v1/validations/league.validation.ts b/src/v1/validations/league.validation.ts index c6b8839..c1ca5d7 100644 --- a/src/v1/validations/league.validation.ts +++ b/src/v1/validations/league.validation.ts @@ -1,143 +1,114 @@ -import * as yup from "yup"; +import { InferType, object, string, number, date } from "yup"; import prisma from "../../database/prisma"; // Validation for creating a new league -export const createLeagueSchema = yup.object().shape({ - input: yup.object().shape({ - id: yup.string().uuid(), - name: yup - .string() - .required("League name is required") - .min(3, "League name must be at least 3 characters") - .max(50, "League name cannot exceed 50 characters"), - demotion_rate: yup - .number() - .required("Demotion rate is required") - .min(0, "Demotion rate must be between 0 and 1") - .max(1, "Demotion rate must be between 0 and 1") - .test( - "is-decimal", - "Demotion rate must be a decimal between 0 and 1", - (value) => value !== undefined && value >= 0 && value <= 1 - ), - promotion_rate: yup - .number() - .required("Promotion rate is required") - .min(0, "Promotion rate must be between 0 and 1") - .max(1, "Promotion rate must be between 0 and 1") - .test( - "is-decimal", - "Promotion rate must be a decimal between 0 and 1", - (value) => value !== undefined && value >= 0 && value <= 1 - ), - league_start_date: yup - .date() - .required("League start date is required") - .min(new Date(), "League start date must be in the future"), - league_end_date: yup - .date() - .required("League end date is required") - .min( - yup.ref("league_start_date"), - "League end date must be after start date" - ) - .test( - "is-valid-duration", - "League duration must be at least 1 day", - function (value) { - const start = this.parent.league_start_date; - if (!start || !value) return false; - const duration = value.getTime() - start.getTime(); - return duration >= 24 * 60 * 60 * 1000; // 24 hours in milliseconds - } - ), - }), +export const createLeagueSchema = object({ + id: string().uuid(), + name: string() + .required("League name is required") + .min(3, "League name must be at least 3 characters") + .max(50, "League name cannot exceed 50 characters"), + demotion_rate: number() + .required("Demotion rate is required") + .min(0, "Demotion rate must be between 0 and 1") + .max(1, "Demotion rate must be between 0 and 1") + .test( + "is-decimal", + "Demotion rate must be a decimal between 0 and 1", + (value) => value !== undefined && value >= 0 && value <= 1 + ), + promotion_rate: number() + .required("Promotion rate is required") + .min(0, "Promotion rate must be between 0 and 1") + .max(1, "Promotion rate must be between 0 and 1") + .test( + "is-decimal", + "Promotion rate must be a decimal between 0 and 1", + (value) => value !== undefined && value >= 0 && value <= 1 + ), + league_start_date: date(), + league_end_date: date() }); +export type CreateLeagueSchema = InferType; // Validation for assigning a user to a league -export const assignUserToLeagueSchema = yup.object().shape({ - input: yup.object().shape({ - id: yup.string().uuid(), - user_id: yup - .string() - .uuid("Invalid user ID format") - .required("User ID is required"), - league_id: yup - .string() - .uuid("Invalid league ID format") - .required("League ID is required"), - }).test( - "unique-league-assignment", - "User can only be assigned to one league at a time", - async function(value) { - if (!value.user_id || !value.league_id) return false; - - try { - const existingAssignment = await prisma.leagueUser.findFirst({ - where: { - user_id: value.user_id, - league_id: { - not: value.league_id // Allow updating the same league - } +export const assignUserToLeagueSchema = object({ + id: string().uuid(), + user_id: string() + .uuid("Invalid user ID format") + .required("User ID is required"), + league_id: string() + .uuid("Invalid league ID format") + .required("League ID is required"), +}).test( + "unique-league-assignment", + "User can only be assigned to one league at a time", + async function(value) { + if (!value.user_id || !value.league_id) return false; + + try { + const existingAssignment = await prisma.leagueUser.findFirst({ + where: { + user_id: value.user_id, + league_id: { + not: value.league_id // Allow updating the same league } - }); - return !existingAssignment; - } catch (error) { - return false; - } + } + }); + return !existingAssignment; + } catch (error) { + return false; } - ), -}); + } +); +export type AssignUserToLeagueSchema = InferType; // Validation for adding user experience -export const addUserExperienceSchema = yup.object().shape({ - input: yup.object().shape({ - id: yup.string().uuid(), - user_id: yup - .string() - .uuid("Invalid user ID format") - .required("User ID is required"), - amount: yup - .number() - .positive("Experience amount must be positive") - .required("Experience amount is required") - .test( - "is-valid-amount", - "Experience amount must be a reasonable number (0-1000000)", - (value) => value !== undefined && value >= 0 && value <= 1000000 - ), - }).test( - "user-in-league", - "User must be assigned to a league to gain experience", - async function(value) { - if (!value.user_id) return false; - - try { - const userLeague = await prisma.leagueUser.findFirst({ - where: { - user_id: value.user_id - } - }); - return !!userLeague; - } catch (error) { - return false; - } +export const addUserExperienceSchema = object({ + id: string().uuid(), + user_id: string() + .uuid("Invalid user ID format") + .required("User ID is required"), + amount: number() + .positive("Experience amount must be positive") + .required("Experience amount is required") + .test( + "is-valid-amount", + "Experience amount must be a reasonable number (0-1000000)", + (value) => value !== undefined && value >= 0 && value <= 1000000 + ), +}).test( + "user-in-league", + "User must be assigned to a league to gain experience", + async function(value) { + if (!value.user_id) return false; + + try { + const userLeague = await prisma.leagueUser.findFirst({ + where: { + user_id: value.user_id + } + }); + return !!userLeague; + } catch (error) { + return false; } - ), -}); + } +); +export type AddUserExperienceSchema = InferType; // Additional validation for getting league leaderboard -export const getLeagueLeaderboardSchema = yup.object().shape({ - leagueId: yup - .string() +export const getLeagueLeaderboardSchema = object({ + leagueId: string() .uuid("Invalid league ID format") .required("League ID is required"), }); +export type GetLeagueLeaderboardSchema = InferType; // Additional validation for getting user league status -export const getUserLeagueStatusSchema = yup.object().shape({ - userId: yup - .string() +export const getUserLeagueStatusSchema = object({ + userId: string() .uuid("Invalid user ID format") .required("User ID is required"), -}); \ No newline at end of file +}); +export type GetUserLeagueStatusSchema = InferType; \ No newline at end of file diff --git a/tests/league.test.ts b/tests/league.test.ts index 26515c9..d642010 100644 --- a/tests/league.test.ts +++ b/tests/league.test.ts @@ -11,6 +11,7 @@ describe("League Routes", () => { const response = await axios.post("/api/v1/createLeague", { input: league, }); + console.log(response, "wownice"); expect(response.status).toBe(200); const { data } = response; expect(data.message).toBe("League created successfully"); @@ -19,6 +20,7 @@ describe("League Routes", () => { expect(data.league.demotion_rate).toBe(league.demotion_rate); expect(data.league.promotion_rate).toBe(league.promotion_rate); } catch (error) { + console.log((error as Error).message, "wow error happened"); if (axiosRoot.isAxiosError(error)) { console.log(error.response?.data); expect(error.response?.status).toBe(200); @@ -27,88 +29,88 @@ describe("League Routes", () => { } }); -// test("Assign Users to Leagues", async () => { -// for (const leagueUser of leagueUsers) { -// try { -// const response = await axios.post("/api/v1/assignUserToLeague", { -// input: leagueUser, -// }); -// expect(response.status).toBe(200); -// const { data } = response; -// expect(data.message).toBe("User assigned to league successfully"); -// expect(data.leagueUser.id).toBeDefined(); -// expect(data.leagueUser.user_id).toBe(leagueUser.user_id); -// expect(data.leagueUser.league_id).toBe(leagueUser.league_id); -// } catch (error) { -// if (axiosRoot.isAxiosError(error)) { -// console.log(error.response?.data); -// expect(error.response?.status).toBe(200); -// } -// } -// } -// }); + test("Assign Users to Leagues", async () => { + for (const leagueUser of leagueUsers) { + try { + const response = await axios.post("/api/v1/assignUserToLeague", { + input: leagueUser, + }); + expect(response.status).toBe(200); + const { data } = response; + expect(data.message).toBe("User assigned to league successfully"); + expect(data.leagueUser.id).toBeDefined(); + expect(data.leagueUser.user_id).toBe(leagueUser.user_id); + expect(data.leagueUser.league_id).toBe(leagueUser.league_id); + } catch (error) { + if (axiosRoot.isAxiosError(error)) { + console.log(error.response?.data); + expect(error.response?.status).toBe(200); + } + } + } + }); -// test("Add User Experience", async () => { -// for (const experience of userExperiences) { -// try { -// const response = await axios.post("/api/v1/addUserExperience", { -// input: experience, -// }); -// expect(response.status).toBe(200); -// const { data } = response; -// expect(data.message).toBe("User experience added successfully"); -// expect(data.userExperience.id).toBeDefined(); -// expect(data.userExperience.user_id).toBe(experience.user_id); -// expect(data.userExperience.amount).toBe(experience.amount); -// } catch (error) { -// if (axiosRoot.isAxiosError(error)) { -// console.log(error.response?.data); -// expect(error.response?.status).toBe(200); -// } -// } -// } -// }); + test("Add User Experience", async () => { + for (const experience of userExperiences) { + try { + const response = await axios.post("/api/v1/addUserExperience", { + input: experience, + }); + expect(response.status).toBe(200); + const { data } = response; + expect(data.message).toBe("User experience added successfully"); + expect(data.userExperience.id).toBeDefined(); + expect(data.userExperience.user_id).toBe(experience.user_id); + expect(data.userExperience.amount).toBe(experience.amount); + } catch (error) { + if (axiosRoot.isAxiosError(error)) { + console.log(error.response?.data); + expect(error.response?.status).toBe(200); + } + } + } + }); -// test("Get League Leaderboard", async () => { -// for (const league of leagues) { -// try { -// const response = await axios.get(`/api/v1/leagueLeaderboard/${league.id}`); -// expect(response.status).toBe(200); -// const { data } = response; -// expect(data.leaderboard).toBeDefined(); -// expect(Array.isArray(data.leaderboard)).toBe(true); + test("Get League Leaderboard", async () => { + for (const league of leagues) { + try { + const response = await axios.get(`/api/v1/leagueLeaderboard/${league.id}`); + expect(response.status).toBe(200); + const { data } = response; + expect(data.leaderboard).toBeDefined(); + expect(Array.isArray(data.leaderboard)).toBe(true); -// // Verify leaderboard is sorted by experience (highest first) -// const sortedCorrectly = data.leaderboard.every((user: any, index: number, array: any[]) => { -// if (index === 0) return true; -// return user.experience <= array[index - 1].experience; -// }); -// expect(sortedCorrectly).toBe(true); -// } catch (error) { -// if (axiosRoot.isAxiosError(error)) { -// console.log(error.response?.data); -// expect(error.response?.status).toBe(200); -// } -// } -// } -// }); + // Verify leaderboard is sorted by experience (highest first) + const sortedCorrectly = data.leaderboard.every((user: any, index: number, array: any[]) => { + if (index === 0) return true; + return user.experience <= array[index - 1].experience; + }); + expect(sortedCorrectly).toBe(true); + } catch (error) { + if (axiosRoot.isAxiosError(error)) { + console.log(error.response?.data); + expect(error.response?.status).toBe(200); + } + } + } + }); -// test("Get User League Status", async () => { -// for (const leagueUser of leagueUsers) { -// try { -// const response = await axios.get(`/api/v1/userLeagueStatus/${leagueUser.user_id}`); -// expect(response.status).toBe(200); -// const { data } = response; -// expect(data.userStatus).toBeDefined(); -// expect(data.userStatus.league_id).toBe(leagueUser.league_id); -// expect(data.userStatus.current_rank).toBeDefined(); -// expect(data.userStatus.experience).toBeDefined(); -// } catch (error) { -// if (axiosRoot.isAxiosError(error)) { -// console.log(error.response?.data); -// expect(error.response?.status).toBe(200); -// } -// } -// } -// }); + test("Get User League Status", async () => { + for (const leagueUser of leagueUsers) { + try { + const response = await axios.get(`/api/v1/userLeagueStatus/${leagueUser.user_id}`); + expect(response.status).toBe(200); + const { data } = response; + expect(data.userStatus).toBeDefined(); + expect(data.userStatus.league_id).toBe(leagueUser.league_id); + expect(data.userStatus.current_rank).toBeDefined(); + expect(data.userStatus.experience).toBeDefined(); + } catch (error) { + if (axiosRoot.isAxiosError(error)) { + console.log(error.response?.data); + expect(error.response?.status).toBe(200); + } + } + } + }); }); \ No newline at end of file