Skip to content
Merged
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
12 changes: 12 additions & 0 deletions infra/controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,24 @@ async function setSessionCookie(sessionToken, response) {
response.setHeader("Set-Cookie", setCookie);
}

async function clearSessionCookie(response) {
const setCookie = cookie.serialize("session_id", "invalid", {
path: "/",
maxAge: -1,
secure: process.env.NODE_ENV === "production",
httpOnly: true,
});

response.setHeader("Set-Cookie", setCookie);
}

const controller = {
errorHandlers: {
onNoMatch: onNoMatchHandler,
onError: onErrorHandler,
},
setSessionCookie,
clearSessionCookie,
};

export default controller;
27 changes: 26 additions & 1 deletion models/session.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ async function findOneValidByToken(sessionToken) {
if (results.rowCount === 0) {
throw new UnauthorizedError({
message: "Usuário não possui sessão ativa.",
action: "Verifique se este usuario está logado e tente novamente.",
action: "Verifique se este usuário está logado e tente novamente.",
});
}

Expand Down Expand Up @@ -84,11 +84,36 @@ async function renew(sessionId) {
}
}

async function expireById(sessionId) {
const expiredSessionObject = await runUpdateQuery(sessionId);
return expiredSessionObject;

async function runUpdateQuery(sessionId) {
const results = await database.query({
text: `
UPDATE
sessions
SET
expires_at = expires_at - interval '1 year',
updated_at = NOW()
WHERE
id = $1
RETURNING
*
;`,
values: [sessionId],
});

return results.rows[0];
}
}

const session = {
create,
EXPIRATION_IN_MILLISECONDS,
findOneValidByToken,
renew,
expireById,
};

export default session;
11 changes: 11 additions & 0 deletions pages/api/v1/sessions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import session from "models/session";
const router = createRouter();

router.post(postHandler);
router.delete(deleteHandler);

export default router.handler(controller.errorHandlers);

Expand All @@ -24,3 +25,13 @@ async function postHandler(request, response) {

return response.status(201).json(newSession);
}

async function deleteHandler(request, response) {
const sessionToken = request.cookies.session_id;

const sessionObject = await session.findOneValidByToken(sessionToken);
const expiredSession = await session.expireById(sessionObject.id);
controller.clearSessionCookie(response);

return response.status(200).json(expiredSession);
}
144 changes: 144 additions & 0 deletions tests/integration/api/v1/sessions/delete.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { version as uuidVersion } from "uuid";
import orchestrator from "tests/orchestrator.js";
import session from "models/session";
import setCookieParser from "set-cookie-parser";

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

describe("DELETE api/v1/session", () => {
describe("Default user", () => {
/*------------------ TESTE 1 ------------------*/
test("With nonexistent session", async () => {
const nonexistentToken =
"f0b62a5ff97ae607701ceeee2e3c4987c4b9debb534410e2444f9eb2288b6e3b90158a71d086e31eabef9b36cbb549e1";

const response = await fetch("http://localhost:3000/api/v1/sessions", {
method: "DELETE",
headers: {
cookie: `session_id=${nonexistentToken}`,
},
});

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

const responseBody = await response.json();

expect(responseBody).toEqual({
name: "UnauthorizedError",
message: "Usuário não possui sessão ativa.",
action: "Verifique se este usuário está logado e tente novamente.",
status_code: 401,
});
});

/*------------------ TESTE 2 ------------------*/
test("With expired session", async () => {
jest.useFakeTimers({
now: new Date(Date.now() - session.EXPIRATION_IN_MILLISECONDS),
});

const createdUser = await orchestrator.createUser();

const sessionObject = await orchestrator.createSession(createdUser.id);

jest.useRealTimers();

const response = await fetch("http://localhost:3000/api/v1/sessions", {
method: "DELETE",
headers: {
Cookie: `session_id=${sessionObject.token}`,
},
});

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

const responseBody = await response.json();

expect(responseBody).toEqual({
name: "UnauthorizedError",
message: "Usuário não possui sessão ativa.",
action: "Verifique se este usuário está logado e tente novamente.",
status_code: 401,
});
});

/*------------------ TESTE 3 ------------------*/
test("With valid session", async () => {
const createdUser = await orchestrator.createUser();

const sessionObject = await orchestrator.createSession(createdUser.id);

const response = await fetch("http://localhost:3000/api/v1/sessions", {
method: "DELETE",
headers: {
Cookie: `session_id=${sessionObject.token}`,
},
});

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

const responseBody = await response.json();

expect(responseBody).toEqual({
id: sessionObject.id,
token: sessionObject.token,
user_id: sessionObject.user_id,
expires_at: responseBody.expires_at,
created_at: responseBody.created_at,
updated_at: responseBody.updated_at,
});

expect(uuidVersion(responseBody.id)).toBe(4);
expect(Date.parse(responseBody.expires_at)).not.toBeNaN();
expect(Date.parse(responseBody.created_at)).not.toBeNaN();
expect(Date.parse(responseBody.updated_at)).not.toBeNaN();

expect(
responseBody.expires_at < sessionObject.expires_at.toISOString(),
).toBe(true);
expect(
responseBody.updated_at > sessionObject.updated_at.toISOString(),
).toBe(true);

// Set-Cookie assertions
const parsedSetCookie = setCookieParser(response, {
map: true,
});

expect(parsedSetCookie.session_id).toEqual({
name: "session_id",
value: "invalid",
maxAge: -1,
path: "/",
httpOnly: true,
});

// Double check assertions
const doubleCheckResponse = await fetch(
"http://localhost:3000/api/v1/user",
{
headers: {
Cookie: `session_id=${sessionObject.token}`,
},
},
);

expect(doubleCheckResponse.status).toBe(401);

const doubleCheckResponseBody = await doubleCheckResponse.json();

expect(doubleCheckResponseBody).toEqual({
name: "UnauthorizedError",
message: "Usuário não possui sessão ativa.",
action: "Verifique se este usuário está logado e tente novamente.",
status_code: 401,
});
});

/*------------------ TESTE 2 ------------------*/
});
});
4 changes: 2 additions & 2 deletions tests/integration/api/v1/user/get.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ describe("GET api/v1/user", () => {
expect(responseBody).toEqual({
name: "UnauthorizedError",
message: "Usuário não possui sessão ativa.",
action: "Verifique se este usuario está logado e tente novamente.",
action: "Verifique se este usuário está logado e tente novamente.",
status_code: 401,
});
});
Expand Down Expand Up @@ -113,7 +113,7 @@ describe("GET api/v1/user", () => {
expect(responseBody).toEqual({
name: "UnauthorizedError",
message: "Usuário não possui sessão ativa.",
action: "Verifique se este usuario está logado e tente novamente.",
action: "Verifique se este usuário está logado e tente novamente.",
status_code: 401,
});
});
Expand Down