From bdf9f2c5519bb1e52562060a6a7df813036a4666 Mon Sep 17 00:00:00 2001 From: Felipe Batista Date: Sat, 14 Feb 2026 22:16:07 +0000 Subject: [PATCH 1/3] build: install `@faker-js/faker` --- package-lock.json | 29 ++++++++++++++++++----------- package.json | 1 + 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index c28936e..673e5c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "devDependencies": { "@commitlint/cli": "19.4.0", "@commitlint/config-conventional": "19.2.2", + "@faker-js/faker": "9.7.0", "commitizen": "4.3.0", "concurrently": "8.2.2", "cz-conventional-changelog": "3.3.0", @@ -68,7 +69,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1227,6 +1227,23 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@faker-js/faker": { + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.7.0.tgz", + "integrity": "sha512-aozo5vqjCmDoXLNUJarFZx2IN/GgGaogY4TMJ6so/WLZOWpSV7fvj2dmrV6sEAnUm1O7aCrhTibjpzeDFgNqbg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.0.0", + "npm": ">=9.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -2243,7 +2260,6 @@ "integrity": "sha512-czWPzKIAXucn9PtsttxmumiQ9N0ok9FrBwgRWrwmVLlp86BrMExzvXRLFYRJ+Ex3g6yqj+KuaxfX1JTgV2lpfg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -2695,7 +2711,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3272,7 +3287,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -4582,7 +4596,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -4911,7 +4924,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -7033,7 +7045,6 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -8691,7 +8702,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.12.0.tgz", "integrity": "sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.6.4", "pg-pool": "^3.6.2", @@ -9035,7 +9045,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -9048,7 +9057,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -10086,7 +10094,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, diff --git a/package.json b/package.json index 7b0cbbe..70d7f59 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "devDependencies": { "@commitlint/cli": "19.4.0", "@commitlint/config-conventional": "19.2.2", + "@faker-js/faker": "9.7.0", "commitizen": "4.3.0", "concurrently": "8.2.2", "cz-conventional-changelog": "3.3.0", From 20ff101d42be892be85cfa5197f97e5b5d0160a3 Mon Sep 17 00:00:00 2001 From: Felipe Batista Date: Sat, 14 Feb 2026 22:17:18 +0000 Subject: [PATCH 2/3] fix: throw a `ValidationError` when receive a empty password --- models/password.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/models/password.js b/models/password.js index 495ccd4..21e91e1 100644 --- a/models/password.js +++ b/models/password.js @@ -1,6 +1,13 @@ import bcryptjs from "bcryptjs"; +import { ValidationError } from "infra/errors.js"; async function hash(password) { + if (!password) { + throw new ValidationError({ + message: "A senha de usuário é obritória.", + action: "Informe a senha de usuário para realizar esta operação.", + }); + } const rounds = getNumberOfRounds(); const pepper = getPepper(); return await bcryptjs.hash(password + pepper, rounds); From 0c2de787ad0bd4557d18c3f2965dfc62556beaac Mon Sep 17 00:00:00 2001 From: Felipe Batista Date: Sat, 14 Feb 2026 22:18:07 +0000 Subject: [PATCH 3/3] feat: create and use `orchestrator.createUser()` --- .../api/v1/users/[username]/get.test.js | 66 +++------- .../api/v1/users/[username]/patch.test.js | 124 +++--------------- tests/orchestrator.js | 16 ++- 3 files changed, 53 insertions(+), 153 deletions(-) diff --git a/tests/integration/api/v1/users/[username]/get.test.js b/tests/integration/api/v1/users/[username]/get.test.js index f369a8e..acc0fb4 100644 --- a/tests/integration/api/v1/users/[username]/get.test.js +++ b/tests/integration/api/v1/users/[username]/get.test.js @@ -10,75 +10,49 @@ beforeAll(async () => { describe("GET /api/v1/users/[username]", () => { describe("Anonymous user", () => { test("With exact case match", async () => { - const response1 = await fetch("http://localhost:3000/api/v1/users", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - username: "CaseMatch", - email: "case.match@felipebatista.dev", - password: "senha123", - }), + await orchestrator.createUser({ + username: "CaseMatch", }); - expect(response1.status).toBe(201); - const response2 = await fetch( + const response = await fetch( "http://localhost:3000/api/v1/users/CaseMatch", ); - expect(response2.status).toBe(200); + expect(response.status).toBe(200); - const response2Body = await response2.json(); + const responseBody = await response.json(); - expect(response2Body).toEqual({ - id: response2Body.id, + expect(responseBody).toEqual({ + ...responseBody, username: "CaseMatch", - email: "case.match@felipebatista.dev", - password: response2Body.password, - created_at: response2Body.created_at, - updated_at: response2Body.updated_at, }); - expect(uuidVersion(response2Body.id)).toEqual(4); - expect(Date.parse(response2Body.created_at)).not.toBeNaN(); - expect(Date.parse(response2Body.updated_at)).not.toBeNaN(); + expect(uuidVersion(responseBody.id)).toEqual(4); + expect(Date.parse(responseBody.created_at)).not.toBeNaN(); + expect(Date.parse(responseBody.updated_at)).not.toBeNaN(); }); test("With mismatch case", async () => { - const response1 = await fetch("http://localhost:3000/api/v1/users", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - username: "MismatchCase", - email: "mismatch.case@felipebatista.dev", - password: "senha123", - }), + await orchestrator.createUser({ + username: "MismatchCase", }); - expect(response1.status).toBe(201); - const response2 = await fetch( + const response = await fetch( "http://localhost:3000/api/v1/users/mismatchcase", ); - expect(response2.status).toBe(200); + expect(response.status).toBe(200); - const response2Body = await response2.json(); + const responseBody = await response.json(); - expect(response2Body).toEqual({ - id: response2Body.id, + expect(responseBody).toEqual({ + ...responseBody, username: "MismatchCase", - email: "mismatch.case@felipebatista.dev", - password: response2Body.password, - created_at: response2Body.created_at, - updated_at: response2Body.updated_at, }); - expect(uuidVersion(response2Body.id)).toEqual(4); - expect(Date.parse(response2Body.created_at)).not.toBeNaN(); - expect(Date.parse(response2Body.updated_at)).not.toBeNaN(); + expect(uuidVersion(responseBody.id)).toEqual(4); + expect(Date.parse(responseBody.created_at)).not.toBeNaN(); + expect(Date.parse(responseBody.updated_at)).not.toBeNaN(); }); test("With nonexistent username", async () => { diff --git a/tests/integration/api/v1/users/[username]/patch.test.js b/tests/integration/api/v1/users/[username]/patch.test.js index a6dd434..f4b4515 100644 --- a/tests/integration/api/v1/users/[username]/patch.test.js +++ b/tests/integration/api/v1/users/[username]/patch.test.js @@ -31,31 +31,12 @@ describe("PATCH /api/v1/users/[username]", () => { }); test("With duplicated 'username'", async () => { - const user1 = await fetch("http://localhost:3000/api/v1/users", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - username: "user1", - email: "user1@felipebatista.dev", - password: "senha123", - }), + await orchestrator.createUser({ + username: "user1", }); - expect(user1.status).toBe(201); - - const user2 = await fetch("http://localhost:3000/api/v1/users", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - username: "user2", - email: "user2@felipebatista.dev", - password: "senha123", - }), + await orchestrator.createUser({ + username: "user2", }); - expect(user2.status).toBe(201); const response = await fetch("http://localhost:3000/api/v1/users/user2", { method: "PATCH", @@ -78,34 +59,16 @@ describe("PATCH /api/v1/users/[username]", () => { }); test("With duplicated 'email'", async () => { - const user1 = await fetch("http://localhost:3000/api/v1/users", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - username: "email1", - email: "email1@felipebatista.dev", - password: "senha123", - }), + await orchestrator.createUser({ + email: "email1@felipebatista.dev", }); - expect(user1.status).toBe(201); - const user2 = await fetch("http://localhost:3000/api/v1/users", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - username: "email2", - email: "email2@felipebatista.dev", - password: "senha123", - }), + const newUser2 = await orchestrator.createUser({ + email: "email2@felipebatista.dev", }); - expect(user2.status).toBe(201); const response = await fetch( - "http://localhost:3000/api/v1/users/email2", + `http://localhost:3000/api/v1/users/${newUser2.username}`, { method: "PATCH", headers: { @@ -128,21 +91,9 @@ describe("PATCH /api/v1/users/[username]", () => { }); test("With unique 'username'", async () => { - const user1 = await fetch("http://localhost:3000/api/v1/users", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - username: "uniqueUser1", - email: "uniqueUser1@felipebatista.dev", - password: "senha123", - }), - }); - expect(user1.status).toBe(201); - + const newUser = await orchestrator.createUser(); const response = await fetch( - "http://localhost:3000/api/v1/users/uniqueUser1", + `http://localhost:3000/api/v1/users/${newUser.username}`, { method: "PATCH", headers: { @@ -157,33 +108,18 @@ describe("PATCH /api/v1/users/[username]", () => { expect(response.status).toBe(200); const responseBody = await response.json(); expect(responseBody).toEqual({ - id: responseBody.id, + ...responseBody, username: "uniqueUser2", - email: "uniqueUser1@felipebatista.dev", - password: responseBody.password, - created_at: responseBody.created_at, - updated_at: responseBody.updated_at, }); expect(responseBody.updated_at > responseBody.created_at).toBe(true); }); test("With unique 'email'", async () => { - const user1 = await fetch("http://localhost:3000/api/v1/users", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - username: "uniqueEmail1", - email: "uniqueEmail1@felipebatista.dev", - password: "senha123", - }), - }); - expect(user1.status).toBe(201); + const newUser = await orchestrator.createUser(); const response = await fetch( - "http://localhost:3000/api/v1/users/uniqueEmail1", + `http://localhost:3000/api/v1/users/${newUser.username}`, { method: "PATCH", headers: { @@ -198,33 +134,20 @@ describe("PATCH /api/v1/users/[username]", () => { expect(response.status).toBe(200); const responseBody = await response.json(); expect(responseBody).toEqual({ - id: responseBody.id, - username: "uniqueEmail1", + ...responseBody, email: "uniqueEmail2@felipebatista.dev", - password: responseBody.password, - created_at: responseBody.created_at, - updated_at: responseBody.updated_at, }); expect(responseBody.updated_at > responseBody.created_at).toBe(true); }); test("With new 'password'", async () => { - const user1 = await fetch("http://localhost:3000/api/v1/users", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - username: "newPassword1", - email: "newPassword1@felipebatista.dev", - password: "newPassword1", - }), + const newUser = await orchestrator.createUser({ + password: "newPassword1", }); - expect(user1.status).toBe(201); const response = await fetch( - "http://localhost:3000/api/v1/users/newPassword1", + `http://localhost:3000/api/v1/users/${newUser.username}`, { method: "PATCH", headers: { @@ -238,18 +161,9 @@ describe("PATCH /api/v1/users/[username]", () => { expect(response.status).toBe(200); const responseBody = await response.json(); - expect(responseBody).toEqual({ - id: responseBody.id, - username: "newPassword1", - email: "newPassword1@felipebatista.dev", - password: responseBody.password, - created_at: responseBody.created_at, - updated_at: responseBody.updated_at, - }); - expect(responseBody.updated_at > responseBody.created_at).toBe(true); - const userInDatabase = await user.findOneByUsername("newPassword1"); + const userInDatabase = await user.findOneByUsername(newUser.username); const isCorrectPassword = await password.compare( "newPassword2", userInDatabase.password, diff --git a/tests/orchestrator.js b/tests/orchestrator.js index 5e30664..6f2996c 100644 --- a/tests/orchestrator.js +++ b/tests/orchestrator.js @@ -1,6 +1,8 @@ import retry from "async-retry"; -import database from "infra/database"; -import migrator from "models/migrator"; +import { faker } from "@faker-js/faker"; +import database from "infra/database.js"; +import migrator from "models/migrator.js"; +import user from "models/user.js"; async function waitForAllServices() { await waitForWebServer(); @@ -28,9 +30,19 @@ async function clearDatabase() { async function runPendingMigrations() { await migrator.runPendingMigrations(); } + +async function createUser(userObject) { + return await user.create({ + username: + userObject?.username || faker.internet.username().replace(/[-._]/g, ""), + email: userObject?.email || faker.internet.email(), + password: userObject?.password || "validpassword", + }); +} const orchestrator = { waitForAllServices, clearDatabase, + createUser, runPendingMigrations, };