Commits

Shiran Pasternak  committed c3f9c83

Added several tests. Various fixes.
Testing requires a live Redis connection at the moment. :(

  • Participants
  • Parent commits be23d32

Comments (0)

Files changed (7)

-/node_modules
+/node_modules
+.tm_properties
+PACKAGE   = web-cache
+MOCHA     = ./node_modules/mocha/bin/mocha
+MOCHAOPTS =
+NPM      ?= npm
+
+all: deps test
+
+deps: package.json
+	@ $(NPM) install
+	
+test: deps
+	@ $(MOCHA) $(MOCHAOPTS)
+	
+.PHONY: test deps all
 ###client.middleware(params)
 Provides Redis-based caching for the web server. `params` is an associative list with the following supported properties:
 
-* `prefix`: (*string*) The prefix to use for caching. Useful for running multiple caches on the same server.
- 
-  **Default** `web-cache`
+* `prefix`: *string* \[default: `web-cache`]  The prefix to use for caching. Useful for running multiple caches on the same server.
   
-* `expire`: (*integer*) The age of items (in seconds) at which to expire them from the cache.
-
-  **Default:** `86400` (one day)
+* `expire`: (*integer*) \[default: `86400`  The age of items (in seconds) at which to expire them from the cache.
   
-* `path`: (*string* or *RegExp*) The path matching routes that should be cached.
-
-  **Default:** `/`
-
-* `exclude`: (*array* of *string* or *RegExp*) A list of routes which the cache should exclude.
-
-  **Default:** `null`
+* `path`: (*string* or *Re  The path matching routes that should be cached.
 
-* `host`: The Redis host.
+* `exclude`: *array* of *string  A list of routes which the cache should exclude.
 
-  **Default:** `127.0.0.1`
+* `host`: *string* \[default: `127.0.0.1`] The Redis host.
 
-* `port`: The Redis port.
+* `port`: *string* \[default: `6379`] The Redis port.
 
-  **Default:** `6379`
+* `clean`: *boolean* \[default: `false`] Remove all currently-cached items.
 
 ##Limitations
 The following are temporary and are being implemented, or thought about.
 * NO TESTS! (Yet)
 
 ##License
-Copyright (c) 2013 Shiran Pasternak <shiranpasternak@gmail.com>
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
+    Copyright (c) 2013 Shiran Pasternak <shiranpasternak@gmail.com>
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to
+    deal in the Software without restriction, including without limitation the
+    rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+    sell copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+    DEALINGS IN THE SOFTWARE.
 TODOs
 =====
 
-v0.0.1
-------
-* Handle non-JSON content
+* Handle non-JSON content
+* Cache multiple content types for URL routes

File lib/web-cache.js

     path:     '/',
     maxItems: 5000,
     expire:   ONE_DAY,
-    prefix:   "web-cache"
+    prefix:   "web-cache",
+    clean:    false
 };
 
 function cacheResponse(res, fn, key, cache) {
     return function (body) {
         // Will not cache empty values
         if (body) {
-            cache.lruSet(key, body);
+            cache.lruSet(key, body, function (err, reply) {
+                fn.call(res, body);
+            });
+        } else {
+            fn.call(res, body);
         }
-        fn.call(res, body);
     }
 }
 
 function initializeParams(inParams) {
     var params = {};
-    for (var property in DEFAULTS) { params[property] = DEFAULTS[property]; }
-    for (var property in inParams) { params[property] = inParams[property]; }
+    [DEFAULTS, inParams].forEach(function (list) {
+        for (var prop in list) params[prop] = list[prop];
+    });
     return params;
 }
 
 function lruDecorate(cache, params) {
-    function lruKey(key) {
-        return [params.prefix, key].join("-");
-    }
+    cache.lruKey = function (key) {
+        return [params.prefix, key].join(":");
+    };
     cache.lruGet = function (key, fn) {
-        cache.get(lruKey(key), function (err, reply) {
+        var lkey = cache.lruKey(key);
+        cache.get(lkey, function (err, reply) {
             if (reply) {
-                reply = JSON.parse(reply);
                 cache.lruSet(key, reply);
             }
             fn(err, reply);
         });
     };
     cache.lruSet = function (key, val, fn) {
-        var lkey = lruKey(key);
-        var retval = cache.set(lkey, JSON.stringify(val), fn);
+        var lkey = cache.lruKey(key);
+        var retval = cache.set(lkey, val, function () {
+            if ("function" === typeof fn)
+                fn.call(null, arguments)
+        });
         cache.expire(lkey, params.expire);
         return retval;
     }
 
 exports.middleware = function (params) {
     params = initializeParams(params);
-    
+
     var cache = Redis.createClient(params.port, params.host);
     lruDecorate(cache, params);
     
     if ("string" !== typeof params.prefix) {
         throw new Error("'prefix' property must be a string");
     }
-    if (!params.path instanceof RegExp) {
-        params.path = new RegExp("^" + params.path);
+    if ("string" === typeof params.path) {
+        params.path = new RegExp("^" + params.path + '\\b');
     }
+    params.exclude = params.exclude || [];
     for (var i = 0; i < params.exclude.length; i++) {
-        if (!params.exclude[i] instanceof RegExp) {
-            params.exclude[i] = new RegExp("^" + params.exclude[i] + "$");
+        if ("string" === typeof params.exclude[i]) {
+            params.exclude[i] = new RegExp("^" + params.exclude[i] + "\\b");
         }
     }
+    if (params.clean) {
+        cache.keys(cache.lruKey('*'), function (err, keys) {
+            cache.del(keys, function () {
+            });
+        });
+    }
+    
+    var timesCalled = 0;
+    
     return function (req, res, next) {
         if (req.url.match(params.path) === null) {
             return next();
                 return next();
             }
         }
-        res.contentType = 'application/json';
-        
         cache.lruGet(req.url, function (err, reply) {
+            res.set("Content-Type", "application/json");
             if (reply) {
-                return res.send(reply);
+                res.send(reply);
+                return res.end();
             }
             res.end = cacheResponse(res, res.end, req.url, cache);
             next();

File package.json

             "bugs": {
              "url": "http://bitbucket.org/gingi/web-cache/issues"
             },
-        "licenses": [ { "type": "MIT" } ]
+        "licenses": [ { "type": "MIT" } ],
+ "devDependencies": {
+         "express": ">=3.0",
+           "mocha": ">=1.8.1",
+          "should": ">=1.2.1",
+       "supertest": ">=0.5.1",
+           "async": ">=0.1.22"
+ }
 }

File test/test.js

+var cache   = require(__dirname + '/../lib/web-cache.js')
+
+var should  = require('should');
+var express = require('express');
+var request = require('supertest');
+var async   = require('async');
+
+describe('web-cache', function () {
+    it("should export constructors", function () {
+        cache.middleware.should.be.a('function');
+    });
+    describe('middleware', function () {
+        it("should return a valid function with no arguments", function () {
+            cache.middleware().should.be.a('function');
+        });
+    });
+});
+
+describe('redis server', function () {
+    it("should complain when not running", function () {
+        // cache.middleware({port: 53131}).should.throw(/Server not running/);
+    })
+})
+
+describe('HTTP app', function () {
+    var app = express()
+        .use(cache.middleware({
+            path: "/count",
+            prefix: 'test-web-cache',
+            clean: true
+        }));
+    var counter = 0;
+    var respond = function (req, res) {
+        counter++;
+        res.send({ value: counter });
+    };
+    app.get('/count',  respond);
+    app.get('/count2', respond);
+    
+    it("should cache routes", function (done) {
+        var req = request(app);
+        var times = 2;
+        var reqs = [];
+        for (var i = 0; i < times; i++) {
+            (function (i) { // Scoping 'i'
+                reqs.push(function (callback) {
+                    req.get('/count')
+                        .expect("Content-type", /json/)
+                        .expect(200)
+                        .expect({ value: 1 })
+                        .end(function (err, res) {
+                            if (err) throw err;
+                            callback(null, i);
+                        });
+                });
+            })(i);
+        }
+        async.series(reqs, function (err, results) { done(); });
+    });
+    it("should not cache other routes", function (done) {
+        var times = 3;
+        var reqs = [];
+        var start = counter + 1;
+        for (var i = start; i < start + times; i++) {
+            (function (i) { // Scoping 'i'
+                reqs.push(function (callback) { 
+                    request(app).get('/count2')
+                        .expect(200)
+                        .expect({ value: i }, function (err) {
+                            if (err) throw err;
+                            callback(null, i);
+                        });
+                });
+            })(i);
+        }
+        async.series(reqs, function () { done() });
+    });
+});
+
+describe('expire param', function () {
+    var seconds = 3;
+    var app = express()
+        .use(cache.middleware({
+            expire: seconds
+        }));
+    app.get('/path')
+    it("should expire cache", function (done) {
+        done();
+    })
+})