From 616b7ebefaee5471ffca6cf1bac2c102b054c1ee Mon Sep 17 00:00:00 2001 From: Matelz <49626198+Matelz@users.noreply.github.com> Date: Tue, 13 May 2025 19:25:24 -0300 Subject: [PATCH 1/2] docs: add status badge --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index df01c9a..175635b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![CI/Deno Test](https://github.com/PoliEats/Backend/actions/workflows/deno_test.yml/badge.svg)](https://github.com/PoliEats/Backend/actions/workflows/deno_test.yml) + # PoliEats 🍽 - Backend ## Descrição: From 0dac6d5e02895b6d0a07b3a88ffe82fc85af6af0 Mon Sep 17 00:00:00 2001 From: Matelz <49626198+Matelz@users.noreply.github.com> Date: Tue, 13 May 2025 19:54:37 -0300 Subject: [PATCH 2/2] feat: add validation schema for user and decorator --- .github/workflows/deno_test.yml | 2 +- README.md | 42 +++- deno.json | 7 +- src/database/MockDatabase.ts | 16 +- src/database/schema.ts | 36 +-- src/interfaces/IDatabase.ts | 2 +- src/interfaces/IUser.ts | 16 +- src/lc/tools.ts | 6 +- src/main.ts | 13 +- src/middlewares/ValidateJWT.ts | 42 ++-- src/mocks/User.ts | 40 ++-- src/schemas/zodSchema.ts | 17 ++ src/services/AuthenticationService.ts | 202 +++++++++-------- src/services/decorators.ts | 21 ++ tests/authentication.test.ts | 308 +++++++++++++------------- tests/routes.test.ts | 266 +++++++++++----------- tsconfig.json | 6 + 17 files changed, 571 insertions(+), 471 deletions(-) create mode 100644 src/schemas/zodSchema.ts create mode 100644 src/services/decorators.ts create mode 100644 tsconfig.json diff --git a/.github/workflows/deno_test.yml b/.github/workflows/deno_test.yml index ce2b46f..eb5d84d 100644 --- a/.github/workflows/deno_test.yml +++ b/.github/workflows/deno_test.yml @@ -21,4 +21,4 @@ jobs: deno-version: v2.3.1 - name: Run unit tests - run: deno task test \ No newline at end of file + run: deno task test diff --git a/README.md b/README.md index 175635b..c28b27b 100644 --- a/README.md +++ b/README.md @@ -4,46 +4,66 @@ ## Descrição: -O PoliEats é um chatbot desenvolvido para auxiliar os alunos, alunos e demais visitantes do colégio **Poliedro** a -fazerem pedidos de comida e bebida a partir de uma interface de chat totalmente automatizada. O bot é capaz de responder perguntas frequentes, fornecer informações sobre o cardápio e realizar pedidos de forma rápida e eficiente. O objetivo principal do PoliEats é facilitar a experiência de compra dos usuários, tornando o processo mais ágil e prático. +O PoliEats é um chatbot desenvolvido para auxiliar os alunos, alunos e demais +visitantes do colégio **Poliedro** a fazerem pedidos de comida e bebida a partir +de uma interface de chat totalmente automatizada. O bot é capaz de responder +perguntas frequentes, fornecer informações sobre o cardápio e realizar pedidos +de forma rápida e eficiente. O objetivo principal do PoliEats é facilitar a +experiência de compra dos usuários, tornando o processo mais ágil e prático. ## Funcionalidades: -- **Cardápio**: O bot fornece informações detalhadas sobre o cardápio, incluindo preços e opções disponíveis. -- **Pedidos**: Os usuários podem fazer pedidos diretamente pelo bot, que irá encaminhar as informações para a equipe responsável. -- **Perguntas Frequentes**: O bot é capaz de responder perguntas frequentes sobre o colégio, cardápio e outros assuntos relacionados. -- **Horários**: O bot fornece informações sobre os horários de funcionamento do colégio e do serviço de alimentação. + +- **Cardápio**: O bot fornece informações detalhadas sobre o cardápio, incluindo + preços e opções disponíveis. +- **Pedidos**: Os usuários podem fazer pedidos diretamente pelo bot, que irá + encaminhar as informações para a equipe responsável. +- **Perguntas Frequentes**: O bot é capaz de responder perguntas frequentes + sobre o colégio, cardápio e outros assuntos relacionados. +- **Horários**: O bot fornece informações sobre os horários de funcionamento do + colégio e do serviço de alimentação. ## Tecnologias Utilizadas: + - **TypeScript**: Linguagem de programação utilizada para desenvolver o backend. - **Deno**: Ambiente de execução para o TypeScript. -- **PostgreSQL**: Banco de dados utilizado para armazenar informações sobre o cardápio, pedidos e usuários. -- **DrizzleORM**: ORM utilizado para facilitar a interação com o banco de dados PostgreSQL. -- **Mistral AI**: Modelo de linguagem utilizado para processar as mensagens dos usuários e gerar respostas. -- **LangChain**: Biblioteca utilizada para integrar o modelo de linguagem com o bot e facilitar a construção de fluxos de conversa. +- **PostgreSQL**: Banco de dados utilizado para armazenar informações sobre o + cardápio, pedidos e usuários. +- **DrizzleORM**: ORM utilizado para facilitar a interação com o banco de dados + PostgreSQL. +- **Mistral AI**: Modelo de linguagem utilizado para processar as mensagens dos + usuários e gerar respostas. +- **LangChain**: Biblioteca utilizada para integrar o modelo de linguagem com o + bot e facilitar a construção de fluxos de conversa. ## Como executar o projeto: + 1. Clone o repositório: + ```bash git clone https://github.com/PoliEats/Backend.git cd Backend ``` 2. Instale as dependências: + ```bash deno install ``` 3. Configure o .env: + ```bash cp .env.example .env ``` 4. Configure o banco de dados: + ```bash deno task db:migrate ``` 5. Execute o projeto: + ```bash deno task start -``` \ No newline at end of file +``` diff --git a/deno.json b/deno.json index a8ecd45..a4f111c 100644 --- a/deno.json +++ b/deno.json @@ -31,5 +31,10 @@ "nodeModulesDir": "auto", "unstable": [ "sloppy-imports" - ] + ], + "fmt": { + "options": { + "indentWidth": 2 + } + } } diff --git a/src/database/MockDatabase.ts b/src/database/MockDatabase.ts index 291e93c..ae92877 100644 --- a/src/database/MockDatabase.ts +++ b/src/database/MockDatabase.ts @@ -1,10 +1,14 @@ // deno-lint-ignore-file import { PgTableWithColumns } from "drizzle-orm/pg-core/table"; -import { InferInsertModel, InferSelectModel, TableConfig } from "drizzle-orm/table"; +import { + InferInsertModel, + InferSelectModel, + TableConfig, +} from "drizzle-orm/table"; import { IDatabase } from "../interfaces/IDatabase.ts"; export class MockDatabase implements IDatabase { - constructor() { + constructor() { this.data = new Map(); } @@ -81,7 +85,11 @@ export class MockDatabase implements IDatabase { return Promise.resolve(results); } - selectByField(table: PgTableWithColumns, field: keyof InferSelectModel>, value: string | number): Promise>[]> { + selectByField( + table: PgTableWithColumns, + field: keyof InferSelectModel>, + value: string | number, + ): Promise>[]> { const results: InferSelectModel>[] = []; this.data.forEach((record) => { if (record.table === table && record[field] === value) { @@ -90,4 +98,4 @@ export class MockDatabase implements IDatabase { }); return Promise.resolve(results); } -} \ No newline at end of file +} diff --git a/src/database/schema.ts b/src/database/schema.ts index 1d07295..646538a 100644 --- a/src/database/schema.ts +++ b/src/database/schema.ts @@ -1,26 +1,26 @@ import { pgTable, serial, text, timestamp } from "drizzle-orm/pg-core"; export const user = pgTable("user", { - id: serial("id").primaryKey(), - name: text("name").notNull(), - email: text("email").notNull().unique(), - document: text("document").notNull().unique(), - createdAt: timestamp("created_at").notNull().defaultNow(), - updatedAt: timestamp("updated_at").notNull().$onUpdate(() => new Date()), -}) + id: serial("id").primaryKey(), + name: text("name").notNull(), + email: text("email").notNull().unique(), + document: text("document").notNull().unique(), + createdAt: timestamp("created_at").notNull().defaultNow(), + updatedAt: timestamp("updated_at").notNull().$onUpdate(() => new Date()), +}); export const salt = pgTable("salt", { - id: serial("id").primaryKey(), - userId: serial("user_id").references(() => user.id), - salt: text("salt").notNull(), - createdAt: timestamp("created_at").notNull().defaultNow(), - updatedAt: timestamp("updated_at").notNull().$onUpdate(() => new Date()), + id: serial("id").primaryKey(), + userId: serial("user_id").references(() => user.id), + salt: text("salt").notNull(), + createdAt: timestamp("created_at").notNull().defaultNow(), + updatedAt: timestamp("updated_at").notNull().$onUpdate(() => new Date()), }); export const password = pgTable("password", { - id: serial("id").primaryKey(), - userId: serial("user_id").references(() => user.id), - password: text("password").notNull(), - createdAt: timestamp("created_at").notNull().defaultNow(), - updatedAt: timestamp("updated_at").notNull().$onUpdate(() => new Date()), -}); \ No newline at end of file + id: serial("id").primaryKey(), + userId: serial("user_id").references(() => user.id), + password: text("password").notNull(), + createdAt: timestamp("created_at").notNull().defaultNow(), + updatedAt: timestamp("updated_at").notNull().$onUpdate(() => new Date()), +}); diff --git a/src/interfaces/IDatabase.ts b/src/interfaces/IDatabase.ts index acd8314..6c55d13 100644 --- a/src/interfaces/IDatabase.ts +++ b/src/interfaces/IDatabase.ts @@ -73,4 +73,4 @@ export interface IDatabase { field: keyof InferSelectModel>, value: string | number, ): Promise>[]>; -} \ No newline at end of file +} diff --git a/src/interfaces/IUser.ts b/src/interfaces/IUser.ts index 2f87ec3..a6cad0e 100644 --- a/src/interfaces/IUser.ts +++ b/src/interfaces/IUser.ts @@ -2,13 +2,13 @@ * User Interface * This interface defines the structure of a user object. * It includes properties such as id, name, email, document, createdAt, and updatedAt. -*/ + */ export interface IUser { - id: number; - name: string; - email: string; - document: string; - createdAt: Date; - updatedAt: Date; -} \ No newline at end of file + id: number; + name: string; + email: string; + document: string; + createdAt: Date; + updatedAt: Date; +} diff --git a/src/lc/tools.ts b/src/lc/tools.ts index 6c4eb4f..ea8507f 100644 --- a/src/lc/tools.ts +++ b/src/lc/tools.ts @@ -95,14 +95,14 @@ export const createOrder = tool( const isConfirmed = await new Promise((resolve) => { pendingConfirmation.set(newOrder.id, { resolve }); - + setTimeout(() => { if (pendingConfirmation.has(newOrder.id)) { pendingConfirmation.delete(newOrder.id); resolve(false); } }, 30000); - }) + }); if (isConfirmed) { orders.push(newOrder); @@ -110,7 +110,7 @@ export const createOrder = tool( io.emit( "new_order", JSON.stringify({ - newOrder + newOrder, }), ); diff --git a/src/main.ts b/src/main.ts index 4ed91e7..b4a7784 100644 --- a/src/main.ts +++ b/src/main.ts @@ -15,7 +15,7 @@ const db = new MockDatabase(); const authenticationService = new AuthenticationService(db); const JWTmiddleware = new ValidateJWT(authenticationService); -app.use(cookieParser()) +app.use(cookieParser()); app.use(express.json()); app.use(cors({ @@ -37,12 +37,19 @@ app.post("/auth/register", async (req, res) => { const { name, email, document, password } = req.body; try { const userId = await authenticationService.registerUser( - { id: 123, name, email, document, createdAt: new Date(), updatedAt: new Date() }, + { + id: 123, + name, + email, + document, + createdAt: new Date(), + updatedAt: new Date(), + }, password, ); const token = await authenticationService.createJWT(userId); - + res.cookie("token", token, { httpOnly: true, secure: false, diff --git a/src/middlewares/ValidateJWT.ts b/src/middlewares/ValidateJWT.ts index 13ba4b6..278b3b1 100644 --- a/src/middlewares/ValidateJWT.ts +++ b/src/middlewares/ValidateJWT.ts @@ -2,27 +2,27 @@ import { NextFunction, Request, Response } from "express"; import { AuthenticationService } from "../services/AuthenticationService.ts"; export class ValidateJWT { - private authService: AuthenticationService; - - constructor(authService: AuthenticationService) { - this.authService = authService; - this.validateToken = this.validateToken.bind(this); - } + private authService: AuthenticationService; + + constructor(authService: AuthenticationService) { + this.authService = authService; + this.validateToken = this.validateToken.bind(this); + } - async validateToken(req: Request, res: Response, next: NextFunction) { - const token = req.cookies.token; - - if (!token) { - res.status(401).json({ error: "Unauthorized" }); - return; - } + async validateToken(req: Request, res: Response, next: NextFunction) { + const token = req.cookies.token; - await this.authService.verifyJWT(token) - .then(() => { - next(); - }) - .catch(() => { - res.status(401).json({ error: "Invalid token" }); - }); + if (!token) { + res.status(401).json({ error: "Unauthorized" }); + return; } -} \ No newline at end of file + + await this.authService.verifyJWT(token) + .then(() => { + next(); + }) + .catch(() => { + res.status(401).json({ error: "Invalid token" }); + }); + } +} diff --git a/src/mocks/User.ts b/src/mocks/User.ts index 2b9145c..c903aac 100644 --- a/src/mocks/User.ts +++ b/src/mocks/User.ts @@ -2,27 +2,27 @@ import { Faker, pt_BR } from "@faker-js/faker"; import { IUser } from "../interfaces/IUser.ts"; export function generateMockUser(): IUser { - const faker = new Faker({ - locale: pt_BR, - }) - - return { - id: faker.number.int({ min: 1, max: 1000 }), - name: faker.person.fullName(), - email: faker.internet.email(), - document: faker.string.numeric(11), - createdAt: faker.date.past(), - updatedAt: faker.date.recent(), + const faker = new Faker({ + locale: pt_BR, + }); + + return { + id: faker.number.int({ min: 1, max: 1000 }), + name: faker.person.fullName(), + email: faker.internet.email(), + document: faker.string.numeric(11), + createdAt: faker.date.past(), + updatedAt: faker.date.recent(), }; } export function generateMockPassword(): string { - const faker = new Faker({ - locale: pt_BR, - }) - - return faker.internet.password({ - length: 8, - memorable: true, - }); -} \ No newline at end of file + const faker = new Faker({ + locale: pt_BR, + }); + + return faker.internet.password({ + length: 8, + memorable: true, + }); +} diff --git a/src/schemas/zodSchema.ts b/src/schemas/zodSchema.ts new file mode 100644 index 0000000..af351c8 --- /dev/null +++ b/src/schemas/zodSchema.ts @@ -0,0 +1,17 @@ +import z from "zod"; + +export const userSchema = z.object({ + id: z.number().describe("ID do usuário").optional(), + name: z.string().describe("Nome do usuário"), + email: z.string().email().describe("Email do usuário"), + document: z.string().refine( + (value) => { + const regex = /^\d{3}.?\d{3}.?\d{3}\-?\d{2}$/; + return regex.test(value); + }, + ).transform((value) => value.replace(/\D/g, "")).describe( + "Documento do usuário", + ), + createdAt: z.date().describe("Data de criação do usuário"), + updatedAt: z.date().describe("Data de atualização do usuário"), +}); diff --git a/src/services/AuthenticationService.ts b/src/services/AuthenticationService.ts index 3969ebf..c8ef869 100644 --- a/src/services/AuthenticationService.ts +++ b/src/services/AuthenticationService.ts @@ -1,122 +1,138 @@ import bcrypt from "bcrypt"; import { JWTPayload, jwtVerify, SignJWT } from "jose"; import { JWSInvalid } from "jose/errors"; -import { password as passwordTable, salt as saltTable, user as userTable } from "../database/schema.ts"; +import { + password as passwordTable, + salt as saltTable, + user as userTable, +} from "../database/schema.ts"; import { IDatabase } from "../interfaces/IDatabase.ts"; import { IUser } from "../interfaces/IUser.ts"; +import { userSchema } from "../schemas/zodSchema.ts"; +import { validateData } from "./decorators.ts"; export class AuthenticationService { - private db: IDatabase; - private secretKey: Uint8Array; + private db: IDatabase; + private secretKey: Uint8Array; + + constructor(db: IDatabase) { + this.db = db; + + this.secretKey = new TextEncoder().encode("poli_eats"); + } + + @validateData(userSchema) + async registerUser(user: IUser, password: string): Promise { + const existingUser = await this.db.selectByField( + userTable, + "email", + user.email, + ); + if (existingUser.length > 0) { + throw new Error("User already exists"); + } - constructor(db: IDatabase) { - this.db = db; + const newUser = await this.db.insert(userTable, user); - this.secretKey = new TextEncoder().encode("poli_eats"); - } + const salt = await bcrypt.genSalt(10); + const hashedPassword = await bcrypt.hash(password, salt); - async registerUser(user: IUser, password: string): Promise { - const existingUser = await this.db.selectByField(userTable, "email", user.email); - if (existingUser.length > 0) { - throw new Error("User already exists"); - } - - const newUser = await this.db.insert(userTable, user); - - const salt = await bcrypt.genSalt(10); - const hashedPassword = await bcrypt.hash(password, salt); - - await this.db.insert(passwordTable, { - userId: newUser.id, - password: hashedPassword, - }); - - await this.db.insert(saltTable, { - userId: newUser.id, - salt: salt, - }); - return newUser.id; - } + await this.db.insert(passwordTable, { + userId: newUser.id, + password: hashedPassword, + }); + + await this.db.insert(saltTable, { + userId: newUser.id, + salt: salt, + }); + return newUser.id; + } + + async createJWT(userId: number): Promise { + const user = await this.getUserById(userId); - async createJWT(userId: number): Promise { - const user = await this.getUserById(userId); - - if (!user) { - throw new Error("User not found"); - } - - const payload = { - id: user.id, - name: user.name, - }; - - const jwt = await new SignJWT(payload) - .setProtectedHeader({ alg: "HS256" }) - .setIssuedAt() - .setExpirationTime("2h") - .sign(this.secretKey); - - return jwt; + if (!user) { + throw new Error("User not found"); } - async verifyJWT(token: string): Promise { - try { - const { payload } = await jwtVerify(token, this.secretKey); - return payload; - } catch (error: unknown) { - return error instanceof JWSInvalid ? null : Promise.reject(error); - } + const payload = { + id: user.id, + name: user.name, + }; + + const jwt = await new SignJWT(payload) + .setProtectedHeader({ alg: "HS256" }) + .setIssuedAt() + .setExpirationTime("2h") + .sign(this.secretKey); + + return jwt; + } + + async verifyJWT(token: string): Promise { + try { + const { payload } = await jwtVerify(token, this.secretKey); + return payload; + } catch (error: unknown) { + return error instanceof JWSInvalid ? null : Promise.reject(error); } + } - async loginUser(email: string, password: string): Promise { - const user = await this.db.selectByField(userTable, "email", email); + async loginUser(email: string, password: string): Promise { + const user = await this.db.selectByField(userTable, "email", email); - if (!user) { - throw new Error("User not found"); - } + if (!user) { + throw new Error("User not found"); + } - const userPassword = await this.db.selectByField(passwordTable, "userId", user[0].id); - if (!userPassword) { - throw new Error("User password not found"); - } + const userPassword = await this.db.selectByField( + passwordTable, + "userId", + user[0].id, + ); + if (!userPassword) { + throw new Error("User password not found"); + } - const salt = await this.db.selectByField(saltTable, "userId", user[0].id); - - if (!salt) { - throw new Error("User salt not found"); - } + const salt = await this.db.selectByField(saltTable, "userId", user[0].id); - const hashedPassword = await bcrypt.hash(password, salt[0].salt); + if (!salt) { + throw new Error("User salt not found"); + } - if (userPassword[0].password !== hashedPassword) { - throw new Error("Invalid password"); - } + const hashedPassword = await bcrypt.hash(password, salt[0].salt); - return user[0]; + if (userPassword[0].password !== hashedPassword) { + throw new Error("Invalid password"); } - async updateUser(id: number, user: IUser): Promise { - const existingUser = await this.db.select(userTable, id); - if (!existingUser) { - throw new Error("User not found"); - } + return user[0]; + } - const updatedUser = await this.db.update(userTable, id, user); - return updatedUser.id; + @validateData(userSchema) + async updateUser(user: IUser, id: number): Promise { + const existingUser = await this.db.select(userTable, id); + if (!existingUser) { + throw new Error("User not found"); } - async deleteUser(id: number): Promise { - const existingUser = await this.db.select(userTable, id); - if (!existingUser) { - throw new Error("User not found"); - } + const updatedUser = await this.db.update(userTable, id, user); + return updatedUser.id; + } - const deleted = await this.db.delete(userTable, id); - return deleted; + async deleteUser(id: number): Promise { + const existingUser = await this.db.select(userTable, id); + if (!existingUser) { + throw new Error("User not found"); } - async getUserById(id: number): Promise { - const user = await this.db.select(userTable, id); - return user; - } -} \ No newline at end of file + const deleted = await this.db.delete(userTable, id); + return deleted; + } + + async getUserById(id: number): Promise { + const user = await this.db.select(userTable, id); + return user; + } +} diff --git a/src/services/decorators.ts b/src/services/decorators.ts new file mode 100644 index 0000000..68d49b8 --- /dev/null +++ b/src/services/decorators.ts @@ -0,0 +1,21 @@ +import { ZodSchema } from "zod"; + +export function validateData(schema: ZodSchema) { + return function ( + _target: unknown, + _propertyKey: string, + descriptor: PropertyDescriptor, + ) { + const originalMethod = descriptor.value; + + descriptor.value = async function (...args: unknown[]) { + try { + await schema.safeParseAsync(args[0]); + + return originalMethod.apply(this, [args[0], args[1]]); + } catch (error) { + throw new Error(`Validation failed: ${error}`); + } + }; + }; +} diff --git a/tests/authentication.test.ts b/tests/authentication.test.ts index 9ba7a86..5d34d46 100644 --- a/tests/authentication.test.ts +++ b/tests/authentication.test.ts @@ -6,157 +6,157 @@ import { generateMockPassword, generateMockUser } from "../src/mocks/User.ts"; import { AuthenticationService } from "../src/services/AuthenticationService.ts"; describe("User authentication", () => { - it("should register a new user", async () => { - const db = new MockDatabase(); - const authService = new AuthenticationService(db); - - const user = generateMockUser(); - const password = generateMockPassword(); - - const userId = await authService.registerUser(user, password); - const registeredUser = await authService.getUserById(userId); - - expect(registeredUser).toBeDefined(); - expect(registeredUser?.name).toBe(user.name); - expect(registeredUser?.email).toBe(user.email); - expect(registeredUser?.document).toBe(user.document); - expect(registeredUser?.createdAt).toBeDefined(); - expect(registeredUser?.updatedAt).toBeDefined(); - }); - - it("should not register a user with an existing email", async () => { - const db = new MockDatabase(); - const authService = new AuthenticationService(db); - - const user = generateMockUser(); - const password = generateMockPassword(); - - await authService.registerUser(user, password); - - try { - await authService.registerUser(user, password); - } catch (error: unknown) { - if (!(error instanceof Error)) { - throw error; - } - expect(error.message).toBe("User already exists"); - } - }); - - it("should login a user with valid credentials", async () => { - const db = new MockDatabase(); - const authService = new AuthenticationService(db); - - const user = generateMockUser(); - const password = generateMockPassword(); - - await authService.registerUser(user, password); - - const loggedInUser = await authService.loginUser(user.email, password); - - expect(loggedInUser).toBeDefined(); - expect(loggedInUser?.email).toBe(user.email); - }); - - it("should not login a user with invalid credentials", async () => { - const db = new MockDatabase(); - const authService = new AuthenticationService(db); - - const user = generateMockUser(); - const password = generateMockPassword(); - - await authService.registerUser(user, password); - - try { - await authService.loginUser(user.email, "wrongpassword"); - } catch (error: unknown) { - if (!(error instanceof Error)) { - throw error; - } - expect(error.message).toBe("Invalid password"); - } - }); - - it("should update an existing user", async () => { - const db = new MockDatabase(); - const authService = new AuthenticationService(db); - - const user = generateMockUser(); - const password = generateMockPassword(); - - const userId = await authService.registerUser(user, password); - - const updatedUser = { ...user, name: "Updated Name" }; - await authService.updateUser(userId, updatedUser); - - const fetchedUser = await authService.getUserById(userId); - - expect(fetchedUser).toBeDefined(); - expect(fetchedUser?.name).toBe(updatedUser.name); - }); - - it("should delete an existing user", async () => { - const db = new MockDatabase(); - const authService = new AuthenticationService(db); - - const user = generateMockUser(); - const password = generateMockPassword(); - - const userId = await authService.registerUser(user, password); - - const deleted = await authService.deleteUser(userId); - - expect(deleted).toBe(true); - - const fetchedUser = await authService.getUserById(userId); - - expect(fetchedUser).toBeNull(); - }); - - it("should generate a JWT token for a user", async () => { - const db = new MockDatabase(); - const authService = new AuthenticationService(db); - - const user = generateMockUser(); - const password = generateMockPassword(); - - const userId = await authService.registerUser(user, password); - - const token = await authService.createJWT(userId); - - expect(token).toBeDefined(); - }); - - it("should verify a valid JWT token", async () => { - const db = new MockDatabase(); - const authService = new AuthenticationService(db); - - const user = generateMockUser(); - const password = generateMockPassword(); - - const userId = await authService.registerUser(user, password); - - const token = await authService.createJWT(userId); - - const payload = await authService.verifyJWT(token); - - expect(payload).toBeDefined(); - expect(payload?.id).toBe(userId); - }); - - it("should not verify an invalid JWT token", async () => { - const db = new MockDatabase(); - const authService = new AuthenticationService(db); - - const invalidToken = "invalidtoken"; - - try { - await authService.verifyJWT(invalidToken); - } catch (error: unknown) { - if (!(error instanceof JWSInvalid)) { - throw error; - } - expect(error.code).toBe("ERR_JWS_INVALID"); - } - }); -}) \ No newline at end of file + it("should register a new user", async () => { + const db = new MockDatabase(); + const authService = new AuthenticationService(db); + + const user = generateMockUser(); + const password = generateMockPassword(); + + const userId = await authService.registerUser(user, password); + const registeredUser = await authService.getUserById(userId); + + expect(registeredUser).toBeDefined(); + expect(registeredUser?.name).toBe(user.name); + expect(registeredUser?.email).toBe(user.email); + expect(registeredUser?.document).toBe(user.document); + expect(registeredUser?.createdAt).toBeDefined(); + expect(registeredUser?.updatedAt).toBeDefined(); + }); + + it("should not register a user with an existing email", async () => { + const db = new MockDatabase(); + const authService = new AuthenticationService(db); + + const user = generateMockUser(); + const password = generateMockPassword(); + + await authService.registerUser(user, password); + + try { + await authService.registerUser(user, password); + } catch (error: unknown) { + if (!(error instanceof Error)) { + throw error; + } + expect(error.message).toBe("User already exists"); + } + }); + + it("should login a user with valid credentials", async () => { + const db = new MockDatabase(); + const authService = new AuthenticationService(db); + + const user = generateMockUser(); + const password = generateMockPassword(); + + await authService.registerUser(user, password); + + const loggedInUser = await authService.loginUser(user.email, password); + + expect(loggedInUser).toBeDefined(); + expect(loggedInUser?.email).toBe(user.email); + }); + + it("should not login a user with invalid credentials", async () => { + const db = new MockDatabase(); + const authService = new AuthenticationService(db); + + const user = generateMockUser(); + const password = generateMockPassword(); + + await authService.registerUser(user, password); + + try { + await authService.loginUser(user.email, "wrongpassword"); + } catch (error: unknown) { + if (!(error instanceof Error)) { + throw error; + } + expect(error.message).toBe("Invalid password"); + } + }); + + it("should update an existing user", async () => { + const db = new MockDatabase(); + const authService = new AuthenticationService(db); + + const user = generateMockUser(); + const password = generateMockPassword(); + + const userId = await authService.registerUser(user, password); + + const updatedUser = { ...user, name: "Updated Name" }; + await authService.updateUser(updatedUser, userId); + + const fetchedUser = await authService.getUserById(userId); + + expect(fetchedUser).toBeDefined(); + expect(fetchedUser?.name).toBe(updatedUser.name); + }); + + it("should delete an existing user", async () => { + const db = new MockDatabase(); + const authService = new AuthenticationService(db); + + const user = generateMockUser(); + const password = generateMockPassword(); + + const userId = await authService.registerUser(user, password); + + const deleted = await authService.deleteUser(userId); + + expect(deleted).toBe(true); + + const fetchedUser = await authService.getUserById(userId); + + expect(fetchedUser).toBeNull(); + }); + + it("should generate a JWT token for a user", async () => { + const db = new MockDatabase(); + const authService = new AuthenticationService(db); + + const user = generateMockUser(); + const password = generateMockPassword(); + + const userId = await authService.registerUser(user, password); + + const token = await authService.createJWT(userId); + + expect(token).toBeDefined(); + }); + + it("should verify a valid JWT token", async () => { + const db = new MockDatabase(); + const authService = new AuthenticationService(db); + + const user = generateMockUser(); + const password = generateMockPassword(); + + const userId = await authService.registerUser(user, password); + + const token = await authService.createJWT(userId); + + const payload = await authService.verifyJWT(token); + + expect(payload).toBeDefined(); + expect(payload?.id).toBe(userId); + }); + + it("should not verify an invalid JWT token", async () => { + const db = new MockDatabase(); + const authService = new AuthenticationService(db); + + const invalidToken = "invalidtoken"; + + try { + await authService.verifyJWT(invalidToken); + } catch (error: unknown) { + if (!(error instanceof JWSInvalid)) { + throw error; + } + expect(error.code).toBe("ERR_JWS_INVALID"); + } + }); +}); diff --git a/tests/routes.test.ts b/tests/routes.test.ts index 7d1eab9..473afaf 100644 --- a/tests/routes.test.ts +++ b/tests/routes.test.ts @@ -5,138 +5,138 @@ import { wsServer } from "../src/main.ts"; import { generateMockPassword, generateMockUser } from "../src/mocks/User.ts"; describe("POST /auth/register", () => { - it("should register a new user", async () => { - const newUser = generateMockUser(); - const password = generateMockPassword(); - - const res = await request(wsServer) - .post("/auth/register") - .send({ - name: newUser.name, - email: newUser.email, - document: newUser.document, - password: password, - }) - - expect(res.status).toBe(201); - }); - - it("should not register a user with an existing email", async () => { - const newUser = generateMockUser(); - const password = generateMockPassword(); - - await request(wsServer) - .post("/auth/register") - .send({ - name: newUser.name, - email: newUser.email, - document: newUser.document, - password: password, - }) - - const res = await request(wsServer) - .post("/auth/register") - .send({ - name: newUser.name, - email: newUser.email, - document: newUser.document, - password: password, - }) - - expect(res.status).toBe(400); - }); - - it("should not register a user with missing fields", async () => { - const res = await request(wsServer) - .post("/auth/register") - .send({ - name: "John Doe", - email: "test@test.com", - document: "12345678901", - }) - expect(res.status).toBe(400); - }); -}) + it("should register a new user", async () => { + const newUser = generateMockUser(); + const password = generateMockPassword(); + + const res = await request(wsServer) + .post("/auth/register") + .send({ + name: newUser.name, + email: newUser.email, + document: newUser.document, + password: password, + }); + + expect(res.status).toBe(201); + }); + + it("should not register a user with an existing email", async () => { + const newUser = generateMockUser(); + const password = generateMockPassword(); + + await request(wsServer) + .post("/auth/register") + .send({ + name: newUser.name, + email: newUser.email, + document: newUser.document, + password: password, + }); + + const res = await request(wsServer) + .post("/auth/register") + .send({ + name: newUser.name, + email: newUser.email, + document: newUser.document, + password: password, + }); + + expect(res.status).toBe(400); + }); + + it("should not register a user with missing fields", async () => { + const res = await request(wsServer) + .post("/auth/register") + .send({ + name: "John Doe", + email: "test@test.com", + document: "12345678901", + }); + expect(res.status).toBe(400); + }); +}); describe("POST /auth/login", () => { - it("should login a user with valid credentials", async () => { - const newUser = generateMockUser(); - const password = generateMockPassword(); - - await request(wsServer) - .post("/auth/register") - .send({ - name: newUser.name, - email: newUser.email, - document: newUser.document, - password: password, - }) - - const res = await request(wsServer) - .post("/auth/login") - .send({ - email: newUser.email, - password: password, - }) - - expect(res.status).toBe(200); - }); - - it("should not login a user with invalid credentials", async () => { - const newUser = generateMockUser(); - const password = generateMockPassword(); - - await request(wsServer) - .post("/auth/register") - .send({ - name: newUser.name, - email: newUser.email, - document: newUser.document, - password: password, - }) - - const res = await request(wsServer) - .post("/auth/login") - .send({ - email: newUser.email, - password: "wrongpassword", - }) - expect(res.status).toBe(401); - }); - - it("should be able to see hidden content with a valid JWT", async () => { - const newUser = generateMockUser(); - const password = generateMockPassword(); - - await request(wsServer) - .post("/auth/register") - .send({ - name: newUser.name, - email: newUser.email, - document: newUser.document, - password: password, - }) - - const loginRes = await request(wsServer) - .post("/auth/login") - .send({ - email: newUser.email, - password: password, - }) - - const token = loginRes.headers["set-cookie"][0].split("=")[1].split(";")[0]; - - const res = await request(wsServer) - .get("/hidden") - .set("Cookie", `token=${token}`) - - expect(res.status).toBe(200); - }); - - it("should not be able to see hidden content without a valid JWT", async () => { - const res = await request(wsServer) - .get("/hidden") - - expect(res.status).toBe(401); - }); -}); \ No newline at end of file + it("should login a user with valid credentials", async () => { + const newUser = generateMockUser(); + const password = generateMockPassword(); + + await request(wsServer) + .post("/auth/register") + .send({ + name: newUser.name, + email: newUser.email, + document: newUser.document, + password: password, + }); + + const res = await request(wsServer) + .post("/auth/login") + .send({ + email: newUser.email, + password: password, + }); + + expect(res.status).toBe(200); + }); + + it("should not login a user with invalid credentials", async () => { + const newUser = generateMockUser(); + const password = generateMockPassword(); + + await request(wsServer) + .post("/auth/register") + .send({ + name: newUser.name, + email: newUser.email, + document: newUser.document, + password: password, + }); + + const res = await request(wsServer) + .post("/auth/login") + .send({ + email: newUser.email, + password: "wrongpassword", + }); + expect(res.status).toBe(401); + }); + + it("should be able to see hidden content with a valid JWT", async () => { + const newUser = generateMockUser(); + const password = generateMockPassword(); + + await request(wsServer) + .post("/auth/register") + .send({ + name: newUser.name, + email: newUser.email, + document: newUser.document, + password: password, + }); + + const loginRes = await request(wsServer) + .post("/auth/login") + .send({ + email: newUser.email, + password: password, + }); + + const token = loginRes.headers["set-cookie"][0].split("=")[1].split(";")[0]; + + const res = await request(wsServer) + .get("/hidden") + .set("Cookie", `token=${token}`); + + expect(res.status).toBe(200); + }); + + it("should not be able to see hidden content without a valid JWT", async () => { + const res = await request(wsServer) + .get("/hidden"); + + expect(res.status).toBe(401); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..0ed6f8d --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "target": "ES5", + "experimentalDecorators": true + } +}