From 0c4ba588448691056be3bab76cfc478a7f8ca320 Mon Sep 17 00:00:00 2001 From: Justin Wind Date: Sun, 3 Mar 2024 13:48:19 -0800 Subject: [PATCH] add addCookie helper to common utilities --- CHANGELOG.md | 1 + lib/common.js | 61 ++++++++++++++++++++++++++++++++++++++++++++++ test/lib/common.js | 37 ++++++++++++++++++++++++++++ 3 files changed, 99 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e31efa4..ad80d3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Releases and notable changes to this project are documented here. ## [v2.1.0] - TBD - cookies are now parsed and populated into ctx.cookie as a default behavior +- new addCookie common helper - added HTTP status and message enums - updated devDependencies diff --git a/lib/common.js b/lib/common.js index 6b72129..162b751 100644 --- a/lib/common.js +++ b/lib/common.js @@ -194,7 +194,68 @@ const unfoldHeaderLines = (lines) => { return lines; }; +/** + * Adds a new cookie. + * @param {http.ServerResponse} res + * @param {String} name + * @param {String} value + * @param {Object=} opt + * @param {String=} opt.domain + * @param {Date=} opt.expires + * @param {Boolean=} opt.httpOnly + * @param {Number=} opt.maxAge + * @param {String=} opt.path + * @param {String=} opt.sameSite + * @param {Boolean=} opt.secure + */ +function addCookie(res, name, value, opt = {}) { + const options = { + domain: undefined, + expires: undefined, + httpOnly: false, + maxAge: undefined, + path: undefined, + sameSite: undefined, + secure: false, + ...opt, + }; + // TODO: validate name, value + const cookieParts = [ + `${name}=${value}`, + ]; + if (options.domain) { + cookieParts.push(`Domain=${options.domain}`); + } + if (options.expires) { + if (!(options.expires instanceof Date)) { + throw new TypeError('cookie expires must be Date'); + } + cookieParts.push(`Expires=${options.expires.toUTCString()}`); + } + if (options.httpOnly) { + cookieParts.push('HttpOnly'); + } + if (options.maxAge) { + cookieParts.push(`Max-Age=${options.maxAge}`); + } + if (options.path) { + cookieParts.push(`Path=${options.path}`); + } + if (options.sameSite) { + if (!(['Strict', 'Lax', 'None'].includes(options.sameSite))) { + throw new RangeError('cookie sameSite value not valid'); + } + cookieParts.push(`SameSite=${options.sameSite}`); + } + if (options.secure) { + cookieParts.push('Secure'); + } + res.appendHeader(Enum.Header.SetCookie, cookieParts.join('; ')); +} + + module.exports = { + addCookie, fileScope, generateETag, get, diff --git a/test/lib/common.js b/test/lib/common.js index 6140d87..4b10d38 100644 --- a/test/lib/common.js +++ b/test/lib/common.js @@ -351,4 +351,41 @@ describe('common', function () { }); }); // unfoldHeaderLines + describe('addCookie', function () { + let res, name, value; + beforeEach(function () { + res = { + appendHeader: sinon.stub(), + }; + name = 'someCookieName'; + value = 'someCookieValue'; + }); + it('covers no options', function () { + common.addCookie(res, name, value, {}); + assert(res.appendHeader.called); + }); + it('covers all options', function () { + common.addCookie(res, name, value, { + domain: 'example.com', + expires: new Date(), + httpOnly: true, + maxAge: 9999999, + path: '/foo', + sameSite: 'Lax', + secure: true, + }); + assert(res.appendHeader.called); + }); + it('covers invalid expires', function () { + assert.throws(() => common.addCookie(res, name, value, { expires: 'never' }), TypeError); + }); + it('covers invalid sameSite', function () { + assert.throws(() => common.addCookie(res, name, value, { sameSite: 'Whatever' })); + }); + it('covers no options', function () { + common.addCookie(res, name, value); + assert(res.appendHeader.called); + }); + }); // addCookie + }); -- 2.45.2