From 054d82f132d75eae7d442cff4119ba0c22c1a7b3 Mon Sep 17 00:00:00 2001 From: Lucas Martins Date: Tue, 10 Feb 2026 19:33:27 -0300 Subject: [PATCH 1/6] build: install `bcryptjs` --- package-lock.json | 56 +++++++++++++++++++++++++++++------------------ package.json | 1 + 2 files changed, 36 insertions(+), 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index bae63b4..cf7f163 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "async-retry": "1.3.3", + "bcryptjs": "3.0.2", "dotenv": "17.2.0", "dotenv-expand": "12.0.2", "eslint": "9.31.0", @@ -1099,9 +1100,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -1829,9 +1830,10 @@ } }, "node_modules/@jest/core/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -2082,9 +2084,10 @@ } }, "node_modules/@jest/reporters/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -3753,6 +3756,15 @@ ], "license": "MIT" }, + "node_modules/bcryptjs": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", + "integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -4357,9 +4369,9 @@ "license": "Python-2.0" }, "node_modules/cosmiconfig/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -7175,9 +7187,10 @@ } }, "node_modules/jest-cli/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -7560,9 +7573,10 @@ } }, "node_modules/jest-runtime/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -7774,9 +7788,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index b6fe3f9..9430ba9 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "description": "", "dependencies": { "async-retry": "1.3.3", + "bcryptjs": "3.0.2", "dotenv": "17.2.0", "dotenv-expand": "12.0.2", "eslint": "9.31.0", From 4e56359fc73bcfa8837572cdc6689673c1931f64 Mon Sep 17 00:00:00 2001 From: Lucas Martins Date: Tue, 10 Feb 2026 19:34:31 -0300 Subject: [PATCH 2/6] feat: hash `password` in `user.create` --- models/password.js | 23 +++++++++++++++++++ models/user.js | 8 +++++++ .../api/v1/users/[username]/get.test.js | 4 ++-- tests/integration/api/v1/users/post.test.js | 18 ++++++++++++++- 4 files changed, 50 insertions(+), 3 deletions(-) diff --git a/models/password.js b/models/password.js index e69de29..a38b497 100644 --- a/models/password.js +++ b/models/password.js @@ -0,0 +1,23 @@ +import bcryptjs from "bcryptjs"; + +const pepper = process.env.PASSWORD_PEPPER || ""; + +async function hash(password) { + const rounds = getNumberofRounds(); + return await bcryptjs.hash(password + pepper, rounds); +} + +function getNumberofRounds() { + return process.env.NODE_ENV === "production" ? 14 : 1; +} + +async function compare(providedPassword, storedPassword) { + return await bcryptjs.compare(providedPassword, storedPassword); +} + +const password = { + hash, + compare, +}; + +export default password; diff --git a/models/user.js b/models/user.js index 369b7c4..7d4b2d7 100644 --- a/models/user.js +++ b/models/user.js @@ -1,4 +1,5 @@ import database from "infra/database"; +import password from "models/password.js"; import { ValidationError, NotFoundError } from "infra/errors.js"; async function findOneByUsername(username) { @@ -37,6 +38,8 @@ async function create(userInputValues) { await validateUniqueUsername(userInputValues.username); + await hashPasswordInObject(userInputValues); + const newUser = await runInsertQuery(userInputValues); return newUser; @@ -82,6 +85,11 @@ async function create(userInputValues) { } } + async function hashPasswordInObject(userInputValues) { + const hashedPassword = await password.hash(userInputValues.password); + userInputValues.password = hashedPassword; + } + async function runInsertQuery(userInputValues) { const results = await database.query({ text: ` diff --git a/tests/integration/api/v1/users/[username]/get.test.js b/tests/integration/api/v1/users/[username]/get.test.js index 07c6b1d..07bd692 100644 --- a/tests/integration/api/v1/users/[username]/get.test.js +++ b/tests/integration/api/v1/users/[username]/get.test.js @@ -36,7 +36,7 @@ describe("GET /api/v1/users/[username]", () => { id: response2Body.id, username: "CamelCaseUser", email: "snake_case@curso.dev", - password: "senha123", + password: response2Body.password, created_at: response2Body.created_at, updated_at: response2Body.updated_at, }); @@ -73,7 +73,7 @@ describe("GET /api/v1/users/[username]", () => { id: response2Body.id, username: "DifferentCaseUser", email: "different_case@curso.dev", - password: "senha123", + password: response2Body.password, created_at: response2Body.created_at, updated_at: response2Body.updated_at, }); diff --git a/tests/integration/api/v1/users/post.test.js b/tests/integration/api/v1/users/post.test.js index 799c476..7919479 100644 --- a/tests/integration/api/v1/users/post.test.js +++ b/tests/integration/api/v1/users/post.test.js @@ -1,5 +1,7 @@ import { version as uuidVersion } from "uuid"; import orchestrator from "tests/orchestrator.js"; +import user from "models/user.js"; +import password from "models/password.js"; beforeAll(async () => { await orchestrator.waitForAllServices(); @@ -33,7 +35,7 @@ describe("POST /api/v1/users", () => { id: responseBody.id, username: "bitsdonerd", email: "bitsdonerd@gmail.com", - password: "senha123", + password: responseBody.password, created_at: responseBody.created_at, updated_at: responseBody.updated_at, // tunelamento da resposta para ser ela mesma }); @@ -41,6 +43,20 @@ describe("POST /api/v1/users", () => { expect(uuidVersion(responseBody.id)).toBe(4); // Verifica se o ID é um UUID v4 expect(Date.parse(responseBody.created_at)).not.toBeNaN(); // Verifica se created_at é uma data válida expect(Date.parse(responseBody.updated_at)).not.toBeNaN(); + + const userInDatabase = await user.findOneByUsername("bitsdonerd"); + + const correctPassowordMatch = await password.compare( + "senha123", + userInDatabase.password, + ); // Verifica se a senha foi corretamente hasheada e armazenada no banco de dados + expect(correctPassowordMatch).toBe(true); + + const incorrectPassowordMatch = await password.compare( + "senhaErrada", + userInDatabase.password, + ); + expect(incorrectPassowordMatch).toBe(false); }); test("With duplicated email", async () => { From d2433d37e48f540e22d1a024d4ec7585d8463ab0 Mon Sep 17 00:00:00 2001 From: Lucas Martins Date: Sat, 14 Feb 2026 13:29:57 -0300 Subject: [PATCH 3/6] feat: add suport for `PATCH` on `/api/v1/users/[username]` --- models/user.js | 140 ++++++--- pages/api/v1/users/[username]/index.js | 18 +- .../api/v1/users/[username]/patch.test.js | 286 ++++++++++++++++++ 3 files changed, 394 insertions(+), 50 deletions(-) create mode 100644 tests/integration/api/v1/users/[username]/patch.test.js diff --git a/models/user.js b/models/user.js index 7d4b2d7..e54e9ea 100644 --- a/models/user.js +++ b/models/user.js @@ -34,39 +34,85 @@ async function findOneByUsername(username) { } async function create(userInputValues) { - await validateUniqueEmail(userInputValues.email); - await validateUniqueUsername(userInputValues.username); + await validateUniqueEmail(userInputValues.email); + await hashPasswordInObject(userInputValues); const newUser = await runInsertQuery(userInputValues); return newUser; - async function validateUniqueEmail(email) { + async function runInsertQuery(userInputValues) { const results = await database.query({ text: ` - SELECT - email - FROM - users - WHERE - LOWER(email) = LOWER($1) + INSERT INTO + users (username, email, password) + VALUES + ($1, $2, $3) + RETURNING * ;`, - values: [email], + values: [ + userInputValues.username, + userInputValues.email, + userInputValues.password, + ], }); - if (results.rowCount > 0) { - throw new ValidationError({ - message: "Email already in use", - action: "Please change the email and try again", - }); - } + return results.rows[0]; + } +} + +async function update(username, userInputValues) { + const currentUser = await findOneByUsername(username); + + if ("username" in userInputValues) { + await validateUniqueUsername(userInputValues.username); + } - async function validateUniqueUsername(username) { + if ("email" in userInputValues) { + await validateUniqueEmail(userInputValues.email); + } + + if ("password" in userInputValues) { + await hashPasswordInObject(userInputValues); + } + + const userWithNewValues = { ...currentUser, ...userInputValues }; + const updatedUser = await runUpdateQuery(userWithNewValues); + return updatedUser; + + async function runUpdateQuery(userWithNewValues) { const results = await database.query({ text: ` + UPDATE + users + SET + username = $2, + email = $3, + password = $4, + updated_at = timezone('utc'::text, now()) + WHERE + id = $1 + RETURNING * + ;`, + values: [ + userWithNewValues.id, + userWithNewValues.username, + userWithNewValues.email, + userWithNewValues.password, + ] + }) + + return results.rows[0]; + + } +} + +async function validateUniqueUsername(username) { + const results = await database.query({ + text: ` SELECT username FROM @@ -74,45 +120,47 @@ async function create(userInputValues) { WHERE LOWER(username) = LOWER($1) ;`, - values: [username], - }); - - if (results.rowCount > 0) { - throw new ValidationError({ - message: "Username already in use", - action: "Please change the username and try again", - }); - } - } + values: [username], + }); - async function hashPasswordInObject(userInputValues) { - const hashedPassword = await password.hash(userInputValues.password); - userInputValues.password = hashedPassword; + if (results.rowCount > 0) { + throw new ValidationError({ + message: "Username already in use", + action: "Please change the username and try again", + }); } +} - async function runInsertQuery(userInputValues) { - const results = await database.query({ - text: ` - INSERT INTO - users (username, email, password) - VALUES - ($1, $2, $3) - RETURNING * +async function validateUniqueEmail(email) { + const results = await database.query({ + text: ` + SELECT + email + FROM + users + WHERE + LOWER(email) = LOWER($1) ;`, - values: [ - userInputValues.username, - userInputValues.email, - userInputValues.password, - ], - }); + values: [email], + }); - return results.rows[0]; + if (results.rowCount > 0) { + throw new ValidationError({ + message: "Email already in use", + action: "Please change the email and try again", + }); } } +async function hashPasswordInObject(userInputValues) { + const hashedPassword = await password.hash(userInputValues.password); + userInputValues.password = hashedPassword; +} + const user = { create, findOneByUsername, -}; + update, +} export default user; diff --git a/pages/api/v1/users/[username]/index.js b/pages/api/v1/users/[username]/index.js index 342f72f..77bd06b 100644 --- a/pages/api/v1/users/[username]/index.js +++ b/pages/api/v1/users/[username]/index.js @@ -4,14 +4,24 @@ import user from "models/user.js"; const router = createRouter(); -router.get(getMigrationsHandler); +router.get(getHandler); +router.patch(patchHandler); -export default router.handler(controller.errorHandlers); - -async function getMigrationsHandler(request, response) { +async function getHandler(request, response) { // api/v1/users/[username] const username = request.query.username; const userFound = await user.findOneByUsername(username); return response.status(200).json(userFound); } + +async function patchHandler(request, response) { + // api/v1/users/[username] + const username = request.query.username; + const userInputValues = request.body; + + const updatedUser = await user.update(username, userInputValues); + return response.status(200).json(updatedUser); +} + +export default router.handler(controller.errorHandlers); \ No newline at end of file diff --git a/tests/integration/api/v1/users/[username]/patch.test.js b/tests/integration/api/v1/users/[username]/patch.test.js new file mode 100644 index 0000000..e3bfe0f --- /dev/null +++ b/tests/integration/api/v1/users/[username]/patch.test.js @@ -0,0 +1,286 @@ +import { version as uuidVersion } from "uuid"; +import orchestrator from "tests/orchestrator.js"; +import user from "models/user.js"; +import password from "models/password.js"; + +beforeAll(async () => { + await orchestrator.waitForAllServices(); + await orchestrator.clearDatabase(); + await orchestrator.runPendingMigrations(); +}); + +describe("PATCH /api/v1/users/[username]", () => { + describe("Anonymous user", () => { + + test("With nonexistent 'username'", async () => { + const response = await fetch( + "http://localhost:3000/api/v1/users/NonExistentUser", { + method: "PATCH", + } + ); + + expect(response.status).toBe(404); + + const responseBody = await response.json(); + + expect(responseBody).toEqual({ + name: "NotFoundError", + message: "User not found", + action: "Please check the username and try again", + status_code: 404, + }); + }); + + test("With duplicated 'username'", async () => { + const user1Response = await fetch("http://localhost:3000/api/v1/users", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + username: "user1", + email: "user1@curso.dev", + password: "senha123", + }), + }); + + expect(user1Response.status).toBe(201); + + const user2Response = await fetch("http://localhost:3000/api/v1/users", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + username: "user2", + email: "user2@curso.dev", + password: "senha123", + }), + }); + + expect(user2Response.status).toBe(201); + + const response = await fetch("http://localhost:3000/api/v1/users/user2", { + method: "PATCH", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + username: "user1", + }), + }); + + expect(response.status).toBe(400); + + const responseBody = await response.json(); + + expect(responseBody).toEqual({ + name: "ValidationError", + message: "Username already in use", + action: "Please change the username and try again", + status_code: 400, + }); + }); + + test("With duplicated 'email'", async () => { + const email1Response = await fetch("http://localhost:3000/api/v1/users", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + username: "email1", + email: "email1@curso.dev", + password: "senha123", + }), + }); + + expect(email1Response.status).toBe(201); + + const email2Response = await fetch("http://localhost:3000/api/v1/users", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + username: "email2", + email: "email2@curso.dev", + password: "senha123", + }), + }); + + expect(email2Response.status).toBe(201); + + const response = await fetch("http://localhost:3000/api/v1/users/email2", { + method: "PATCH", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + email: "email1@curso.dev", + }), + }); + + expect(response.status).toBe(400); + + const responseBody = await response.json(); + + expect(responseBody).toEqual({ + name: "ValidationError", + message: "Email already in use", + action: "Please change the email and try again", + status_code: 400, + }); + }); + + test("With unique 'username'", async () => { + const user1Response = await fetch("http://localhost:3000/api/v1/users", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + username: "uniqueUser1", + email: "uniqueUser1@curso.dev", + password: "senha123", + }), + }); + + expect(user1Response.status).toBe(201); + + const response = await fetch("http://localhost:3000/api/v1/users/uniqueUser1", { + method: "PATCH", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + username: "uniqueUser2", + }), + }); + + expect(response.status).toBe(200); + + const responseBody = await response.json(); + + expect(responseBody).toEqual({ + id: responseBody.id, + username: "uniqueUser2", + email: "uniqueUser1@curso.dev", + password: responseBody.password, + created_at: responseBody.created_at, + updated_at: responseBody.updated_at, + }); + + expect(uuidVersion(responseBody.id)).toBe(4); + expect(Date.parse(responseBody.created_at)).not.toBeNaN(); + expect(Date.parse(responseBody.updated_at)).not.toBeNaN(); + + expect(responseBody.updated_at > responseBody.created_at).toBe(true); + + }); + + test("With unique 'email'", async () => { + const user1Response = await fetch("http://localhost:3000/api/v1/users", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + username: "uniqueEmail1", + email: "uniqueEmail1@curso.dev", + password: "senha123", + }), + }); + + expect(user1Response.status).toBe(201); + + const response = await fetch("http://localhost:3000/api/v1/users/uniqueEmail1", { + method: "PATCH", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + email: "uniqueEmail2@curso.dev", + }), + }); + + expect(response.status).toBe(200); + + const responseBody = await response.json(); + + expect(responseBody).toEqual({ + id: responseBody.id, + username: "uniqueEmail1", + email: "uniqueEmail2@curso.dev", + password: responseBody.password, + created_at: responseBody.created_at, + updated_at: responseBody.updated_at, + }); + + expect(uuidVersion(responseBody.id)).toBe(4); + expect(Date.parse(responseBody.created_at)).not.toBeNaN(); + expect(Date.parse(responseBody.updated_at)).not.toBeNaN(); + + expect(responseBody.updated_at > responseBody.created_at).toBe(true); + + }); + + test("With new 'password'", async () => { + const user1Response = await fetch("http://localhost:3000/api/v1/users", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + username: "newPassword1", + email: "newPassword1@curso.dev", + password: "newPassword1", + }), + }); + + expect(user1Response.status).toBe(201); + + const response = await fetch("http://localhost:3000/api/v1/users/newPassword1", { + method: "PATCH", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + password: "newPassword2", + }), + }); + + expect(response.status).toBe(200); + + const responseBody = await response.json(); + + expect(responseBody).toEqual({ + id: responseBody.id, + username: "newPassword1", + email: "newPassword1@curso.dev", + password: responseBody.password, + created_at: responseBody.created_at, + updated_at: responseBody.updated_at, + }); + + expect(uuidVersion(responseBody.id)).toBe(4); + expect(Date.parse(responseBody.created_at)).not.toBeNaN(); + expect(Date.parse(responseBody.updated_at)).not.toBeNaN(); + + expect(responseBody.updated_at > responseBody.created_at).toBe(true); + + const userInDatabase = await user.findOneByUsername("newPassword1"); + const correctPassowordMatch = await password.compare( + "newPassword2", + userInDatabase.password, + ); + expect(correctPassowordMatch).toBe(true); + + const incorrectPassowordMatch = await password.compare( + "newPassword1", + userInDatabase.password, + ); + expect(incorrectPassowordMatch).toBe(false); + }); + }); +}); From 523be5d8da098da2acdddc95963cc10281e37612 Mon Sep 17 00:00:00 2001 From: Lucas Martins Date: Sat, 14 Feb 2026 13:32:34 -0300 Subject: [PATCH 4/6] style: use litting to fix code style --- models/user.js | 8 +- pages/api/v1/users/[username]/index.js | 2 +- .../api/v1/users/[username]/patch.test.js | 86 +++++++++++-------- 3 files changed, 52 insertions(+), 44 deletions(-) diff --git a/models/user.js b/models/user.js index e54e9ea..fe5e517 100644 --- a/models/user.js +++ b/models/user.js @@ -68,7 +68,6 @@ async function update(username, userInputValues) { if ("username" in userInputValues) { await validateUniqueUsername(userInputValues.username); - } if ("email" in userInputValues) { @@ -102,11 +101,10 @@ async function update(username, userInputValues) { userWithNewValues.username, userWithNewValues.email, userWithNewValues.password, - ] - }) + ], + }); return results.rows[0]; - } } @@ -161,6 +159,6 @@ const user = { create, findOneByUsername, update, -} +}; export default user; diff --git a/pages/api/v1/users/[username]/index.js b/pages/api/v1/users/[username]/index.js index 77bd06b..ce714de 100644 --- a/pages/api/v1/users/[username]/index.js +++ b/pages/api/v1/users/[username]/index.js @@ -24,4 +24,4 @@ async function patchHandler(request, response) { return response.status(200).json(updatedUser); } -export default router.handler(controller.errorHandlers); \ No newline at end of file +export default router.handler(controller.errorHandlers); diff --git a/tests/integration/api/v1/users/[username]/patch.test.js b/tests/integration/api/v1/users/[username]/patch.test.js index e3bfe0f..878d05c 100644 --- a/tests/integration/api/v1/users/[username]/patch.test.js +++ b/tests/integration/api/v1/users/[username]/patch.test.js @@ -11,12 +11,12 @@ beforeAll(async () => { describe("PATCH /api/v1/users/[username]", () => { describe("Anonymous user", () => { - test("With nonexistent 'username'", async () => { const response = await fetch( - "http://localhost:3000/api/v1/users/NonExistentUser", { - method: "PATCH", - } + "http://localhost:3000/api/v1/users/NonExistentUser", + { + method: "PATCH", + }, ); expect(response.status).toBe(404); @@ -111,15 +111,18 @@ describe("PATCH /api/v1/users/[username]", () => { expect(email2Response.status).toBe(201); - const response = await fetch("http://localhost:3000/api/v1/users/email2", { - method: "PATCH", - headers: { - "Content-Type": "application/json", + const response = await fetch( + "http://localhost:3000/api/v1/users/email2", + { + method: "PATCH", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + email: "email1@curso.dev", + }), }, - body: JSON.stringify({ - email: "email1@curso.dev", - }), - }); + ); expect(response.status).toBe(400); @@ -148,15 +151,18 @@ describe("PATCH /api/v1/users/[username]", () => { expect(user1Response.status).toBe(201); - const response = await fetch("http://localhost:3000/api/v1/users/uniqueUser1", { - method: "PATCH", - headers: { - "Content-Type": "application/json", + const response = await fetch( + "http://localhost:3000/api/v1/users/uniqueUser1", + { + method: "PATCH", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + username: "uniqueUser2", + }), }, - body: JSON.stringify({ - username: "uniqueUser2", - }), - }); + ); expect(response.status).toBe(200); @@ -176,7 +182,6 @@ describe("PATCH /api/v1/users/[username]", () => { expect(Date.parse(responseBody.updated_at)).not.toBeNaN(); expect(responseBody.updated_at > responseBody.created_at).toBe(true); - }); test("With unique 'email'", async () => { @@ -194,15 +199,18 @@ describe("PATCH /api/v1/users/[username]", () => { expect(user1Response.status).toBe(201); - const response = await fetch("http://localhost:3000/api/v1/users/uniqueEmail1", { - method: "PATCH", - headers: { - "Content-Type": "application/json", + const response = await fetch( + "http://localhost:3000/api/v1/users/uniqueEmail1", + { + method: "PATCH", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + email: "uniqueEmail2@curso.dev", + }), }, - body: JSON.stringify({ - email: "uniqueEmail2@curso.dev", - }), - }); + ); expect(response.status).toBe(200); @@ -222,7 +230,6 @@ describe("PATCH /api/v1/users/[username]", () => { expect(Date.parse(responseBody.updated_at)).not.toBeNaN(); expect(responseBody.updated_at > responseBody.created_at).toBe(true); - }); test("With new 'password'", async () => { @@ -240,15 +247,18 @@ describe("PATCH /api/v1/users/[username]", () => { expect(user1Response.status).toBe(201); - const response = await fetch("http://localhost:3000/api/v1/users/newPassword1", { - method: "PATCH", - headers: { - "Content-Type": "application/json", + const response = await fetch( + "http://localhost:3000/api/v1/users/newPassword1", + { + method: "PATCH", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + password: "newPassword2", + }), }, - body: JSON.stringify({ - password: "newPassword2", - }), - }); + ); expect(response.status).toBe(200); From 7c365f437c4409bb021827a2a2f340fb0d1ddb5d Mon Sep 17 00:00:00 2001 From: Lucas Martins Date: Sat, 14 Feb 2026 13:52:40 -0300 Subject: [PATCH 5/6] fix: testing new PR open --- tests/integration/api/v1/users/[username]/patch.test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/integration/api/v1/users/[username]/patch.test.js b/tests/integration/api/v1/users/[username]/patch.test.js index 878d05c..2ae9749 100644 --- a/tests/integration/api/v1/users/[username]/patch.test.js +++ b/tests/integration/api/v1/users/[username]/patch.test.js @@ -284,12 +284,13 @@ describe("PATCH /api/v1/users/[username]", () => { "newPassword2", userInDatabase.password, ); - expect(correctPassowordMatch).toBe(true); const incorrectPassowordMatch = await password.compare( "newPassword1", userInDatabase.password, ); + + expect(correctPassowordMatch).toBe(true); expect(incorrectPassowordMatch).toBe(false); }); }); From 40a3c2276841b9c10725cbe2d80db771cd61c577 Mon Sep 17 00:00:00 2001 From: Lucas Martins Date: Sat, 14 Feb 2026 13:59:56 -0300 Subject: [PATCH 6/6] fix: react Server Components CVE vulnerabilities --- package-lock.json | 80 +++++++++++++++++++++++------------------------ package.json | 2 +- 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/package-lock.json b/package-lock.json index cf7f163..055ffbe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "dotenv-expand": "12.0.2", "eslint": "9.31.0", "eslint-config-next": "15.4.3", - "next": "15.4.3", + "next": "^15.4.10", "next-connect": "1.0.0", "node-pg-migrate": "^7.6.1", "pg": "8.16.3", @@ -2294,9 +2294,9 @@ } }, "node_modules/@next/env": { - "version": "15.4.3", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.4.3.tgz", - "integrity": "sha512-lKJ9KJAvaWzqurIsz6NWdQOLj96mdhuDMusLSYHw9HBe2On7BjUwU1WeRvq19x7NrEK3iOgMeSBV5qEhVH1cMw==", + "version": "15.4.10", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.4.10.tgz", + "integrity": "sha512-knhmoJ0Vv7VRf6pZEPSnciUG1S4bIhWx+qTYBW/AjxEtlzsiNORPk8sFDCEvqLfmKuey56UB9FL1UdHEV3uBrg==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -2337,9 +2337,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.4.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.4.3.tgz", - "integrity": "sha512-YAhZWKeEYY7LHQJiQ8fe3Y6ymfcDcTn7rDC8PDu/pdeIl1Z2LHD4uyPNuQUGCEQT//MSNv6oZCeQzZfTCKZv+A==", + "version": "15.4.8", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.4.8.tgz", + "integrity": "sha512-Pf6zXp7yyQEn7sqMxur6+kYcywx5up1J849psyET7/8pG2gQTVMjU3NzgIt8SeEP5to3If/SaWmaA6H6ysBr1A==", "cpu": [ "arm64" ], @@ -2353,9 +2353,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.4.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.4.3.tgz", - "integrity": "sha512-ZPHRdd51xaxCMpT4viQ6h8TgYM1zPW1JIeksPY9wKlyvBVUQqrWqw8kEh1sa7/x0Ied+U7pYHkAkutrUwxbMcg==", + "version": "15.4.8", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.4.8.tgz", + "integrity": "sha512-xla6AOfz68a6kq3gRQccWEvFC/VRGJmA/QuSLENSO7CZX5WIEkSz7r1FdXUjtGCQ1c2M+ndUAH7opdfLK1PQbw==", "cpu": [ "x64" ], @@ -2369,9 +2369,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.4.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.4.3.tgz", - "integrity": "sha512-QUdqftCXC5vw5cowucqi9FeOPQ0vdMxoOHLY0J5jPdercwSJFjdi9CkEO4Xkq1eG4t1TB/BG81n6rmTsWoILnw==", + "version": "15.4.8", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.4.8.tgz", + "integrity": "sha512-y3fmp+1Px/SJD+5ntve5QLZnGLycsxsVPkTzAc3zUiXYSOlTPqT8ynfmt6tt4fSo1tAhDPmryXpYKEAcoAPDJw==", "cpu": [ "arm64" ], @@ -2385,9 +2385,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.4.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.4.3.tgz", - "integrity": "sha512-HTL31NsmoafX+r5g91Yj3+q34nrn1xKmCWVuNA+fUWO4X0pr+n83uGzLyEOn0kUqbMZ40KmWx+4wsbMoUChkiQ==", + "version": "15.4.8", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.4.8.tgz", + "integrity": "sha512-DX/L8VHzrr1CfwaVjBQr3GWCqNNFgyWJbeQ10Lx/phzbQo3JNAxUok1DZ8JHRGcL6PgMRgj6HylnLNndxn4Z6A==", "cpu": [ "arm64" ], @@ -2401,9 +2401,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.4.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.4.3.tgz", - "integrity": "sha512-HRQLWoeFkKXd2YCEEy9GhfwOijRm37x4w5r0MMVHxBKSA6ms3JoPUXvGhfHT6srnGRcEUWNrQ2vzkHir5ZWTSw==", + "version": "15.4.8", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.4.8.tgz", + "integrity": "sha512-9fLAAXKAL3xEIFdKdzG5rUSvSiZTLLTCc6JKq1z04DR4zY7DbAPcRvNm3K1inVhTiQCs19ZRAgUerHiVKMZZIA==", "cpu": [ "x64" ], @@ -2417,9 +2417,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.4.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.4.3.tgz", - "integrity": "sha512-NyXUx6G7AayaRGUsVPenuwhyAoyxjQuQPaK50AXoaAHPwRuif4WmSrXUs8/Y0HJIZh8E/YXRm9H7uuGfiacpuQ==", + "version": "15.4.8", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.4.8.tgz", + "integrity": "sha512-s45V7nfb5g7dbS7JK6XZDcapicVrMMvX2uYgOHP16QuKH/JA285oy6HcxlKqwUNaFY/UC6EvQ8QZUOo19cBKSA==", "cpu": [ "x64" ], @@ -2433,9 +2433,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.4.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.4.3.tgz", - "integrity": "sha512-2CUTmpzN/7cL1a7GjdLkDFlfH3nwMwW8a6JiaAUsL9MtKmNNO3fnXqnY0Zk30fii3hVEl4dr7ztrpYt0t2CcGQ==", + "version": "15.4.8", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.4.8.tgz", + "integrity": "sha512-KjgeQyOAq7t/HzAJcWPGA8X+4WY03uSCZ2Ekk98S9OgCFsb6lfBE3dbUzUuEQAN2THbwYgFfxX2yFTCMm8Kehw==", "cpu": [ "arm64" ], @@ -2449,9 +2449,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.4.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.4.3.tgz", - "integrity": "sha512-i54YgUhvrUQxQD84SjAbkfWhYkOdm/DNRAVekCHLWxVg3aUbyC6NFQn9TwgCkX5QAS2pXCJo3kFboSFvrsd7dA==", + "version": "15.4.8", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.4.8.tgz", + "integrity": "sha512-Exsmf/+42fWVnLMaZHzshukTBxZrSwuuLKFvqhGHJ+mC1AokqieLY/XzAl3jc/CqhXLqLY3RRjkKJ9YnLPcRWg==", "cpu": [ "x64" ], @@ -8283,12 +8283,12 @@ "license": "MIT" }, "node_modules/next": { - "version": "15.4.3", - "resolved": "https://registry.npmjs.org/next/-/next-15.4.3.tgz", - "integrity": "sha512-uW7Qe6poVasNIE1X382nI29oxSdFJzjQzTgJFLD43MxyPfGKKxCMySllhBpvqr48f58Om+tLMivzRwBpXEytvA==", + "version": "15.4.10", + "resolved": "https://registry.npmjs.org/next/-/next-15.4.10.tgz", + "integrity": "sha512-itVlc79QjpKMFMRhP+kbGKaSG/gZM6RCvwhEbwmCNF06CdDiNaoHcbeg0PqkEa2GOcn8KJ0nnc7+yL7EjoYLHQ==", "license": "MIT", "dependencies": { - "@next/env": "15.4.3", + "@next/env": "15.4.10", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", @@ -8301,14 +8301,14 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.4.3", - "@next/swc-darwin-x64": "15.4.3", - "@next/swc-linux-arm64-gnu": "15.4.3", - "@next/swc-linux-arm64-musl": "15.4.3", - "@next/swc-linux-x64-gnu": "15.4.3", - "@next/swc-linux-x64-musl": "15.4.3", - "@next/swc-win32-arm64-msvc": "15.4.3", - "@next/swc-win32-x64-msvc": "15.4.3", + "@next/swc-darwin-arm64": "15.4.8", + "@next/swc-darwin-x64": "15.4.8", + "@next/swc-linux-arm64-gnu": "15.4.8", + "@next/swc-linux-arm64-musl": "15.4.8", + "@next/swc-linux-x64-gnu": "15.4.8", + "@next/swc-linux-x64-musl": "15.4.8", + "@next/swc-win32-arm64-msvc": "15.4.8", + "@next/swc-win32-x64-msvc": "15.4.8", "sharp": "^0.34.3" }, "peerDependencies": { diff --git a/package.json b/package.json index 9430ba9..0abf895 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "dotenv-expand": "12.0.2", "eslint": "9.31.0", "eslint-config-next": "15.4.3", - "next": "15.4.3", + "next": "^15.4.10", "next-connect": "1.0.0", "node-pg-migrate": "^7.6.1", "pg": "8.16.3",