next-iron-session
Version:
Next.js stateless session utility using signed and encrypted cookies to store data
708 lines (655 loc) • 18.9 kB
JavaScript
import { advanceBy, clear } from "jest-date-mock";
import ironStore from "iron-store";
import { withIronSession, ironSession, applySession } from "./index.js";
const password = "Gbm49ATjnqnkCCCdhV4uDBhbfnPqsCW0";
const cookieName = "test";
test("without a password", () => {
return new Promise((done) => {
const handler = () => {};
expect(() => {
withIronSession(handler, { cookieName });
}).toThrowErrorMatchingInlineSnapshot(
`"next-iron-session: Missing parameter \`password\`"`,
);
done();
});
});
test("without a cookieName", () => {
return new Promise((done) => {
const handler = () => {};
expect(() => {
withIronSession(handler, { password });
}).toThrowErrorMatchingInlineSnapshot(
`"next-iron-session: Missing parameter \`cookieName\`"`,
);
done();
});
});
test("withSession((req, res) => {}, {password, cookieName})", () => {
return new Promise((done) => {
const handler = (req, res) => {
expect(req).toMatchInlineSnapshot(`
Object {
"headers": Object {
"cookie": "sg=1",
},
"session": Object {
"destroy": [Function],
"get": [Function],
"save": [Function],
"set": [Function],
"unset": [Function],
},
}
`);
expect(res).toMatchInlineSnapshot(`
Object {
"json": [Function],
}
`);
done();
};
const wrappedHandler = withIronSession(handler, { password, cookieName });
wrappedHandler(
{
headers: { cookie: "sg=1" },
},
{ json: function () {} },
);
});
});
test("withSession(({req, res}) => {}, {password, cookieName})", () => {
return new Promise((done) => {
const handler = ({ req, res }) => {
expect(req).toMatchInlineSnapshot(`
Object {
"headers": Object {
"cookie": "ssr=1",
},
"session": Object {
"destroy": [Function],
"get": [Function],
"save": [Function],
"set": [Function],
"unset": [Function],
},
}
`);
expect(res).toMatchInlineSnapshot(`
Object {
"json": [Function],
}
`);
done();
};
const wrappedHandler = withIronSession(handler, { password, cookieName });
wrappedHandler({
req: { headers: { cookie: "ssr=1" } },
res: { json: function () {} },
});
});
});
test("req.session.set", () => {
return new Promise((done) => {
const handler = (req) => {
expect(req.session.set("user", { id: 20 })).toMatchInlineSnapshot(`
Object {
"id": 20,
}
`);
done();
};
const wrappedHandler = withIronSession(handler, { password, cookieName });
wrappedHandler(
{
headers: { cookie: "" },
},
{},
);
});
});
test("req.session.unset", () => {
return new Promise((done) => {
const handler = (req) => {
req.session.set("state", { id: 20 });
expect(req.session.get("state")).toMatchInlineSnapshot(`
Object {
"id": 20,
}
`);
req.session.unset("state");
expect(req.session.get("state")).toMatchInlineSnapshot(`undefined`);
done();
};
const wrappedHandler = withIronSession(handler, { password, cookieName });
wrappedHandler(
{
headers: { cookie: "" },
},
{},
);
});
});
test("req.session.save creates a seal and stores it in a cookie", () => {
return new Promise((done) => {
const handler = async (req, res) => {
await req.session.save();
const headerName = res.setHeader.mock.calls[0][0];
expect(headerName).toMatchInlineSnapshot(`"set-cookie"`);
const headerValue = res.setHeader.mock.calls[0][1];
expect(Array.isArray(headerValue)).toBe(true);
expect(headerValue).toHaveLength(1);
const cookie = headerValue[0];
const seal = cookie.split(";")[0].split("=")[1];
expect(seal).toHaveLength(263);
const cookieParams = cookie.split(";").slice(1).join(";");
expect(cookieParams).toMatchInlineSnapshot(
`" Max-Age=1295940; Path=/; HttpOnly; Secure; SameSite=Lax"`,
);
done();
};
const wrappedHandler = withIronSession(handler, { password, cookieName });
wrappedHandler(
{
headers: { cookie: "" },
},
{
setHeader: jest.fn(),
getHeader: jest.fn(),
},
);
});
});
test("withSession((req, res) => {}, {password}) with existing session (SG)", async () => {
return new Promise((done) => {
const handler = (req) => {
expect(req.session.get()).toMatchInlineSnapshot(`
Object {
"user": Object {
"id": 3,
},
}
`);
done();
};
const wrappedHandler = withIronSession(handler, { password, cookieName });
wrappedHandler(
{
headers: {
cookie:
"test=Fe26.2**4e769b9b7b921621ed5658cfc0d7d8e267dc8ee93663c2803c257b31111394e3*jRXOJHmt_BDG9nNTXcVRXQ*UHpK9GYp7SXTiEsxTzTUq_tQD_-ZUp7PguEXy-bRFuBE4fW74-9wm9UtlWO2rlwB**d504d6d197d183efec0ae6d3c2378c43048c8752d6c3c591c92289ed01142b3c*3NG2fCo8A53CXPU8rEAMnDB7X9UkwzTaHieumPBqyTw",
},
},
{},
);
});
});
test("withSession(({req, res}) => {}, {password}) with existing session (SSR)", async () => {
return new Promise((done) => {
const handler = ({ req }) => {
expect(req.session.get()).toMatchInlineSnapshot(`
Object {
"user": Object {
"id": 3,
},
}
`);
done();
};
const wrappedHandler = withIronSession(handler, { password, cookieName });
wrappedHandler({
req: {
headers: {
cookie:
"test=Fe26.2**4e769b9b7b921621ed5658cfc0d7d8e267dc8ee93663c2803c257b31111394e3*jRXOJHmt_BDG9nNTXcVRXQ*UHpK9GYp7SXTiEsxTzTUq_tQD_-ZUp7PguEXy-bRFuBE4fW74-9wm9UtlWO2rlwB**d504d6d197d183efec0ae6d3c2378c43048c8752d6c3c591c92289ed01142b3c*3NG2fCo8A53CXPU8rEAMnDB7X9UkwzTaHieumPBqyTw",
},
},
});
});
});
test("When ttl is 0, maxAge have a specific value", () => {
return new Promise((done) => {
const handler = async (req, res) => {
await req.session.save();
const headerValue = res.setHeader.mock.calls[0][1];
const cookie = headerValue[0];
const maxAgeParam = cookie.split(";")[1];
expect(maxAgeParam).toMatchInlineSnapshot(`" Max-Age=2147483587"`);
done();
};
const wrappedHandler = withIronSession(handler, {
password,
cookieName,
ttl: 0,
});
wrappedHandler(
{
headers: { cookie: "" },
},
{
setHeader: jest.fn(),
getHeader: jest.fn(),
},
);
});
});
test("req.session.destroy", () => {
return new Promise((done) => {
const handler = async (req, res) => {
req.session.set("user", { id: 12421 });
req.session.destroy();
expect(req.session.get()).toMatchInlineSnapshot(`Object {}`);
expect(res.setHeader.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"set-cookie",
Array [
"test=; Max-Age=0; Path=/; HttpOnly; Secure; SameSite=Lax",
],
]
`);
done();
};
const wrappedHandler = withIronSession(handler, {
password,
cookieName,
ttl: 0,
});
wrappedHandler(
{
headers: { cookie: "coucou=true" },
},
{
setHeader: jest.fn(),
getHeader: jest.fn(),
},
);
});
});
test("When trying to use an expired seal", async () => {
const ttl = 100;
const store = await ironStore({ password, ttl: ttl * 1000 });
store.set("user", { id: 20 });
const seal = await store.seal();
function whenNotExpired() {
return new Promise((done) => {
advanceBy(100 * 1000);
const handler = async (req) => {
expect(req.session.get("user")).toMatchInlineSnapshot(`
Object {
"id": 20,
}
`);
done();
};
const wrappedHandler = withIronSession(handler, {
password,
cookieName,
ttl,
});
wrappedHandler(
{
headers: { cookie: `test=${seal}` },
},
{
setHeader: jest.fn(),
},
);
});
}
function whenExpired() {
return new Promise((done) => {
advanceBy(60 * 1000);
const handler = async (req) => {
expect(req.session.get("user")).toMatchInlineSnapshot(`undefined`);
done();
};
const wrappedHandler = withIronSession(handler, {
password,
cookieName,
ttl,
});
wrappedHandler(
{
headers: { cookie: `test=${seal}` },
},
{
setHeader: jest.fn(),
},
);
});
}
return whenNotExpired().then(whenExpired).then(clear);
});
test("It throws Iron errors when passing a wrong password (password length must be >= 32)", async () => {
const handler = async (req, res) => {
await req.session.save();
const headerValue = res.setHeader.mock.calls[0][1];
const cookie = headerValue[0];
const maxAgeParam = cookie.split(";")[1];
expect(maxAgeParam).toMatchInlineSnapshot(`" Max-Age=2147483587"`);
};
const wrappedHandler = withIronSession(handler, {
password: "dsadsadsadsadsadadsa",
cookieName,
ttl: 0,
});
await expect(
wrappedHandler(
{
headers: {
cookie:
"test=Fe26.2**4e769b9b7b921621ed5658cfc0d7d8e267dc8ee93663c2803c257b31111394e3*jRXOJHmt_BDG9nNTXcVRXQ*UHpK9GYp7SXTiEsxTzTUq_tQD_-ZUp7PguEXy-bRFuBE4fW74-9wm9UtlWO2rlwB**d504d6d197d183efec0ae6d3c2378c43048c8752d6c3c591c92289ed01142b3c*3NG2fCo8A53CXPU8rEAMnDB7X9UkwzTaHieumPBqyTw",
},
},
{
setHeader: jest.fn(),
},
),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Password string too short (min 32 characters required)"`,
);
});
test("when no cookies at all", () => {
return new Promise((done) => {
const handler = (req) => {
expect(req.session.set("user", { id: 20 })).toMatchInlineSnapshot(`
Object {
"id": 20,
}
`);
done();
};
const wrappedHandler = withIronSession(handler, { password, cookieName });
wrappedHandler(
{
headers: {},
},
{},
);
});
});
test("When trying to use a wrong seal (example: password was updated server-side without rotation)", async () => {
const firstPassword = "Bb0EyombqcDK58k870btymbGJrgZFrN2";
const secondPassword = "182XhM1mAzottfvzPMN0nh20HMwZprBc";
const store = await ironStore({ password: firstPassword });
store.set("user", { id: 20 });
const seal = await store.seal();
return new Promise((done) => {
const handler = (req) => {
expect(req.session.get("user")).toMatchInlineSnapshot(`undefined`);
done();
};
const wrappedHandler = withIronSession(handler, {
password: secondPassword,
cookieName,
});
wrappedHandler(
{
headers: { cookie: `test=${seal}` },
},
{},
);
});
});
test("moving from <=3.1.2 seals to multi passwords creates a new session", async () => {
return new Promise((done) => {
const handler = (req) => {
expect(req.session.get("user")).toMatchInlineSnapshot(`undefined`);
done();
};
const wrappedHandler = withIronSession(handler, {
password: [{ id: 1, password }],
cookieName,
});
wrappedHandler(
{
headers: {
cookie:
"test=Fe26.2**4e769b9b7b921621ed5658cfc0d7d8e267dc8ee93663c2803c257b31111394e3*jRXOJHmt_BDG9nNTXcVRXQ*UHpK9GYp7SXTiEsxTzTUq_tQD_-ZUp7PguEXy-bRFuBE4fW74-9wm9UtlWO2rlwB**d504d6d197d183efec0ae6d3c2378c43048c8752d6c3c591c92289ed01142b3c*3NG2fCo8A53CXPU8rEAMnDB7X9UkwzTaHieumPBqyTw",
},
},
{},
);
});
});
test("Password rotation", async () => {
const firstPassword = [
{ id: 1, password: "BcTv8NKLVfGcTt18HqGf2DhEnmJrLbNU" },
];
const secondPassword = [
{ id: 2, password: "scKVNPWFippYjA3tRjJPuPnK7ocj4Vnn" },
{ id: 1, password: "BcTv8NKLVfGcTt18HqGf2DhEnmJrLbNU" },
];
const store = await ironStore({ password: firstPassword });
store.set("user", { id: 20 });
const seal = await store.seal();
return new Promise((done) => {
const handler = (req) => {
expect(req.session.get("user")).toMatchInlineSnapshot(`
Object {
"id": 20,
}
`);
done();
};
const wrappedHandler = withIronSession(handler, {
password: secondPassword,
cookieName,
});
wrappedHandler(
{
headers: { cookie: `test=${seal}` },
},
{},
);
});
});
test("Connect middleware ironSession({password, cookieName})", () => {
return new Promise((done) => {
const req = {
headers: { cookie: "sg=1" },
};
const res = { json: function () {} };
const handler = ironSession({ password, cookieName });
handler(req, res, function () {
expect(req).toMatchInlineSnapshot(`
Object {
"headers": Object {
"cookie": "sg=1",
},
"session": Object {
"destroy": [Function],
"get": [Function],
"save": [Function],
"set": [Function],
"unset": [Function],
},
}
`);
expect(res).toMatchInlineSnapshot(`
Object {
"json": [Function],
}
`);
done();
});
});
});
test("Express middleware with error", () => {
return new Promise((done) => {
const req = {
headers: {
cookie:
"test=Fe26.2**4e769b9b7b921621ed5658cfc0d7d8e267dc8ee93663c2803c257b31111394e3*jRXOJHmt_BDG9nNTXcVRXQ*UHpK9GYp7SXTiEsxTzTUq_tQD_-ZUp7PguEXy-bRFuBE4fW74-9wm9UtlWO2rlwB**d504d6d197d183efec0ae6d3c2378c43048c8752d6c3c591c92289ed01142b3c*3NG2fCo8A53CXPU8rEAMnDB7X9UkwzTaHieumPBqyTw",
},
};
const res = {};
const handler = ironSession({
password: "wrong password length",
cookieName,
});
handler(req, res, function (err) {
expect(err).toMatchInlineSnapshot(
`[Error: Password string too short (min 32 characters required)]`,
);
done();
});
});
});
test("applySession(req, res, {password, cookieName})", async () => {
const req = {
headers: { cookie: "sg=1" },
};
const res = { json: function () {} };
await applySession(req, res, { password, cookieName });
expect(req).toMatchInlineSnapshot(`
Object {
"headers": Object {
"cookie": "sg=1",
},
"session": Object {
"destroy": [Function],
"get": [Function],
"save": [Function],
"set": [Function],
"unset": [Function],
},
}
`);
expect(res).toMatchInlineSnapshot(`
Object {
"json": [Function],
}
`);
});
test("applySession(req, res, {cookieName})", async () => {
const req = {};
const res = {};
await expect(async function () {
await applySession(req, res, { cookieName });
}).rejects.toThrowErrorMatchingInlineSnapshot(
`"next-iron-session: Missing parameter \`password\`"`,
);
});
test("applySession(req, res, {password})", async () => {
const req = {};
const res = {};
await expect(async function () {
await applySession(req, res, { password });
}).rejects.toThrowErrorMatchingInlineSnapshot(
`"next-iron-session: Missing parameter \`cookieName\`"`,
);
});
test("ironSession({cookieName})", () => {
expect(function () {
ironSession({ cookieName });
}).toThrowErrorMatchingInlineSnapshot(
`"next-iron-session: Missing parameter \`password\`"`,
);
});
test("ironSession({password})", () => {
expect(function () {
ironSession({ password });
}).toThrowErrorMatchingInlineSnapshot(
`"next-iron-session: Missing parameter \`cookieName\`"`,
);
});
test("it throws when cookie length is too big", () => {
return new Promise((done) => {
const handler = async (req) => {
req.session.set("user", "somevalue".repeat(500));
await expect(async function () {
await req.session.save();
}).rejects.toThrowErrorMatchingInlineSnapshot(
`"next-iron-session: Cookie length is too big 6341, browsers will refuse it"`,
);
done();
};
const wrappedHandler = withIronSession(handler, { password, cookieName });
wrappedHandler(
{
headers: { cookie: "" },
},
{
setHeader: jest.fn(),
},
);
});
});
test("it handles previously set cookies (single value)", () => {
return new Promise((done) => {
const handler = async (req, res) => {
await req.session.save();
const headerValue = res.setHeader.mock.calls[0][1];
expect(headerValue.length).toBe(2);
expect(headerValue[0]).toBe("existingCookie=value");
done();
};
const wrappedHandler = withIronSession(handler, { password, cookieName });
wrappedHandler(
{
headers: { cookie: "" },
},
{
setHeader: jest.fn(),
getHeader: function () {
return "existingCookie=value";
},
},
);
});
});
test("it handles previously set cookies (multiple values) on save()", () => {
return new Promise((done) => {
const handler = async (req, res) => {
await req.session.save();
const headerValue = res.setHeader.mock.calls[0][1];
expect(headerValue.length).toBe(3);
expect(headerValue[0]).toBe("existingCookie=value");
expect(headerValue[1]).toBe("anotherCookie=value2");
done();
};
const wrappedHandler = withIronSession(handler, { password, cookieName });
wrappedHandler(
{
headers: { cookie: "" },
},
{
setHeader: jest.fn(),
getHeader: function () {
return ["existingCookie=value", "anotherCookie=value2"];
},
},
);
});
});
test("it handles previously set cookies (multiple values) on destroy()", () => {
return new Promise((done) => {
const handler = async (req, res) => {
await req.session.destroy();
expect(res.setHeader.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"set-cookie",
Array [
"existingCookie=value",
"anotherCookie=value2",
"test=; Max-Age=0; Path=/; HttpOnly; Secure; SameSite=Lax",
],
]
`);
done();
};
const wrappedHandler = withIronSession(handler, { password, cookieName });
wrappedHandler(
{
headers: { cookie: "" },
},
{
setHeader: jest.fn(),
getHeader: function () {
return ["existingCookie=value", "anotherCookie=value2"];
},
},
);
});
});