Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions infra/migrations/1711495753953_Teste.js → infra/migrations/1748084895204_users.js
100755 → 100644
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
/* eslint-disable camelcase */

exports.shorthands = undefined;

exports.up = (pgm) => {
pgm.createTable("users", {
id: {
Expand Down Expand Up @@ -45,4 +41,4 @@ exports.up = (pgm) => {
});
};

exports.down = false;
exports.down = false;
164 changes: 164 additions & 0 deletions models/user.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import database from "infra/database.js";
import password from "models/password.js";
import { ValidationError, NotFoundError } from "infra/errors.js";

async function findOneByUsername(username) {
const userFound = await runSelectQuery(username);

return userFound;

async function runSelectQuery(username) {
const results = await database.query({
text: `
SELECT
*
FROM
users
WHERE
LOWER(username) = LOWER($1)
LIMIT
1
;`,
values: [username],
});

if (results.rowCount === 0) {
throw new NotFoundError({
message: "O username informado não foi encontrado no sistema.",
action: "Verifique se o username está digitado corretamente.",
});
}

return results.rows[0];
}
}

async function create(userInputValues) {
await validateUniqueUsername(userInputValues.username);
await validateUniqueEmail(userInputValues.email);
await hashPasswordInObject(userInputValues);

const newUser = await runInsertQuery(userInputValues);
return newUser;

async function runInsertQuery(userInputValues) {
const results = await database.query({
text: `
INSERT INTO
users (username, email, password)
VALUES
($1, $2, $3)
RETURNING
*
;`,
values: [
userInputValues.username,
userInputValues.email,
userInputValues.password,
],
});
return results.rows[0];
}
}

async function update(username, userInputValues) {
const currentUser = await findOneByUsername(username);

if ("username" in userInputValues) {
await validateUniqueUsername(userInputValues.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', 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
users
WHERE
LOWER(username) = LOWER($1)
;`,
values: [username],
});

if (results.rowCount > 0) {
throw new ValidationError({
message: "O username informado já está sendo utilizado.",
action: "Utilize outro username para realizar esta operação.",
});
}
}

async function validateUniqueEmail(email) {
const results = await database.query({
text: `
SELECT
email
FROM
users
WHERE
LOWER(email) = LOWER($1)
;`,
values: [email],
});

if (results.rowCount > 0) {
throw new ValidationError({
message: "O email informado já está sendo utilizado.",
action: "Utilize outro email para realizar esta operação.",
});
}
}

async function hashPasswordInObject(userInputValues) {
const hashedPassword = await password.hash(userInputValues.password);
userInputValues.password = hashedPassword;
}

const user = {
create,
findOneByUsername,
update,
};

export default user;
24 changes: 24 additions & 0 deletions pages/api/v1/users/[username]/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { createRouter } from "next-connect";
import controller from "infra/controller.js";
import user from "models/user.js";

const router = createRouter();

router.get(getHandler);
router.patch(patchHandler);

export default router.handler(controller.errorHandlers);

async function getHandler(request, response) {
const username = request.query.username;
const userFound = await user.findOneByUsername(username);
return response.status(200).json(userFound);
}

async function patchHandler(request, response) {
const username = request.query.username;
const userInputValues = request.body;

const updatedUser = await user.update(username, userInputValues);
return response.status(200).json(updatedUser);
}
15 changes: 15 additions & 0 deletions pages/api/v1/users/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { createRouter } from "next-connect";
import controller from "infra/controller.js";
import user from "models/user.js";

const router = createRouter();

router.post(postHandler);

export default router.handler(controller.errorHandlers);

async function postHandler(request, response) {
const userInputValues = request.body;
const newUser = await user.create(userInputValues);
return response.status(201).json(newUser);
}
1 change: 1 addition & 0 deletions tests/integration/api/v1/migrations/post.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import orchestrator from "tests/orchestrator.js";
beforeAll(async () => {
await orchestrator.waitForAllServices();
await orchestrator.clearDatabase();
await orchestrator.runPendingMigrations();
});

describe("POST /api/v1/migrations", () => {
Expand Down
83 changes: 83 additions & 0 deletions tests/integration/api/v1/users/[username]/get.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { version as uuidVersion } from "uuid";
import orchestrator from "tests/orchestrator.js";

beforeAll(async () => {
await orchestrator.waitForAllServices();
await orchestrator.clearDatabase();
await orchestrator.runPendingMigrations();
});

describe("GET /api/v1/users/[username]", () => {
describe("Anonymous user", () => {
test("With exact case match", async () => {
const createdUser = await orchestrator.createUser({
username: "MesmoCase",
});

const response = await fetch(
"http://localhost:3000/api/v1/users/MesmoCase",
);

expect(response.status).toBe(200);

const responseBody = await response.json();

expect(responseBody).toEqual({
id: responseBody.id,
username: "MesmoCase",
email: createdUser.email,
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();
});

test("With case mismatch", async () => {
const createdUser = await orchestrator.createUser({
username: "CaseDiferente",
});

const response = await fetch(
"http://localhost:3000/api/v1/users/casediferente",
);

expect(response.status).toBe(200);

const responseBody = await response.json();

expect(responseBody).toEqual({
id: responseBody.id,
username: "CaseDiferente",
email: createdUser.email,
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();
});

test("With nonexistent username", async () => {
const response = await fetch(
"http://localhost:3000/api/v1/users/UsuarioInexistente",
);

expect(response.status).toBe(404);

const responseBody = await response.json();

expect(responseBody).toEqual({
name: "NotFoundError",
message: "O username informado não foi encontrado no sistema.",
action: "Verifique se o username está digitado corretamente.",
status_code: 404,
});
});
});
});
Loading
Loading