// a NODE_ENV of test will supress console output to stderr which
// connect likes to do when next() is called with a non-falsey error
// message.  We test such codepaths here.
process.env.NODE_ENV = 'test';

var vows = require("vows"),
    assert = require("assert"),
    cookieSessions = require("../lib/client-sessions"),
    express = require("express"),
    request = require("request");

// screw you Vows
process.on('uncaughtException', function(err) {
  console.error('Uncaught:', err.stack);
  process.exit(1);
});

function create_app() {
  // set up the session middleware
  // XXX: same secret is important for a test
  var middleware = cookieSessions({
    cookieName: 'session',
    secret: 'yo',
    activeDuration: 0,
    cookie: {
      maxAge: 5000
    }
  });

  var app = express();
  app.use(middleware);

  // set up a second cookie storage middleware
  var secureStoreMiddleware = cookieSessions({
    cookieName: 'securestore',
    secret: 'yo',
    activeDuration: 0,
    cookie: {
      maxAge: 5000
    }
  });

  app.use(secureStoreMiddleware);

  return app;
}

var startingPort = 9000;
function createBrowser(server) {
  var jar = request.jar();
  var browser = {
    get: function(url, options, callback) {
      if (typeof options === 'function') {
        callback = options;
        options = {};
      }
      if (typeof callback !== 'function') {
        throw new TypeError('callback must be a function');
      }
      // Ensure that server is ready to take connections
      if (server && !server.__listening) {
        (server.__deferred = server.__deferred || []).push([url, options, callback]);
        if (!server.__started) {
          server.__listener = server.listen(server.__port = ++startingPort, '127.0.0.1', function(){
            process.nextTick(function(){
              server.__deferred.forEach(function(args){
                browser.get.apply(browser, args);
              });
            });
            server.__listening = true;
          });
          server.__started = true;
        }
        return;
      }
      url = 'http://127.0.0.1:' + server.__port + url;
      options.uri = url;
      options.jar = jar;
      request.get(options, function(err, res, body) {
        if (err) console.error('ERR', err.stack);
        browser.cookies = jar.getCookies(url);
        callback(res, body);
      });


    },
    done: function() {
      server.__listener.close();
      server.__listener = null;
      server.__listening = false;
      server.__started = false;
    }
  };
  return browser;
}

var suite = vows.describe('client-sessions');

suite.addBatch({
  "middleware" : {
    topic: function() {
      var self = this;
      var middleware = cookieSessions({
        cookieName: 'session',
        secret: 'yo',
        activeDuration: 0,
        cookie: {
          maxAge: 5000
        }
      });

      var req = {
        headers: {}
      };
      var res = {};

      middleware(req, res, function(err) {
        self.callback(err, req, res);
      });
    },
    "includes a session object": function(err, req) {
      assert.isObject(req.session);
    },
    "session object stores and retrieves values properly": function(err, req) {
      req.session.foo = 'bar';
      assert.equal(req.session.foo, 'bar');
    },
    "session object has reset function": function(err, req) {
      assert.isFunction(req.session.reset);
    },
    "session object has setDuration function": function(err, req) {
      assert.isFunction(req.session.setDuration);
    },
    "set variables and clear them yields no variables": function(err, req) {
      req.session.bar = 'baz';
      req.session.reset();
      assert.isUndefined(req.session.bar);
    },
    "set variables does the right thing for Object.keys": function(err, req) {
      req.session.reset();
      req.session.foo = 'foobar';
      assert.equal(Object.keys(req.session).length, 1);
      assert.equal(Object.keys(req.session)[0], 'foo');
    },
    "reset preserves variables when asked": function(err, req) {
      req.session.reset();
      req.session.foo = 'foobar';
      req.session.bar = 'foobar2';

      req.session.reset(['foo']);

      assert.isUndefined(req.session.bar);
      assert.equal(req.session.foo, 'foobar');
    },
    "set session property absorbs set object": function(err, req) {
      req.session.reset();
      req.session.foo = 'quux';

      req.session = { bar: 'baz' };

      assert.isUndefined(req.session.foo);
      assert.isFunction(req.session.reset);
      assert.isFunction(req.session.setDuration);
      assert.equal(req.session.bar, 'baz');

      assert.throws(function() {
        req.session = 'blah';
      }, TypeError);
    }
  }
});

suite.addBatch({
  "a single request object" : {
    topic: function() {
      var self = this;

      // simple app
      var app = create_app();

      app.get("/foo", function(req, res) {
        req.session.foo = 'foobar';
        res.send("hello");
      });

      var browser = createBrowser(app);
      browser.get("/foo", function(res, $) {
        browser.done();
        self.callback(null, res);
      });
    },
    "includes a set-cookie header": function(err, res) {
      assert.isArray(res.headers['set-cookie']);
    },
    "only one set-cookie header": function(err, res) {
      assert.equal(res.headers['set-cookie'].length, 1);
    },
    "with an expires attribute": function(err, res) {
      assert.match(res.headers['set-cookie'][0], /expires/);
    },
    "with a path attribute": function(err, res) {
      assert.match(res.headers['set-cookie'][0], /path/);
    },
    "with an httpOnly attribute": function(err, res) {
      assert.match(res.headers['set-cookie'][0], /httponly/);
    }
  }
});

suite.addBatch({
  "across two requests" : {
    topic: function() {
      var self = this;

      // simple app
      var app = create_app();

      app.get("/foo", function(req, res) {
        req.session.reset();
        req.session.foo = 'foobar';
        req.session.bar = [1, 2, 3];
        res.send("foo");
      });

      app.get("/bar", function(req, res) {
        self.callback(null, req);
        res.send("bar");
      });

      var browser = createBrowser(app);
      browser.get("/foo", function(res, $) {
        browser.get("/bar", function(res, $) {
          browser.done();
        });
      });
    },
    "session maintains state": function(err, req) {
      assert.equal(req.session.foo, 'foobar');
      assert.equal(req.session.bar.length, 3);
      assert.equal(req.session.bar[0], 1);
      assert.equal(req.session.bar[1], 2);
      assert.equal(req.session.bar[2], 3);
    }
  }
});

suite.addBatch({
  "across two requests" : {
    topic: function() {
      var self = this;

      // simple app
      var app = create_app();

      app.get("/foo", function(req, res) {
        req.session.reset();
        req.session.foo = 'foobar';
        res.send("foo");
      });

      app.get("/bar", function(req, res) {
        self.callback(null, req);
        res.send("bar");
      });

      var browser = createBrowser(app);
      browser.get("/foo", function(res, $) {
        browser.get("/bar", function(res, $) {
          browser.done();
        });
      });
    },
    "resetting a session with an existing cookie value yields no variables": function(err, req) {
      req.session.reset();
      assert.isUndefined(req.session.foo);
    }
  }
});

suite.addBatch({
  "across three requests" : {
    topic: function() {
      var self = this;

      // simple app
      var app = create_app();

      app.get("/foo", function(req, res) {
        req.session.reset();
        req.session.foo = 'foobar';
        req.session.bar = 'foobar2';
        res.send("foo");
      });

      app.get("/bar", function(req, res) {
        delete req.session.bar;
        res.send("bar");
      });

      app.get("/baz", function(req, res) {
        self.callback(null, req);
        res.send("baz");
      });

      var browser = createBrowser(app);
      browser.get("/foo", function(res, $) {
        browser.get("/bar", function(res, $) {
          browser.get("/baz", function(res, $) {
            browser.done();
          });
        });
      });
    },
    "session maintains state": function(err, req) {
      assert.equal(req.session.foo, 'foobar');
      assert.isUndefined(req.session.bar);
    }
  },
  "across three requests with deep objects" : {
    topic: function() {
      var self = this;

      // simple app
      var app = create_app();

      app.get("/foo", function(req, res) {
        req.session.reset();
        req.session.foo = 'foobar';
        req.session.bar = { a: 'b' };
        res.send("foo");
      });

      app.get("/bar", function(req, res) {
        req.session.bar.c = 'd';
        res.send("bar");
      });

      app.get("/baz", function(req, res) {
        self.callback(null, req);
        res.send("baz");
      });

      var browser = createBrowser(app);
      browser.get("/foo", function(res, $) {
        browser.get("/bar", function(res, $) {
          browser.get("/baz", function(res, $) {
            browser.done();
          });
        });
      });
    },
    "session maintains state": function(err, req) {
      assert.equal(req.session.foo, 'foobar');
      assert.equal(req.session.bar.c, 'd');
    }
  }
});

suite.addBatch({
  "reading from an existing session" : {
    topic: function() {
      var self = this;

      // simple app
      var app = create_app();

      app.get("/foo", function(req, res) {
        req.session.foo = 'foobar';
        res.send("foo");
      });

      app.get("/bar", function(req, res) {
        res.send(req.session.foo);
      });

      var browser = createBrowser(app);
      browser.get("/foo", function(res, $) {
        browser.get("/bar", function(res, $) {
          browser.done();
          // observe the response to the second request
          self.callback(null, res);
        });
      });
    },
    "does not set a cookie": function(err, res) {
      assert.isUndefined(res.headers['set-cookie']);
    }
  },
  "reading from a non-existing session" : {
    topic: function() {
      var self = this;

      // simple app
      var app = create_app();

      app.get("/foo", function(req, res) {
        // this should send undefined, not null
        res.send(req.session.foo);
      });

      var browser = createBrowser(app);
      browser.get("/foo", function(res, $) {
        browser.done();
        self.callback(null, res, $);
      });
    },
    "does not set a cookie": function(err, res, body) {
      assert.isUndefined(res.headers['set-cookie']);
      assert.equal(body, ''); // undefined becomes an empty string
    }
  }
});

suite.addBatch({
  "writing to a session" : {
    topic: function() {
      var self = this;

      // simple app
      var app = create_app();

      app.get("/foo", function(req, res) {
        req.session.foo = 'foobar';
        res.send("foo");
      });

      app.get("/bar", function(req, res) {
        req.session.reset();
        req.session.reset();
        req.session.bar = 'bar';
        req.session.baz = 'baz';
        res.send("bar");
      });

      var browser = createBrowser(app);
      browser.get("/foo", function(res, $) {
        browser.get("/bar", function(res, $) {
          browser.done();
          // observe the response to the second request
          self.callback(null, res);
        });
      });
    },
    "sets a cookie": function(err, res) {
      assert.isArray(res.headers['set-cookie']);
    },
    "and only one cookie": function(err, res) {
      assert.equal(res.headers['set-cookie'].length, 1);
    }
  }
});

function create_app_with_duration() {
  // simple app
  var app = express();
  app.use(cookieSessions({
    cookieName: 'session',
    secret: 'yo',
    activeDuration: 0,
    duration: 500 // 0.5 seconds
  }));

  app.get("/foo", function(req, res) {
    req.session.reset();
    req.session.foo = 'foobar';
    res.send("foo");
  });

  return app;
}

suite.addBatch({
  "querying within duration" : {
    topic: function() {
      var self = this;

      var app = create_app_with_duration();
      app.get("/bar", function(req, res) {
        self.callback(null, req);
        res.send("bar");
      });

      var browser = createBrowser(app);
      browser.get("/foo", function(res, $) {
        setTimeout(function () {
          browser.get("/bar", function(res, $) {
            browser.done();
          });
        }, 200);
      });
    },
    "session still has state": function(err, req) {
      assert.equal(req.session.foo, 'foobar');
    }
  }
});

suite.addBatch({
  "modifying the session": {
    topic: function() {
      var self = this;

      var app = create_app_with_duration();
      app.get("/bar", function(req, res) {
        self.callback(null, req);
        res.send("bar");
      });

      var browser = createBrowser(app);
      var firstCreatedAt, secondCreatedAt;
      browser.get("/foo", function(res, $) {
        browser.get("/bar", function(res, $) {
          browser.done();
        });
      });
    },
    "doesn't change createdAt": function(err, req) {
      assert.equal(req.session.foo, 'foobar');
    }
  }
});

suite.addBatch({
  "querying outside the duration time": {
    topic: function() {
      var self = this;

      var app = create_app_with_duration();
      app.get("/bar", function(req, res) {
        self.callback(null, req);
        res.send("bar");
      });

      var browser = createBrowser(app);
      browser.get("/foo", function(res, $) {
        setTimeout(function () {
          browser.get("/bar", function(res, $) {
            browser.done();
          });
        }, 800);
      });
    },
    "session no longer has state": function(err, req) {
      assert.isUndefined(req.session.foo);
    }
  }
});

suite.addBatch({
  "querying twice, each at 2/5 duration time": {
    topic: function() {
      var self = this;

      var app = create_app_with_duration();
      app.get("/bar", function(req, res) {
        req.session.baz = Math.random();
        res.send("bar");
      });

      app.get("/bar2", function(req, res) {
        self.callback(null, req);
        res.send("bar2");
      });

      var browser = createBrowser(app);
      // first query resets the session to full duration
      browser.get("/foo", function(res, $) {
        setTimeout(function () {
          // this query should NOT reset the session
          browser.get("/bar", function(res, $) {
            setTimeout(function () {
              // so the session should still be valid
              browser.get("/bar2", function(res, $) {
                browser.done();
              });
            }, 200);
          });
        }, 200);
      });
    },
    "session still has state": function(err, req) {
      assert.isDefined(req.session.baz);
    }
  }
});

suite.addBatch({
  "querying twice, each at 3/5 duration time": {
    topic: function() {
      var self = this;

      var app = create_app_with_duration();
      app.get("/bar", function(req, res) {
        req.session.baz = Math.random();
        res.send("bar");
      });

      app.get("/bar2", function(req, res) {
        self.callback(null, req);
        res.send("bar2");
      });

      var browser = createBrowser(app);
      // first query resets the session to full duration
      browser.get("/foo", function(res, $) {
        setTimeout(function () {
          // this query should NOT reset the session
          browser.get("/bar", function(res, $) {
            setTimeout(function () {
              // so the session should be dead by now
              browser.get("/bar2", function(res, $) {
                browser.done();
              });
            }, 300);
          });
        }, 300);
      });
    },
    "session no longer has state": function(err, req) {
      assert.isUndefined(req.session.baz);
    }
  }
});

function create_app_with_duration_modification() {
  // simple app
  var app = express();

  app.use(cookieSessions({
    cookieName: 'session',
    secret: 'yobaby',
    activeDuration: 0,
    duration: 5000 // 5.0 seconds
  }));

  app.get("/create", function(req, res) {
    req.session.foo = "foo";
    res.send("created");
  });

  app.get("/augment", function(req, res) {
    req.session.bar = "bar";
    res.send("augmented");
  });

  // invoking this will change the session duration to 500ms
  app.get("/change", function(req, res) {
    req.session.setDuration(500);
    res.send("duration changed");
  });


  return app;
}

suite.addBatch({
  "after changing cookie duration and querying outside the modified duration": {
    topic: function() {
      var self = this;

      var app = create_app_with_duration_modification();
      app.get("/complete", function(req, res) {
        self.callback(null, req);
        res.send("bar");
      });

      var browser = createBrowser(app);
      browser.get("/create", function(res, $) {
        browser.get("/change", function(res, $) {
          setTimeout(function () {
            browser.get("/complete", function(res, $) {
              browser.done();
            });
          }, 700);
        });
      });
    },
    "session no longer has state": function(err, req) {
      assert.isUndefined(req.session.foo);
    }
  }
});

var initialCookie;
var updatedCookie;

suite.addBatch({
  "changing duration": {
    topic: function() {
      var self = this;

      var app = create_app_with_duration_modification();
      app.get("/complete", function(req, res) {
        self.callback(null, req);
        res.send("bar");
      });

      var browser = createBrowser(app);
      browser.get("/create", function(res, $) {
        initialCookie = browser.cookies[0].value;
        browser.get("/change", function(res, $) {
          updatedCookie = browser.cookies[0].value;
          browser.get("/complete", function(res, $) {
            browser.done();
          });
        });
      });
    },
    "doesn't affect session variables": function(err, req) {
      assert.equal(req.session.foo, "foo");
    },
    "does update creation time": function(err, req) {
      assert.notEqual(initialCookie.split('.')[2],
                      updatedCookie.split('.')[2],
                      "after duration update, creation should be updated");
    },
    "does update duration": function(err, req) {
      assert.strictEqual(parseInt(initialCookie.split('.')[3], 10), 5000);
      assert.strictEqual(parseInt(updatedCookie.split('.')[3], 10), 500);
    }
  }
});

suite.addBatch({
  "after changing duration then setting a new session variable": {
    topic: function() {
      var self = this;

      var app = create_app_with_duration_modification();
      app.get("/set_then_duration", function(req, res) {
        req.session.baz = "baz";
        req.session.setDuration(500);
        res.send("did it");
      });


      app.get("/complete", function(req, res) {
        self.callback(null, req);
        res.send("bar");
      });

      var browser = createBrowser(app);
      browser.get("/create", function(res, $) {
        browser.get("/set_then_duration", function(res, $) {
          browser.get("/complete", function(res, $) {
            browser.done();
          });
        });
      });
    },
    "variable is visible": function(err, req) {
      assert.equal(req.session.foo, "foo");
      assert.equal(req.session.baz, "baz");
    }
  }
});

suite.addBatch({
  "after setting a new session variable then changing duration": {
    topic: function() {
      var self = this;

      var app = create_app_with_duration_modification();
      app.get("/set_then_duration", function(req, res) {
        req.session.setDuration(500);
        req.session.baz = "baz";
        res.send("did it");
      });


      app.get("/complete", function(req, res) {
        self.callback(null, req);
        res.send("bar");
      });

      var browser = createBrowser(app);
      browser.get("/create", function(res, $) {
        browser.get("/set_then_duration", function(res, $) {
          browser.get("/complete", function(res, $) {
            browser.done();
          });
        });
      });
    },
    "variable is visible": function(err, req) {
      assert.equal(req.session.foo, "foo");
      assert.equal(req.session.baz, "baz");
    }
  }
});

suite.addBatch({
  "setting new variables then invoking setDuration": {
    topic: function() {
      var self = this;

      var app = create_app_with_duration_modification();
      app.get("/complete", function(req, res) {
        self.callback(null, req);
        res.send("bar");
      });

      var browser = createBrowser(app);
      browser.get("/create", function(res, $) {
        browser.get("/change", function(res, $) {
          browser.get("/augment", function(res, $) {
            browser.get("/complete", function(res, $) {
              browser.done();
            });
          });
        });
      });
    },
    "both variables are visible": function(err, req) {
      assert.equal(req.session.foo, "foo");
      assert.equal(req.session.bar, "bar");
    }
  }
});

function create_app_with_secure(firstMiddleware) {
  // set up the session middleware
  var middleware = cookieSessions({
    cookieName: 'session',
    secret: 'yo',
    activeDuration: 0,
    cookie: {
      maxAge: 5000,
      secure: true
    }
  });

  var app = express();
  if (firstMiddleware)
    app.use(firstMiddleware);

  app.use(middleware);

  return app;
}

suite.addBatch({
  "across two requests, without proxySecure, secure cookies" : {
    topic: function() {
      var self = this;

      var app = create_app_with_secure();

      app.get("/foo", function(req, res) {
        res.send("foo");
      });

      var browser = createBrowser(app);
      browser.get("/foo", function(res, $) {
        browser.done();
        self.callback(null, res);
      });
    },
    "cannot be set": function(err, res) {
      assert.equal(res.statusCode, 500);
    }
  }
});

suite.addBatch({
  "across two requests, with proxySecure, secure cookies" : {
    topic: function() {
      var self = this;

      var app = create_app_with_secure(function(req, res, next) {
        // say it is proxySecure
        req.connection.proxySecure = true;
        next();
      });

      app.get("/foo", function(req, res) {
        res.send("foo");
      });

      var browser = createBrowser(app);
      browser.get("/foo", function(res, $) {
        browser.done();
        self.callback(null, res);
      });

    },
    "can be set": function(err, res) {
      assert.equal(res.statusCode, 200);
    }
  }
});


suite.addBatch({
  "public encode and decode util methods" : {
    topic: function() {
      var self = this;

      var app = create_app();
      app.get("/foo", function(req, res) {
        self.callback(null, req);
        res.send("hello");
      });

      var browser = createBrowser(app);
      browser.get("/foo", function(res, $) {
        browser.done();
      });
    },
    "encode " : function(err, req){
      var result = cookieSessions.util.encode({cookieName: 'session', secret: 'yo'}, {foo:'bar'});
      var result_arr = result.split(".");
      assert.equal(result_arr.length, 5);
    },
    "encode and decode - is object" : function(err, req){
      var encoded = cookieSessions.util.encode({cookieName: 'session', secret: 'yo'}, {foo:'bar'});
      var decoded = cookieSessions.util.decode({cookieName: 'session', secret: 'yo'}, encoded);
      assert.isObject(decoded);
    },
    "encode and decode - has all values" : function(err, req){
      var encoded = cookieSessions.util.encode({cookieName: 'session', secret: 'yo'}, {foo:'bar', bar:'foo'});
      var decoded = cookieSessions.util.decode({cookieName: 'session', secret: 'yo'}, encoded);
      assert.equal(decoded.content.foo, 'bar');
      assert.equal(decoded.content.bar, 'foo');
      assert.isNumber(decoded.duration);
      assert.isNumber(decoded.createdAt);
    },
    "encode and decode - override duration and createdAt" : function(err, req){
      var encoded = cookieSessions.util.encode({cookieName: 'session', secret: 'yo'}, {foo:'bar', bar:'foo'}, 5000, 1355408039221);
      var decoded = cookieSessions.util.decode({cookieName: 'session', secret: 'yo'}, encoded);
      assert.equal(decoded.duration, 5000);
      assert.equal(decoded.createdAt, 1355408039221);
    },
    "encode and decode - default duration" : function(err, req){
      var encoded = cookieSessions.util.encode({cookieName: 'session', secret: 'yo'}, {foo:'bar'});
      var decoded = cookieSessions.util.decode({cookieName: 'session', secret: 'yo'}, encoded);
      assert.equal(decoded.duration, 86400000);
    },
    "encode and decode - tampered HMAC" : function(err, req){
      var encodedReal = 'LVB3G2lnPF75RzsT9mz7jQ.RT1Lcq0dOJ_DMRHyWJ4NZPjBXr2WzkFcUC4NO78gbCQ.1371704898483.5000.ILEusgnajT1sqCWLuzaUt-HFn2KPjYNd38DhI7aRCb9';
      var encodedFake = encodedReal.substring(0, encodedReal.length - 1) + 'A';

      var decodedReal = cookieSessions.util.decode({cookieName: 'session', secret: 'yo'}, encodedReal);
      assert.isObject(decodedReal);
      var decodedFake = cookieSessions.util.decode({cookieName: 'session', secret: 'yo'}, encodedFake);
      assert.isUndefined(decodedFake);
    },
    "decode - invalid input" : function(err, req){
        var notEnoughComponents = 'LVB3G2lnPF75RzsT9mz7jQ.RT1Lcq0dOJ_DMRHyWJ4NZPjBXr2WzkFcUC4NO78gbCQ.1371704898483.5000';
        assert.isUndefined(cookieSessions.util.decode({cookieName: 'session', secret: 'yo'}, notEnoughComponents));

        var invalidBase64 = 'LVB3G2lnPF75RzsT9mz7jQ.RT1Lcq0dOJ_DMRHyWJ4NZPjBXr2WzkFcUC4NO78gb.1371704898483.5000.ILEusgnajT1sqCWLuzaUt-HFn2KPjYNd38DhI7aRCb9';
        assert.isUndefined(cookieSessions.util.decode({cookieName: 'session', secret: 'yo'}, invalidBase64));
    }
  }
});

suite.addBatch({
  "two middlewares": {
    topic: function() {
      var self = this;

      var app = create_app();
      app.get("/foo", function(req, res) {
        self.callback(null, req);
        res.send("hello");
      });

      var browser = createBrowser(app);
      browser.get("/foo", function(res, $){
        browser.done();
      });
    },
    "We can write to both stores": function(err, req) {
      req.session.foo = 'bar';
      req.securestore.foo = 'buzz';
      req.securestore.widget = 4;

      assert.equal(req.session.foo, 'bar');
      assert.equal(req.securestore.foo, 'buzz');
      assert.equal(req.securestore.widget, 4);
    }
  }
});

suite.addBatch({
  "specifying requestKey different than cookieName": {
    topic: function() {
      var self = this;

      var app = express();
      app.use(cookieSessions({
        cookieName: 'ooga_booga_momma',
        activeDuration: 0,
        requestKey: 'ses',
        secret: 'yo'
      }));

      app.get('/foo', function(req, res) {
        self.callback(null, req);
        res.send('hello');
      });

      var browser = createBrowser(app);
      browser.get("/foo", function(res, $){
        browser.done();
      });
    },
    "session is defined as req[requestKey]": function(err, req) {
      assert.isObject(req.ses);
      assert.strictEqual(Object.keys(req.ses).length, 0);
      assert.isUndefined(req.session);
      assert.isUndefined(req.ooga_booga_momma);
    }
  }
});

suite.addBatch({
  "swapping two cookies": {
    topic: function() {
      var self = this;
      var app = create_app(); //important that they use the same secret
      app.get('/foo', function(req, res) {
        req.session.foo = 'bar';
        req.securestore.foo = 'buzz';
        req.securestore.widget = 4;
        res.send('hello');
      });
      app.get('/bar', function(req, res) {
        self.callback(null, req);
        res.send('bye');
      });

      createBrowser(app).get('/foo', function(res, $){
        var cookies = res.headers['set-cookie'];
        var firstCookie = cookies[0];
        var secondCookie = cookies[1];

        function getCookieName(cookieHeader) {
          return cookieHeader.substring(0, cookieHeader.indexOf('='));
        }

        function getCookieValue(cookieHeader) {
          return cookieHeader.substring(cookieHeader.indexOf('='), cookieHeader.indexOf(';'));
        }

        var firstHijack = getCookieName(firstCookie) + getCookieValue(secondCookie);
        var secondHijack = getCookieName(secondCookie) + getCookieValue(firstCookie);

        var browser = createBrowser(app);
        browser.get('/bar', {
            headers: { 'Cookie': firstHijack + '; ' + secondHijack } 
        }, function(res, $){
          browser.done();
        });

      });
    },
    "doesn't keep using cookie": function(err, req) {
      // session.foo should not be what securestore.foo was, or else
      // we swapped cookies!
      assert.notEqual(req.session.foo, 'buzz');
      assert.notEqual(req.session.widget, 4);
      assert.notEqual(req.securestore.foo, 'bar');
    }
  }
});

suite.addBatch({
  "missing cookie maxAge": {
    topic: function() {
      var self = this;

      var app = express();
      app.use(cookieSessions({
        cookieName: 'session',
        duration: 50000,
        activeDuration: 0,
        secret: 'yo'
      }));

      app.get("/foo", function(req, res) {
        req.session.foo = 'foobar';
        res.send("hello");
      });

      var browser = createBrowser(app);
      browser.get("/foo", function(res, $) {
        browser.done();
        self.callback(null, res);
      });
    },
    "still has an expires attribute": function(err, res) {
      assert.match(res.headers['set-cookie'][0], /expires/, "cookie is a session cookie");
    },
    "which roughly matches the session duration": function(err, res) {
      var expiryValue = res.headers['set-cookie'][0].replace(/^.*expires=([^;]+);.*$/, "$1");
      var expiryDate = new Date(expiryValue);
      var cookieDuration = expiryDate.getTime() - Date.now();
      assert(Math.abs(50000 - cookieDuration) < 1500, "expiry is pretty far from the specified duration");
    }
  },
  "changing the duration": {
    topic: function() {
      var self = this;

      var app = express();
      app.use(cookieSessions({
        cookieName: 'session',
        duration: 500,
        activeDuration: 0,
        secret: 'yo'
      }));

      app.get("/foo", function(req, res) {
        req.session.foo = 'foobar';
        res.send("hello");
      });

      app.get("/bar", function(req, res) {
        req.session.setDuration(5000);
        res.send("bar");
      });

      var browser = createBrowser(app);
      browser.get("/foo", function(res, $) {
        setTimeout(function () {
          browser.get("/bar", function(res, $) {
            browser.done();
            self.callback(null, res);
          });
        }, 200);
      });
    },
    "updates the cookie expiry": function(err, res) {
      var expiryValue = res.headers['set-cookie'][0].replace(/^.*expires=([^;]+);.*$/, "$1");
      var expiryDate = new Date(expiryValue);
      var cookieDuration = expiryDate.getTime() - Date.now();
      assert(Math.abs(cookieDuration - 5000) < 1000, "expiry is pretty far from the specified duration");
    }
  },
  "active user with session close to expiration": {
    topic: function() {
      var app = express();
      var self = this;
      app.use(cookieSessions({
        cookieName: 'session',
        duration: 300,
        activeDuration: 500,
        secret: 'yo'
      }));

      app.get("/foo", function(req, res) {
        req.session.foo = 'foobar';
        res.send("hello");
      });

      app.get("/bar", function(req, res) {
        req.session.bar = 'baz';
        res.send('hi');
      });

      app.get("/baz", function(req, res) {
        res.json({ "msg": req.session.foo + req.session.bar });
      });

      var browser = createBrowser(app);
      browser.get("/foo", function() {
        browser.get("/bar", function() {
          setTimeout(function () {
            browser.get("/baz", {json: true}, function(res, first) {
              setTimeout(function() {
                browser.get('/baz', {json: true}, function(res, second) {
                  browser.done();
                  self.callback(null, first, second);
                });
              }, 1000);
            });
          }, 400);
        });
      });

    },
    "extends session duration": function(err, extended, tooLate) {
      assert.equal(extended.msg, 'foobarbaz');
      assert.equal(tooLate.msg, null);
    }
  }
});

var shared_browser1;
var shared_browser2;

suite.addBatch({
  "non-ephemeral cookie": {
    topic: function() {
      var self = this;

      var app = express();
      app.use(cookieSessions({
        cookieName: 'session',
        duration: 5000,
        secret: 'yo',
        cookie: {
          ephemeral: false
        }
      }));

      app.get("/foo", function(req, res) {
        req.session.foo = 'foobar';
        res.send("hello");
      });

      app.get("/bar", function(req, res) {
        req.session.setDuration(6000, true);
        res.send("hello");
      });

      shared_browser1 = createBrowser(app);
      shared_browser1.get("/foo", function(res, $) {
        self.callback(null, res);
      });
    },
    "has an expires attribute": function(err, res) {
      assert.match(res.headers['set-cookie'][0], /expires/, "cookie is a session cookie");
    },
    "changing to an ephemeral one": {
      topic: function() {
        var self = this;
        shared_browser1.get("/bar", function(res, $) {
          shared_browser1.done();
          self.callback(null, res);
        });
      },
      "removes its expires attribute": function(err, res) {
        assert.strictEqual(res.headers['set-cookie'][0].indexOf('expires='), -1, "cookie is not ephemeral");
      }
    }
  },
  "ephemeral cookie": {
    topic: function() {
      var self = this;

      var app = express();
      app.use(cookieSessions({
        cookieName: 'session',
        duration: 50000,
        activeDuration: 0,
        secret: 'yo',
        cookie: {
          ephemeral: true
        }
      }));

      app.get("/foo", function(req, res) {
        req.session.foo = 'foobar';
        res.send("hello");
      });

      app.get("/bar", function(req, res) {
        req.session.setDuration(6000, false);
        res.send("hello");
      });

      shared_browser2 = createBrowser(app);
      shared_browser2.get("/foo", function(res, $) {
        self.callback(null, res);
      });
    },
    "doesn't have an expires attribute": function(err, res) {
      assert.strictEqual(res.headers['set-cookie'][0].indexOf('expires='), -1, "cookie is not ephemeral");
    },
    "changing to an non-ephemeral one": {
      topic: function() {
        var self = this;
        shared_browser2.get("/bar", function(res, $) {
          shared_browser2.done();
          self.callback(null, res);
        });
      },
      "gains an expires attribute": function(err, res) {
        assert.match(res.headers['set-cookie'][0], /expires/, "cookie is a session cookie");
      }
    }
  }
});

suite.addBatch({
  "sameSite cookie": {
    topic: function() {
      var self = this;
      var app = express();

      app.use(cookieSessions({
        cookieName: 'session',
        secret: 'yo',
        activeDuration: 0,
        cookie: {
          sameSite: 'lax'
        }
      }));

      app.get("/foo", function(req, res) {
        req.session.foo = 'foobar';
        res.send("hello");
      });

      var browser = createBrowser(app);
      browser.get("/foo", function(res, $) {
        browser.done();
        self.callback(null, res);
      });
    },
    "has samesite attribute": function(err, res) {
      assert.match(res.headers['set-cookie'][0], /samesite=lax/, "cookie uses samesite");
    }
  }
});

var sixtyFourByteKey = new Buffer(
  '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
  'binary'
);
var HMAC_EXPECT = {
  // aligned so you can see the dropN effect:
  'sha256':
    'PRYaxV/8RkMyIT/Ib+tIUOWiSn+0EvodJ5rtG1FQHz0=',
  'sha256-drop128':
    'PRYaxV/8RkMyIT/Ib+tIUA==',
  'sha384':
    'MND9nz6pxbQC5m41ZPRXhJIuqTj9/hu4gtWZ8t8LgdFLQFWQfC8jhijB0NHLpeA7',
  'sha384-drop192':
    'MND9nz6pxbQC5m41ZPRXhJIuqTj9/hu4',
  'sha512':
    'Hr4KLVLyglIwQ43C9U2bmieWBVLnD/F+lzCSF072Ds2b87MK+gbnR0p75A+I+5ez+aiemMGuMZyKVAUWfMMaUA==',
  'sha512-drop256':
    'Hr4KLVLyglIwQ43C9U2bmieWBVLnD/F+lzCSF072Ds0='
};

function testHmac(algo) {
  var block = {};
  block.topic = function() {
    var opts = {
      signatureAlgorithm: algo,
      signatureKey: sixtyFourByteKey
    };
    var iv = new Buffer('01234567890abcdef','binary'); // 128-bits
    var ciphertext = new Buffer('0123456789abcdef0123','binary');
    var duration = 876543210;
    var createdAt = 1234567890;

    return cookieSessions.util.computeHmac(
      opts, iv, ciphertext, duration, createdAt
    ).toString('base64');
  };

  block['equals test vector'] = function(val) {
    assert.equal(val, HMAC_EXPECT[algo]);
  };

  return block;
}

suite.addBatch({
  "computeHmac": {
    "sha256": testHmac('sha256'),
    "sha256-drop128": testHmac('sha256-drop128'),
    "sha384": testHmac('sha384'),
    "sha384-drop192": testHmac('sha384-drop192'),
    "sha512": testHmac('sha512'),
    "sha512-drop256": testHmac('sha512-drop256'),
  }
});

suite.export(module);
