From e4ec0a0de7653a3c4bd24aad4bffee3adc3155c4 Mon Sep 17 00:00:00 2001
From: Brian Warner <warner@lothar.com>
Date: Mon, 30 Jul 2012 17:38:31 -0700
Subject: [PATCH] mysql schema: use TIMESTAMP for lastPasswordReset, not BIGINT

Since MySQL TIMESTAMP is quantized to whole seconds, also change tests
to add a 2s stall before changing the password, to make sure
lastPasswordReset gets a new value.
---
 lib/db/json.js                |  4 ++--
 lib/db/mysql.js               | 14 +++++++-------
 tests/password-update-test.js | 32 +++++++++++++++++++++++++-------
 3 files changed, 34 insertions(+), 16 deletions(-)

diff --git a/lib/db/json.js b/lib/db/json.js
index 946f74366..6c9555b65 100644
--- a/lib/db/json.js
+++ b/lib/db/json.js
@@ -33,7 +33,7 @@ var dbPath = path.join(configuration.get('var_path'), "authdb.json");
  *    {
  *      id: <numerical user id>
  *      password: "somepass",
- *      lastPasswordReset: 123456, (ms-since-epoch, integer)
+ *      lastPasswordReset: 123456, (seconds-since-epoch, integer)
  *      emails: {
  *        "lloyd@hilaiel.com": {
  *          type: 'secondary'
@@ -43,7 +43,7 @@ var dbPath = path.join(configuration.get('var_path'), "authdb.json");
  *  ]
  */
 
-function now() { return new Date().getTime(); }
+function now() { return Math.floor(new Date().getTime() / 1000); }
 
 function getNextUserID() {
   var max = 1;
diff --git a/lib/db/mysql.js b/lib/db/mysql.js
index 3ed307250..62839715c 100644
--- a/lib/db/mysql.js
+++ b/lib/db/mysql.js
@@ -65,7 +65,7 @@ const schemas = [
   "CREATE TABLE IF NOT EXISTS user (" +
     "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
     "passwd CHAR(64)," +
-    "lastPasswordReset BIGINT" +
+    "lastPasswordReset TIMESTAMP DEFAULT 0 NOT NULL" +
     ") ENGINE=InnoDB;",
 
   "CREATE TABLE IF NOT EXISTS email (" +
@@ -89,7 +89,7 @@ const schemas = [
     ") ENGINE=InnoDB;",
 ];
 
-function now() { return new Date().getTime().toString(); }
+function now() { return Math.floor(new Date().getTime() / 1000); }
 
 // log an unexpected database error
 function logUnexpectedError(detail) {
@@ -371,7 +371,7 @@ exports.completeCreateUser = function(secret, cb) {
 
     // we're creating a new account, add appropriate entries into user and email tables.
     client.query(
-      "INSERT INTO user(passwd, lastPasswordReset) VALUES(?,?)",
+      "INSERT INTO user(passwd, lastPasswordReset) VALUES(?,FROM_UNIXTIME(?))",
       [ o.passwd, now() ],
       function(err, info) {
         if (err) return cb(err);
@@ -451,7 +451,7 @@ exports.addPrimaryEmailToAccount = function(uid, emailToAdd, cb) {
 exports.createUserWithPrimaryEmail = function(email, cb) {
   // create a new user acct with no password
   client.query(
-    "INSERT INTO user(lastPasswordReset) VALUES(?)",
+    "INSERT INTO user(lastPasswordReset) VALUES(FROM_UNIXTIME(?))",
     [ now() ],
     function(err, info) {
       if (err) return cb(err);
@@ -515,7 +515,7 @@ exports.checkAuth = function(uid, cb) {
 
 exports.lastPasswordReset = function(uid, cb) {
   client.query(
-    'SELECT lastPasswordReset FROM user WHERE id = ?',
+    'SELECT UNIX_TIMESTAMP(lastPasswordReset) AS lastPasswordReset FROM user WHERE id = ?',
     [ uid ],
     function (err, rows) {
       cb(err, (rows && rows.length == 1) ? rows[0].lastPasswordReset : undefined);
@@ -524,7 +524,7 @@ exports.lastPasswordReset = function(uid, cb) {
 
 exports.updatePassword = function(uid, hash, invalidateSessions, cb) {
   var query = invalidateSessions ?
-    'UPDATE user SET passwd = ?, lastPasswordReset = ? WHERE id = ?' :
+    'UPDATE user SET passwd = ?, lastPasswordReset = FROM_UNIXTIME(?) WHERE id = ?' :
     'UPDATE user SET passwd = ? WHERE id = ?';
   var args = invalidateSessions ? [ hash, now(), uid ] : [ hash, uid ];
   client.query(query, args,
@@ -591,7 +591,7 @@ exports.cancelAccount = function(uid, cb) {
 
 exports.addTestUser = function(email, hash, cb) {
   client.query(
-    "INSERT INTO user(passwd, lastPasswordReset) VALUES(?)",
+    "INSERT INTO user(passwd, lastPasswordReset) VALUES(FROM_UNIXTIME(?))",
     [ hash, now() ],
     function(err, info) {
       if (err) return cb(err);
diff --git a/tests/password-update-test.js b/tests/password-update-test.js
index 173794d51..e2d9b77a3 100755
--- a/tests/password-update-test.js
+++ b/tests/password-update-test.js
@@ -141,13 +141,31 @@ suite.addBatch({
 });
 
 suite.addBatch({
-  "updating the password": {
-    topic: wsapi.post('/wsapi/update_password', {
-      oldpass: OLD_PASSWORD,
-      newpass: NEW_PASSWORD
-    }),
-    "works as expected": function(err, r) {
-      assert.strictEqual(JSON.parse(r.body).success, true);
+  "after waiting for lastPasswordReset's now() to increment": {
+    topic: function() {
+      // we introduce a 2s delay here to ensure that the now() call in
+      // lib/db/{json,mysql}.js will return a different value than it did
+      // during complete_user_creation(), thus expiring the old session still
+      // hanging out in context2. now() returns an integer
+      // seconds-since-epoch, so the shortest delay that will reliably get a
+      // different result is 1.0s+epsilon (depending upon the resolution of
+      // the system clock). To avoid this stall (and make the test suite run
+      // 2s faster), either:
+      //  1: change now() to include a mutable offset, expose a
+      //     db.addNowOffset() to "accelerate the universe", have this code
+      //     add 1s instead of using setTimeout. Or:
+      //  2: add a db function to modify (increment) lastPasswordReset by 1s,
+      //     have this code call it instead of using setTimeout
+      setTimeout(this.callback, 2000);
+      },
+    "updating the password": {
+      topic: wsapi.post('/wsapi/update_password', {
+        oldpass: OLD_PASSWORD,
+        newpass: NEW_PASSWORD
+      }),
+      "works as expected": function(err, r) {
+        assert.strictEqual(JSON.parse(r.body).success, true);
+      }
     }
   }
 });
-- 
GitLab