better-sqlite3-session-store
Version:
A session store for express-session.
431 lines (366 loc) • 11.4 kB
JavaScript
// @format
const test = require("ava").serial;
const sqlite = require("better-sqlite3");
const session = require("express-session");
const { unlinkSync, existsSync } = require("fs");
const differenceInSeconds = require("date-fns/differenceInSeconds");
const add = require("date-fns/add");
const SqliteStore = require("../src/index.js")(session);
const dbName = "test.db";
const dbOptions = {
verbose: console.log,
};
const teardown = () => {
try {
unlinkSync(dbName);
} catch (err) {
if (!existsSync(dbName)) {
//noop
} else {
throw err;
}
}
};
test.afterEach(teardown);
test("if interface methods run safely when `cb` isn't present", (t) => {
const db = new sqlite(dbName, dbOptions);
const s = new SqliteStore({
client: db,
});
s.all();
s.destroy("non-existent");
s.clear();
s.length();
s.get("non-existent");
const sid = "123";
const sess = { cookie: { maxAge: 2000 }, name: "sample name" };
s.set(sid, sess);
s.touch(sid, sess);
t.pass();
});
test("if initializing store works", (t) => {
const db = new sqlite(dbName, dbOptions);
const s = new SqliteStore({
client: db,
});
const [sid, sess, expire] = db.prepare("PRAGMA table_info (sessions)").all();
t.assert(sid.name === "sid" && sid.type === "TEXT");
t.assert(sess.name === "sess" && sess.type === "JSON");
t.assert(expire.name === "expire" && expire.type === "TEXT");
});
test("if initialization can be run twice without any errors", (t) => {
const db = new sqlite(dbName, dbOptions);
const s = new SqliteStore({
client: db,
});
const s2 = new SqliteStore({
client: db,
});
const [sid, sess, expire] = db.prepare("PRAGMA table_info (sessions)").all();
t.assert(sid.name === "sid" && sid.type === "TEXT");
t.assert(sess.name === "sess" && sess.type === "JSON");
t.assert(expire.name === "expire" && expire.type === "TEXT");
});
test("if error is thrown when client is missing from options", (t) => {
t.throws(() => new SqliteStore());
});
test("if it saves a new session record", (t) => {
const db = new sqlite(dbName, dbOptions);
const s = new SqliteStore({
client: db,
});
const sid = "123";
const sess = { cookie: { maxAge: 5000 }, name: "sample name" };
s.set(sid, sess, (err, rows) => {
t.assert(!err);
t.assert(rows);
});
const dbSess = db.prepare("SELECT * FROM sessions WHERE sid = ?").get(sid);
t.assert(dbSess.sess === JSON.stringify(sess));
t.assert(dbSess.sid === sid);
const diff = differenceInSeconds(new Date(dbSess.expire), new Date());
t.assert(diff * 1000 >= sess.cookie.maxAge - 1000);
});
test("if it overwrites an already-existing session", (t) => {
const db = new sqlite(dbName, dbOptions);
const s = new SqliteStore({
client: db,
});
const sid = "123";
const sess = { cookie: { maxAge: 2000 }, name: "sample name" };
s.set(sid, sess, (err, rows) => {
t.assert(!err);
t.assert(rows);
});
const dbSess = db.prepare("SELECT * FROM sessions WHERE sid = ?").get(sid);
t.assert(dbSess.sess === JSON.stringify(sess));
t.assert(dbSess.sid === sid);
const sess2 = { cookie: { maxAge: 5000 }, name: "replaced name" };
s.set(sid, sess2, (err, rows) => {
t.assert(!err);
t.assert(rows);
});
const dbSess2 = db.prepare("SELECT * FROM sessions WHERE sid = ?").get(sid);
t.assert(dbSess2.sess === JSON.stringify(sess2));
t.assert(dbSess2.sid === sid);
});
test("if it saves a session with a missing maxAge too", (t) => {
const db = new sqlite(dbName, dbOptions);
const s = new SqliteStore({
client: db,
});
const sid = "123";
const sess = { cookie: {}, name: "sample name" };
const oneDayAge = 86400;
s.set(sid, sess, (err, rows) => {
t.assert(!err);
t.assert(rows);
});
const dbSess = db.prepare("SELECT * FROM sessions WHERE sid = ?").get(sid);
t.assert(dbSess.sess === JSON.stringify(sess));
t.assert(dbSess.sid === sid);
t.assert(
differenceInSeconds(new Date(dbSess.expire), new Date()) >= oneDayAge - 5
);
});
test("if get method returns null when no session was found", (t) => {
const db = new sqlite(dbName, dbOptions);
const s = new SqliteStore({
client: db,
});
s.get("non-existent", (err, res) => {
t.assert(!err);
t.assert(res === null);
});
});
test("if an expired session is ignored when trying to get a session", async (t) => {
const db = new sqlite(dbName, dbOptions);
const s = new SqliteStore({
client: db,
});
const sid = "123";
// NOTE: Session expires immediately with maxAge being 1 second.
const sess = { cookie: { maxAge: 1 }, name: "sample name" };
s.set(sid, sess, (err, res) => {
t.assert(!err);
t.assert(res);
});
// NOTE: Wait 2 sec to make sure that session is expired.
await new Promise((resolve) => setTimeout(resolve, 2000));
s.get(sid, (err, res) => {
t.assert(!err);
t.assert(res === null);
});
});
test("if an active session is retrieved when calling get", (t) => {
const db = new sqlite(dbName, dbOptions);
const s = new SqliteStore({
client: db,
});
const sid = "123";
const sess = { cookie: { maxAge: 100 * 1000 }, name: "sample name" };
s.set(sid, sess, (err, res) => {
t.assert(!err);
t.assert(res);
});
s.get(sid, (err, dbSess) => {
t.assert(!err);
t.deepEqual(sess, dbSess);
});
});
test("if a session is destroyed", (t) => {
const db = new sqlite(dbName, dbOptions);
const s = new SqliteStore({
client: db,
});
const sid = "123";
const sess = { cookie: { maxAge: 100 }, name: "sample name" };
s.set(sid, sess, (err, res) => {
t.assert(!err);
t.assert(res);
});
s.destroy(sid, (err, res) => {
t.assert(!err);
t.assert(res);
});
const { numOfSessions } = db
.prepare("SELECT COUNT(*) as numOfSessions FROM sessions")
.get();
t.assert(numOfSessions === 0);
});
test("if non-existent session can be destroyed without throwing an error too", (t) => {
const db = new sqlite(dbName, dbOptions);
const s = new SqliteStore({
client: db,
});
const sid = "123";
s.destroy(sid, (err, res) => {
t.assert(!err);
t.assert(res);
});
});
test("if counting all sessions is possible", (t) => {
const db = new sqlite(dbName, dbOptions);
const s = new SqliteStore({
client: db,
});
s.length((err, res) => {
t.assert(typeof res === "number");
t.assert(res === 0);
});
const sid = "123";
const sess = { cookie: { maxAge: 100 }, name: "sample name" };
s.set(sid, sess, (err, res) => {
t.assert(!err);
t.assert(res);
});
s.length((err, res) => {
t.assert(typeof res === "number");
t.assert(res === 1);
});
});
test("if all sessions can be cleared", (t) => {
const db = new sqlite(dbName, dbOptions);
const s = new SqliteStore({
client: db,
});
const sid = "123";
const sid2 = "456";
const sess = { cookie: { maxAge: 100 }, name: "sample name" };
s.set(sid, sess, (err, res) => {
t.assert(!err);
t.assert(res);
});
s.set(sid2, sess, (err, res) => {
t.assert(!err);
t.assert(res);
});
s.length((err, count) => t.assert(count === 2));
s.clear((err, res) => {
t.assert(!err);
t.assert(res);
});
s.length((err, count) => t.assert(count === 0));
});
test("if session is touched when expires key is present", (t) => {
const db = new sqlite(dbName, dbOptions);
const s = new SqliteStore({
client: db,
});
const sid = "123";
const sess = { cookie: { maxAge: 100 * 1000 }, name: "sample name" };
s.set(sid, sess, (err, res) => {
t.assert(!err);
t.assert(res);
});
const newExpires = add(new Date(), { minutes: 1337 }).toISOString();
const sess2 = { cookie: { expires: newExpires }, name: "sample name" };
s.touch(sid, sess2, (err, res) => {
t.assert(!err);
t.assert(res);
});
const res = db.prepare(`SELECT expire FROM sessions WHERE sid = ?`).get(sid);
t.assert(new Date(res.expire).toISOString() === newExpires);
});
test("if an inactive session is ignored when touching", async (t) => {
const db = new sqlite(dbName, dbOptions);
const s = new SqliteStore({
client: db,
});
const sid = "123";
const sess = { cookie: { maxAge: 1 * 1000 }, name: "sample name" };
s.set(sid, sess, (err, res) => {
t.assert(!err);
t.assert(res);
});
// NOTE: Wait 2 sec to make sure that session is expired.
await new Promise((resolve) => setTimeout(resolve, 2000));
const newExpires = add(new Date(), { seconds: 1337 }).toISOString();
const sess2 = { cookie: { expires: newExpires }, name: "sample name" };
s.touch(sid, sess2, (err, res) => {
t.assert(!err);
t.assert(res);
});
const res = db.prepare(`SELECT expire FROM sessions WHERE sid = ?`).get(sid);
t.assert(differenceInSeconds(new Date(), new Date(res.expire)) < 5);
});
test("that if `expires` is omitted from cookie, a default TTL is used", (t) => {
const db = new sqlite(dbName, dbOptions);
const s = new SqliteStore({
client: db,
});
const sid = "123";
const sess = { cookie: { maxAge: 100 * 1000 }, name: "sample name" };
s.set(sid, sess, (err, res) => {
t.assert(!err);
t.assert(res);
});
const newExpires = add(new Date(), { minutes: 1337 }).toISOString();
const sess2 = { cookie: {}, name: "sample name" };
s.touch(sid, sess2, (err, res) => {
t.assert(!err);
t.assert(res);
});
const res = db.prepare(`SELECT expire FROM sessions WHERE sid = ?`).get(sid);
t.assert(
Math.abs(
differenceInSeconds(
new Date(res.expire),
add(new Date(), { seconds: 86400 })
) < 5
)
);
});
test("if an expired session is deleted by continously deleting expired sessions", async (t) => {
const db = new sqlite(dbName, dbOptions);
const s = new SqliteStore({
client: db,
expired: {
clear: true,
intervalMs: 3000,
},
});
const sid = "123";
// NOTE: Session expires immediately with maxAge being 1 second.
const sess = { cookie: { maxAge: 1 * 1000 }, name: "instant expire" };
s.set(sid, sess, (err, res) => {
t.assert(!err);
t.assert(res);
});
const sid2 = "456";
const sess2 = { cookie: { maxAge: 100 * 1000 }, name: "long expire" };
s.set(sid2, sess2, (err, res) => {
t.assert(!err);
t.assert(res);
});
// NOTE: Wait to make sure that first session is expired.
await new Promise((resolve) => setTimeout(resolve, 4000));
s.get(sid, (err, res) => {
t.assert(!err);
t.assert(res === null);
});
s.get(sid2, (err, res) => {
t.assert(!err);
t.deepEqual(sess2, res);
});
});
test("what happens when table is deleted and all methods are invoked (they should throw)", (t) => {
const db = new sqlite(dbName, dbOptions);
const s = new SqliteStore({
client: db,
expired: {
clear: true,
intervalMs: 3000,
},
});
db.prepare("DROP table sessions").run();
const sid = "123";
// NOTE: Session expires immediately with maxAge being 1 second.
const sess = { cookie: { maxAge: 1 }, name: "instant expire" };
s.set(sid, sess, (err, res) => t.assert(err));
s.get(sid, (err, res) => t.assert(err));
s.touch(sid, null, (err, res) => t.assert(err));
s.clear((err, res) => t.assert(err));
s.length((err, res) => t.assert(err));
s.destroy(sid, (err, res) => t.assert(err));
});