UNPKG

better-sqlite3-session-store

Version:
431 lines (366 loc) 11.4 kB
// @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)); });