From d91855200d68e70d4e72de868e9c872c7fcc179b Mon Sep 17 00:00:00 2001 From: Joel Chen Date: Sat, 5 Aug 2017 20:02:56 -0700 Subject: [PATCH] Open-sourcing electrode-cookies --- packages/electrode-cookies/.eslintignore | 2 + packages/electrode-cookies/.eslintrc | 3 + packages/electrode-cookies/.npmignore | 226 +++++++++++++ packages/electrode-cookies/README.md | 112 +++++++ .../electrode-cookies/cookies-js/index.js | 179 ++++++++++ packages/electrode-cookies/hapi-plugin.js | 25 ++ packages/electrode-cookies/lib/csindex.js | 8 + packages/electrode-cookies/lib/index.js | 91 +++++ packages/electrode-cookies/package.json | 48 +++ packages/electrode-cookies/test/.eslintrc | 3 + packages/electrode-cookies/test/mocha.opts | 2 + .../test/spec/csindex.spec.js | 43 +++ .../test/spec/hapi-plugin.spec.js | 20 ++ .../electrode-cookies/test/spec/index.spec.js | 314 ++++++++++++++++++ packages/electrode-cookies/xclap.js | 1 + 15 files changed, 1077 insertions(+) create mode 100644 packages/electrode-cookies/.eslintignore create mode 100644 packages/electrode-cookies/.eslintrc create mode 100644 packages/electrode-cookies/.npmignore create mode 100644 packages/electrode-cookies/README.md create mode 100644 packages/electrode-cookies/cookies-js/index.js create mode 100644 packages/electrode-cookies/hapi-plugin.js create mode 100644 packages/electrode-cookies/lib/csindex.js create mode 100644 packages/electrode-cookies/lib/index.js create mode 100644 packages/electrode-cookies/package.json create mode 100644 packages/electrode-cookies/test/.eslintrc create mode 100644 packages/electrode-cookies/test/mocha.opts create mode 100644 packages/electrode-cookies/test/spec/csindex.spec.js create mode 100644 packages/electrode-cookies/test/spec/hapi-plugin.spec.js create mode 100644 packages/electrode-cookies/test/spec/index.spec.js create mode 100644 packages/electrode-cookies/xclap.js diff --git a/packages/electrode-cookies/.eslintignore b/packages/electrode-cookies/.eslintignore new file mode 100644 index 000000000..62562b74a --- /dev/null +++ b/packages/electrode-cookies/.eslintignore @@ -0,0 +1,2 @@ +coverage +node_modules diff --git a/packages/electrode-cookies/.eslintrc b/packages/electrode-cookies/.eslintrc new file mode 100644 index 000000000..5f6ea6525 --- /dev/null +++ b/packages/electrode-cookies/.eslintrc @@ -0,0 +1,3 @@ +--- +extends: + - "./node_modules/electrode-archetype-njs-module-dev/config/eslint/.eslintrc-node" diff --git a/packages/electrode-cookies/.npmignore b/packages/electrode-cookies/.npmignore new file mode 100644 index 000000000..125cf2bca --- /dev/null +++ b/packages/electrode-cookies/.npmignore @@ -0,0 +1,226 @@ + +# Created by https://www.gitignore.io/api/osx,node + +### OSX ### +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + + +### Node ### +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git +node_modules + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +### Vim template +[._]*.s[a-w][a-z] +[._]s[a-w][a-z] +*.un~ +Session.vim +.netrwhist +*~ +### GitBook template +# Node rules: +## Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +## Dependency directory +## Commenting this out is preferred by some people, see +## https://docs.npmjs.com/misc/faq#should-i-check-my-node_modules-folder-into-git +node_modules + +# Book build output +_book + +# eBook build output +*.epub +*.mobi +*.pdf +### TortoiseGit template +# Project-level settings +/.tgitconfig +### Xcode template +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## Build generated +build/ +DerivedData + +## Various settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata + +## Other +*.xccheckout +*.moved-aside +*.xcuserstate +### Emacs template +# -*- mode: gitignore; -*- +*~ +\#*\# +/.emacs.desktop +/.emacs.desktop.lock +*.elc +auto-save-list +tramp +.\#* + +# Org-mode +.org-id-locations +*_archive + +# flymake-mode +*_flymake.* + +# eshell files +/eshell/history +/eshell/lastdir + +# elpa packages +/elpa/ + +# reftex files +*.rel + +# AUCTeX auto folder +/auto/ + +# cask packages +.cask/ +### OSX template +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio + +*.iml + +## Directory-based project format: +.idea/ +# if you remove the above rule, at least ignore the following: + +# User-specific stuff: +# .idea/workspace.xml +# .idea/tasks.xml +# .idea/dictionaries + +# Sensitive or high-churn files: +# .idea/dataSources.ids +# .idea/dataSources.xml +# .idea/sqlDataSources.xml +# .idea/dynamic.xml +# .idea/uiDesigner.xml + +# Gradle: +# .idea/gradle.xml +# .idea/libraries + +# Mongo Explorer plugin: +# .idea/mongoSettings.xml + +## File-based project format: +*.ipr +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +/clap.js +/.npmignore +.nyc_output diff --git a/packages/electrode-cookies/README.md b/packages/electrode-cookies/README.md new file mode 100644 index 000000000..c3d2b2225 --- /dev/null +++ b/packages/electrode-cookies/README.md @@ -0,0 +1,112 @@ +# Electrode Cookies + +Electrode isomorphic cookies lib. + +## Install + + npm install electrode-cookies --save + +## Usage + +This module offers reading and setting cookies in React code that works in both the browser or when doing Server Side Rendering. + +In your pure server only code, you can also use this module to read and set cookies, but you **MUST** pass the `request` object in the options. Otherwise an assert error will be thrown. + +In NodeJS land: + +```js +const Cookies = require("electrode-cookies"); +``` + +### Reading cookies + +In ReactJS land: + +```js +import Cookies from "electrode-cookies"; +const value = Cookies.get("test-cookie"); +``` + +In NodeJS land: + +> Note the difference is that `request` is passed in options. + +```js +const Cookies = require("electrode-cookies"); +const value = Cookies.get("test-cookie", { request }); +``` + +### Writing cookies + +In ReactJS land: + +```js +import Cookies from "electrode-cookies"; +Cookies.set( "foo", "bar", { path: "/", domain: ".walmart.com" } ); +``` + +In NodeJS land: + +> Note the difference is that `request` is passed in options. + +```js +const Cookies = require("electrode-cookies"); +Cookies.set( "foo", "bar", { request, path: "/", domain: ".walmart.com" } ); +``` + +## Electrode Server Setup + +The cookie writing on server side requires support from a Hapi plugin. If you use [electrode-server], then it should have setup the plugin for you by default. Otherwise, you need to register the [hapi plugin](hapi-plugin.js). + +## APIs + +### [Cookies.get](#cookiesget) + +`Cookies.get(key, [options])` + +Parameters: + +- `key` - name of the cookie +- `options` - (optional) **_Available for Server side only._** options for getting the cookie + - `request` - The server `request` object (**Required on server**). + - `matchSubStr` - If `true`, then do substring matching of key with all cookie keys. + - `skipEncoding` - (applies only if `matchSubStr` is `true`) If `true`, then do not encode the key or decode the value. + +Returns the value of the cookie for `key`. + +### [Cookies.set](#cookiesset) + +`Cookies.set(key, value, [options])` + +Set a cookie with `key` and `value`. + +Parameters: + +- `key` - name of the cookie +- `value` - value of the cookie +- `options` - (optional) options for the cookie + - `request` - On the server side, the `request` object (**Required on server**). + - `path` - string path of the cookie **_Default:_** `"/"` + - `domain` - string domain of the cookie + - `expires` - number of seconds the cookie will expire + - `secure` - A boolean of whether or not the cookie should only be available over SSL **_Default:_** false + - `httpOnly` - A boolean of whether or not the cookie should only be available over HTTP(S) **_Default:_** false + - `forceAuthEncoding` - Forces non-standard encoding for `+` and `/` characters, use with auth cookies. + - `skipEncoding` - Skip encoding/escaping of the cookie value. See [source](https://gecgithub01.walmart.com/electrode/electrode-cookies/blob/master/lib/index.js) for details. + +### [Cookies.expire](#cookiesexpire) + +`Cookies.expire(key, [options])` + +Expires a cookie specified by `key`. + +Parameters: + +- `key` - name of the cookie +- `options` - (optional) options for the cookie + - `path` - string path of the cookie **_Default:_** `"/"` + - `domain` - string domain of the cookie + - `secure` - A boolean of whether or not the cookie should only be available over SSL **_Default:_** false + - `request` - The server request object (**Required on server**) + +[electrode-server]: https://gecgithub01.walmart.com/electrode/electrode-server diff --git a/packages/electrode-cookies/cookies-js/index.js b/packages/electrode-cookies/cookies-js/index.js new file mode 100644 index 000000000..60b3603bc --- /dev/null +++ b/packages/electrode-cookies/cookies-js/index.js @@ -0,0 +1,179 @@ +/* + * Cookies.js - 1.2.2 + * https://github.com/ScottHamper/Cookies + * + * This is free and unencumbered software released into the public domain. + */ +(function(global, undefined) { + "use strict"; + + var factory = function(window) { + if (typeof window.document !== "object") { + throw new Error("Cookies.js requires a `window` with a `document` object"); + } + + var Cookies = function(key, value, options) { + return arguments.length === 1 ? Cookies.get(key) : Cookies.set(key, value, options); + }; + + // Allows for setter injection in unit tests + Cookies._document = window.document; + + // Used to ensure cookie keys do not collide with + // built-in `Object` properties + Cookies._cacheKeyPrefix = "cookey."; // Hurr hurr, :) + + Cookies._maxExpireDate = new Date("Fri, 31 Dec 9999 23:59:59 UTC"); + + Cookies.defaults = { + path: "/", + secure: false + }; + + Cookies.get = function(key) { + if (Cookies._cachedDocumentCookie !== Cookies._document.cookie) { + Cookies._renewCache(); + } + + var value = Cookies._cache[Cookies._cacheKeyPrefix + key]; + + return value === undefined ? undefined : decodeURIComponent(value); + }; + + Cookies.set = function(key, value, options) { + options = Cookies._getExtendedOptions(options); + options.expires = Cookies._getExpiresDate(value === undefined ? -1 : options.expires); + + Cookies._document.cookie = Cookies._generateCookieString(key, value, options); + + return Cookies; + }; + + Cookies.expire = function(key, options) { + return Cookies.set(key, undefined, options); + }; + + Cookies._getExtendedOptions = function(options) { + return { + path: (options && options.path) || Cookies.defaults.path, + domain: (options && options.domain) || Cookies.defaults.domain, + expires: (options && options.expires) || Cookies.defaults.expires, + secure: options && options.secure !== undefined ? options.secure : Cookies.defaults.secure, + skipEncoding: options && options.skipEncoding + }; + }; + + Cookies._isValidDate = function(date) { + return Object.prototype.toString.call(date) === "[object Date]" && !isNaN(date.getTime()); + }; + + Cookies._getExpiresDate = function(expires, now) { + now = now || new Date(); + + if (typeof expires === "number") { + expires = + expires === Infinity ? Cookies._maxExpireDate : new Date(now.getTime() + expires * 1000); + } else if (typeof expires === "string") { + expires = new Date(expires); + } + + if (expires && !Cookies._isValidDate(expires)) { + throw new Error("`expires` parameter cannot be converted to a valid Date instance"); + } + + return expires; + }; + + Cookies._generateCookieString = function(key, value, options) { + options = options || {}; + + if (!options.skipEncoding) { + key = key.replace(/[^#$&+\^`|]/g, encodeURIComponent); + key = key.replace(/\(/g, "%28").replace(/\)/g, "%29"); + // \--9 being exclude chars - to 9, with 9 being before : + value = (value + "").replace(/[^!#&-+\--9<-\[\]-~]/g, encodeURIComponent); + } + + var cookieString = key + "=" + value; + cookieString += options.path ? ";path=" + options.path : ""; + cookieString += options.domain ? ";domain=" + options.domain : ""; + cookieString += options.expires ? ";expires=" + options.expires.toUTCString() : ""; + cookieString += options.secure ? ";secure" : ""; + + return cookieString; + }; + + Cookies._getCacheFromString = function(documentCookie) { + var cookieCache = {}; + var cookiesArray = documentCookie ? documentCookie.split("; ") : []; + + for (var i = 0; i < cookiesArray.length; i++) { + var cookieKvp = Cookies._getKeyValuePairFromCookieString(cookiesArray[i]); + + if (cookieCache[Cookies._cacheKeyPrefix + cookieKvp.key] === undefined) { + cookieCache[Cookies._cacheKeyPrefix + cookieKvp.key] = cookieKvp.value; + } + } + + return cookieCache; + }; + + Cookies._getKeyValuePairFromCookieString = function(cookieString) { + // "=" is a valid character in a cookie value according to RFC6265, so cannot `split('=')` + var separatorIndex = cookieString.indexOf("="); + + // IE omits the "=" when the cookie value is an empty string + separatorIndex = separatorIndex < 0 ? cookieString.length : separatorIndex; + + var key = cookieString.substr(0, separatorIndex); + var decodedKey; + try { + decodedKey = decodeURIComponent(key); + } catch (e) { + if (console && typeof console.error === "function") { + console.error('Could not decode cookie with key "' + key + '"', e); + } + } + + return { + key: decodedKey, + value: cookieString.substr(separatorIndex + 1) // Defer decoding value until accessed + }; + }; + + Cookies._renewCache = function() { + Cookies._cache = Cookies._getCacheFromString(Cookies._document.cookie); + Cookies._cachedDocumentCookie = Cookies._document.cookie; + }; + + Cookies._areEnabled = function() { + var testKey = "cookies.js"; + var areEnabled = Cookies.set(testKey, 1).get(testKey) === "1"; + Cookies.expire(testKey); + return areEnabled; + }; + + Cookies.enabled = Cookies._areEnabled(); + + return Cookies; + }; + + var cookiesExport = typeof global.document === "object" ? factory(global) : factory; + + // AMD support + if (typeof define === "function" && define.amd) { + define(function() { + return cookiesExport; + }); + // CommonJS/Node.js support + } else if (typeof exports === "object") { + // Support Node.js specific `module.exports` (which can be a function) + if (typeof module === "object" && typeof module.exports === "object") { + exports = module.exports = cookiesExport; + } + // But always support CommonJS module 1.1.1 spec (`exports` cannot be a function) + exports.Cookies = cookiesExport; + } else { + global.Cookies = cookiesExport; + } +})(typeof window === "undefined" ? this : window); diff --git a/packages/electrode-cookies/hapi-plugin.js b/packages/electrode-cookies/hapi-plugin.js new file mode 100644 index 000000000..49a13cb05 --- /dev/null +++ b/packages/electrode-cookies/hapi-plugin.js @@ -0,0 +1,25 @@ +"use strict"; + +var each = require("lodash/each"); + +function electrodeCookiesRegister(server, options, next) { + server.ext("onPreResponse", (request, reply) => { + if (request.app.replyStates) { + each(request.app.replyStates, (state, name) => { + reply.state(name, state.value, state.options); + }); + } + + return reply.continue(); + }); + + next(); +} + +electrodeCookiesRegister.attributes = { + pkg: require("./package.json") +}; + +module.exports = { + register: electrodeCookiesRegister +}; diff --git a/packages/electrode-cookies/lib/csindex.js b/packages/electrode-cookies/lib/csindex.js new file mode 100644 index 000000000..28da4c64e --- /dev/null +++ b/packages/electrode-cookies/lib/csindex.js @@ -0,0 +1,8 @@ +/* eslint-disable */ +var cookies = require("../cookies-js"); + +module.exports = { + get: cookies.get, + set: cookies.set, + expire: cookies.expire +}; diff --git a/packages/electrode-cookies/lib/index.js b/packages/electrode-cookies/lib/index.js new file mode 100644 index 000000000..247c58400 --- /dev/null +++ b/packages/electrode-cookies/lib/index.js @@ -0,0 +1,91 @@ +"use strict"; + +const safeGet = require("lodash/get"); +const reduce = require("lodash/reduce"); +const assert = require("assert"); + +const replacers = { "(": "%28", ")": "%29" }; + +const encodeKey = key => + key.replace(/[^#$&+\^`|]/g, encodeURIComponent).replace(/[\(\)]/g, m => replacers[m]); + +const cookies = { + get: (key, options) => { + options = options || {}; + assert(options.request, "The request option is not set"); + + if (options.matchSubStr) { + const substring = options.skipEncoding === true ? key : encodeKey(key); + + const NOT_FOUND = -1; + + try { + return reduce( + options.request.state, + (result, value, k) => { + if (k.indexOf(substring) > NOT_FOUND) { + result[k] = + options.skipEncoding === true || value === undefined + ? value + : decodeURIComponent(value); + } + return result; + }, + {} + ); + } catch (err) { + return null; + } + } + + try { + const value = options.request.state[encodeKey(key)]; + return value === undefined ? undefined : decodeURIComponent(value); + } catch (err) { + return null; + } + }, + + set: (key, value, options) => { + options = options || {}; + assert(options.request, "The request option is not set"); + + const MSEC = 1000; + + const setOptions = { + path: options.path || "/", + ttl: options.expires && options.expires * MSEC, + isHttpOnly: options.httpOnly, + isSecure: options.secure, + domain: options.domain, + strictHeader: safeGet(options, "strictHeader", true) + }; + + const request = options.request; + + if (!request.app.replyStates) { + request.app.replyStates = {}; + } + + if (options.skipEncoding !== true) { + key = encodeKey(key); + value = (typeof value === "string" ? value : JSON.stringify(value)) + // \--9 being exclude chars - to 9, with 9 being before : + .replace(/[^!#&-+\--9<-\[\]-~]/g, encodeURIComponent); + } + + if (options.forceAuthEncoding) { + value = value.replace(/[+/]/g, encodeURIComponent); + } + + request.app.replyStates[key] = { value, options: setOptions }; + }, + + expire: (key, options) => { + options = options || {}; + options.expires = 0; + cookies.set(key, "x", options); + } +}; + +module.exports = cookies; diff --git a/packages/electrode-cookies/package.json b/packages/electrode-cookies/package.json new file mode 100644 index 000000000..6df793e85 --- /dev/null +++ b/packages/electrode-cookies/package.json @@ -0,0 +1,48 @@ +{ + "name": "electrode-cookies", + "version": "2.0.0", + "description": "Electrode ISO cookies lib", + "main": "lib/index.js", + "browser": "lib/csindex.js", + "scripts": { + "test": "clap check", + "format": "prettier --write --print-width 100 *.js `find . -type d -d 1 -exec echo '{}/**/*.js' \\; | egrep -v '(/node_modules/|/dist/|/coverage/)'`" + }, + "repository": { + "type": "git", + "url": "https://github.com/electrode-io/electrode.git" + }, + "keywords": [], + "license": "Apache-2.0", + "dependencies": { + "lodash": "^4.14.1" + }, + "devDependencies": { + "bluebird": "^2.10.2", + "electrode-archetype-njs-module-dev": "^2.2.0", + "electrode-server": "^1.2.2", + "eslint-config-prettier": "^2.3.0", + "jsdom": "^9.4.2", + "jsdom-global": "^2.0.0", + "mock-require": "^1.3.0", + "prettier": "^1.5.3", + "set-cookie-parser": "^1.0.1", + "superagent": "^1.7.2", + "xclap": "^0.2.0" + }, + "nyc": { + "all": true, + "reporter": [ + "lcov", + "text", + "text-summary" + ], + "exclude": [ + "coverage", + "*clap.js", + "gulpfile.js", + "dist", + "test" + ] + } +} diff --git a/packages/electrode-cookies/test/.eslintrc b/packages/electrode-cookies/test/.eslintrc new file mode 100644 index 000000000..e9da91c41 --- /dev/null +++ b/packages/electrode-cookies/test/.eslintrc @@ -0,0 +1,3 @@ +--- +extends: + - "../node_modules/electrode-archetype-njs-module-dev/config/eslint/.eslintrc-test" \ No newline at end of file diff --git a/packages/electrode-cookies/test/mocha.opts b/packages/electrode-cookies/test/mocha.opts new file mode 100644 index 000000000..022f99b50 --- /dev/null +++ b/packages/electrode-cookies/test/mocha.opts @@ -0,0 +1,2 @@ +--require node_modules/electrode-archetype-njs-module-dev/config/test/setup.js +--recursive diff --git a/packages/electrode-cookies/test/spec/csindex.spec.js b/packages/electrode-cookies/test/spec/csindex.spec.js new file mode 100644 index 000000000..5aeaf167c --- /dev/null +++ b/packages/electrode-cookies/test/spec/csindex.spec.js @@ -0,0 +1,43 @@ +"use strict"; + +const cleanup = require("jsdom-global")(); +const mockReq = require("mock-require"); +const expect = require("chai").expect; + +describe("csindex", () => { + let csIndex; + beforeEach(() => { + mockReq.reRequire("../../cookies-js"); + mockReq.reRequire("../../lib/csindex"); + csIndex = require("../../lib/csindex"); + }); + + after(() => { + cleanup(); + }); + + it(".set should encode value", function() { + const document = global.document; + const key = "($;enc:)"; + const value = "i$xx:x;"; + csIndex.set(key, value); + let cookie = document.cookie; + expect(cookie).includes("%28$%3Benc%3A%29=i%24xx%3Ax%3B"); + const verifyValue = csIndex.get(key); + expect(verifyValue).to.equal(value); + csIndex.expire(key); + cookie = document.cookie; + expect(cookie).to.be.empty; + }); + + it(".set should honor skipEncoding option", function() { + const document = global.document; + const key = "!enc:)"; + const value = "i$xx:x"; + csIndex.set(key, value, { skipEncoding: true }); + const cookie = document.cookie; + expect(cookie).to.equal(`${key}=${value}`); + const verifyValue = csIndex.get(key); + expect(verifyValue).to.equal(value); + }); +}); diff --git a/packages/electrode-cookies/test/spec/hapi-plugin.spec.js b/packages/electrode-cookies/test/spec/hapi-plugin.spec.js new file mode 100644 index 000000000..12afc8806 --- /dev/null +++ b/packages/electrode-cookies/test/spec/hapi-plugin.spec.js @@ -0,0 +1,20 @@ +"use strict"; + +const expect = require("chai").expect; +const cookiesPlugin = require("../../hapi-plugin"); + +describe("Hapi plugin", () => { + it("should handle request.app.replyStates not being set", done => { + const request = { app: {} }; + const reply = { continue: () => {} }; + const server = { + ext: (event, handler) => { + expect(event).to.equal("onPreResponse"); + handler(request, reply); + done(); + } + }; + + cookiesPlugin.register(server, {}, () => {}); + }); +}); diff --git a/packages/electrode-cookies/test/spec/index.spec.js b/packages/electrode-cookies/test/spec/index.spec.js new file mode 100644 index 000000000..bc7f0b7d1 --- /dev/null +++ b/packages/electrode-cookies/test/spec/index.spec.js @@ -0,0 +1,314 @@ +"use strict"; + +/* eslint-disable quotes, max-len */ + +const electrodeServer = require("electrode-server"); +const superAgent = require("superagent"); +const Cookies = require("../../"); +const CookieParser = require("set-cookie-parser"); +const _ = require("lodash"); +const expect = require("chai").expect; + +function makeConfig() { + return { + services: { + autoInit: false, + autoDiscovery: false + }, + plugins: { + "electrode-cookies/hapi-plugin": { + register: require("../../hapi-plugin.js") + } + } + }; +} + +describe("cookies", function() { + this.timeout(5000); + + let currentServer; + + const startServer = config => { + return electrodeServer(config).tap(server => { + currentServer = server; + }); + }; + + afterEach(done => { + if (currentServer) { + currentServer.stop(err => { + currentServer = undefined; + done(err); + }); + } else { + done(); + } + }); + + it("should set cookie", () => { + const handler = (request, reply) => { + Cookies.set("test", "bar", { + path: "/", + expires: 0, + secure: true, + domain: ".walmart.com", + httpOnly: false, + request + }); + Cookies.set("test2", "bar2", { + path: "/test", + expires: 1000, + secure: false, + domain: "x.walmart.com", + httpOnly: true, + request + }); + Cookies.set("test3", "bar3", { + expires: 50000, + secure: false, + request + }); + Cookies.set("test4", "bar4", { request }); + Cookies.expire("test5", { request }); + Cookies.set("($;enc:)", "(i$xx:x;)", { + expires: 50000, + secure: false, + request + }); + Cookies.set("----", { test: "12345", flag: true }, { request }); + const test1 = Cookies.get("test1", { request }); + Cookies.set("affiliate", "reflectorid=123:wmlspartner=wmlspartnerID:lastupd=9876630", { + strictHeader: false, + skipEncoding: true, + request + }); + Cookies.set("reflector", `"reflectorid=9834123:lastupd=98765:firstcreate=87654"`, { + strictHeader: false, + skipEncoding: true, + request + }); + Cookies.set("($!enc:)", "(i$xx:x)", { + expires: 50000, + secure: false, + strictHeader: false, + skipEncoding: true, + request + }); + Cookies.set("plusforwardslash", "+/", { + strictHeader: false, + forceAuthEncoding: true, + request + }); + reply({ test1, now: Date.now() }); + }; + + const serverConfig = makeConfig(); + + return startServer(serverConfig) + .then(server => { + server.route({ + method: "get", + path: "/test", + handler + }); + + return new Promise((resolve, reject) => { + superAgent("http://localhost:3000/test") + .set("cookie", "test1=hello") + .end((err, response) => { + return err ? reject(err) : resolve(response); + }); + }); + }) + .then(response => { + expect(response.body).to.have.keys(["test1", "now"]); + expect(response.headers["set-cookie"]).to.be.an("array").with.length(11); + + const cookies = CookieParser.parse(response.headers["set-cookie"]); + + const verifyCookie = data => { + const c = _.find(cookies, x => x.name === data.name); + expect(c, `No cookie found with name "${data.name}"`).to.exist; + if (data.hasOwnProperty("maxAge")) { + const expires = data.maxAge > 0 ? response.body.now + data.maxAge * 1000 : 0; + expect(c.expires.toGMTString()).to.equal(new Date(expires).toGMTString()); + delete c.expires; + } + expect( + _.pick(c, "name", "value", "path", "maxAge", "domain", "secure", "httpOnly") + ).to.deep.equal(data); + }; + + verifyCookie({ + name: "test", + value: "bar", + path: "/", + maxAge: 0, + domain: ".walmart.com", + secure: true + }); + verifyCookie({ + name: "test2", + value: "bar2", + path: "/test", + maxAge: 1000, + domain: "x.walmart.com", + httpOnly: true + }); + verifyCookie({ + name: "test3", + value: "bar3", + path: "/", + maxAge: 50000 + }); + verifyCookie({ + name: "test4", + value: "bar4", + path: "/" + }); + verifyCookie({ + name: "test5", + value: "x", + path: "/", + maxAge: 0 + }); + verifyCookie({ + name: "%28$%3Benc%3A%29", + value: "(i%24xx%3Ax%3B)", + path: "/", + maxAge: 50000 + }); + verifyCookie({ + name: "----", + value: "{%22test%22%3A%2212345%22%2C%22flag%22%3Atrue}", + path: "/" + }); + verifyCookie({ + name: "affiliate", + value: "reflectorid=123:wmlspartner=wmlspartnerID:lastupd=9876630", + path: "/" + }); + verifyCookie({ + name: "reflector", + value: `"reflectorid=9834123:lastupd=98765:firstcreate=87654"`, + path: "/" + }); + verifyCookie({ + name: "($!enc:)", + value: "(i$xx:x)", + path: "/", + maxAge: 50000 + }); + verifyCookie({ + name: "plusforwardslash", + value: "%2B%2F", + path: "/" + }); + + expect(response.body.test1).to.equal("hello"); + }); + }); + + it("should get cookie", () => { + const handler = (request, reply) => { + try { + expect(Cookies.get("test", { request })).to.equal("bar"); + expect(Cookies.get("test2", { request })).to.equal("bar2"); + expect(Cookies.get("test3", { request })).to.equal("bar3"); + expect(Cookies.get("test4", { request })).to.equal("bar4"); + expect(Cookies.get("test5", { request })).to.equal(""); + expect(Cookies.get("($;enc:)", { request })).to.equal("(i$xx:x;)"); + expect(Cookies.get("----", { request })).to.equal(`{"test":"12345","flag":true}`); + expect(Cookies.get("qwer", { request })).to.equal(undefined); + expect(Cookies.get("AID", { request })).to.equal( + "wmlspartner=wmtlabs:reflectorid=0085370:lastupd=146984" + ); + expect(Cookies.get("com.wm.reflector", { request })).to.equal( + "wmlspartner:abcd@lastupd:456@reflectorid:qwerty" + ); + reply({ now: Date.now() }); + } catch (err) { + reply(err.toString()).code(500); + } + }; + + const serverConfig = makeConfig(); + + return startServer(serverConfig).then(server => { + server.route({ + method: "get", + path: "/test", + handler + }); + + return new Promise((resolve, reject) => { + superAgent("http://localhost:3000/test") + .set( + "cookie", + 'com.wm.reflector="wmlspartner:abcd@lastupd:456@reflectorid:qwerty";AID=wmlspartner%3Dwmtlabs%3Areflectorid%3D0085370%3Alastupd%3D146984;test=bar;test2=bar2;test3=bar3;test4=bar4;test5=;%28$%3Benc%3A%29=(i%24xx%3Ax%3B);----={%22test%22%3A%2212345%22%2C%22flag%22%3Atrue};' + ) // eslint-disable-line + .end(err => { + return err ? reject(err) : resolve(); + }); + }); + }); + }); + + it("should get cookie by matching substring", () => { + const handler = (request, reply) => { + try { + expect(Cookies.get("te", { matchSubStr: true, request })).to.deep.equal({ + test: "bar", + test2: "bar2", + test3: "bar3", + test4: "bar4", + test5: "" + }); + expect(Cookies.get("!$key", { skipEncoding: true, request })).to.equal("(i$xx:x;)"); + expect(Cookies.get("!$k", { matchSubStr: true, request })).to.deep.equal({ + "!$key": "(i$xx:x;)" + }); + expect( + Cookies.get("%25!$foo", { matchSubStr: true, skipEncoding: true, request }) + ).to.deep.equal({ "%25!$foo-key": "%%%%" }); + expect(Cookies.get("----", { request })).to.equal(`{"test":"12345","flag":true}`); + + expect(Cookies.get("qwer", { request })).to.be.undefined; + expect(Cookies.get("AID", { request })).to.equal( + "wmlspartner=wmtlabs:reflectorid=0085370:lastupd=146984" + ); + expect(Cookies.get("com.wm.reflector", { request })).to.equal( + "wmlspartner:abcd@lastupd:456@reflectorid:qwerty" + ); + reply({ now: Date.now() }); + } catch (err) { + reply(err.toString()).code(500); + } + }; + + const serverConfig = makeConfig(); + + return startServer(serverConfig).then(server => { + server.route({ + method: "get", + path: "/test", + handler + }); + + return new Promise((resolve, reject) => { + superAgent("http://localhost:3000/test") + .set( + "cookie", + 'com.wm.reflector="wmlspartner:abcd@lastupd:456@reflectorid:qwerty";AID=wmlspartner%3Dwmtlabs%3Areflectorid%3D0085370%3Alastupd%3D146984;test=bar;test2=bar2;test3=bar3;test4=bar4;test5=;!$key=(i%24xx%3Ax%3B);----={%22test%22%3A%2212345%22%2C%22flag%22%3Atrue};%25!$foo-key=%%%%' + ) // eslint-disable-line + .end(err => { + return err ? reject(err) : resolve(); + }); + }); + }); + }); + + it("should throw when request not passed in options", () => { + expect(() => Cookies.set("test", "value")).to.throw(); + }); +}); diff --git a/packages/electrode-cookies/xclap.js b/packages/electrode-cookies/xclap.js new file mode 100644 index 000000000..ea371779c --- /dev/null +++ b/packages/electrode-cookies/xclap.js @@ -0,0 +1 @@ +require("electrode-archetype-njs-module-dev")();