├── .eslintignore
├── .npmignore
├── .eslintrc.js
├── smoke-test
├── seed_data.txt
├── express
│ ├── app.js
│ ├── package.json
│ └── bin
│ │ └── www
├── test.bats
└── run.sh
├── errors
└── ember-cli-deploy-error.js
├── .travis.yml
├── .gitmodules
├── index.js
├── test
├── helpers
│ └── test-api.js
├── index-test.js
└── fetch-test.js
├── .gitignore
├── package.json
├── UPGRADING.md
├── fetch.js
├── CHANGELOG.md
└── README.md
/.eslintignore:
--------------------------------------------------------------------------------
1 | smoke-test/express
2 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | /test
2 | /smoke-test
3 | .eslintrc.js
4 | .travis.yml
5 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "env": {
3 | "es6": true,
4 | "node": true
5 | },
6 | "extends": "eslint:recommended",
7 | "plugins": [
8 | "mocha"
9 | ],
10 | };
11 |
--------------------------------------------------------------------------------
/smoke-test/seed_data.txt:
--------------------------------------------------------------------------------
1 | FLUSHALL
2 | SET myapp:index:abc123 "
this is abc123"
3 | SET myapp:index:def456 "this is def456"
4 | SET myapp:index:current "abc123"
5 |
--------------------------------------------------------------------------------
/smoke-test/express/app.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const app = express();
3 |
4 | var nodeEmberCliDeployRedis = require('../../');
5 |
6 | app.use('/*', nodeEmberCliDeployRedis('myapp:index', {
7 | host: '127.0.0.1'
8 | }));
9 |
10 | module.exports = app;
11 |
--------------------------------------------------------------------------------
/smoke-test/express/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "node-ember-cli-deploy-redis-smoke-test",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "start": "node ./bin/www"
7 | },
8 | "dependencies": {
9 | "express": "~4.16.0",
10 | "debug": "^4.1.0"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/errors/ember-cli-deploy-error.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = function EmberCliDeployError(message, critical) {
4 | Error.captureStackTrace(this, this.constructor);
5 | this.name = this.constructor.name;
6 | this.message = message;
7 | this.critical = critical;
8 | };
9 |
10 | require('util').inherits(module.exports, Error);
11 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | ---
2 | language: node_js
3 | node_js:
4 | - "6"
5 | - "8"
6 | - "10"
7 | services:
8 | - docker
9 |
10 | cache:
11 | directories:
12 | - node_modules
13 |
14 | jobs:
15 | include:
16 | - stage: lint
17 | node_js: "10"
18 | script: eslint .
19 |
20 | stages:
21 | - lint # fast fail if linting does not pass
22 | - test
23 |
24 | script:
25 | - npm test && npm run smoke-test
26 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "smoke-test/test-helper/bats-support"]
2 | path = smoke-test/test-helper/bats-support
3 | url = https://github.com/ztombol/bats-support
4 | [submodule "smoke-test/test-helper/bats-assert"]
5 | path = smoke-test/test-helper/bats-assert
6 | url = https://github.com/ztombol/bats-assert
7 | [submodule "smoke-test/test-helper/bats"]
8 | path = smoke-test/test-helper/bats
9 | url = https://github.com/sstephenson/bats
10 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const fetchIndex = require('./fetch');
4 |
5 | module.exports = function (keyPrefix, connectionInfo, opts) {
6 | return function(req, res, next) {
7 | return new Promise(function (resolve) {
8 | fetchIndex(req, keyPrefix, connectionInfo, opts).then(function(indexHtml) {
9 | res.status(200).send(indexHtml);
10 | resolve();
11 | }).catch(function(err) {
12 | next(err);
13 | });
14 | });
15 | };
16 | };
17 |
--------------------------------------------------------------------------------
/test/helpers/test-api.js:
--------------------------------------------------------------------------------
1 | const ioRedisClientApi = {
2 | _storage: {},
3 | get: function(key){
4 | return Promise.resolve(this._storage[key]);
5 | },
6 | set: function(key, value){
7 | this._storage[key] = value;
8 | return Promise.resolve(value);
9 | },
10 | del: function(key){
11 | delete this._storage[key];
12 | return Promise.resolve();
13 | },
14 | flushall: function(){
15 | this._storage = {};
16 | return Promise.resolve();
17 | }
18 | };
19 |
20 | const ioRedisApi = function() {
21 | return ioRedisClientApi;
22 | };
23 |
24 | module.exports = {
25 | ioRedisClientApi: ioRedisClientApi,
26 | ioRedisApi: ioRedisApi
27 | };
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | # Runtime data
7 | pids
8 | *.pid
9 | *.seed
10 |
11 | # Directory for instrumented libs generated by jscoverage/JSCover
12 | lib-cov
13 |
14 | # Coverage directory used by tools like istanbul
15 | coverage
16 |
17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
18 | .grunt
19 |
20 | # node-waf configuration
21 | .lock-wscript
22 |
23 | # Compiled binary addons (http://nodejs.org/api/addons.html)
24 | build/Release
25 |
26 | # Dependency directory
27 | node_modules
28 |
29 | # Optional npm cache directory
30 | .npm
31 |
32 | # Optional REPL history
33 | .node_repl_history
34 |
35 | # Smoke Testing
36 | smoke-test/express/node_modules
37 | smoke-test/express/package-lock.json
38 |
--------------------------------------------------------------------------------
/smoke-test/test.bats:
--------------------------------------------------------------------------------
1 | load "$BATS_TEST_DIRNAME/test-helper/bats-support/load.bash"
2 | load "$BATS_TEST_DIRNAME/test-helper/bats-assert/load.bash"
3 |
4 | setup() {
5 | # See https://redis.io/topics/mass-insert for more information
6 | docker cp "$BATS_TEST_DIRNAME/seed_data.txt" $DOCKER_CONTAINER_NAME:/data
7 | docker exec $DOCKER_CONTAINER_NAME cat -- seed_data.txt | redis-cli --pipe
8 | }
9 |
10 | @test "it returns the current index key by default" {
11 | run curl -s localhost:3000
12 | assert_success
13 | assert_output 'this is abc123'
14 | }
15 |
16 | @test "it returns another index key when specified" {
17 | run curl -s localhost:3000/?index_key=def456
18 | assert_success
19 | assert_output 'this is def456'
20 | }
21 |
22 | @test "it returns a 500 when the specified key is not found in redis" {
23 | run curl -s localhost:3000/?index_key=ghi789
24 | assert_success
25 | assert_output --partial "Internal Server Error"
26 | }
27 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "node-ember-cli-deploy-redis",
3 | "version": "1.0.1",
4 | "description": "An ExpressJS middleware to serve EmberJS apps deployed by ember-cli-deploy",
5 | "main": "index.js",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/blimmer/node-ember-cli-deploy-redis.git"
9 | },
10 | "keywords": [
11 | "ember-cli-deploy",
12 | "express",
13 | "redis"
14 | ],
15 | "engines": {
16 | "node": "^6 || ^8 || >=10"
17 | },
18 | "scripts": {
19 | "test": "NODE_ENV=test ./node_modules/.bin/mocha ./test/**/*-test.js",
20 | "smoke-test": "./smoke-test/run.sh"
21 | },
22 | "author": "Ben Limmer (http://benlimmer.com/)",
23 | "license": "ISC",
24 | "bugs": {
25 | "url": "https://github.com/blimmer/node-ember-cli-deploy-redis/issues"
26 | },
27 | "homepage": "https://github.com/blimmer/node-ember-cli-deploy-redis",
28 | "devDependencies": {
29 | "chai": "^4.2.0",
30 | "eslint": "^5.9.0",
31 | "eslint-plugin-mocha": "^5.2.0",
32 | "mocha": "^5.2.0",
33 | "node-mocks-http": "^1.5.2",
34 | "rewire": "^4.0.1",
35 | "sinon": "^7.3.2",
36 | "sinon-chai": "^3.3.0"
37 | },
38 | "dependencies": {
39 | "ioredis": "^4.2.0",
40 | "lodash": "^4.7.0",
41 | "memoizee": "^0.4.14"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/smoke-test/run.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 |
3 | set -e
4 |
5 | # This script starts up a redis database and seeds it with some fixture data
6 | # to do a quick smoke-test that things are working as expected.
7 |
8 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
9 |
10 | REDIS_DOCKER_IMAGE='redis:5-alpine'
11 | DOCKER_CONTAINER_NAME='node-ember-cli-deploy-redis-smoke-test'
12 | EXPRESS_APP_PID=''
13 |
14 | _start-redis() {
15 | echo "Starting redis container..."
16 | docker run -p 6379:6379 --name $DOCKER_CONTAINER_NAME -d $REDIS_DOCKER_IMAGE
17 | }
18 |
19 | _start-express-app() {
20 | echo "Starting express app..."
21 | pushd "$SCRIPT_DIR/express"
22 | npm install --no-package-lock
23 | NODE_ENV=production npm start &
24 | EXPRESS_APP_PID=$!
25 |
26 | echo "Waiting for express app to start up..."
27 | while ! nc -z localhost 3000; do
28 | sleep 0.1
29 | done
30 | echo "Express app is started!"
31 |
32 | popd
33 | }
34 |
35 | # If this gets more complex, we might want to use BATS
36 | # https://github.com/sstephenson/bats
37 | _test() {
38 | echo "Running tests..."
39 | DOCKER_CONTAINER_NAME=$DOCKER_CONTAINER_NAME "$SCRIPT_DIR"/test-helper/bats/bin/bats "$SCRIPT_DIR"/test.bats
40 | }
41 |
42 | _stop-redis() {
43 | echo "Killing redis container..."
44 | docker stop $DOCKER_CONTAINER_NAME > /dev/null
45 | docker rm $DOCKER_CONTAINER_NAME > /dev/null
46 | }
47 |
48 | _stop-express-app() {
49 | if [ $EXPRESS_APP_PID != "" ]; then
50 | echo "Killing express app..."
51 | kill $EXPRESS_APP_PID
52 | fi
53 | }
54 |
55 | _cleanup() {
56 | _stop-express-app
57 | _stop-redis
58 | }
59 |
60 | main() {
61 | _start-redis
62 | _start-express-app
63 | _test
64 | }
65 |
66 | trap _cleanup EXIT
67 | main
68 |
--------------------------------------------------------------------------------
/test/index-test.js:
--------------------------------------------------------------------------------
1 | var expect = require('chai').expect;
2 | var { describe, before, beforeEach, afterEach, it } = require('mocha');
3 |
4 | var sinon = require('sinon');
5 | var httpMocks = require('node-mocks-http');
6 | var rewire = require('rewire');
7 |
8 | var middleware = rewire('../index');
9 | var EmberCliDeployError = require('../errors/ember-cli-deploy-error');
10 |
11 | var htmlString = '1';
12 |
13 | describe('express middleware', function() {
14 | var sandbox, req, res, fetchIndexStub;
15 | before(function() {
16 | sandbox = sinon.createSandbox();
17 | });
18 |
19 | beforeEach(function() {
20 | req = httpMocks.createRequest();
21 | res = httpMocks.createResponse();
22 |
23 | fetchIndexStub = sandbox.stub();
24 | middleware.__set__('fetchIndex', fetchIndexStub);
25 | });
26 |
27 | afterEach(function() {
28 | sandbox.restore();
29 | });
30 |
31 | describe('success', function() {
32 | it('returns a 200 and sends the html', function(done) {
33 | fetchIndexStub.returns(Promise.resolve(htmlString));
34 |
35 | middleware()(req, res).then(function() {
36 | expect(res.statusCode).to.equal(200);
37 | var data = res._getData();
38 | expect(data).to.equal(htmlString);
39 | done();
40 | });
41 | });
42 | });
43 |
44 | describe('failure', function() {
45 | it('calls the `next` function with the error', function(done) {
46 | var error = new EmberCliDeployError();
47 | fetchIndexStub.returns(Promise.reject(error));
48 |
49 | function nextExpectation() {
50 | expect(arguments).to.have.length(1);
51 | expect(arguments[0]).to.equal(error);
52 | done();
53 | }
54 |
55 | middleware()(req, res, nextExpectation).then(function() {
56 | done('Promise should not have resolved');
57 | });
58 | });
59 | });
60 | });
61 |
--------------------------------------------------------------------------------
/smoke-test/express/bin/www:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * Module dependencies.
5 | */
6 |
7 | var app = require('../app');
8 | var debug = require('debug')('node-ember-cli-deploy-redis-smoke-test:server');
9 | var http = require('http');
10 |
11 | /**
12 | * Get port from environment and store in Express.
13 | */
14 |
15 | var port = normalizePort(process.env.PORT || '3000');
16 | app.set('port', port);
17 |
18 | /**
19 | * Create HTTP server.
20 | */
21 |
22 | var server = http.createServer(app);
23 |
24 | /**
25 | * Listen on provided port, on all network interfaces.
26 | */
27 |
28 | server.listen(port);
29 | server.on('error', onError);
30 | server.on('listening', onListening);
31 |
32 | /**
33 | * Normalize a port into a number, string, or false.
34 | */
35 |
36 | function normalizePort(val) {
37 | var port = parseInt(val, 10);
38 |
39 | if (isNaN(port)) {
40 | // named pipe
41 | return val;
42 | }
43 |
44 | if (port >= 0) {
45 | // port number
46 | return port;
47 | }
48 |
49 | return false;
50 | }
51 |
52 | /**
53 | * Event listener for HTTP server "error" event.
54 | */
55 |
56 | function onError(error) {
57 | if (error.syscall !== 'listen') {
58 | throw error;
59 | }
60 |
61 | var bind = typeof port === 'string'
62 | ? 'Pipe ' + port
63 | : 'Port ' + port;
64 |
65 | // handle specific listen errors with friendly messages
66 | switch (error.code) {
67 | case 'EACCES':
68 | console.error(bind + ' requires elevated privileges');
69 | process.exit(1);
70 | break;
71 | case 'EADDRINUSE':
72 | console.error(bind + ' is already in use');
73 | process.exit(1);
74 | break;
75 | default:
76 | throw error;
77 | }
78 | }
79 |
80 | /**
81 | * Event listener for HTTP server "listening" event.
82 | */
83 |
84 | function onListening() {
85 | var addr = server.address();
86 | var bind = typeof addr === 'string'
87 | ? 'pipe ' + addr
88 | : 'port ' + addr.port;
89 | debug('Listening on ' + bind);
90 | }
91 |
--------------------------------------------------------------------------------
/UPGRADING.md:
--------------------------------------------------------------------------------
1 | # Upgrading
2 |
3 | ## 0.4.x -> 1.x
4 |
5 | Version 1.x introduces a few changes that might affect your upgrade path. Please
6 | read this guide carefully to successfully upgrade your app.
7 |
8 | ### Node Version
9 |
10 | Previously, this library supported versions < Node 6. To conform to the
11 | [Node LTS Support Schedule](https://github.com/nodejs/Release), this library
12 | now only supports Node 6 and beyond. If you're using an older version of Node,
13 | you'll need to use a pre 1.x version of this library.
14 |
15 | ### Errors Passed Up to Application
16 |
17 | Previously the middleware would explicitly render an error if a requested
18 | revision key was not found. However, this prevented users from choosing their
19 | own behavior when an error is encountered.
20 |
21 | This change will now let the
22 | [default express error handler](https://expressjs.com/en/guide/error-handling.html#the-default-error-handler),
23 | or a custom-defined error handler manage this behavior.
24 |
25 | If you'd like to retain the existing behavior, write a small middleware function such as:
26 |
27 | ```javascript
28 | app.use(function (err, req, res, next) {
29 | res.status(500).send(err);
30 | });
31 | ```
32 |
33 | See [the documentation](https://expressjs.com/en/guide/error-handling.html#writing-error-handlers)
34 | for more information on writing a custom error handler.
35 |
36 | ### Bluebird Replaced with Native `Promise`
37 |
38 | If you use a custom `fetch` method, note that it will now return a native Node
39 | `Promise` instead of a `Bluebird` promise. `Bluebird` exposes some features
40 | that are not available in native `Promise`.
41 |
42 | ### `ioredis` upgrade
43 |
44 | The version of `ioredis` was upgraded from 0.1.x to 0.4.x. If you're passing
45 | more advanced configuration to ioredis, please verify that they are compatible
46 | with newer versions of `ioredis`.
47 |
48 | ### `memoizee` upgrade
49 |
50 | If you are not opting into memoization, this is will not be relevant to you.
51 |
52 | The version of `memoizee` was upgraded from 0.3.x to 0.4.x. If you're passing
53 | custom configuration parameters to `memoizee`, please confirm that they're
54 | compatible.
55 |
56 | ### `database` redis parameter
57 |
58 | A deprecation was introduced in 2016 that warned if you passed your database
59 | as `database` instead of `db`. That deprecation and backwards-compatibility
60 | has been removed in 1.x.
61 |
62 | You'll need to change invocations that look like this:
63 |
64 | ```javascript
65 | app.use('/*', nodeEmberCliDeployRedis('myapp:index', {
66 | database: 0
67 | }));
68 | ```
69 |
70 | to use the `db` parameter:
71 |
72 | ```javascript
73 | app.use('/*', nodeEmberCliDeployRedis('myapp:index', {
74 | db: 0
75 | }));
76 | ```
77 |
--------------------------------------------------------------------------------
/fetch.js:
--------------------------------------------------------------------------------
1 | const _defaultsDeep = require('lodash/defaultsDeep');
2 |
3 | const EmberCliDeployError = require('./errors/ember-cli-deploy-error');
4 |
5 | const ioRedis = require('ioredis');
6 | const memoize = require('memoizee');
7 | let redisClient;
8 |
9 | const defaultConnectionInfo = {
10 | host: "127.0.0.1",
11 | port: 6379
12 | };
13 |
14 | const _defaultOpts = {
15 | revisionQueryParam: 'index_key',
16 | memoize: false,
17 | memoizeOpts: {
18 | maxAge: 5000, // ms
19 | preFetch: true,
20 | max: 4, // a sane default (current pointer, current html and two indexkeys in cache)
21 | }
22 | };
23 |
24 | let opts;
25 | function _getOpts(opts) {
26 | opts = opts || {};
27 | return _defaultsDeep({}, opts, _defaultOpts);
28 | }
29 |
30 | let initialized = false;
31 | function _initialize(connectionInfo, passedOpts) {
32 | opts = _getOpts(passedOpts);
33 | let config = connectionInfo ? connectionInfo : defaultConnectionInfo;
34 |
35 | redisClient = new ioRedis(config);
36 |
37 | if (opts.memoize === true) {
38 | let memoizeOpts = opts.memoizeOpts;
39 | memoizeOpts.async = false; // this should never be overwritten by the consumer
40 | memoizeOpts.length = 1;
41 |
42 | redisClient.get = memoize(redisClient.get, memoizeOpts);
43 | }
44 |
45 | initialized = true;
46 | }
47 |
48 | function fetchIndex(req, keyPrefix, connectionInfo, passedOpts) {
49 | if (!initialized) {
50 | _initialize(connectionInfo, passedOpts);
51 | }
52 |
53 | let indexkey;
54 | if (req.query[opts.revisionQueryParam]) {
55 | let queryKey = req.query[opts.revisionQueryParam].replace(/[^A-Za-z0-9]/g, '');
56 | indexkey = `${keyPrefix}:${queryKey}`;
57 | }
58 |
59 | let customIndexKeyWasSpecified = !!indexkey;
60 | function retrieveIndexKey(){
61 | if (indexkey) {
62 | return Promise.resolve(indexkey);
63 | } else {
64 | return redisClient.get(keyPrefix + ":current").then(function(result){
65 | if (!result) { throw new Error(); }
66 | return keyPrefix + ":" + result;
67 | }).catch(function(){
68 | throw new EmberCliDeployError("There's no " + keyPrefix + ":current revision. The site is down.", true);
69 | });
70 | }
71 | }
72 |
73 | return retrieveIndexKey().then(function(indexkey){
74 | return redisClient.get(indexkey);
75 | }).then(function(indexHtml){
76 | if (!indexHtml) { throw new Error(); }
77 | return indexHtml;
78 | }).catch(function(err){
79 | if (err.name === 'EmberCliDeployError') {
80 | throw err;
81 | } else {
82 | throw new EmberCliDeployError("There's no " + indexkey + " revision. The site is down.", !customIndexKeyWasSpecified);
83 | }
84 | });
85 | }
86 |
87 | module.exports = fetchIndex;
88 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | ## [1.0.1](https://github.com/blimmer/node-ember-cli-deploy-redis/tree/1.0.1) (2019-08-31)
4 | [Full Changelog](https://github.com/blimmer/node-ember-cli-deploy-redis/compare/v1.0.0...1.0.1)
5 |
6 | **Merged pull requests:**
7 |
8 | - Bump lodash from 4.17.11 to 4.17.13 [\#24](https://github.com/blimmer/node-ember-cli-deploy-redis/pull/24) ([dependabot[bot]](https://github.com/apps/dependabot))
9 | - Bump eslint-utils from 1.3.1 to 1.4.2 [\#23](https://github.com/blimmer/node-ember-cli-deploy-redis/pull/23) ([dependabot[bot]](https://github.com/apps/dependabot))
10 | - Bump js-yaml from 3.12.0 to 3.13.1 [\#22](https://github.com/blimmer/node-ember-cli-deploy-redis/pull/22) ([dependabot[bot]](https://github.com/apps/dependabot))
11 |
12 | ## [v1.0.0](https://github.com/blimmer/node-ember-cli-deploy-redis/tree/v1.0.0) (2018-11-28)
13 | [Full Changelog](https://github.com/blimmer/node-ember-cli-deploy-redis/compare/v0.4.1...v1.0.0)
14 |
15 | **Merged pull requests:**
16 |
17 | - \[POTENITALLY BREAKING CHANGES\] Upgrade memoizee and ioredis [\#21](https://github.com/blimmer/node-ember-cli-deploy-redis/pull/21) ([blimmer](https://github.com/blimmer))
18 | - Use native Promise instead of Bluebird. [\#20](https://github.com/blimmer/node-ember-cli-deploy-redis/pull/20) ([blimmer](https://github.com/blimmer))
19 | - \[BREAKING CHANGE\] Pass error back to app from middleware instead of rendering a `500` [\#19](https://github.com/blimmer/node-ember-cli-deploy-redis/pull/19) ([blimmer](https://github.com/blimmer))
20 | - Convert smoke-test suite to bats [\#18](https://github.com/blimmer/node-ember-cli-deploy-redis/pull/18) ([blimmer](https://github.com/blimmer))
21 | - Add smoke tests [\#17](https://github.com/blimmer/node-ember-cli-deploy-redis/pull/17) ([blimmer](https://github.com/blimmer))
22 | - Upgrade dev dependencies, supported node \(\>=6\) and replace jshint [\#16](https://github.com/blimmer/node-ember-cli-deploy-redis/pull/16) ([blimmer](https://github.com/blimmer))
23 | - Remove `database` deprecation and fallback. [\#15](https://github.com/blimmer/node-ember-cli-deploy-redis/pull/15) ([blimmer](https://github.com/blimmer))
24 |
25 | ## [v0.4.1](https://github.com/blimmer/node-ember-cli-deploy-redis/tree/v0.4.1) (2016-04-03)
26 | [Full Changelog](https://github.com/blimmer/node-ember-cli-deploy-redis/compare/0.4.0...v0.4.1)
27 |
28 | **Merged pull requests:**
29 |
30 | - Dependency upgrades. [\#13](https://github.com/blimmer/node-ember-cli-deploy-redis/pull/13) ([blimmer](https://github.com/blimmer))
31 |
32 | ## [0.4.0](https://github.com/blimmer/node-ember-cli-deploy-redis/tree/0.4.0) (2016-02-05)
33 | [Full Changelog](https://github.com/blimmer/node-ember-cli-deploy-redis/compare/v0.3.0...0.4.0)
34 |
35 | **Implemented enhancements:**
36 |
37 | - Throttle calls to redis.get [\#4](https://github.com/blimmer/node-ember-cli-deploy-redis/issues/4)
38 |
39 | **Merged pull requests:**
40 |
41 | - IORedis / Memoization Support [\#12](https://github.com/blimmer/node-ember-cli-deploy-redis/pull/12) ([blimmer](https://github.com/blimmer))
42 |
43 | ## [v0.3.0](https://github.com/blimmer/node-ember-cli-deploy-redis/tree/v0.3.0) (2015-11-08)
44 | [Full Changelog](https://github.com/blimmer/node-ember-cli-deploy-redis/compare/v0.2.0...v0.3.0)
45 |
46 | **Merged pull requests:**
47 |
48 | - ember cli deploy 0.5.0 compatibility [\#10](https://github.com/blimmer/node-ember-cli-deploy-redis/pull/10) ([blimmer](https://github.com/blimmer))
49 |
50 | ## [v0.2.0](https://github.com/blimmer/node-ember-cli-deploy-redis/tree/v0.2.0) (2015-07-08)
51 | [Full Changelog](https://github.com/blimmer/node-ember-cli-deploy-redis/compare/v0.1.1...v0.2.0)
52 |
53 | **Closed issues:**
54 |
55 | - api inconsistency [\#6](https://github.com/blimmer/node-ember-cli-deploy-redis/issues/6)
56 |
57 | **Merged pull requests:**
58 |
59 | - v0.2.0 RC [\#8](https://github.com/blimmer/node-ember-cli-deploy-redis/pull/8) ([blimmer](https://github.com/blimmer))
60 | - Fix API inconsistency between fetch and middleware [\#7](https://github.com/blimmer/node-ember-cli-deploy-redis/pull/7) ([knownasilya](https://github.com/knownasilya))
61 |
62 | ## [v0.1.1](https://github.com/blimmer/node-ember-cli-deploy-redis/tree/v0.1.1) (2015-06-02)
63 | [Full Changelog](https://github.com/blimmer/node-ember-cli-deploy-redis/compare/v0.1.0...v0.1.1)
64 |
65 | **Merged pull requests:**
66 |
67 | - Expose the test api for mocking [\#5](https://github.com/blimmer/node-ember-cli-deploy-redis/pull/5) ([ronco](https://github.com/ronco))
68 |
69 | ## [v0.1.0](https://github.com/blimmer/node-ember-cli-deploy-redis/tree/v0.1.0) (2015-06-01)
70 | [Full Changelog](https://github.com/blimmer/node-ember-cli-deploy-redis/compare/v0.0.1...v0.1.0)
71 |
72 | **Implemented enhancements:**
73 |
74 | - Allow using as a middleware [\#2](https://github.com/blimmer/node-ember-cli-deploy-redis/issues/2)
75 | - Allow working with then-redis and node\_redis [\#1](https://github.com/blimmer/node-ember-cli-deploy-redis/issues/1)
76 |
77 | **Merged pull requests:**
78 |
79 | - Rework to have internal then-redis dependency. [\#3](https://github.com/blimmer/node-ember-cli-deploy-redis/pull/3) ([blimmer](https://github.com/blimmer))
80 |
81 | ## [v0.0.1](https://github.com/blimmer/node-ember-cli-deploy-redis/tree/v0.0.1) (2015-05-24)
82 |
83 |
84 | \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)*
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # node-ember-cli-deploy-redis [](https://travis-ci.org/blimmer/node-ember-cli-deploy-redis)
2 |
3 | ExpressJS middleware to fetch the current (or specified) revision of your Ember App deployed by [ember-cli-deploy](https://github.com/ember-cli/ember-cli-deploy).
4 |
5 | ## Why?
6 |
7 | [ember-cli-deploy](https://github.com/ember-cli/ember-cli-deploy) is great. It allows you to run
8 | multiple versions in production at the same time and view revisions without impacting users.
9 | However, [the example provided](https://github.com/philipheinser/ember-lightning) uses [koa](http://koajs.com/)
10 | and many of us are not. This package allows you to easily fetch current and specified `index.html`
11 | revisions from [redis](http://redis.io) with [Express](expressjs.com) and other Node servers.
12 |
13 | ## Installation
14 |
15 | It's important to choose the right version of this library to match the version of
16 | ember-cli-deploy you're using.
17 |
18 | | ember-cli-deploy version | node-ember-cli-deploy-redis |
19 | |--------------------------|-----------------------------|
20 | | pre 0.5 | ^0.2.0 or lower |
21 | | 0.5 and beyond | ^0.3.0 or newer |
22 |
23 | Make sure to look at the
24 | [older documentation](https://github.com/blimmer/node-ember-cli-deploy-redis/blob/v0.2.0/README.md)
25 | if you're on a pre 0.5 ember-cli-deploy release.
26 | See [the changelog](https://github.com/blimmer/node-ember-cli-deploy-redis/blob/develop/CHANGELOG.md#030---2015-11-07)
27 | for an upgrade guide.
28 |
29 | ## Usage
30 |
31 | There are two main ways of using this library. For most simple Express servers, you'll
32 | want to simply use the middleware. However, if you need more flexibility, you'll
33 | want to use the internal `fetch` methods, with custom logic.
34 |
35 | ### ExpressJS Middleware
36 |
37 | 1. `require` the package
38 | 2. `use` the package in your app
39 |
40 | #### Example
41 |
42 | ```javascript
43 | const express = require('express');
44 | const app = express();
45 |
46 | const nodeEmberCliDeployRedis = require('node-ember-cli-deploy-redis');
47 | app.use('/*', nodeEmberCliDeployRedis('myapp:index', {
48 | host: 'redis.example.org', // default is localhost
49 | port: 6929, // default is 6379
50 | password: 'passw0rd!', // default is undefined
51 | db: 0 // default is undefined
52 | }));
53 | ```
54 |
55 |
56 |
57 | ### Custom Fetch Method
58 |
59 | 1. `require` the package
60 | 2. Use the `fetchIndex` method
61 | 3. Render the index string as you wish.
62 |
63 | #### Example
64 |
65 | ```javascript
66 | const express = require('express');
67 | const app = express();
68 |
69 | const fetchIndex = require('node-ember-cli-deploy-redis/fetch');
70 |
71 | app.get('/', function(req, res) {
72 | fetchIndex(req, 'myapp:index', {
73 | host: 'redis.example.org',
74 | port: 6929,
75 | password: 'passw0rd!',
76 | db: 0
77 | }).then(function (indexHtml) {
78 | indexHtml = serverVarInjectHelper.injectServerVariables(indexHtml, req);
79 | res.status(200).send(indexHtml);
80 | }).catch(function(err) {
81 | res.status(500).send('Oh noes!\n' + err.message);
82 | });
83 | });
84 | ```
85 | Check out [location-aware-ember-server](https://github.com/blimmer/location-aware-ember-server) for a running example.
86 |
87 | ## Documentation
88 |
89 | ### `nodeEmberCliDeployRedis(keyPrefix, connectionInfo, options)` (middleware constructor)
90 |
91 | * keyPrefix (required) - the application name, specified for ember deploy
92 | the keys in redis are prefaced with this name. For instance, if your redis keys are `my-app:index:current`, you'd pass `my-app:index`.
93 | * connectionInfo (required) - the configuration to connect to redis.
94 | internally, this library uses [ioredis](https://github.com/luin/ioredis), so pass a configuration supported by ioredis. please see their README for more information.
95 | * options (optional) - a hash of params to override [the defaults](https://github.com/blimmer/node-ember-cli-deploy-redis/blob/develop/README.md#options)
96 |
97 | ### `fetchIndex(request, keyPrefix, connectionInfo, options)`
98 |
99 | Arguments
100 |
101 | * request (required) - the request object
102 | the request object is used to check for the presence of `revisionQueryParam`
103 | * keyPrefix (required) - the application name, specified for ember deploy
104 | the keys in redis are prefaced with this name. For instance, if your redis keys are `my-app:index:current`, you'd pass `my-app:index`.
105 | * connectionInfo (required) - the configuration to connect to redis.
106 | internally, this library uses [ioredis](https://github.com/luin/ioredis), so pass a configuration supported by ioredis.
107 | * options (optional) - a hash of params to override [the defaults](https://github.com/blimmer/node-ember-cli-deploy-redis/blob/develop/README.md#options)
108 |
109 | Returns
110 |
111 | * a `Promise`
112 | when resolved, it returns the requested `index.html` string
113 | when failed, it returns an [EmberCliDeployError](https://github.com/blimmer/node-ember-cli-deploy-redis/blob/develop/errors/ember-cli-deploy-error.js).
114 |
115 | ### options
116 |
117 | * `revisionQueryParam` (defaults to `index_key`)
118 | the query parameter to specify a revision (e.g. `http://example.org/?index_key=abc123`). the key will be automatically prefaced with your `keyPrefix` for security.
119 | * `memoize` (defaults to `false`)
120 | enable memoizing Redis `get`s. see [the memoization section](#Memoization) for more details.
121 | * `memoizeOpts` ([see defaults](https://github.com/blimmer/node-ember-cli-deploy-redis/blob/master/fetch.js#L18))
122 | customize memoization parameters. see [the memoization section](#Memoization) for more details.
123 |
124 | ## Memoization
125 |
126 | Since the majority of the requests will be serving the `current` version of your
127 | app, you can enable memoization to reduce the load on Redis. By default, memoization
128 | is disabled. To enable it, simply pass:
129 |
130 | ```javascript
131 | memoize: true
132 | ```
133 |
134 | in your options hash. Additionally, you can pass options to the underlying memoization
135 | library ([memoizee](https://github.com/medikoo/memoizee)). Check out their documentation,
136 | and the [defaults](https://github.com/blimmer/node-ember-cli-deploy-redis/blob/master/fetch.js#L18)
137 | for this library.
138 |
139 | ### Example
140 |
141 | ```javascript
142 | app.use('/*', nodeEmberCliDeployRedis(
143 | 'myapp:index',
144 | { host: 'redis.example.org' },
145 | { memoize: true },
146 | ));
147 | ```
148 |
149 | ## Testing
150 |
151 | In order to facilitate unit testing and/or integration testing this
152 | library exports a mockable redis api. You will need to use a
153 | dependency injection framework such as
154 | [rewire](https://github.com/jhnns/rewire) to activate this testing api.
155 |
156 | ### Usage with rewire (mocha syntax)
157 |
158 | ```javascript
159 | // my-module.js
160 | const fetchIndex = require('node-ember-cli-deploy-redis/fetch');
161 | const indexWrapper = function(req, res) {
162 | return fetchIndex(req, 'app', {
163 | // real redis config
164 | }).then(function (indexHtml)) {
165 | // do something with index
166 | });
167 | };
168 | module.exports = indexWrapper;
169 |
170 | // my-module-test.js
171 | const redisTestApi = require('node-ember-cli-deploy-redis/test/helpers/test-api');
172 | const fetchIndex = rewire('node-ember-cli-deploy-redis/fetch');
173 | const redis = redisTestApi.ioRedisApi;
174 | const myModule = rewire('my-module');
175 |
176 | describe('my module', function() {
177 | afterEach(function() {
178 | fetchIndex.__set__('_initialized', false);
179 | });
180 |
181 | it('grabs my content', function() {
182 | // inject mocked content
183 | myModule.__set__('fetchIndex', fetchIndex);
184 | fetchIndex.__set__('ioRedis', redis);
185 | redis.set('app:abc123', "hello test world
");
186 | myModule(req, res).then(function(){
187 | // assertions here
188 | })
189 | });
190 | });
191 | ```
192 |
193 | ## Notes
194 |
195 | * Don't create any other redis keys you don't want exposed to the public under your `keyPrefix`.
196 |
197 | ## Contributing
198 |
199 | Comments/PRs/Issues are welcome!
200 |
201 | ### Cloning
202 |
203 | This project utilizes git submodules. Please use the following command to clone:
204 |
205 | ```console
206 | git clone --recurse-submodules https://github.com/blimmer/node-ember-cli-deploy-redis.git
207 | ```
208 |
209 | ### Running Project Unit Tests
210 |
211 | ```console
212 | npm test
213 | ```
214 |
215 | ### Running Project Smoke Tests
216 |
217 | ```console
218 | npm run smoke-test
219 | ```
220 |
--------------------------------------------------------------------------------
/test/fetch-test.js:
--------------------------------------------------------------------------------
1 | const { expect } = require('chai');
2 | require('chai').use(require("sinon-chai"));
3 |
4 | const { describe, it, before, after, beforeEach, afterEach } = require('mocha');
5 | const context = describe;
6 |
7 | const sinon = require('sinon');
8 | const rewire = require('rewire');
9 |
10 | const fetchIndex = rewire('../fetch');
11 |
12 | const basicReq = {
13 | query: {}
14 | };
15 |
16 | const testApi = require('./helpers/test-api');
17 | const redisClientApi = testApi.ioRedisClientApi;
18 | const ioRedisApi = testApi.ioRedisApi;
19 |
20 | describe('fetch', function() {
21 | let sandbox;
22 | before(function() {
23 | sandbox = sinon.createSandbox();
24 | fetchIndex.__set__('ioRedis', ioRedisApi);
25 | });
26 |
27 | afterEach(function() {
28 | fetchIndex.__set__('initialized', false);
29 | sandbox.restore();
30 | });
31 |
32 | describe('_initialize', function () {
33 | let _initialize;
34 | before(function() {
35 | _initialize = fetchIndex.__get__('_initialize');
36 | });
37 | after(function() {
38 | fetchIndex.__set__('ioRedis', ioRedisApi);
39 | });
40 |
41 | describe('redis client initialize', function() {
42 | let ioRedisInitStub;
43 | beforeEach(function() {
44 | ioRedisInitStub = sandbox.stub();
45 | fetchIndex.__set__('ioRedis', ioRedisInitStub);
46 | });
47 |
48 | it('sets up redis (defaults)', function() {
49 | _initialize();
50 |
51 | expect(ioRedisInitStub).to.have.been.calledOnce;
52 | expect(ioRedisInitStub).to.have.been.calledWithNew;
53 | expect(ioRedisInitStub).to.have.been.calledOnceWith({
54 | host: "127.0.0.1",
55 | port: 6379
56 | });
57 | });
58 |
59 | it('sets up redis (config passed)', function() {
60 | let configString = 'redis://h:passw0rd@example.org:6929';
61 | _initialize(configString);
62 |
63 | expect(ioRedisInitStub).to.have.been.calledOnce;
64 | expect(ioRedisInitStub).to.have.been.calledWithNew;
65 | expect(ioRedisInitStub).to.have.been.calledOnceWith(configString);
66 | });
67 | });
68 |
69 | describe('memoization', function() {
70 | let memoizeStub;
71 | beforeEach(function() {
72 | memoizeStub = sandbox.stub();
73 | fetchIndex.__set__('memoize', memoizeStub);
74 | });
75 |
76 | after(function() {
77 | fetchIndex.__set__('memoize', require('memoizee'));
78 | });
79 |
80 | context('not enabled (default)', function() {
81 | it('does not enable memoize redis.get', function() {
82 | _initialize();
83 | expect(memoizeStub.called).to.be.false;
84 | });
85 | });
86 |
87 | context('enabled', function() {
88 | it('passes default options to memoize', function() {
89 | _initialize({}, { memoize: true });
90 |
91 | expect(memoizeStub).to.have.been.calledOnceWith(undefined, {
92 | maxAge: 5000,
93 | preFetch: true,
94 | max: 4,
95 | async: false,
96 | length: 1,
97 | });
98 | });
99 |
100 | it('allows overriding existing properties', function() {
101 | let myOpts = {
102 | maxAge: 10000,
103 | preFetch: 0.6,
104 | max: 2,
105 | };
106 |
107 | _initialize({}, {
108 | memoize: true,
109 | memoizeOpts: myOpts,
110 | });
111 |
112 | expect(memoizeStub).calledOnceWith(undefined, {
113 | maxAge: 10000,
114 | preFetch: 0.6,
115 | max: 2,
116 | async: false,
117 | length: 1,
118 | });
119 | });
120 |
121 | it('allows adding additional memoizee options', function() {
122 | const myDispose = function() {
123 | // some custom dispose logic because I'm a masochist
124 | };
125 | let myOpts = {
126 | dispose: myDispose
127 | };
128 |
129 | _initialize({}, {
130 | memoize: true,
131 | memoizeOpts: myOpts,
132 | });
133 |
134 | expect(memoizeStub).calledOnceWith(undefined, {
135 | async: false,
136 | dispose: myDispose,
137 | length: 1,
138 | max: 4,
139 | maxAge: 5000,
140 | preFetch: true
141 | });
142 | });
143 |
144 | it('does not allow overriding async flag', function() {
145 | let myOpts = {
146 | async: true,
147 | };
148 |
149 | _initialize({}, {
150 | memoize: true,
151 | memoizeOpts: myOpts,
152 | });
153 |
154 | expect(memoizeStub).to.have.been.calledOnceWith(undefined, {
155 | maxAge: 5000,
156 | preFetch: true,
157 | max: 4,
158 | async: false,
159 | length: 1,
160 | });
161 | });
162 |
163 | it('does not allow overriding the length property', function() {
164 | let myOpts = {
165 | length: 2,
166 | };
167 |
168 | _initialize({}, {
169 | memoize: true,
170 | memoizeOpts: myOpts,
171 | });
172 |
173 | expect(memoizeStub).to.have.been.calledOnceWith(undefined, {
174 | maxAge: 5000,
175 | preFetch: true,
176 | max: 4,
177 | async: false,
178 | length: 1,
179 | });
180 | });
181 | });
182 | });
183 |
184 | describe('revisionQueryParam', function() {
185 | it('has a default revisionQueryParam', function() {
186 | _initialize();
187 |
188 | let opts = fetchIndex.__get__('opts');
189 | expect(opts.revisionQueryParam).to.equal('index_key');
190 | });
191 |
192 | it('allows override of revisionQueryParam', function() {
193 | _initialize({}, {revisionQueryParam: 'foobar'});
194 |
195 | let opts = fetchIndex.__get__('opts');
196 | expect(opts.revisionQueryParam).to.equal('foobar');
197 | });
198 | });
199 |
200 | it('sets initialized flag', function() {
201 | expect(fetchIndex.__get__('initialized')).to.be.false;
202 | _initialize();
203 | expect(fetchIndex.__get__('initialized')).to.be.true;
204 | });
205 | });
206 |
207 | describe('fetchIndex', function() {
208 | let redis, redisSpy;
209 |
210 | before(function() {
211 | redis = redisClientApi;
212 | });
213 |
214 | beforeEach(function() {
215 | redisSpy = sandbox.spy(redis, 'get');
216 | });
217 |
218 | afterEach(function() {
219 | redis.flushall();
220 | });
221 |
222 | it('normalizes spaces in revisionQueryParam', function(done) {
223 | const req = {
224 | query: {
225 | index_key: 'abc 123'
226 | }
227 | };
228 |
229 | redis.set('myapp:index:abc123', 'foo').then(function(){
230 | fetchIndex(req, 'myapp:index').then(function() {
231 | expect(redisSpy).to.have.been.calledWith('myapp:index:abc123');
232 | expect(redisSpy).to.not.have.been.calledWith('myapp:index:abc 123');
233 | done();
234 | }).catch(function(err) {
235 | done(err);
236 | });
237 | });
238 |
239 | });
240 |
241 | it('removes special chars revisionQueryParam', function(done) {
242 | const req = {
243 | query: {
244 | index_key: 'ab@*#!c(@)123'
245 | }
246 | };
247 |
248 | redis.set('myapp:index:abc123', 'foo').then(function(){
249 | fetchIndex(req, 'myapp:index').then(function() {
250 | expect(redisSpy).to.have.been.calledWith('myapp:index:abc123');
251 | expect(redisSpy).to.not.have.been.calledWith('myapp:index:ab@*#!c(@)123');
252 | done();
253 | }).catch(function(err) {
254 | done(err);
255 | });
256 | });
257 | });
258 |
259 | it('fails the promise with a critical error if keyPrefix:current is not present', function(done) {
260 | redis.del('myapp:index:current').then(function(){
261 | fetchIndex(basicReq, 'myapp:index').then(function() {
262 | done("Promise should not have resolved.");
263 | }).catch(function(err) {
264 | expect(redisSpy).to.have.been.calledWith('myapp:index:current');
265 | expect(err.critical).to.be.true;
266 | done();
267 | });
268 | });
269 | });
270 |
271 | it('fails the promise with a critical error if revision pointed to by keyPrefix:current is not present', function(done) {
272 | redis.set('myapp:index:current', 'abc123').then(function(){
273 | return redis.del('myapp:index:abc123');
274 | }).then(function(){
275 | fetchIndex(basicReq, 'myapp:index').then(function() {
276 | done("Promise should not have resolved.");
277 | }).catch(function(err) {
278 | expect(redisSpy).to.have.been.calledWith('myapp:index:current');
279 | expect(redisSpy).to.have.been.calledWith('myapp:index:abc123');
280 | expect(err.critical).to.be.true;
281 | done();
282 | });
283 | });
284 | });
285 |
286 | it('fails the promise with a non-critical error if revision requestd by query param is not present', function(done) {
287 | const req = {
288 | query: {
289 | index_key: 'abc123'
290 | }
291 | };
292 | redis.del('myapp:index:abc123').then(function(){
293 | fetchIndex(req, 'myapp:index').then(function() {
294 | done("Promise should not have resolved.");
295 | }).catch(function(err) {
296 | expect(redisSpy).to.have.been.calledWith('myapp:index:abc123');
297 | expect(err.critical).to.be.false;
298 | done();
299 | });
300 | });
301 | });
302 |
303 | it('resolves the promise with the index html requested', function(done) {
304 | const currentHtmlString = '1';
305 | Promise.all([
306 | redis.set('myapp:index:current', 'abc123'),
307 | redis.set('myapp:index:abc123', currentHtmlString),
308 | ]).then(function(){
309 | fetchIndex(basicReq, 'myapp:index').then(function(html) {
310 | expect(redisSpy).to.have.been.calledWith('myapp:index:current');
311 | expect(redisSpy).to.have.been.calledWith('myapp:index:abc123');
312 | expect(html).to.equal(currentHtmlString);
313 | done();
314 | }).catch(function() {
315 | done("Promise should not have failed.");
316 | });
317 | });
318 | });
319 |
320 | it('resolves the promise with the index html requested (specific revision)', function(done) {
321 | const currentHtmlString = '1';
322 | const newDeployHtmlString = '2';
323 | const req = {
324 | query: {
325 | index_key: 'def456'
326 | }
327 | };
328 | Promise.all([
329 | redis.set('myapp:index:current', 'abc123'),
330 | redis.set('myapp:index:abc123', currentHtmlString),
331 | redis.set('myapp:index:def456', newDeployHtmlString)
332 | ]).then(function(){
333 | fetchIndex(req, 'myapp:index').then(function(html) {
334 | expect(redisSpy).to.not.have.been.calledWith('myapp:index:current');
335 | expect(redisSpy).to.have.been.calledWith('myapp:index:def456');
336 | expect(html).to.equal(newDeployHtmlString);
337 | done();
338 | }).catch(function() {
339 | done("Promise should not have failed.");
340 | });
341 | });
342 | });
343 |
344 | it('memoizes results from redis when turned on', function(done) {
345 | const currentHtmlString = '1';
346 | Promise.all([
347 | redis.set('myapp:index:current', 'abc123'),
348 | redis.set('myapp:index:abc123', currentHtmlString),
349 | fetchIndex(basicReq, 'myapp:index', null, { memoize: true }),
350 | fetchIndex(basicReq, 'myapp:index', null, { memoize: true }),
351 | fetchIndex(basicReq, 'myapp:index', null, { memoize: true }),
352 | ]).then(function(){
353 | expect(redisSpy).to.have.been.calledWith('myapp:index:current');
354 | expect(redisSpy).to.have.been.calledWith('myapp:index:abc123');
355 | done();
356 | });
357 | });
358 | });
359 | });
360 |
--------------------------------------------------------------------------------