diff --git a/modulo5/testes-erros/.gitignore b/modulo5/testes-erros/.gitignore new file mode 100644 index 0000000..d93d5e4 --- /dev/null +++ b/modulo5/testes-erros/.gitignore @@ -0,0 +1,5 @@ +.env +.DS_STORE +node_modules +build +coverage \ No newline at end of file diff --git a/modulo5/testes-erros/jest.config.js b/modulo5/testes-erros/jest.config.js new file mode 100644 index 0000000..a5062c6 --- /dev/null +++ b/modulo5/testes-erros/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + roots: ["/tests"], + transform: { + "^.+\\.tsx?$": "ts-jest", + }, + testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$", + moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], +} \ No newline at end of file diff --git a/modulo5/testes-erros/package.json b/modulo5/testes-erros/package.json new file mode 100644 index 0000000..cbf4ad7 --- /dev/null +++ b/modulo5/testes-erros/package.json @@ -0,0 +1,40 @@ +{ + "name": "template-jest", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "start": "node ./build/src/index.js", + "build": "tsc", + "dev": "ts-node-dev ./src/index.ts", + "migrations": "tsc && node ./build/src/database/migrations/Migrations.js", + "test": "jest" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@types/bcryptjs": "^2.4.2", + "@types/cors": "^2.8.12", + "@types/express": "^4.17.13", + "@types/jest": "^28.1.6", + "@types/jsonwebtoken": "^8.5.8", + "@types/knex": "^0.16.1", + "@types/node": "^18.0.6", + "@types/uuid": "^8.3.4", + "jest": "^28.1.3", + "ts-jest": "^28.0.8", + "ts-node-dev": "^2.0.0", + "typescript": "^4.7.4" + }, + "dependencies": { + "bcryptjs": "^2.4.3", + "cors": "^2.8.5", + "dotenv": "^16.0.1", + "express": "^4.18.1", + "jsonwebtoken": "^8.5.1", + "knex": "^2.2.0", + "mysql": "^2.18.1", + "uuid": "^8.3.2" + } +} diff --git a/modulo5/testes-erros/requests.rest b/modulo5/testes-erros/requests.rest new file mode 100644 index 0000000..94497e1 --- /dev/null +++ b/modulo5/testes-erros/requests.rest @@ -0,0 +1,46 @@ +### Endpoint de teste +GET http://localhost:3003/ping + +### 1) Signup +POST http://localhost:3003/users/signup +Content-Type: application/json + +{ + "name": "alice", + "email": "alice@gmail.com", + "password": "alice99" +} + +### 2) Login +POST http://localhost:3003/users/login +Content-Type: application/json + +{ + "email": "astrodev@gmail.com", + "password": "bananinha" +} + +### 3) Create post +POST http://localhost:3003/posts +Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEwMSIsInJvbGUiOiJBRE1JTiIsImlhdCI6MTY2NDI4MzY3MCwiZXhwIjoxNjY0MzcwMDcwfQ.1N7AOM3aE1pMnygnLSaGhuutArCndMWt4TektFlUuKU +Content-Type: application/json + +{ + "content": "Hello world!" +} + +### 4) Get posts +GET http://localhost:3003/posts +Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEwMSIsInJvbGUiOiJBRE1JTiIsImlhdCI6MTY2NDI4MzY3MCwiZXhwIjoxNjY0MzcwMDcwfQ.1N7AOM3aE1pMnygnLSaGhuutArCndMWt4TektFlUuKU + +### 5) Delete post +DELETE http://localhost:3003/posts/048e2da8-f739-4780-a28b-e4c6732d2c83 +Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEwMSIsInJvbGUiOiJBRE1JTiIsImlhdCI6MTY2NDI4MzY3MCwiZXhwIjoxNjY0MzcwMDcwfQ.1N7AOM3aE1pMnygnLSaGhuutArCndMWt4TektFlUuKU + +### 6) Like post +POST http://localhost:3003/posts/like/ed4b5302-c6f0-45b6-8055-6c519bbf62f0 +Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEwMSIsInJvbGUiOiJBRE1JTiIsImlhdCI6MTY2NDI4MzY3MCwiZXhwIjoxNjY0MzcwMDcwfQ.1N7AOM3aE1pMnygnLSaGhuutArCndMWt4TektFlUuKU + +### 7) Remove Like from post +DELETE http://localhost:3003/posts/like/ed4b5302-c6f0-45b6-8055-6c519bbf62f0 +Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEwMSIsInJvbGUiOiJBRE1JTiIsImlhdCI6MTY2NDI4MzY3MCwiZXhwIjoxNjY0MzcwMDcwfQ.1N7AOM3aE1pMnygnLSaGhuutArCndMWt4TektFlUuKU diff --git a/modulo5/testes-erros/src/business/PingBusiness.ts b/modulo5/testes-erros/src/business/PingBusiness.ts new file mode 100644 index 0000000..d426790 --- /dev/null +++ b/modulo5/testes-erros/src/business/PingBusiness.ts @@ -0,0 +1,9 @@ +export class PingBusiness { + public ping = async () => { + const response = { + message: "Pong!" + } + + return response + } +} \ No newline at end of file diff --git a/modulo5/testes-erros/src/business/PostBusiness.ts b/modulo5/testes-erros/src/business/PostBusiness.ts new file mode 100644 index 0000000..75b4700 --- /dev/null +++ b/modulo5/testes-erros/src/business/PostBusiness.ts @@ -0,0 +1,187 @@ +import { PostDatabase } from "../database/PostDatabase" +import { AuthenticationError } from "../errors/AuthenticationError" +import { AuthorizationError } from "../errors/AuthorizationError" +import { ConflictError } from "../errors/ConflictError" +import { NotFoundError } from "../errors/NotFoundError" +import { ParamsError } from "../errors/ParamsError" +import { IAddLikeInputDTO, IAddLikeOutputDTO, ICreatePostInputDTO, ICreatePostOutputDTO, IDeletePostInputDTO, IDeletePostOutputDTO, IGetPostsInputDTO, IGetPostsOutputDTO, ILikeDB, IRemoveLikeInputDTO, IRemoveLikeOutputDTO, Post } from "../models/Post" +import { USER_ROLES } from "../models/User" +import { Authenticator } from "../services/Authenticator" +import { IdGenerator } from "../services/IdGenerator" + +export class PostBusiness { + constructor( + private postDatabase: PostDatabase, + private idGenerator: IdGenerator, + private authenticator: Authenticator + ) {} + + public createPost = async (input: ICreatePostInputDTO) => { + const { token, content } = input + + const payload = this.authenticator.getTokenPayload(token) + + if (!payload) { + throw new AuthenticationError() + } + + if (typeof content !== "string") { + throw new ParamsError("Parâmetro 'content' inválido") + } + + if (content.length < 1) { + throw new ParamsError("Parâmetro 'content' inválido: mínimo de 1 caracteres") + } + + const id = this.idGenerator.generate() + + const post = new Post( + id, + content, + payload.id + ) + + await this.postDatabase.createPost(post) + + const response: ICreatePostOutputDTO = { + message: "Post criado com sucesso", + post + } + + return response + } + + public getPosts = async (input: IGetPostsInputDTO) => { + const { token } = input + + const payload = this.authenticator.getTokenPayload(token) + + if (!payload) { + throw new AuthenticationError() + } + + const postsDB = await this.postDatabase.getPosts() + + const posts = postsDB.map(postDB => { + return new Post( + postDB.id, + postDB.content, + postDB.user_id + ) + }) + + for (let post of posts) { + const postId = post.getId() + const likes = await this.postDatabase.getLikes(postId) + post.setLikes(likes) + } + + const response: IGetPostsOutputDTO = { + posts + } + + return response + } + + public deletePost = async (input: IDeletePostInputDTO) => { + const { token, postId } = input + + const payload = this.authenticator.getTokenPayload(token) + + if (!payload) { + throw new AuthenticationError() + } + + const postDB = await this.postDatabase.findPostById(postId) + + if (!postDB) { + throw new NotFoundError("Post não encontrado") + } + + if (payload.role === USER_ROLES.NORMAL) { + if (postDB.user_id !== payload.id) { + throw new AuthorizationError() + } + } + + await this.postDatabase.deletePost(postId) + + const response: IDeletePostOutputDTO = { + message: "Post deletado com sucesso" + } + + return response + } + + public addLike = async (input: IAddLikeInputDTO) => { + const { token, postId } = input + + const payload = this.authenticator.getTokenPayload(token) + + if (!payload) { + throw new AuthenticationError() + } + + const postDB = await this.postDatabase.findPostById(postId) + + if (!postDB) { + throw new NotFoundError("Post não encontrado") + } + + const isAlreadyLiked = await this.postDatabase.findLike( + postId, + payload.id + ) + + if (isAlreadyLiked) { + throw new ConflictError("Já deu like") + } + + const likeDB: ILikeDB = { + id: this.idGenerator.generate(), + post_id: postId, + user_id: payload.id + } + + await this.postDatabase.addLike(likeDB) + + const response: IAddLikeOutputDTO = { + message: "Like realizado com sucesso" + } + + return response + } + + public removeLike = async (input: IRemoveLikeInputDTO) => { + const { token, postId } = input + + const payload = this.authenticator.getTokenPayload(token) + + if (!payload) { + throw new AuthenticationError() + } + + const postDB = await this.postDatabase.findPostById(postId) + + if (!postDB) { + throw new NotFoundError("Post não encontrado") + } + + const isAlreadyLiked = await this.postDatabase.findLike( + postId, + payload.id + ) + + if (!isAlreadyLiked) { + throw new NotFoundError("Ainda não deu like") + } + + await this.postDatabase.removeLike(postId, payload.id) + + const response: IRemoveLikeOutputDTO = { + message: "Like removido com sucesso" + } + + return response + } +} \ No newline at end of file diff --git a/modulo5/testes-erros/src/business/UserBusiness.ts b/modulo5/testes-erros/src/business/UserBusiness.ts new file mode 100644 index 0000000..ddd6009 --- /dev/null +++ b/modulo5/testes-erros/src/business/UserBusiness.ts @@ -0,0 +1,136 @@ +import { UserDatabase } from "../database/UserDatabase" +import { AuthenticationError } from "../errors/AuthenticationError" +import { ConflictError } from "../errors/ConflictError" +import { NotFoundError } from "../errors/NotFoundError" +import { ParamsError } from "../errors/ParamsError" +import { ILoginInputDTO, ILoginOutputDTO, ISignupInputDTO, ISignupOutputDTO, User, USER_ROLES } from "../models/User" +import { Authenticator, ITokenPayload } from "../services/Authenticator" +import { HashManager } from "../services/HashManager" +import { IdGenerator } from "../services/IdGenerator" + +export class UserBusiness { + constructor( + private userDatabase: UserDatabase, + private idGenerator: IdGenerator, + private hashManager: HashManager, + private authenticator: Authenticator + ) {} + + public signup = async (input: ISignupInputDTO) => { + const { name, email, password } = input + + if (typeof name !== "string") { + throw new ParamsError("Parâmetro 'name' inválido") + } + + if (typeof email !== "string") { + throw new ParamsError("Parâmetro 'email' inválido") + } + + if (typeof password !== "string") { + throw new ParamsError("Parâmetro 'password' inválido") + } + + if (name.length < 3) { + throw new ParamsError("Parâmetro 'name' inválido: mínimo de 3 caracteres") + } + + if (password.length < 6) { + throw new ParamsError("Parâmetro 'password' inválido: mínimo de 6 caracteres") + } + + if (!email.match(/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/g)) { + throw new ParamsError("Parâmetro 'email' inválido") + } + + const isEmailAlreadyExists = await this.userDatabase.findByEmail(email) + + if (isEmailAlreadyExists) { + throw new ConflictError("Email já cadastrado") + } + + const id = this.idGenerator.generate() + const hashedPassword = await this.hashManager.hash(password) + + const user = new User( + id, + name, + email, + hashedPassword, + USER_ROLES.NORMAL + ) + + await this.userDatabase.createUser(user) + + const payload: ITokenPayload = { + id: user.getId(), + role: user.getRole() + } + + const token = this.authenticator.generateToken(payload) + + const response: ISignupOutputDTO = { + message: "Cadastro realizado com sucesso", + token + } + + return response + } + + public login = async (input: ILoginInputDTO) => { + const { email, password } = input + + if (typeof email !== "string") { + throw new ParamsError("Parâmetro 'email' inválido") + } + + if (typeof password !== "string") { + throw new ParamsError("Parâmetro 'password' inválido") + } + + if (password.length < 6) { + throw new ParamsError("Parâmetro 'password' inválido: mínimo de 6 caracteres") + } + + if (!email.match(/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/g)) { + throw new ParamsError("Parâmetro 'email' inválido") + } + + const userDB = await this.userDatabase.findByEmail(email) + + if (!userDB) { + throw new NotFoundError("Email não cadastrado") + } + + const user = new User( + userDB.id, + userDB.name, + userDB.email, + userDB.password, + userDB.role + ) + + const isPasswordCorrect = await this.hashManager.compare( + password, + user.getPassword() + ) + + if (!isPasswordCorrect) { + throw new AuthenticationError("Password incorreto") + } + + const payload: ITokenPayload = { + id: user.getId(), + role: user.getRole() + } + + const token = this.authenticator.generateToken(payload) + + const response: ILoginOutputDTO = { + message: "Login realizado com sucesso", + token + } + + return response + } +} \ No newline at end of file diff --git a/modulo5/testes-erros/src/controller/PingController.ts b/modulo5/testes-erros/src/controller/PingController.ts new file mode 100644 index 0000000..5397413 --- /dev/null +++ b/modulo5/testes-erros/src/controller/PingController.ts @@ -0,0 +1,24 @@ +import { Request, Response } from "express" +import { PingBusiness } from "../business/PingBusiness" +import { BaseError } from "../errors/BaseError" + +export class PingController { + constructor( + private pingBusiness: PingBusiness + ) {} + + public ping = async (req: Request, res: Response) => { + let errorCode = 400 + try { + const response = await this.pingBusiness.ping() + + res.status(200).send(response) + } catch (error) { + console.log(error) + if (error instanceof BaseError) { + return res.status(error.statusCode).send({ message: error.message }) + } + res.status(500).send({ message: "Erro inesperado" }) + } + } +} \ No newline at end of file diff --git a/modulo5/testes-erros/src/controller/PostController.ts b/modulo5/testes-erros/src/controller/PostController.ts new file mode 100644 index 0000000..99584b1 --- /dev/null +++ b/modulo5/testes-erros/src/controller/PostController.ts @@ -0,0 +1,99 @@ +import { Request, Response } from "express"; +import { PostBusiness } from "../business/PostBusiness"; +import { BaseError } from "../errors/BaseError"; +import { IAddLikeInputDTO, ICreatePostInputDTO, IDeletePostInputDTO, IGetPostsInputDTO, IRemoveLikeInputDTO } from "../models/Post"; + +export class PostController { + constructor( + private postBusiness: PostBusiness + ) {} + + public createPost = async (req: Request, res: Response) => { + try { + const input: ICreatePostInputDTO = { + token: req.headers.authorization as string, + content: req.body.content + } + + const response = await this.postBusiness.createPost(input) + res.status(201).send(response) + } catch (error) { + console.log(error) + if (error instanceof BaseError) { + return res.status(error.statusCode).send({ message: error.message }) + } + res.status(500).send({ message: "Erro inesperado" }) + } + } + + public getPosts = async (req: Request, res: Response) => { + try { + const input: IGetPostsInputDTO = { + token: req.headers.authorization as string + } + + const response = await this.postBusiness.getPosts(input) + res.status(200).send(response) + } catch (error) { + console.log(error) + if (error instanceof BaseError) { + return res.status(error.statusCode).send({ message: error.message }) + } + res.status(500).send({ message: "Erro inesperado" }) + } + } + + public deletePost = async (req: Request, res: Response) => { + try { + const input: IDeletePostInputDTO = { + token: req.headers.authorization as string, + postId: req.params.id + } + + const response = await this.postBusiness.deletePost(input) + res.status(200).send(response) + } catch (error) { + console.log(error) + if (error instanceof BaseError) { + return res.status(error.statusCode).send({ message: error.message }) + } + res.status(500).send({ message: "Erro inesperado" }) + } + } + + public addLike = async (req: Request, res: Response) => { + try { + const input: IAddLikeInputDTO = { + token: req.headers.authorization as string, + postId: req.params.id + } + + const response = await this.postBusiness.addLike(input) + res.status(200).send(response) + } catch (error) { + console.log(error) + if (error instanceof BaseError) { + return res.status(error.statusCode).send({ message: error.message }) + } + res.status(500).send({ message: "Erro inesperado" }) + } + } + + public removeLike = async (req: Request, res: Response) => { + try { + const input: IRemoveLikeInputDTO = { + token: req.headers.authorization as string, + postId: req.params.id + } + + const response = await this.postBusiness.removeLike(input) + res.status(200).send(response) + } catch (error) { + console.log(error) + if (error instanceof BaseError) { + return res.status(error.statusCode).send({ message: error.message }) + } + res.status(500).send({ message: "Erro inesperado" }) + } + } +} \ No newline at end of file diff --git a/modulo5/testes-erros/src/controller/UserController.ts b/modulo5/testes-erros/src/controller/UserController.ts new file mode 100644 index 0000000..48397b5 --- /dev/null +++ b/modulo5/testes-erros/src/controller/UserController.ts @@ -0,0 +1,47 @@ +import { Request, Response } from "express"; +import { UserBusiness } from "../business/UserBusiness"; +import { BaseError } from "../errors/BaseError"; +import { ILoginInputDTO, ISignupInputDTO } from "../models/User"; + +export class UserController { + constructor( + private userBusiness: UserBusiness + ) {} + + public signup = async (req: Request, res: Response) => { + try { + const input: ISignupInputDTO = { + name: req.body.name, + email: req.body.email, + password: req.body.password + } + + const response = await this.userBusiness.signup(input) + res.status(201).send(response) + } catch (error) { + console.log(error) + if (error instanceof BaseError) { + return res.status(error.statusCode).send({ message: error.message }) + } + res.status(500).send({ message: "Erro inesperado" }) + } + } + + public login = async (req: Request, res: Response) => { + try { + const input: ILoginInputDTO = { + email: req.body.email, + password: req.body.password + } + + const response = await this.userBusiness.login(input) + res.status(201).send(response) + } catch (error) { + console.log(error) + if (error instanceof BaseError) { + return res.status(error.statusCode).send({ message: error.message }) + } + res.status(500).send({ message: "Erro inesperado" }) + } + } +} \ No newline at end of file diff --git a/modulo5/testes-erros/src/database/BaseDatabase.ts b/modulo5/testes-erros/src/database/BaseDatabase.ts new file mode 100644 index 0000000..fd05f8c --- /dev/null +++ b/modulo5/testes-erros/src/database/BaseDatabase.ts @@ -0,0 +1,18 @@ +import knex from "knex" +import dotenv from "dotenv" + +dotenv.config() + +export abstract class BaseDatabase { + protected static connection = knex({ + client: "mysql", + connection: { + host: process.env.DB_HOST, + port: 3306, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_DATABASE, + multipleStatements: true + }, + }) +} diff --git a/modulo5/testes-erros/src/database/PostDatabase.ts b/modulo5/testes-erros/src/database/PostDatabase.ts new file mode 100644 index 0000000..e6740d1 --- /dev/null +++ b/modulo5/testes-erros/src/database/PostDatabase.ts @@ -0,0 +1,83 @@ +import { ILikeDB, IPostDB, Post } from "../models/Post" +import { BaseDatabase } from "./BaseDatabase" + +export class PostDatabase extends BaseDatabase { + public static TABLE_POSTS = "Labook_Posts" + public static TABLE_LIKES = "Labook_Likes" + + public toPostDBModel = (post: Post): IPostDB => { + const postDB: IPostDB = { + id: post.getId(), + content: post.getContent(), + user_id: post.getUserId() + } + + return postDB + } + + public createPost = async (post: Post): Promise => { + const postDB = this.toPostDBModel(post) + + await BaseDatabase + .connection(PostDatabase.TABLE_POSTS) + .insert(postDB) + } + + public getPosts = async (): Promise => { + const postsDB: IPostDB[] = await BaseDatabase + .connection(PostDatabase.TABLE_POSTS) + .select() + + return postsDB + } + + public getLikes = async (postId: string): Promise => { + const result: any = await BaseDatabase + .connection(PostDatabase.TABLE_LIKES) + .select() + .count("id AS likes") + .where({ post_id: postId }) + + return result[0].likes as number + } + + public findPostById = async (postId: string): Promise => { + const postsDB: IPostDB[] = await BaseDatabase + .connection(PostDatabase.TABLE_POSTS) + .select() + .where({ id: postId }) + + return postsDB[0] + } + + public deletePost = async (postId: string): Promise => { + await BaseDatabase + .connection(PostDatabase.TABLE_POSTS) + .delete() + .where({ id: postId }) + } + + public findLike = async (postId: string, userId: string): Promise => { + const likesDB: ILikeDB[] = await BaseDatabase + .connection(PostDatabase.TABLE_LIKES) + .select() + .where({ post_id: postId }) + .andWhere({ user_id: userId }) + + return likesDB[0] + } + + public addLike = async (likeDB: ILikeDB): Promise => { + await BaseDatabase + .connection(PostDatabase.TABLE_LIKES) + .insert(likeDB) + } + + public removeLike = async (postId: string, userId: string): Promise => { + await BaseDatabase + .connection(PostDatabase.TABLE_LIKES) + .delete() + .where({ post_id: postId }) + .andWhere({ user_id: userId }) + } +} \ No newline at end of file diff --git a/modulo5/testes-erros/src/database/UserDatabase.ts b/modulo5/testes-erros/src/database/UserDatabase.ts new file mode 100644 index 0000000..9cf2df5 --- /dev/null +++ b/modulo5/testes-erros/src/database/UserDatabase.ts @@ -0,0 +1,35 @@ +import { IUserDB, User } from "../models/User" +import { BaseDatabase } from "./BaseDatabase" + +export class UserDatabase extends BaseDatabase { + public static TABLE_USERS = "Labook_Users" + + public toUserDBModel = (user: User): IUserDB => { + const userDB: IUserDB = { + id: user.getId(), + name: user.getName(), + email: user.getEmail(), + password: user.getPassword(), + role: user.getRole() + } + + return userDB + } + + public findByEmail = async (email: string): Promise => { + const result: IUserDB[] = await BaseDatabase + .connection(UserDatabase.TABLE_USERS) + .select() + .where({ email }) + + return result[0] + } + + public createUser = async (user: User): Promise => { + const userDB = this.toUserDBModel(user) + + await BaseDatabase + .connection(UserDatabase.TABLE_USERS) + .insert(userDB) + } +} \ No newline at end of file diff --git a/modulo5/testes-erros/src/database/migrations/Migrations.ts b/modulo5/testes-erros/src/database/migrations/Migrations.ts new file mode 100644 index 0000000..291a5ab --- /dev/null +++ b/modulo5/testes-erros/src/database/migrations/Migrations.ts @@ -0,0 +1,77 @@ +import { BaseDatabase } from "../BaseDatabase" +import { PostDatabase } from "../PostDatabase" +import { UserDatabase } from "../UserDatabase" +import { likes, posts, users } from "./data" + +class Migrations extends BaseDatabase { + execute = async () => { + try { + console.log("Creating tables...") + await this.createTables() + console.log("Tables created successfully.") + + console.log("Populating tables...") + await this.insertData() + console.log("Tables populated successfully.") + + console.log("Migrations completed.") + } catch (error) { + console.log("FAILED! Error in migrations...") + if (error instanceof Error) { + console.log(error.message) + } + } finally { + console.log("Ending connection...") + BaseDatabase.connection.destroy() + console.log("Connection closed graciously.") + } + } + + createTables = async () => { + await BaseDatabase.connection.raw(` + DROP TABLE IF EXISTS ${PostDatabase.TABLE_LIKES}; + DROP TABLE IF EXISTS ${PostDatabase.TABLE_POSTS}; + DROP TABLE IF EXISTS ${UserDatabase.TABLE_USERS}; + + CREATE TABLE IF NOT EXISTS ${UserDatabase.TABLE_USERS}( + id VARCHAR(255) PRIMARY KEY, + name VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL, + role ENUM("NORMAL", "ADMIN") DEFAULT "NORMAL" NOT NULL + ); + + CREATE TABLE IF NOT EXISTS ${PostDatabase.TABLE_POSTS}( + id VARCHAR(255) PRIMARY KEY, + content VARCHAR(255) NOT NULL, + user_id VARCHAR(255) NOT NULL, + FOREIGN KEY (user_id) REFERENCES ${UserDatabase.TABLE_USERS}(id) + ); + + CREATE TABLE IF NOT EXISTS ${PostDatabase.TABLE_LIKES}( + id VARCHAR(255) PRIMARY KEY, + post_id VARCHAR(255) NOT NULL, + user_id VARCHAR(255) NOT NULL, + FOREIGN KEY (user_id) REFERENCES ${UserDatabase.TABLE_USERS}(id), + FOREIGN KEY (post_id) REFERENCES ${PostDatabase.TABLE_POSTS}(id) + ); + `) + } + + insertData = async () => { + await BaseDatabase + .connection(UserDatabase.TABLE_USERS) + .insert(users) + + await BaseDatabase + .connection(PostDatabase.TABLE_POSTS) + .insert(posts) + + await BaseDatabase + .connection(PostDatabase.TABLE_LIKES) + .insert(likes) + } +} + +const migrations = new Migrations() +migrations.execute() \ No newline at end of file diff --git a/modulo5/testes-erros/src/database/migrations/data.ts b/modulo5/testes-erros/src/database/migrations/data.ts new file mode 100644 index 0000000..bc6c4c1 --- /dev/null +++ b/modulo5/testes-erros/src/database/migrations/data.ts @@ -0,0 +1,77 @@ +import { ILikeDB, IPostDB } from "../../models/Post" +import { IUserDB, USER_ROLES } from "../../models/User" + +export const users: IUserDB[] = [ + { + id: "101", + name: "Astrodev", + email: "astrodev@gmail.com", + password: "$2a$12$RBAWOHpUvGTE.MEeIohAzec9tlVqtNA/x2PMPt/Hrt0vI437cQdJC", // bananinha + role: USER_ROLES.ADMIN + }, + { + id: "102", + name: "Fulano", + email: "fulano@gmail.com", + password: "$2a$12$PULtVNlAll87D6E8pR/0HO9vbzVDPaUMA89rc5cNmYoAAepbwmkcO", // qwerty00 + role: USER_ROLES.NORMAL + }, + { + id: "103", + name: "Ciclana", + email: "ciclana@gmail.com", + password: "$2a$12$LkWMqS3oPhP2iVMcZOVvWer9ahUPulxjB0EA4TWPxWaRuEEfYGu/i", // asdfg123 + role: USER_ROLES.NORMAL + } +] + +export const posts: IPostDB[] = [ + { + id: "201", + content: "Olá, sou novo por aqui!", + user_id: "101" + }, + { + id: "202", + content: "Bom dia, família!", + user_id: "102" + }, + { + id: "203", + content: "Receba!", + user_id: "103" + } +] + +export const likes: ILikeDB[] = [ + { + id: "301", + post_id: "201", + user_id: "101" + }, + { + id: "302", + post_id: "202", + user_id: "101" + }, + { + id: "303", + post_id: "203", + user_id: "101" + }, + { + id: "304", + post_id: "201", + user_id: "102" + }, + { + id: "305", + post_id: "201", + user_id: "103" + }, + { + id: "306", + post_id: "202", + user_id: "103" + } +] \ No newline at end of file diff --git a/modulo5/testes-erros/src/errors/AuthenticationError.ts b/modulo5/testes-erros/src/errors/AuthenticationError.ts new file mode 100644 index 0000000..0a5bfac --- /dev/null +++ b/modulo5/testes-erros/src/errors/AuthenticationError.ts @@ -0,0 +1,9 @@ +import { BaseError } from "./BaseError"; + +export class AuthenticationError extends BaseError { + constructor( + message: string = "Credenciais inválidas" + ) { + super(401, message) + } +} \ No newline at end of file diff --git a/modulo5/testes-erros/src/errors/AuthorizationError.ts b/modulo5/testes-erros/src/errors/AuthorizationError.ts new file mode 100644 index 0000000..6b9f2a0 --- /dev/null +++ b/modulo5/testes-erros/src/errors/AuthorizationError.ts @@ -0,0 +1,9 @@ +import { BaseError } from "./BaseError"; + +export class AuthorizationError extends BaseError { + constructor( + message: string = "Permissão insuficiente" + ) { + super(403, message) + } +} \ No newline at end of file diff --git a/modulo5/testes-erros/src/errors/BaseError.ts b/modulo5/testes-erros/src/errors/BaseError.ts new file mode 100644 index 0000000..1dae99f --- /dev/null +++ b/modulo5/testes-erros/src/errors/BaseError.ts @@ -0,0 +1,8 @@ +export class BaseError extends Error { + constructor( + public statusCode: number, + message: string + ) { + super(message) + } +} \ No newline at end of file diff --git a/modulo5/testes-erros/src/errors/ConflictError.ts b/modulo5/testes-erros/src/errors/ConflictError.ts new file mode 100644 index 0000000..e5934ef --- /dev/null +++ b/modulo5/testes-erros/src/errors/ConflictError.ts @@ -0,0 +1,9 @@ +import { BaseError } from "./BaseError"; + +export class ConflictError extends BaseError { + constructor( + message: string = "Recurso já existe" + ) { + super(409, message) + } +} \ No newline at end of file diff --git a/modulo5/testes-erros/src/errors/NotFoundError.ts b/modulo5/testes-erros/src/errors/NotFoundError.ts new file mode 100644 index 0000000..697098f --- /dev/null +++ b/modulo5/testes-erros/src/errors/NotFoundError.ts @@ -0,0 +1,9 @@ +import { BaseError } from "./BaseError"; + +export class NotFoundError extends BaseError { + constructor( + message: string = "Recurso não encontrado" + ) { + super(404, message) + } +} \ No newline at end of file diff --git a/modulo5/testes-erros/src/errors/ParamsError.ts b/modulo5/testes-erros/src/errors/ParamsError.ts new file mode 100644 index 0000000..b43539b --- /dev/null +++ b/modulo5/testes-erros/src/errors/ParamsError.ts @@ -0,0 +1,9 @@ +import { BaseError } from "./BaseError"; + +export class ParamsError extends BaseError { + constructor( + message: string = "Parâmetros inválidos ou faltando" + ) { + super(400, message) + } +} \ No newline at end of file diff --git a/modulo5/testes-erros/src/errors/UnprocessableError.ts b/modulo5/testes-erros/src/errors/UnprocessableError.ts new file mode 100644 index 0000000..26973e0 --- /dev/null +++ b/modulo5/testes-erros/src/errors/UnprocessableError.ts @@ -0,0 +1,19 @@ +import { BaseError } from "./BaseError"; + +export class UnprocessableError extends BaseError { + constructor( + message: string = "Parâmetros válidos, porém com erros de semântica" + ) { + super(422, message) + } +} + +/* + Esse erro serve para lidar com requisições estruturalmente válidas, + porém bloqueadas por algum outro motivo +*/ + +// Exemplos: +// pessoa tentando se cadastrar com o nome: "Admin" +// conteúdo do post com palavras proibidas (palavrões) +// etc \ No newline at end of file diff --git a/modulo5/testes-erros/src/index.ts b/modulo5/testes-erros/src/index.ts new file mode 100644 index 0000000..5ba1bbd --- /dev/null +++ b/modulo5/testes-erros/src/index.ts @@ -0,0 +1,20 @@ +import express from 'express' +import cors from 'cors' +import dotenv from "dotenv" +import { pingRouter } from './router/pingRouter' +import { userRouter } from './router/userRouter' +import { postRouter } from './router/postRouter' + +dotenv.config() + +const app = express() +app.use(express.json()) +app.use(cors()) + +app.listen(process.env.PORT || 3003, () => { + console.log(`Servidor rodando na porta ${process.env.PORT || 3003}`) +}) + +app.use("/ping", pingRouter) +app.use("/users", userRouter) +app.use("/posts", postRouter) \ No newline at end of file diff --git a/modulo5/testes-erros/src/models/Post.ts b/modulo5/testes-erros/src/models/Post.ts new file mode 100644 index 0000000..40fe5b1 --- /dev/null +++ b/modulo5/testes-erros/src/models/Post.ts @@ -0,0 +1,97 @@ +export interface IPostDB { + id: string, + content: string, + user_id: string +} + +export interface ILikeDB { + id: string, + post_id: string, + user_id: string +} + +export class Post { + constructor( + private id: string, + private content: string, + private userId: string, + private likes: number = 0 + ) {} + + public getId = () => { + return this.id + } + + public getContent = () => { + return this.content + } + + public getUserId = () => { + return this.userId + } + + public getLikes = () => { + return this.likes + } + + public setId = (newId: string) => { + this.id = newId + } + + public setContent = (newContent: string) => { + this.content = newContent + } + + public setUserId = (newUserId: string) => { + this.userId = newUserId + } + + public setLikes = (newLikes: number) => { + this.likes = newLikes + } +} + +export interface ICreatePostInputDTO { + token: string, + content: string +} + +export interface ICreatePostOutputDTO { + message: string, + post: Post +} + +export interface IGetPostsInputDTO { + token: string +} + +export interface IGetPostsOutputDTO { + posts: Post[] +} + +export interface IDeletePostInputDTO { + token: string, + postId: string +} + +export interface IDeletePostOutputDTO { + message: string +} + +export interface IAddLikeInputDTO { + token: string, + postId: string +} + +export interface IAddLikeOutputDTO { + message: string +} + +export interface IRemoveLikeInputDTO { + token: string, + postId: string +} + +export interface IRemoveLikeOutputDTO { + message: string +} \ No newline at end of file diff --git a/modulo5/testes-erros/src/models/User.ts b/modulo5/testes-erros/src/models/User.ts new file mode 100644 index 0000000..ed49c1c --- /dev/null +++ b/modulo5/testes-erros/src/models/User.ts @@ -0,0 +1,83 @@ +export enum USER_ROLES { + NORMAL = "NORMAL", + ADMIN = "ADMIN" +} + +export interface IUserDB { + id: string, + name: string, + email: string, + password: string, + role: USER_ROLES +} + +export class User { + constructor( + private id: string, + private name: string, + private email: string, + private password: string, + private role: USER_ROLES + ) {} + + public getId = () => { + return this.id + } + + public getName = () => { + return this.name + } + + public getEmail = () => { + return this.email + } + + public getPassword = () => { + return this.password + } + + public getRole = () => { + return this.role + } + + public setId = (newId: string) => { + this.id = newId + } + + public setName = (newName: string) => { + this.name = newName + } + + public setEmail = (newEmail: string) => { + this.email = newEmail + } + + public setPassword = (newPassword: string) => { + this.password = newPassword + } + + public setRole = (newRole: USER_ROLES) => { + this.role = newRole + } +} + +export interface ISignupInputDTO { + name: string, + email: string, + password: string +} + +export interface ISignupOutputDTO { + message: string, + token: string +} + +export interface ILoginInputDTO { + email: string, + password: string +} + +export interface ILoginOutputDTO { + message: string, + token: string +} \ No newline at end of file diff --git a/modulo5/testes-erros/src/router/pingRouter.ts b/modulo5/testes-erros/src/router/pingRouter.ts new file mode 100644 index 0000000..3b328b6 --- /dev/null +++ b/modulo5/testes-erros/src/router/pingRouter.ts @@ -0,0 +1,11 @@ +import { Router } from 'express' +import { PingBusiness } from '../business/PingBusiness' +import { PingController } from '../controller/PingController' + +export const pingRouter = Router() + +const pingController = new PingController( + new PingBusiness() +) + +pingRouter.get("/", pingController.ping) \ No newline at end of file diff --git a/modulo5/testes-erros/src/router/postRouter.ts b/modulo5/testes-erros/src/router/postRouter.ts new file mode 100644 index 0000000..5d98f36 --- /dev/null +++ b/modulo5/testes-erros/src/router/postRouter.ts @@ -0,0 +1,24 @@ +import { Router } from 'express' +import { PostBusiness } from '../business/PostBusiness' +import { PostController } from '../controller/PostController' +import { PostDatabase } from '../database/PostDatabase' +import { Authenticator } from '../services/Authenticator' +import { HashManager } from '../services/HashManager' +import { IdGenerator } from '../services/IdGenerator' + +export const postRouter = Router() + +const postController = new PostController( + new PostBusiness( + new PostDatabase(), + new IdGenerator(), + new Authenticator() + ) +) + +postRouter.post("/", postController.createPost) +postRouter.get("/", postController.getPosts) +postRouter.delete("/:id", postController.deletePost) + +postRouter.post("/like/:id", postController.addLike) +postRouter.delete("/like/:id", postController.removeLike) \ No newline at end of file diff --git a/modulo5/testes-erros/src/router/userRouter.ts b/modulo5/testes-erros/src/router/userRouter.ts new file mode 100644 index 0000000..4548e60 --- /dev/null +++ b/modulo5/testes-erros/src/router/userRouter.ts @@ -0,0 +1,21 @@ +import { Router } from 'express' +import { UserBusiness } from '../business/UserBusiness' +import { UserController } from '../controller/UserController' +import { UserDatabase } from '../database/UserDatabase' +import { Authenticator } from '../services/Authenticator' +import { HashManager } from '../services/HashManager' +import { IdGenerator } from '../services/IdGenerator' + +export const userRouter = Router() + +const userController = new UserController( + new UserBusiness( + new UserDatabase(), + new IdGenerator(), + new HashManager(), + new Authenticator() + ) +) + +userRouter.post("/signup", userController.signup) +userRouter.post("/login", userController.login) \ No newline at end of file diff --git a/modulo5/testes-erros/src/services/Authenticator.ts b/modulo5/testes-erros/src/services/Authenticator.ts new file mode 100644 index 0000000..de5112e --- /dev/null +++ b/modulo5/testes-erros/src/services/Authenticator.ts @@ -0,0 +1,37 @@ +import jwt from 'jsonwebtoken' +import dotenv from "dotenv" +import { USER_ROLES } from '../models/User' + +dotenv.config() + +export interface ITokenPayload { + id: string, + role: USER_ROLES +} + +export class Authenticator { + generateToken = (payload: ITokenPayload): string => { + const token = jwt.sign( + payload, + process.env.JWT_KEY as string, + { + expiresIn: process.env.JWT_EXPIRES_IN + } + ) + + return token + } + + getTokenPayload = (token: string): ITokenPayload | null => { + try { + const payload = jwt.verify( + token, + process.env.JWT_KEY as string + ) + + return payload as ITokenPayload + } catch (error) { + return null + } + } +} \ No newline at end of file diff --git a/modulo5/testes-erros/src/services/HashManager.ts b/modulo5/testes-erros/src/services/HashManager.ts new file mode 100644 index 0000000..bf54501 --- /dev/null +++ b/modulo5/testes-erros/src/services/HashManager.ts @@ -0,0 +1,15 @@ +import bcrypt from 'bcryptjs' + +export class HashManager { + public hash = async (plaintext: string): Promise => { + const rounds = Number(process.env.BCRYPT_SALT_ROUNDS) + const salt = await bcrypt.genSalt(rounds) + const hash = await bcrypt.hash(plaintext, salt) + + return hash + } + + public compare = async (plaintext: string, hash: string): Promise => { + return bcrypt.compare(plaintext, hash) + } +} \ No newline at end of file diff --git a/modulo5/testes-erros/src/services/IdGenerator.ts b/modulo5/testes-erros/src/services/IdGenerator.ts new file mode 100644 index 0000000..a6ba76e --- /dev/null +++ b/modulo5/testes-erros/src/services/IdGenerator.ts @@ -0,0 +1,7 @@ +import { v4 } from 'uuid' + +export class IdGenerator { + public generate = (): string => { + return v4() + } +} \ No newline at end of file diff --git a/modulo5/testes-erros/tests/PostBusiness.test.ts b/modulo5/testes-erros/tests/PostBusiness.test.ts new file mode 100644 index 0000000..4a0f6b1 --- /dev/null +++ b/modulo5/testes-erros/tests/PostBusiness.test.ts @@ -0,0 +1,39 @@ +import { PostBusiness } from "../src/business/PostBusiness" +import { ICreatePostInputDTO, IGetPostsInputDTO, Post } from "../src/models/Post" +import { AuthenticatorMock } from "./mocks/AuthenticatorMock" +import { IdGeneratorMock } from "./mocks/IdGeneratorMock" +import { PostDatabaseMock } from "./mocks/PostDatabaseMock" + +describe("Testando a PostBusiness", () => { + const postBusiness = new PostBusiness( + new PostDatabaseMock(), + new IdGeneratorMock(), + new AuthenticatorMock() + ) + + test("Deve retornar uma lista de posts", async () => { + const input: IGetPostsInputDTO = { + token: "token-mock-normal" + } + + const response = await postBusiness.getPosts(input) + expect(response.posts.length).toBe(3) + expect(response.posts[0]).toBeInstanceOf(Post) + }) + + test("Deve ser possível criar um novo post", async () => { + const input: ICreatePostInputDTO = { + token: "token-mock-normal", + content: "Teste do mock" + } + + const response = await postBusiness.createPost(input) + + expect(response.message).toBe("Post criado com sucesso") + expect(response.post).toBeInstanceOf(Post) + expect(response.post.getId()).toBe("id-mock") + expect(response.post.getContent()).toBe("Teste do mock") + expect(response.post.getLikes()).toBe(0) + expect(response.post.getUserId()).toBe("id-mock") + }) +}) \ No newline at end of file diff --git a/modulo5/testes-erros/tests/UserBusiness.test.ts b/modulo5/testes-erros/tests/UserBusiness.test.ts new file mode 100644 index 0000000..3967d23 --- /dev/null +++ b/modulo5/testes-erros/tests/UserBusiness.test.ts @@ -0,0 +1,99 @@ +import { UserBusiness } from "../src/business/UserBusiness" +import { BaseError } from "../src/errors/BaseError" +import { ParamsError } from "../src/errors/ParamsError" +import { ILoginInputDTO, ISignupInputDTO } from "../src/models/User" +import { AuthenticatorMock } from "./mocks/AuthenticatorMock" +import { HashManagerMock } from "./mocks/HashManagerMock" +import { IdGeneratorMock } from "./mocks/IdGeneratorMock" +import { UserDatabaseMock } from "./mocks/UserDatabaseMock" + +describe("Testando a UserBusiness", () => { + const userBusiness = new UserBusiness( + new UserDatabaseMock(), + new IdGeneratorMock(), + new HashManagerMock(), + new AuthenticatorMock() + ) + + test("Um token é retornado quando o cadastro é bem-sucedido", async () => { + const input: ISignupInputDTO = { + email: "fulano@gmail.com", + name: "Fulano", + password: "fulano123" + } + + const response = await userBusiness.signup(input) + expect(response.message).toBe("Cadastro realizado com sucesso") + expect(response.token).toBe("token-mock-normal") + }) + + test("Um token é retornado quando o login é bem-sucedido", async () => { + const input: ILoginInputDTO = { + email: "astrodev@gmail.com", + password: "bananinha" + } + + const response = await userBusiness.login(input) + expect(response.message).toBe("Login realizado com sucesso") + expect(response.token).toBe("token-mock-admin") + }) + + test("Erro quando 'password' possuir menos de 6 caracteres", async () => { + expect.assertions(2) + + try { + const input: ISignupInputDTO = { + email: "fulano@gmail.com", + name: "Fulano", + password: "123" + } + + await userBusiness.signup(input) + + } catch (error) { + if (error instanceof BaseError) { + expect(error.statusCode).toBe(400) + expect(error.message).toBe("Parâmetro 'password' inválido: mínimo de 6 caracteres") + } + } + }) + + test("Erro quando 'password' for incorreto", async () => { + expect.assertions(2) + + try { + const input: ILoginInputDTO = { + email: "astrodev@gmail.com", + password: "bananinha123" + } + + await userBusiness.login(input) + + } catch (error) { + if (error instanceof BaseError) { + expect(error.statusCode).toBe(401) + expect(error.message).toBe("Password incorreto") + } + } + }) + + test("Erro no cadastro quando 'name' for diferente do tipo string", async () => { + expect.assertions(2) + + try { + const input = { + name: undefined, + email: "fulano@gmail.com", + password: "fulano123" + } as any + + await userBusiness.signup(input) + + } catch (error) { + if (error instanceof BaseError) { + expect(error.statusCode).toBe(400) + expect(error.message).toBe("Parâmetro 'name' inválido") + } + } + }) +}) diff --git a/modulo5/testes-erros/tests/UserBusinessTest/login.test.ts b/modulo5/testes-erros/tests/UserBusinessTest/login.test.ts new file mode 100644 index 0000000..388461b --- /dev/null +++ b/modulo5/testes-erros/tests/UserBusinessTest/login.test.ts @@ -0,0 +1,66 @@ +import { UserBusiness } from "../../src/business/UserBusiness" +import { BaseError } from "../../src/errors/BaseError" +import { ILoginInputDTO, ISignupInputDTO } from "../../src/models/User" +import { AuthenticatorMock } from ".././mocks/AuthenticatorMock" +import { HashManagerMock } from ".././mocks/HashManagerMock" +import { IdGeneratorMock } from ".././mocks/IdGeneratorMock" +import { UserDatabaseMock } from ".././mocks/UserDatabaseMock" + +describe("Testando o método login da UserBusiness", () => { + const userBusiness = new UserBusiness( + new UserDatabaseMock(), + new IdGeneratorMock(), + new HashManagerMock(), + new AuthenticatorMock() + ) + + test("Um token é retornado quando o login é bem-sucedido", async () => { + const input: ILoginInputDTO = { + email: "astrodev@gmail.com", + password: "bananinha" + } + + const response = await userBusiness.login(input) + expect(response.message).toBe("Login realizado com sucesso") + expect(response.token).toBe("token-mock-admin") + }) + + test("Erro quando 'password' possuir menos de 6 caracteres", async () => { + expect.assertions(2) + + try { + const input: ISignupInputDTO = { + email: "fulano@gmail.com", + name: "Fulano", + password: "123" + } + + await userBusiness.signup(input) + + } catch (error) { + if (error instanceof BaseError) { + expect(error.statusCode).toBe(400) + expect(error.message).toBe("Parâmetro 'password' inválido: mínimo de 6 caracteres") + } + } + }) + + test("Erro quando 'password' for incorreto", async () => { + expect.assertions(2) + + try { + const input: ILoginInputDTO = { + email: "astrodev@gmail.com", + password: "bananinha123" + } + + await userBusiness.login(input) + + } catch (error) { + if (error instanceof BaseError) { + expect(error.statusCode).toBe(401) + expect(error.message).toBe("Password incorreto") + } + } + }) +}) diff --git a/modulo5/testes-erros/tests/UserBusinessTest/signup.test.ts b/modulo5/testes-erros/tests/UserBusinessTest/signup.test.ts new file mode 100644 index 0000000..88eb265 --- /dev/null +++ b/modulo5/testes-erros/tests/UserBusinessTest/signup.test.ts @@ -0,0 +1,27 @@ +import { UserBusiness } from "../../src/business/UserBusiness" +import { ISignupInputDTO } from "../../src/models/User" +import { AuthenticatorMock } from ".././mocks/AuthenticatorMock" +import { HashManagerMock } from ".././mocks/HashManagerMock" +import { IdGeneratorMock } from ".././mocks/IdGeneratorMock" +import { UserDatabaseMock } from ".././mocks/UserDatabaseMock" + +describe("Testando o método signup da UserBusiness", () => { + const userBusiness = new UserBusiness( + new UserDatabaseMock(), + new IdGeneratorMock(), + new HashManagerMock(), + new AuthenticatorMock() + ) + + test("Um token é retornado quando o cadastro é bem-sucedido", async () => { + const input: ISignupInputDTO = { + email: "fulano@gmail.com", + name: "Fulano", + password: "fulano123" + } + + const response = await userBusiness.signup(input) + expect(response.message).toBe("Cadastro realizado com sucesso") + expect(response.token).toBe("token-mock-normal") + }) +}) diff --git a/modulo5/testes-erros/tests/mocks/AuthenticatorMock.ts b/modulo5/testes-erros/tests/mocks/AuthenticatorMock.ts new file mode 100644 index 0000000..28355e8 --- /dev/null +++ b/modulo5/testes-erros/tests/mocks/AuthenticatorMock.ts @@ -0,0 +1,37 @@ +import { USER_ROLES } from "../../src/models/User" +import { ITokenPayload } from "../../src/services/Authenticator" + +export class AuthenticatorMock { + public generateToken = (payload: ITokenPayload): string => { + switch (payload.role) { + case USER_ROLES.ADMIN: + return "token-mock-admin" + default: + return "token-mock-normal" + } + } + + public getTokenPayload = (token: string): ITokenPayload | null => { + switch (token) { + case "token-mock-admin": + const adminPayload: ITokenPayload = { + id: "id-mock", + role: USER_ROLES.ADMIN + } + + return adminPayload + + case "token-mock-normal": + const normalPayload: ITokenPayload = { + id: "id-mock", + role: USER_ROLES.NORMAL + } + + return normalPayload + + default: + return null + } + } + +} diff --git a/modulo5/testes-erros/tests/mocks/HashManagerMock.ts b/modulo5/testes-erros/tests/mocks/HashManagerMock.ts new file mode 100644 index 0000000..0d7242c --- /dev/null +++ b/modulo5/testes-erros/tests/mocks/HashManagerMock.ts @@ -0,0 +1,17 @@ +export class HashManagerMock { + public hash = async (plaintext: string): Promise => { + if (plaintext == "bananinha") { + return "hash-bananinha" + } + + return "hash-mock" + } + + public compare = async (plaintext: string, hash: string): Promise => { + if (plaintext == "bananinha" && hash == "hash-bananinha") { + return true + } + + return false + } +} diff --git a/modulo5/testes-erros/tests/mocks/IdGeneratorMock.ts b/modulo5/testes-erros/tests/mocks/IdGeneratorMock.ts new file mode 100644 index 0000000..da58ef4 --- /dev/null +++ b/modulo5/testes-erros/tests/mocks/IdGeneratorMock.ts @@ -0,0 +1,5 @@ +export class IdGeneratorMock { + public generate = (): string => { + return "id-mock" + } +} diff --git a/modulo5/testes-erros/tests/mocks/PostDatabaseMock.ts b/modulo5/testes-erros/tests/mocks/PostDatabaseMock.ts new file mode 100644 index 0000000..a0ecc8c --- /dev/null +++ b/modulo5/testes-erros/tests/mocks/PostDatabaseMock.ts @@ -0,0 +1,129 @@ +import { BaseDatabase } from "../../src/database/BaseDatabase" +import { ILikeDB, IPostDB, Post } from "../../src/models/Post" + +export class PostDatabaseMock extends BaseDatabase { + public static TABLE_POSTS = "Labook_Posts" + public static TABLE_LIKES = "Labook_Likes" + + public toPostDBModel = (post: Post): IPostDB => { + const postDB: IPostDB = { + id: post.getId(), + content: post.getContent(), + user_id: post.getUserId() + } + + return postDB + } + + public createPost = async (post: Post): Promise => { + // const postDB = this.toPostDBModel(post) + + // await BaseDatabase + // .connection(PostDatabase.TABLE_POSTS) + // .insert(postDB) + } + + public getPosts = async (): Promise => { + // const postsDB: IPostDB[] = await BaseDatabase + // .connection(PostDatabase.TABLE_POSTS) + // .select() + + // return postsDB + + const postsDB: IPostDB[] = [ + { + id: "201", + content: "Olá, sou novo por aqui!", + user_id: "101" + }, + { + id: "202", + content: "Bom dia, família!", + user_id: "102" + }, + { + id: "203", + content: "Receba!", + user_id: "103" + } + ] + + return postsDB + } + + public getLikes = async (postId: string): Promise => { + // const result: any = await BaseDatabase + // .connection(PostDatabase.TABLE_LIKES) + // .select() + // .count("id AS likes") + // .where({ post_id: postId }) + + // return result[0].likes as number + + if (postId == "201") return 1 + + return 0 + } + + public findPostById = async (postId: string): Promise => { + // const postsDB: IPostDB[] = await BaseDatabase + // .connection(PostDatabase.TABLE_POSTS) + // .select() + // .where({ id: postId }) + + // return postsDB[0] + + switch(postId) { + case "201": + return { + id: "201", + content: "Olá, sou novo por aqui!", + user_id: "101" + } + + default: + return undefined + } + } + + public deletePost = async (postId: string): Promise => { + // await BaseDatabase + // .connection(PostDatabase.TABLE_POSTS) + // .delete() + // .where({ id: postId }) + } + + public findLike = async (postId: string, userId: string): Promise => { + // const likesDB: ILikeDB[] = await BaseDatabase + // .connection(PostDatabase.TABLE_LIKES) + // .select() + // .where({ post_id: postId }) + // .andWhere({ user_id: userId }) + + // return likesDB[0] + + if (postId == "201" && userId == "id-mock") { + return { + id: "301", + post_id: "201", + user_id: "id-mock" + } + } + + return undefined + } + + public addLike = async (likeDB: ILikeDB): Promise => { + // await BaseDatabase + // .connection(PostDatabase.TABLE_LIKES) + // .insert(likeDB) + } + + public removeLike = async (postId: string, userId: string): Promise => { + // await BaseDatabase + // .connection(PostDatabase.TABLE_LIKES) + // .delete() + // .where({ post_id: postId }) + // .andWhere({ user_id: userId }) + } +} \ No newline at end of file diff --git a/modulo5/testes-erros/tests/mocks/UserDatabaseMock.ts b/modulo5/testes-erros/tests/mocks/UserDatabaseMock.ts new file mode 100644 index 0000000..8c4e356 --- /dev/null +++ b/modulo5/testes-erros/tests/mocks/UserDatabaseMock.ts @@ -0,0 +1,49 @@ +import { IUserDB, User, USER_ROLES } from "../../src/models/User" +import { BaseDatabase } from "../../src/database/BaseDatabase" + +export class UserDatabaseMock extends BaseDatabase { + public static TABLE_USERS = "Labook_Users" + + public toUserDBModel = (user: User) => { + const userDB: IUserDB = { + id: user.getId(), + name: user.getName(), + email: user.getEmail(), + password: user.getPassword(), + role: user.getRole() + } + + return userDB + } + + public findByEmail = async (email: string): Promise => { + switch (email) { + case "usermock@gmail.com": + const normalUser: IUserDB = { + id: "id-mock", + name: "User Mock", + email: "usermock@gmail.com", + password: "hash-mock", + role: USER_ROLES.NORMAL + } + + return normalUser + + case "astrodev@gmail.com": + const adminUser: IUserDB = { + id: "id-mock", + name: "Astrodev", + email: "astrodev@gmail.com", + password: "hash-bananinha", + role: USER_ROLES.ADMIN + } + + return adminUser + + default: + return undefined + } + } + + public createUser = async (user: User): Promise => {} +} \ No newline at end of file diff --git a/modulo5/testes-erros/tsconfig.json b/modulo5/testes-erros/tsconfig.json new file mode 100644 index 0000000..6066d12 --- /dev/null +++ b/modulo5/testes-erros/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "es2021", + "module": "commonjs", + "sourceMap": true, + "outDir": "./build", + "rootDir": "./", + "removeComments": true, + "noImplicitAny": true, + "esModuleInterop": true, + "strict": true + }, + "exclude": ["./tests/*"] +} \ No newline at end of file