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 @@ -43,12 +43,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;
26 changes: 26 additions & 0 deletions models/session.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,37 @@ async function renew(sessionId) {
}
}

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

return expiredSessionObject;

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

return result.rows[0];
}
}

const session = {
EXPIRATION_IN_MILLISECONDS,
create,
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 @@ -6,6 +6,7 @@ import session from "models/session.js";
const router = createRouter();

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

export default router.handler(controller.errorHandlers);

Expand All @@ -23,3 +24,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);
}
119 changes: 119 additions & 0 deletions tests/integration/api/v1/sessions/delete.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { version as uuidVersion } from "uuid";
import setCookieParser from "set-cookie-parser";
import orchestrator from "tests/orchestrator.js";
import session from "models/session";

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

describe("DELETE to api/v1/sessions", () => {
describe("Default user", () => {
test("With nonexistent session", async () => {
const nonexistentToken =
"883f4782ed6c87d7f30ab3351f4614591ddeb148ae73a214d9f05b848d53f4377c973559aba52e907263835ba5dc7a97";

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: "User do not have an active session.",
action: "Verify if you are logged in and try again.",
status_code: 401,
});
});

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: "User do not have an active session.",
action: "Verify if you are logged in and try again.",
status_code: 401,
});
});

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);

// Test Set-Cookie
const parsedSetCookie = setCookieParser(response, { map: true });
expect(parsedSetCookie.session_id).toEqual({
name: "session_id",
value: "invalid",
maxAge: -1,
path: "/",
httpOnly: true,
});

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