add addCookie helper to common utilities
authorJustin Wind <justin.wind+git@gmail.com>
Sun, 3 Mar 2024 21:48:19 +0000 (13:48 -0800)
committerJustin Wind <justin.wind+git@gmail.com>
Sun, 3 Mar 2024 22:14:27 +0000 (14:14 -0800)
CHANGELOG.md
lib/common.js
test/lib/common.js

index e31efa43d3ce5e7a7d344a95f6b13ffeb2116e8a..ad80d3a815f9c96ce9f83ab2aec26d629caeedbd 100644 (file)
@@ -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
 
index 6b72129223cf88e8248706afa5679f853eb5d5ae..162b751ef23805d30bbf97eb71a69f367adf3d59 100644 (file)
@@ -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,
index 6140d874bd9ef41a9591c28194ae772f21c1e3d9..4b10d38965777c2372a4dadbc2fcd446d6af1e75 100644 (file)
@@ -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
+
 });