This commit is contained in:
Danny Coates 2014-07-01 17:20:06 -07:00
Коммит 9a932731ee
27 изменённых файлов: 2736 добавлений и 0 удалений

2
.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,2 @@
node_modules
coverage.html

213
bin/db_patcher.js Normal file
Просмотреть файл

@ -0,0 +1,213 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
var path = require('path')
var fs = require('fs')
var mysql = require('mysql')
var P = require('../promise.js')
var options = require('../config')
var log = require('../log')(options.logLevel, 'db-patcher')
var schemaDir = path.join(__dirname, '..', 'db', 'schema')
var patches = {}
var files = fs.readdirSync(schemaDir)
files.forEach(function(filename) {
var from, to
var m = filename.match(/^patch-(\d+)-(\d+)\.sql$/)
if (m) {
from = parseInt(m[1], 10)
to = parseInt(m[2], 10)
patches[from] = patches[from] || {}
patches[from][to] = fs.readFileSync(path.join(schemaDir, filename), { encoding: 'utf8'})
}
else {
console.warn("Startup error: Unknown file in schema/ directory - '%s'", filename)
process.exit(2)
}
})
// To run any patches we need to switch multipleStatements on
options.master.multipleStatements = true
// when creating the database, we need to connect without a database name
var database = options.master.database
delete options.master.database
var client = mysql.createConnection(options.master)
createDatabase()
.then(changeUser)
.then(checkDbMetadataExists)
.then(readDbPatchLevel)
.then(patchToRequiredLevel)
.then(closeAndReconnect)
.done(
function() {
log.info('Patching complete')
},
function(err) {
log.fatal(err)
process.exit(2)
}
)
// helper functions
function createDatabase() {
var d = P.defer()
log.trace( { op: 'MySql.createSchema:CreateDatabase' } )
client.query(
'CREATE DATABASE IF NOT EXISTS ' + database + ' CHARACTER SET utf8 COLLATE utf8_unicode_ci',
function (err) {
if (err) {
log.error({ op: 'MySql.createSchema:CreateDatabase', err: err.message })
return d.reject(err)
}
d.resolve()
}
)
return d.promise
}
function changeUser() {
var d = P.defer()
log.trace( { op: 'MySql.createSchema:ChangeUser' } )
client.changeUser(
{
user : options.master.user,
password : options.master.password,
database : database
},
function (err) {
if (err) {
log.error({ op: 'MySql.createSchema:ChangeUser', err: err.message })
return d.reject(err)
}
d.resolve()
}
)
return d.promise
}
function checkDbMetadataExists() {
log.trace( { op: 'MySql.createSchema:CheckDbMetadataExists' } )
var d = P.defer()
var query = "SELECT COUNT(*) AS count FROM information_schema.TABLES WHERE table_schema = ? AND table_name = 'dbMetadata'"
client.query(
query,
[ database ],
function (err, result) {
if (err) {
log.trace( { op: 'MySql.createSchema:MakingTheSchema', err: err.message } )
return d.reject(err)
}
d.resolve(result[0].count === 0 ? false : true)
}
)
return d.promise
}
function readDbPatchLevel(dbMetadataExists) {
log.trace( { op: 'MySql.createSchema:ReadDbPatchLevel' } )
if ( dbMetadataExists === false ) {
// the table doesn't exist, so start at patch level 0
return P.resolve(0)
}
// find out what patch level the database is currently at
var d = P.defer()
var query = "SELECT value FROM dbMetadata WHERE name = ?"
client.query(
query,
[ options.patchKey ],
function(err, result) {
if (err) {
log.trace( { op: 'MySql.createSchema:ReadDbPatchLevel', err: err.message } )
return d.reject(err)
}
// convert the patch level from a string to a number
return d.resolve(+result[0].value)
}
)
return d.promise
}
function patchToRequiredLevel(currentPatchLevel) {
log.trace( { op: 'MySql.createSchema:PatchToRequiredLevel' } )
// if we don't need any patches
if ( options.patchLevel === currentPatchLevel ) {
log.trace( { op: 'MySql.createSchema:PatchToRequiredLevel', patch: 'No patch required' } )
return P.resolve()
}
// We don't want any reverse patches to be automatically applied, so
// just emit a warning and carry on.
if ( options.patchLevel < currentPatchLevel ) {
log.warn( { op: 'MySql.createSchema:PatchToRequiredLevel', err: 'Reverse patch required - must be done manually' } )
return P.resolve()
}
log.trace({
op: 'MySql.createSchema:PatchToRequiredLevel',
msg1: 'Patching from ' + currentPatchLevel + ' to ' + options.patchLevel
})
var promise = P.resolve()
var patchesToApply = []
// First, loop through all the patches we need to apply
// to make sure they exist.
while ( currentPatchLevel < options.patchLevel ) {
// check that this patch exists
if ( !patches[currentPatchLevel][currentPatchLevel+1] ) {
log.fatal({
op: 'MySql.createSchema:PatchToRequiredLevel',
err: 'Patch from level ' + currentPatchLevel + ' to ' + (currentPatchLevel+1) + ' does not exist'
});
process.exit(2)
}
patchesToApply.push({
sql : patches[currentPatchLevel][currentPatchLevel+1],
from : currentPatchLevel,
to : currentPatchLevel+1,
})
currentPatchLevel += 1
}
// now apply each patch
patchesToApply.forEach(function(patch) {
promise = promise.then(function() {
var d = P.defer()
log.trace({ op: 'MySql.createSchema:PatchToRequiredLevel', msg1: 'Updating DB for patch ' + patch.from + ' to ' + patch.to })
client.query(
patch.sql,
function(err) {
if (err) return d.reject(err)
d.resolve()
}
)
return d.promise
})
})
return promise
}
function closeAndReconnect() {
var d = P.defer()
log.trace( { op: 'MySql.createSchema:CloseAndReconnect' } )
client.end(
function (err) {
if (err) {
log.error({ op: 'MySql.createSchema:Closed', err: err.message })
return d.reject(err)
}
// create the mysql class
log.trace( { op: 'MySql.createSchema:ResolvingWithNewClient' } )
d.resolve('ok')
}
)
return d.promise
}

21
bin/server.js Normal file
Просмотреть файл

@ -0,0 +1,21 @@
var config = require('../config')
var createServer = require('fxa-auth-db-server')
var error = require('../error')
var log = require('../log')(config.logLevel, 'db-api')
var DB = require('../db/mysql')(log, error)
var version = require('../package.json').version
function shutdown() {
process.nextTick(process.exit)
}
// defer to allow ass code coverage results to complete processing
if (process.env.ASS_CODE_COVERAGE) {
process.on('SIGINT', shutdown)
}
DB.connect(config).done(
function (db) {
var server = createServer(version, db, log, config.port, config.host)
}
)

34
config.js Normal file
Просмотреть файл

@ -0,0 +1,34 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
module.exports = require('rc')(
'fxa_db',
{
logLevel: 'trace',
host: "127.0.0.1",
port: 8000,
patchKey: 'schema-patch-level',
patchLevel: 2,
master: {
user: 'root',
password: '',
database: 'fxa',
host: '127.0.0.1',
port: 3306,
connectionLimit: 10,
waitForConnections: true,
queueLimit: 100
},
slave: {
user: 'root',
password: '',
database: 'fxa',
host: '127.0.0.1',
port: 3306,
connectionLimit: 10,
waitForConnections: true,
queueLimit: 100
}
}
)

587
db/mysql.js Normal file
Просмотреть файл

@ -0,0 +1,587 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
var mysql = require('mysql')
var P = require('../promise')
module.exports = function (log, error) {
// http://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html
var LOCK_ERRNOS = [ 1205, 1206, 1213, 1689 ]
// make a pool of connections that we can draw from
function MySql(options) {
this.patchLevel = 0
// poolCluster will remove the pool after `removeNodeErrorCount` errors.
// We don't ever want to remove a pool because we only have one pool
// for writing and reading each. Connection errors are mostly out of our
// control for automatic recovery so monitoring of 503s is critical.
// Since `removeNodeErrorCount` is Infinity `canRetry` must be false
// to prevent inifinite retry attempts.
this.poolCluster = mysql.createPoolCluster(
{
removeNodeErrorCount: Infinity,
canRetry: false
}
)
// Use separate pools for master and slave connections.
this.poolCluster.add('MASTER', options.master)
this.poolCluster.add('SLAVE', options.slave)
this.getClusterConnection = P.promisify(this.poolCluster.getConnection, this.poolCluster)
this.statInterval = setInterval(
reportStats.bind(this),
options.statInterval || 15000
)
this.statInterval.unref()
}
function reportStats() {
var nodes = Object.keys(this.poolCluster._nodes).map(
function (name) {
return this.poolCluster._nodes[name]
}.bind(this)
)
var stats = nodes.reduce(
function (totals, node) {
totals.errors += node.errorCount
totals.connections += node.pool._allConnections.length
totals.queue += node.pool._connectionQueue.length
totals.free += node.pool._freeConnections.length
return totals
},
{
stat: 'mysql',
errors: 0,
connections: 0,
queue: 0,
free: 0
}
)
log.stat(stats)
}
// this will be called from outside this file
MySql.connect = function(options) {
// check that the database patch level is what we expect (or one above)
var mysql = new MySql(options)
return mysql.readOne("SELECT value FROM dbMetadata WHERE name = ?", options.patchKey)
.then(
function (result) {
mysql.patchLevel = +result.value
if (
mysql.patchLevel < options.patchLevel ||
mysql.patchLevel > options.patchLevel + 1
) {
throw new Error('dbIncorrectPatchLevel')
}
log.trace({
op: 'MySql.connect',
patchLevel: mysql.patchLevel,
patchLevelRequired: options.patchLevel
})
return mysql
}
)
}
MySql.prototype.close = function () {
this.poolCluster.end()
clearInterval(this.statInterval)
return P.resolve()
}
MySql.prototype.ping = function () {
return this.getConnection('MASTER')
.then(
function(connection) {
var d = P.defer()
connection.ping(
function (err) {
connection.release()
return err ? d.reject(err) : d.resolve()
}
)
return d.promise
}
)
}
// CREATE
var CREATE_ACCOUNT = 'INSERT INTO accounts' +
' (uid, normalizedEmail, email, emailCode, emailVerified, kA, wrapWrapKb,' +
' authSalt, verifierVersion, verifyHash, verifierSetAt, createdAt)' +
' VALUES (?, LOWER(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
MySql.prototype.createAccount = function (uid, data) {
data.normalizedEmail = data.email
data.createdAt = data.verifierSetAt = Date.now()
return this.write(
CREATE_ACCOUNT,
[
uid,
data.normalizedEmail,
data.email,
data.emailCode,
data.emailVerified,
data.kA,
data.wrapWrapKb,
data.authSalt,
data.verifierVersion,
data.verifyHash,
data.verifierSetAt,
data.createdAt
]
)
}
var CREATE_SESSION_TOKEN = 'INSERT INTO sessionTokens' +
' (tokenId, tokenData, uid, createdAt)' +
' VALUES (?, ?, ?, ?)'
MySql.prototype.createSessionToken = function (tokenId, sessionToken) {
return this.write(
CREATE_SESSION_TOKEN,
[
tokenId,
sessionToken.data,
sessionToken.uid,
sessionToken.createdAt
]
)
}
var CREATE_KEY_FETCH_TOKEN = 'INSERT INTO keyFetchTokens' +
' (tokenId, authKey, uid, keyBundle, createdAt)' +
' VALUES (?, ?, ?, ?, ?)'
MySql.prototype.createKeyFetchToken = function (tokenId, keyFetchToken) {
return this.write(
CREATE_KEY_FETCH_TOKEN,
[
tokenId,
keyFetchToken.authKey,
keyFetchToken.uid,
keyFetchToken.keyBundle,
keyFetchToken.createdAt
]
)
}
var CREATE_ACCOUNT_RESET_TOKEN = 'REPLACE INTO accountResetTokens' +
' (tokenId, tokenData, uid, createdAt)' +
' VALUES (?, ?, ?, ?)'
MySql.prototype.createAccountResetToken = function (tokenId, accountResetToken) {
return this.write(
CREATE_ACCOUNT_RESET_TOKEN,
[
tokenId,
accountResetToken.data,
accountResetToken.uid,
accountResetToken.createdAt
]
)
}
var CREATE_PASSWORD_FORGOT_TOKEN = 'REPLACE INTO passwordForgotTokens' +
' (tokenId, tokenData, uid, passCode, createdAt, tries)' +
' VALUES (?, ?, ?, ?, ?, ?)'
MySql.prototype.createPasswordForgotToken = function (tokenId, passwordForgotToken) {
return this.write(
CREATE_PASSWORD_FORGOT_TOKEN,
[
tokenId,
passwordForgotToken.data,
passwordForgotToken.uid,
passwordForgotToken.passCode,
passwordForgotToken.createdAt,
passwordForgotToken.tries
]
)
}
var CREATE_PASSWORD_CHANGE_TOKEN = 'REPLACE INTO passwordChangeTokens' +
' (tokenId, tokenData, uid, createdAt)' +
' VALUES (?, ?, ?, ?)'
MySql.prototype.createPasswordChangeToken = function (tokenId, passwordChangeToken) {
return this.write(
CREATE_PASSWORD_CHANGE_TOKEN,
[
tokenId,
passwordChangeToken.data,
passwordChangeToken.uid,
passwordChangeToken.createdAt
]
)
}
// READ
var ACCOUNT_EXISTS = 'SELECT uid FROM accounts WHERE normalizedEmail = LOWER(?)'
MySql.prototype.accountExists = function (email) {
return this.readOne(ACCOUNT_EXISTS, Buffer(email, 'hex').toString('utf8'))
}
var ACCOUNT_DEVICES = 'SELECT tokenId as id FROM sessionTokens WHERE uid = ?'
MySql.prototype.accountDevices = function (uid) {
return this.read(ACCOUNT_DEVICES, uid)
}
var SESSION_TOKEN = 'SELECT t.tokenData, t.uid, t.createdAt,' +
' a.emailVerified, a.email, a.emailCode, a.verifierSetAt' +
' FROM sessionTokens t, accounts a' +
' WHERE t.tokenId = ? AND t.uid = a.uid'
MySql.prototype.sessionToken = function (id) {
return this.readOne(SESSION_TOKEN, id)
}
var KEY_FETCH_TOKEN = 'SELECT t.authKey, t.uid, t.keyBundle, t.createdAt,' +
' a.emailVerified, a.verifierSetAt' +
' FROM keyFetchTokens t, accounts a' +
' WHERE t.tokenId = ? AND t.uid = a.uid'
MySql.prototype.keyFetchToken = function (id) {
return this.readOne(KEY_FETCH_TOKEN, id)
}
var ACCOUNT_RESET_TOKEN = 'SELECT t.uid, t.tokenData, t.createdAt,' +
' a.verifierSetAt' +
' FROM accountResetTokens t, accounts a' +
' WHERE t.tokenId = ? AND t.uid = a.uid'
MySql.prototype.accountResetToken = function (id) {
return this.readOne(ACCOUNT_RESET_TOKEN, id)
}
var PASSWORD_FORGOT_TOKEN = 'SELECT t.tokenData, t.uid, t.createdAt,' +
' t.passCode, t.tries, a.email, a.verifierSetAt' +
' FROM passwordForgotTokens t, accounts a' +
' WHERE t.tokenId = ? AND t.uid = a.uid'
MySql.prototype.passwordForgotToken = function (id) {
return this.readOne(PASSWORD_FORGOT_TOKEN, id)
}
var PASSWORD_CHANGE_TOKEN = 'SELECT t.tokenData, t.uid, t.createdAt, a.verifierSetAt' +
' FROM passwordChangeTokens t, accounts a' +
' WHERE t.tokenId = ? AND t.uid = a.uid'
MySql.prototype.passwordChangeToken = function (id) {
return this.readOne(PASSWORD_CHANGE_TOKEN, id)
}
var EMAIL_RECORD = 'SELECT uid, email, normalizedEmail, emailVerified, emailCode,' +
' kA, wrapWrapKb, verifierVersion, verifyHash, authSalt, verifierSetAt' +
' FROM accounts' +
' WHERE normalizedEmail = LOWER(?)'
MySql.prototype.emailRecord = function (email) {
return this.readOne(EMAIL_RECORD, Buffer(email, 'hex').toString('utf8'))
}
var ACCOUNT = 'SELECT uid, email, normalizedEmail, emailCode, emailVerified, kA,' +
' wrapWrapKb, verifierVersion, verifyHash, authSalt, verifierSetAt, createdAt' +
' FROM accounts WHERE uid = ?'
MySql.prototype.account = function (uid) {
return this.readOne(ACCOUNT, uid)
}
// UPDATE
var UPDATE_PASSWORD_FORGOT_TOKEN = 'UPDATE passwordForgotTokens' +
' SET tries = ? WHERE tokenId = ?'
MySql.prototype.updatePasswordForgotToken = function (tokenId, token) {
return this.write(UPDATE_PASSWORD_FORGOT_TOKEN, [token.tries, tokenId])
}
// DELETE
MySql.prototype.deleteAccount = function (uid) {
return this.transaction(
function (connection) {
var tables = [
'sessionTokens',
'keyFetchTokens',
'accountResetTokens',
'passwordChangeTokens',
'passwordForgotTokens',
'accounts'
]
var queries = deleteFromTablesWhereUid(connection, tables, uid)
return P.all(queries)
}
)
}
var DELETE_SESSION_TOKEN = 'DELETE FROM sessionTokens WHERE tokenId = ?'
MySql.prototype.deleteSessionToken = function (tokenId) {
return this.write(DELETE_SESSION_TOKEN, [tokenId])
}
var DELETE_KEY_FETCH_TOKEN = 'DELETE FROM keyFetchTokens WHERE tokenId = ?'
MySql.prototype.deleteKeyFetchToken = function (tokenId) {
return this.write(DELETE_KEY_FETCH_TOKEN, [tokenId])
}
var DELETE_ACCOUNT_RESET_TOKEN = 'DELETE FROM accountResetTokens WHERE tokenId = ?'
MySql.prototype.deleteAccountResetToken = function (tokenId) {
return this.write(DELETE_ACCOUNT_RESET_TOKEN, [tokenId])
}
var DELETE_PASSWORD_FORGOT_TOKEN = 'DELETE FROM passwordForgotTokens WHERE tokenId = ?'
MySql.prototype.deletePasswordForgotToken = function (tokenId) {
return this.write(DELETE_PASSWORD_FORGOT_TOKEN, [tokenId])
}
var DELETE_PASSWORD_CHANGE_TOKEN = 'DELETE FROM passwordChangeTokens WHERE tokenId = ?'
MySql.prototype.deletePasswordChangeToken = function (tokenId) {
return this.write(DELETE_PASSWORD_CHANGE_TOKEN, [tokenId])
}
// BATCH
var RESET_ACCOUNT = 'UPDATE accounts' +
' SET verifyHash = ?, authSalt = ?, wrapWrapKb = ?, verifierSetAt = ?,' +
' verifierVersion = ?' +
' WHERE uid = ?'
MySql.prototype.resetAccount = function (uid, data) {
return this.transaction(
function (connection) {
var tables = [
'sessionTokens',
'keyFetchTokens',
'accountResetTokens',
'passwordChangeTokens',
'passwordForgotTokens'
]
var queries = deleteFromTablesWhereUid(connection, tables, uid)
queries.push(
query(
connection,
RESET_ACCOUNT,
[
data.verifyHash,
data.authSalt,
data.wrapWrapKb,
Date.now(),
data.verifierVersion,
uid
]
)
)
return P.all(queries)
}
)
}
var VERIFY_EMAIL = 'UPDATE accounts SET emailVerified = true WHERE uid = ?'
MySql.prototype.verifyEmail = function (uid) {
return this.write(VERIFY_EMAIL, [uid])
}
MySql.prototype.forgotPasswordVerified = function (tokenId, accountResetToken) {
return this.transaction(
function (connection) {
return P.all([
query(
connection,
DELETE_PASSWORD_FORGOT_TOKEN,
[tokenId]
),
query(
connection,
CREATE_ACCOUNT_RESET_TOKEN,
[
accountResetToken.tokenId,
accountResetToken.data,
accountResetToken.uid,
accountResetToken.createdAt
]
),
query(
connection,
VERIFY_EMAIL,
[accountResetToken.uid]
)
])
}
)
}
// Internal
MySql.prototype.singleQuery = function (poolName, sql, params) {
return this.getConnection(poolName)
.then(
function (connection) {
return query(connection, sql, params)
.then(
function (result) {
connection.release()
return result
},
function (err) {
connection.release()
throw err
}
)
}
)
}
MySql.prototype.transaction = function (fn) {
return retryable(
function () {
return this.getConnection('MASTER')
.then(
function (connection) {
return query(connection, 'BEGIN')
.then(
function () {
return fn(connection)
}
)
.then(
function (result) {
return query(connection, 'COMMIT')
.then(function () { return result })
}
)
.catch(
function (err) {
log.error({ op: 'MySql.transaction', err: err })
return query(connection, 'ROLLBACK')
.then(function () { throw err })
}
)
.then(
function (result) {
connection.release()
return result
},
function (err) {
connection.release()
throw err
}
)
}
)
}.bind(this),
LOCK_ERRNOS
)
.catch(
function (err) {
throw error.wrap(err)
}
)
}
MySql.prototype.readOne = function (sql, param) {
return this.read(sql, param).then(firstResult)
}
MySql.prototype.read = function (sql, param) {
return this.singleQuery('SLAVE*', sql, [param])
.catch(
function (err) {
log.error({ op: 'MySql.read', sql: sql, id: param, err: err })
throw error.wrap(err)
}
)
}
MySql.prototype.write = function (sql, params) {
return this.singleQuery('MASTER', sql, params)
.then(
function (result) {
log.trace({ op: 'MySql.write', sql: sql, result: result })
return {}
},
function (err) {
log.error({ op: 'MySql.write', sql: sql, err: err })
if (err.errno === 1062) {
err = error.duplicate()
}
else {
err = error.wrap(err)
}
throw err
}
)
}
MySql.prototype.getConnection = function (name) {
return retryable(
this.getClusterConnection,
[1040, 'ECONNREFUSED', 'ETIMEDOUT', 'ECONNRESET']
)
}
function firstResult(results) {
if (!results.length) { throw error.notFound() }
return results[0]
}
function query(connection, sql, params) {
var d = P.defer()
connection.query(
sql,
params || [],
function (err, results) {
if (err) { return d.reject(err) }
d.resolve(results)
}
)
return d.promise
}
function deleteFromTablesWhereUid(connection, tables, uid) {
return tables.map(
function (table) {
return query(connection, 'DELETE FROM ' + table + ' WHERE uid = ?', uid)
}
)
}
function retryable(fn, errnos) {
function success(result) {
return result
}
function failure(err) {
var errno = err.cause ? err.cause.errno : err.errno
log.error({ op: 'MySql.retryable', err: err })
if (errnos.indexOf(errno) === -1) {
throw err
}
return fn()
}
return fn().then(success, failure)
}
// exposed for testing only
MySql.prototype.retryable_ = retryable
return MySql
}

Просмотреть файл

@ -0,0 +1,9 @@
-- Create the 'dbMetadata' table.
-- Note: This should be the only thing in this initial patch.
CREATE TABLE dbMetadata (
name VARCHAR(255) NOT NULL PRIMARY KEY,
value VARCHAR(255) NOT NULL
) ENGINE=InnoDB;
INSERT INTO dbMetadata SET name = 'schema-patch-level', value = '1';

Просмотреть файл

@ -0,0 +1,2 @@
-- -- drop the dbMetadata table
-- DROP TABLE dbMetadata;

Просмотреть файл

@ -0,0 +1,59 @@
-- create all tables
CREATE TABLE IF NOT EXISTS accounts (
uid BINARY(16) PRIMARY KEY,
normalizedEmail VARCHAR(255) NOT NULL UNIQUE KEY,
email VARCHAR(255) NOT NULL,
emailCode BINARY(16) NOT NULL,
emailVerified BOOLEAN NOT NULL DEFAULT FALSE,
kA BINARY(32) NOT NULL,
wrapWrapKb BINARY(32) NOT NULL,
authSalt BINARY(32) NOT NULL,
verifyHash BINARY(32) NOT NULL,
verifierVersion TINYINT UNSIGNED NOT NULL,
verifierSetAt BIGINT UNSIGNED NOT NULL,
createdAt BIGINT UNSIGNED NOT NULL
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS sessionTokens (
tokenId BINARY(32) PRIMARY KEY,
tokenData BINARY(32) NOT NULL,
uid BINARY(16) NOT NULL,
createdAt BIGINT UNSIGNED NOT NULL,
INDEX session_uid (uid)
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS keyFetchTokens (
tokenId BINARY(32) PRIMARY KEY,
authKey BINARY(32) NOT NULL,
uid BINARY(16) NOT NULL,
keyBundle BINARY(96) NOT NULL,
createdAt BIGINT UNSIGNED NOT NULL,
INDEX key_uid (uid)
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS accountResetTokens (
tokenId BINARY(32) PRIMARY KEY,
tokenData BINARY(32) NOT NULL,
uid BINARY(16) NOT NULL UNIQUE KEY,
createdAt BIGINT UNSIGNED NOT NULL
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS passwordForgotTokens (
tokenId BINARY(32) PRIMARY KEY,
tokenData BINARY(32) NOT NULL,
uid BINARY(16) NOT NULL UNIQUE KEY,
passCode BINARY(16) NOT NULL,
createdAt BIGINT UNSIGNED NOT NULL,
tries SMALLINT UNSIGNED NOT NULL
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS passwordChangeTokens (
tokenId BINARY(32) PRIMARY KEY,
tokenData BINARY(32) NOT NULL,
uid BINARY(16) NOT NULL,
createdAt BIGINT UNSIGNED NOT NULL,
INDEX session_uid (uid)
) ENGINE=InnoDB;
UPDATE dbMetadata SET value = '2' WHERE name = 'schema-patch-level';

Просмотреть файл

@ -0,0 +1,10 @@
-- -- drop tables
-- DROP TABLE passwordChangeTokens;
-- DROP TABLE passwordForgotTokens;
-- DROP TABLE accountResetTokens;
-- DROP TABLE keyFetchTokens;
-- DROP TABLE sessionTokens;
-- DROP TABLE accounts;
-- UPDATE dbMetadata SET value = '1' WHERE name = 'schema-patch-level';

54
error.js Normal file
Просмотреть файл

@ -0,0 +1,54 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
var inherits = require('util').inherits
function AppError(options) {
this.message = options.message
this.errno = options.errno
this.error = options.error
this.code = options.code
if (options.stack) this.stack = options.stack
}
inherits(AppError, Error)
AppError.prototype.toString = function () {
return 'Error: ' + this.message
}
AppError.duplicate = function () {
return new AppError(
{
code: 409,
error: 'Conflict',
errno: 101,
message: 'Record already exists'
}
)
}
AppError.notFound = function () {
return new AppError(
{
code: 404,
error: 'Not Found',
errno: 116,
message: 'Not Found'
}
)
}
AppError.wrap = function (err) {
return new AppError(
{
code: 500,
error: 'Internal Server Error',
errno: err.errno,
message: err.code,
stack: err.stack
}
)
}
module.exports = AppError

53
log.js Normal file
Просмотреть файл

@ -0,0 +1,53 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
var util = require('util')
var Logger = require('bunyan')
function Overdrive(options) {
Logger.call(this, options)
}
util.inherits(Overdrive, Logger)
Overdrive.prototype.stat = function (stats) {
stats.op = 'stat'
this.info(stats)
}
module.exports = function (level, name) {
var logStreams = [{ stream: process.stderr, level: level }]
name = name || 'db-api'
var log = new Overdrive(
{
name: name,
streams: logStreams
}
)
process.stdout.on(
'error',
function (err) {
if (err.code === 'EPIPE') {
log.emit('error', err)
}
}
)
Object.keys(console).forEach(
function (key) {
console[key] = function () {
var json = { op: 'console', message: util.format.apply(null, arguments) }
if(log[key]) {
log[key](json)
}
else {
log.warn(json)
}
}
}
)
return log
}

28
package.json Normal file
Просмотреть файл

@ -0,0 +1,28 @@
{
"name": "fxa-auth-db-mysql",
"version": "0.0.0",
"description": "",
"main": "index.js",
"bin": {
"fxa-auth-db-mysql": "db_patcher.js"
},
"scripts": {
"test": "node ./bin/db_patcher.js &>/dev/null && ./scripts/tap-coverage.js test/local test/remote"
},
"author": "",
"license": "MPL 2.0",
"dependencies": {
"bluebird": "2.1.3",
"bunyan": "0.23.1",
"fxa-auth-db-server": "0.15.0",
"mysql": "2.3.2",
"rc": "0.4.0",
"request": "2.36.0"
},
"devDependencies": {
"ass": "0.0.4",
"restify": "2.8.1",
"tap": "0.4.11",
"uuid": "1.4.1"
}
}

5
promise.js Normal file
Просмотреть файл

@ -0,0 +1,5 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
module.exports = require('bluebird')

38
scripts/tap-coverage.js Executable file
Просмотреть файл

@ -0,0 +1,38 @@
#!/usr/bin/env node
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
if (!process.env.NO_COVERAGE) {
var ass = require('ass').enable( {
// exclude files in /client/ and /test/ from code coverage
exclude: [ '/client/', '/test' ]
});
}
var path = require('path'),
spawn = require('child_process').spawn,
fs = require('fs');
var p = spawn(path.join(path.dirname(__dirname), 'node_modules', '.bin', 'tap'),
process.argv.slice(2), { stdio: 'inherit' });
p.on('close', function(code) {
if (!process.env.NO_COVERAGE) {
ass.report('json', function(err, r) {
console.log("code coverage:", r.percent + "%");
process.stdout.write("generating coverage.html: ");
var start = new Date();
ass.report('html', function(err, html) {
fs.writeFileSync(path.join(path.dirname(__dirname), 'coverage.html'),
html);
process.stdout.write("complete in " +
((new Date() - start) / 1000.0).toFixed(1) + "s\n");
process.exit(code);
});
});
} else {
process.exit(code);
}
});

27
test/client-then.js Normal file
Просмотреть файл

@ -0,0 +1,27 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
var restify = require('restify')
var P = require('../promise.js')
var ops = [ 'head', 'get', 'post', 'put', 'del' ]
module.exports = function createClient(cfg) {
var client = restify.createJsonClient(cfg)
// create a thenable version of each operation
ops.forEach(function(name) {
client[name + 'Then'] = function() {
var p = P.defer()
var args = Array.prototype.slice.call(arguments, 0)
args.push(function(err, req, res, obj) {
if (err) return p.reject(err)
p.resolve({ req: req, res: res, obj: obj })
})
client[name].apply(this, args)
return p.promise
}
})
return client
}

6
test/db_server_stub.js Normal file
Просмотреть файл

@ -0,0 +1,6 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
require('ass')
require('../bin/server')

73
test/fake.js Normal file
Просмотреть файл

@ -0,0 +1,73 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
var crypto = require('crypto')
var uuid = require('uuid')
function hex16() { return crypto.randomBytes(16).toString('hex') }
function hex32() { return crypto.randomBytes(32).toString('hex') }
function hex64() { return crypto.randomBytes(64).toString('hex') }
function hex96() { return crypto.randomBytes(96).toString('hex') }
module.exports.newUserDataHex = function() {
var data = {}
// account
data.accountId = hex16()
data.account = {
email: hex16() + '@example.com',
emailCode: hex16(),
emailVerified: false,
verifierVersion: 1,
verifyHash: hex32(),
authSalt: hex32(),
kA: hex32(),
wrapWrapKb: hex32(),
verifierSetAt: Date.now(),
}
// sessionToken
data.sessionTokenId = hex32()
data.sessionToken = {
data : hex32(),
uid : data.accountId,
createdAt: Date.now(),
}
// keyFetchToken
data.keyFetchTokenId = hex32()
data.keyFetchToken = {
authKey : hex32(),
uid : data.accountId,
keyBundle : hex96(),
createdAt: Date.now(),
}
// accountResetToken
data.accountResetTokenId = hex32()
data.accountResetToken = {
data : hex32(),
uid : data.accountId,
createdAt: Date.now(),
}
// passwordChangeToken
data.passwordChangeTokenId = hex32()
data.passwordChangeToken = {
data : hex32(),
uid : data.accountId,
createdAt: Date.now(),
}
// passwordForgotToken
data.passwordForgotTokenId = hex32()
data.passwordForgotToken = {
data : hex32(),
uid : data.accountId,
passCode : hex16(),
tries : 1,
createdAt: Date.now(),
}
return data
}

553
test/local/db_tests.js Normal file
Просмотреть файл

@ -0,0 +1,553 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
require('ass')
var P = require('../../promise')
var test = require('../ptaptest')
var crypto = require('crypto')
var uuid = require('uuid')
var error = require('../../error')
var config = require('../../config')
var log = { trace: console.log, error: console.log }
var DB = require('../../db/mysql')(log, error)
var zeroBuffer16 = Buffer('00000000000000000000000000000000', 'hex')
var zeroBuffer32 = Buffer('0000000000000000000000000000000000000000000000000000000000000000', 'hex')
var ACCOUNT = {
uid: uuid.v4('binary'),
email: ('' + Math.random()).substr(2) + '@bar.com',
emailCode: zeroBuffer16,
emailVerified: false,
verifierVersion: 1,
verifyHash: zeroBuffer32,
authSalt: zeroBuffer32,
kA: zeroBuffer32,
wrapWrapKb: zeroBuffer32,
verifierSetAt: Date.now(),
}
function hex(len) {
return Buffer(crypto.randomBytes(len).toString('hex'), 'hex')
}
function hex16() { return hex(16) }
function hex32() { return hex(32) }
function hex64() { return hex(64) }
function hex96() { return hex(96) }
var SESSION_TOKEN_ID = hex32()
var SESSION_TOKEN = {
data : hex32(),
uid : ACCOUNT.uid,
createdAt: Date.now(),
}
var KEY_FETCH_TOKEN_ID = hex32()
var KEY_FETCH_TOKEN = {
authKey : hex32(),
uid : ACCOUNT.uid,
keyBundle : hex96(),
createdAt: Date.now(),
}
var PASSWORD_FORGOT_TOKEN_ID = hex32()
var PASSWORD_FORGOT_TOKEN = {
data : hex32(),
uid : ACCOUNT.uid,
passCode : hex16(),
tries : 1,
createdAt: Date.now(),
}
var PASSWORD_CHANGE_TOKEN_ID = hex32()
var PASSWORD_CHANGE_TOKEN = {
data : hex32(),
uid : ACCOUNT.uid,
createdAt: Date.now(),
}
var ACCOUNT_RESET_TOKEN_ID = hex32()
var ACCOUNT_RESET_TOKEN = {
data : hex32(),
uid : ACCOUNT.uid,
createdAt: Date.now(),
}
DB.connect(config)
.then(
function (db) {
test(
'ping',
function (t) {
t.plan(1);
return db.ping()
.then(function(account) {
t.pass('Got the ping ok')
}, function(err) {
t.fail('Should not have arrived here')
})
}
)
test(
'account creation',
function (t) {
t.plan(31)
var hexEmail = Buffer(ACCOUNT.email).toString('hex')
return db.accountExists(hexEmail)
.then(function(exists) {
t.fail('account should not yet exist for this email address')
}, function(err) {
t.pass('ok, account could not be found')
})
.then(function() {
return db.createAccount(ACCOUNT.uid, ACCOUNT)
})
.then(function(account) {
t.deepEqual(account, {}, 'Returned an empty object on account creation')
var hexEmail = Buffer(ACCOUNT.email).toString('hex')
return db.accountExists(hexEmail)
})
.then(function(exists) {
t.ok(exists, 'account exists for this email address')
})
.then(function() {
t.pass('Retrieving account using uid')
return db.account(ACCOUNT.uid)
})
.then(function(account) {
t.deepEqual(account.uid, ACCOUNT.uid, 'uid')
t.equal(account.email, ACCOUNT.email, 'email')
t.deepEqual(account.emailCode, ACCOUNT.emailCode, 'emailCode')
t.equal(!!account.emailVerified, ACCOUNT.emailVerified, 'emailVerified')
t.deepEqual(account.kA, ACCOUNT.kA, 'kA')
t.deepEqual(account.wrapWrapKb, ACCOUNT.wrapWrapKb, 'wrapWrapKb')
t.deepEqual(account.verifyHash, ACCOUNT.verifyHash, 'verifyHash')
t.deepEqual(account.authSalt, ACCOUNT.authSalt, 'authSalt')
t.equal(account.verifierVersion, ACCOUNT.verifierVersion, 'verifierVersion')
t.equal(account.verifierSetAt, account.createdAt, 'verifierSetAt has been set to the same as createdAt')
t.ok(account.createdAt)
})
.then(function() {
t.pass('Retrieving account using email')
var hexEmail = Buffer(ACCOUNT.email).toString('hex')
return db.emailRecord(hexEmail)
})
.then(function(account) {
t.deepEqual(account.uid, ACCOUNT.uid, 'uid')
t.equal(account.email, ACCOUNT.email, 'email')
t.deepEqual(account.emailCode, ACCOUNT.emailCode, 'emailCode')
t.equal(!!account.emailVerified, ACCOUNT.emailVerified, 'emailVerified')
t.deepEqual(account.kA, ACCOUNT.kA, 'kA')
t.deepEqual(account.wrapWrapKb, ACCOUNT.wrapWrapKb, 'wrapWrapKb')
t.deepEqual(account.verifyHash, ACCOUNT.verifyHash, 'verifyHash')
t.deepEqual(account.authSalt, ACCOUNT.authSalt, 'authSalt')
t.equal(account.verifierVersion, ACCOUNT.verifierVersion, 'verifierVersion')
t.ok(account.verifierSetAt, 'verifierSetAt is set to a truthy value')
})
// and we piggyback some duplicate query error handling here...
.then(function() {
return db.createAccount(ACCOUNT.uid, ACCOUNT)
})
.then(
function() {
t.fail('this should have resulted in a duplicate account error')
},
function(err) {
t.ok(err, 'trying to create the same account produces an error')
t.equal(err.code, 409, 'code')
t.equal(err.errno, 101, 'errno')
t.equal(err.message, 'Record already exists', 'message')
t.equal(err.error, 'Conflict', 'error')
}
)
}
)
test(
'session token handling',
function (t) {
t.plan(10)
return db.createSessionToken(SESSION_TOKEN_ID, SESSION_TOKEN)
.then(function(result) {
t.deepEqual(result, {}, 'Returned an empty object on session token creation')
return db.sessionToken(SESSION_TOKEN_ID)
})
.then(function(token) {
// tokenId is not returned from db.sessionToken()
t.deepEqual(token.tokenData, SESSION_TOKEN.data, 'token data matches')
t.deepEqual(token.uid, ACCOUNT.uid, 'token belongs to this account')
t.ok(token.createdAt, 'Got a createdAt')
t.equal(!!token.emailVerified, ACCOUNT.emailVerified)
t.equal(token.email, ACCOUNT.email)
t.deepEqual(token.emailCode, ACCOUNT.emailCode)
t.ok(token.verifierSetAt, 'verifierSetAt is set to a truthy value')
})
.then(function() {
return db.deleteSessionToken(SESSION_TOKEN_ID)
})
.then(function(result) {
t.deepEqual(result, {}, 'Returned an empty object on forgot key fetch token deletion')
return db.sessionToken(SESSION_TOKEN_ID)
})
.then(function(token) {
t.fail('Session Token should no longer exist')
}, function(err) {
t.pass('Session Token deleted successfully')
})
}
)
test(
'key fetch token handling',
function (t) {
t.plan(8)
return db.createKeyFetchToken(KEY_FETCH_TOKEN_ID, KEY_FETCH_TOKEN)
.then(function(result) {
t.deepEqual(result, {}, 'Returned an empty object on key fetch token creation')
return db.keyFetchToken(KEY_FETCH_TOKEN_ID)
})
.then(function(token) {
// tokenId is not returned
t.deepEqual(token.authKey, KEY_FETCH_TOKEN.authKey, 'authKey matches')
t.deepEqual(token.uid, ACCOUNT.uid, 'token belongs to this account')
t.ok(token.createdAt, 'Got a createdAt')
t.equal(!!token.emailVerified, ACCOUNT.emailVerified)
// email is not returned
// emailCode is not returned
t.ok(token.verifierSetAt, 'verifierSetAt is set to a truthy value')
})
.then(function() {
return db.deleteKeyFetchToken(KEY_FETCH_TOKEN_ID)
})
.then(function(result) {
t.deepEqual(result, {}, 'Returned an empty object on forgot key fetch token deletion')
return db.keyFetchToken(KEY_FETCH_TOKEN_ID)
})
.then(function(token) {
t.fail('Key Fetch Token should no longer exist')
}, function(err) {
t.pass('Key Fetch Token deleted successfully')
})
}
)
test(
'forgot password token handling',
function (t) {
t.plan(10)
return db.createPasswordForgotToken(PASSWORD_FORGOT_TOKEN_ID, PASSWORD_FORGOT_TOKEN)
.then(function(result) {
t.deepEqual(result, {}, 'Returned an empty object on forgot password token creation')
return db.passwordForgotToken(PASSWORD_FORGOT_TOKEN_ID)
})
.then(function(token) {
// tokenId is not returned
t.deepEqual(token.tokenData, PASSWORD_FORGOT_TOKEN.data, 'token data matches')
t.deepEqual(token.uid, ACCOUNT.uid, 'token belongs to this account')
t.ok(token.createdAt, 'Got a createdAt')
t.deepEqual(token.passCode, PASSWORD_FORGOT_TOKEN.passCode)
t.equal(token.tries, PASSWORD_FORGOT_TOKEN.tries, 'Tries is correct')
t.equal(token.email, ACCOUNT.email)
t.ok(token.verifierSetAt, 'verifierSetAt is set to a truthy value')
})
.then(function() {
return db.deletePasswordForgotToken(PASSWORD_FORGOT_TOKEN_ID)
})
.then(function(result) {
t.deepEqual(result, {}, 'Returned an empty object on forgot password token deletion')
return db.passwordForgotToken(PASSWORD_FORGOT_TOKEN_ID)
})
.then(function(token) {
t.fail('Password Forgot Token should no longer exist')
}, function(err) {
t.pass('Password Forgot Token deleted successfully')
})
}
)
test(
'change password token handling',
function (t) {
t.plan(7)
return db.createPasswordChangeToken(PASSWORD_CHANGE_TOKEN_ID, PASSWORD_CHANGE_TOKEN)
.then(function(result) {
t.deepEqual(result, {}, 'Returned an empty object on change password token creation')
return db.passwordChangeToken(PASSWORD_CHANGE_TOKEN_ID)
})
.then(function(token) {
// tokenId is not returned
t.deepEqual(token.tokenData, PASSWORD_CHANGE_TOKEN.data, 'token data matches')
t.deepEqual(token.uid, ACCOUNT.uid, 'token belongs to this account')
t.ok(token.createdAt, 'Got a createdAt')
t.ok(token.verifierSetAt, 'verifierSetAt is set to a truthy value')
})
.then(function() {
return db.deletePasswordChangeToken(PASSWORD_CHANGE_TOKEN_ID)
})
.then(function(result) {
t.deepEqual(result, {}, 'Returned an empty object on forgot password change deletion')
return db.passwordChangeToken(PASSWORD_CHANGE_TOKEN_ID)
})
.then(function(token) {
t.fail('Password Change Token should no longer exist')
}, function(err) {
t.pass('Password Change Token deleted successfully')
})
}
)
test(
'email verification',
function (t) {
var hexEmail = Buffer(ACCOUNT.email).toString('hex')
return db.emailRecord(hexEmail)
.then(function(emailRecord) {
return db.verifyEmail(emailRecord.uid)
})
.then(function(result) {
t.deepEqual(result, {}, 'Returned an empty object email verification')
return db.account(ACCOUNT.uid)
})
.then(function(account) {
t.ok(account.emailVerified, 'account should now be emailVerified (truthy)')
t.equal(account.emailVerified, 1, 'account should now be emailVerified (1)')
})
.then(function() {
// test verifyEmail for a non-existant account
return db.verifyEmail(uuid.v4('binary'))
})
.then(function(res) {
t.deepEqual(res, {}, 'No matter what happens, we get an empty object back')
}, function(err) {
t.fail('We should not have failed this .verifyEmail() request')
})
}
)
test(
'account reset token handling',
function (t) {
t.plan(7)
return db.createAccountResetToken(ACCOUNT_RESET_TOKEN_ID, ACCOUNT_RESET_TOKEN)
.then(function(result) {
t.deepEqual(result, {}, 'Returned an empty object on account reset token creation')
return db.accountResetToken(ACCOUNT_RESET_TOKEN_ID)
})
.then(function(token) {
// tokenId is not returned
t.deepEqual(token.uid, ACCOUNT.uid, 'token belongs to this account')
t.deepEqual(token.tokenData, ACCOUNT_RESET_TOKEN.data, 'token data matches')
t.ok(token.createdAt, 'Got a createdAt')
t.ok(token.verifierSetAt, 'verifierSetAt is set to a truthy value')
})
.then(function() {
return db.deleteAccountResetToken(ACCOUNT_RESET_TOKEN_ID)
})
.then(function(result) {
t.deepEqual(result, {}, 'Returned an empty object on account reset deletion')
return db.accountResetToken(ACCOUNT_RESET_TOKEN_ID)
})
.then(function(token) {
t.fail('Account Reset Token should no longer exist')
}, function(err) {
t.pass('Account Reset Token deleted successfully')
})
}
)
test(
'db.forgotPasswordVerified',
function (t) {
t.plan(12)
// for this test, we are creating a new account with a different email address
// so that we can check that emailVerified turns from false to true (since
// we already set it to true earlier)
var ACCOUNT = {
uid: uuid.v4('binary'),
email: ('' + Math.random()).substr(2) + '@bar.com',
emailCode: zeroBuffer16,
emailVerified: false,
verifierVersion: 1,
verifyHash: zeroBuffer32,
authSalt: zeroBuffer32,
kA: zeroBuffer32,
wrapWrapKb: zeroBuffer32,
verifierSetAt: Date.now(),
}
var PASSWORD_FORGOT_TOKEN_ID = hex32()
var PASSWORD_FORGOT_TOKEN = {
data : hex32(),
uid : ACCOUNT.uid,
passCode : hex16(),
tries : 1,
createdAt: Date.now(),
}
var ACCOUNT_RESET_TOKEN_ID = hex32()
var ACCOUNT_RESET_TOKEN = {
tokenId : ACCOUNT_RESET_TOKEN_ID,
data : hex32(),
uid : ACCOUNT.uid,
createdAt: Date.now(),
}
return db.createAccount(ACCOUNT.uid, ACCOUNT)
.then(function() {
var hexEmail = Buffer(ACCOUNT.email).toString('hex')
return db.emailRecord(hexEmail)
})
.then(function(result) {
t.pass('.emailRecord() did not error')
return db.createPasswordForgotToken(PASSWORD_FORGOT_TOKEN_ID, PASSWORD_FORGOT_TOKEN)
})
.then(function(passwordForgotToken) {
t.pass('.createPasswordForgotToken() did not error')
return db.forgotPasswordVerified(PASSWORD_FORGOT_TOKEN_ID, ACCOUNT_RESET_TOKEN)
})
.then(function() {
t.pass('.forgotPasswordVerified() did not error')
return db.passwordForgotToken(PASSWORD_FORGOT_TOKEN_ID)
})
.then(function(token) {
t.fail('Password Forgot Token should no longer exist')
}, function(err) {
t.pass('Password Forgot Token deleted successfully')
})
.then(function() {
return db.accountResetToken(ACCOUNT_RESET_TOKEN_ID)
})
.then(function(accountResetToken) {
t.pass('.accountResetToken() did not error')
// tokenId is not returned
t.deepEqual(accountResetToken.uid, ACCOUNT.uid, 'token belongs to this account')
t.deepEqual(accountResetToken.tokenData, ACCOUNT_RESET_TOKEN.data, 'token data matches')
t.ok(accountResetToken.verifierSetAt, 'verifierSetAt is set to a truthy value')
})
.then(function() {
return db.account(ACCOUNT.uid)
})
.then(function(account) {
t.ok(account.emailVerified, 'account should now be emailVerified (truthy)')
t.equal(account.emailVerified, 1, 'account should now be emailVerified (1)')
})
.then(function() {
return db.deleteAccountResetToken(ACCOUNT_RESET_TOKEN_ID)
})
.then(function(result) {
t.deepEqual(result, {}, 'Returned an empty object on account reset deletion')
return db.accountResetToken(ACCOUNT_RESET_TOKEN_ID)
})
.then(function(token) {
t.fail('Account Reset Token should no longer exist')
}, function(err) {
t.pass('Account Reset Token deleted successfully')
})
}
)
test(
'db.accountDevices',
function (t) {
t.plan(3)
var anotherSessionTokenId = hex32()
var anotherSessionToken = {
data : hex32(),
uid : ACCOUNT.uid,
createdAt: Date.now(),
}
db.createSessionToken(SESSION_TOKEN_ID, SESSION_TOKEN)
.then(function(sessionToken) {
return db.createSessionToken(anotherSessionTokenId, anotherSessionToken)
})
.then(function() {
return db.accountDevices(ACCOUNT.uid)
})
.then(function(devices) {
t.equal(devices.length, 2, 'Account devices should be two')
return devices[0]
})
.then(function(sessionToken) {
return db.deleteSessionToken(SESSION_TOKEN_ID)
})
.then(function(sessionToken) {
return db.accountDevices(ACCOUNT.uid)
})
.then(function(devices) {
t.equal(devices.length, 1, 'Account devices should be one')
return devices[0]
})
.then(function(sessionToken) {
return db.deleteSessionToken(anotherSessionTokenId)
})
.then(function(sessionToken) {
return db.accountDevices(ACCOUNT.uid)
})
.then(function(devices) {
t.equal(devices.length, 0, 'Account devices should be zero')
})
}
)
test(
'db.resetAccount',
function (t) {
t.plan(6)
return db.createSessionToken(SESSION_TOKEN_ID, SESSION_TOKEN)
.then(function(sessionToken) {
t.pass('.createSessionToken() did not error')
return db.createAccountResetToken(ACCOUNT_RESET_TOKEN_ID, ACCOUNT_RESET_TOKEN)
})
.then(function() {
t.pass('.createAccountResetToken() did not error')
return db.resetAccount(ACCOUNT.uid, ACCOUNT)
})
.then(function(sessionToken) {
t.pass('.resetAccount() did not error')
return db.accountDevices(ACCOUNT.uid)
})
.then(function(devices) {
t.pass('.accountDevices() did not error')
t.equal(devices.length, 0, 'The devices length should be zero')
})
.then(function() {
// account should STILL exist for this email address
var hexEmail = Buffer(ACCOUNT.email).toString('hex')
return db.accountExists(hexEmail)
})
.then(function(exists) {
t.ok(exists, 'account still exists ok')
}, function(err) {
t.fail('the account for this email address should still exist')
})
}
)
test(
'account deletion',
function (t) {
t.plan(1)
// account should no longer exist for this email address
return db.deleteAccount(ACCOUNT.uid)
.then(function() {
var hexEmail = Buffer(ACCOUNT.email).toString('hex')
return db.accountExists(hexEmail)
})
.then(function(exists) {
t.fail('account should no longer exist for this email address')
}, function(err) {
t.pass('account no longer exists for this email address')
})
}
)
test(
'teardown',
function (t) {
return db.close()
}
)
}
)

43
test/local/error.js Normal file
Просмотреть файл

@ -0,0 +1,43 @@
require('ass')
var test = require('../ptaptest')
test(
'bufferize module',
function (t) {
t.plan(22);
var error = require('../../error')
t.type(error, 'function', 'error module returns a function')
var duplicate = error.duplicate()
t.type(duplicate, 'object', 'duplicate returns an object')
t.ok(duplicate instanceof error, 'is an instance of error')
t.equals(duplicate.code, 409)
t.equals(duplicate.errno, 101)
t.equals(duplicate.message, 'Record already exists')
t.equals(duplicate.error, 'Conflict')
t.equals(duplicate.toString(), 'Error: Record already exists')
var notFound = error.notFound()
t.type(notFound, 'object', 'notFound returns an object')
t.ok(notFound instanceof error, 'is an instance of error')
t.equals(notFound.code, 404)
t.equals(notFound.errno, 116)
t.equals(notFound.message, 'Not Found')
t.equals(notFound.error, 'Not Found')
t.equals(notFound.toString(), 'Error: Not Found')
var err = new Error('Something broke.')
err.code = 'ER_QUERY_INTERRUPTED'
err.errno = 1317
var wrap = error.wrap(err)
t.type(wrap, 'object', 'wrap returns an object')
t.ok(wrap instanceof error, 'is an instance of error')
t.equals(wrap.code, 500)
t.equals(wrap.errno, 1317)
t.equals(wrap.message, 'ER_QUERY_INTERRUPTED')
t.equals(wrap.error, 'Internal Server Error')
t.equals(wrap.toString(), 'Error: ER_QUERY_INTERRUPTED')
t.end()
}
)

Просмотреть файл

@ -0,0 +1,43 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
require('ass')
var P = require('../../promise')
var test = require('tap').test
var error = require('../../error')
var config = require('../../config')
var log = { trace: console.log, error: console.log }
var DB = require('../../db/mysql')(log, error)
config.patchLevel = 1000000
DB.connect(config)
.then(
function (db) {
test(
'the connect should fail and we will never get here',
function (t) {
t.fail('DB.connect should have failed on an incorrect patchVersion')
t.end()
db.close()
}
)
},
function(err) {
test(
'an incorrect patchVersion should throw',
function (t) {
debugger
t.type(err, 'object', 'err is an object')
t.ok(err instanceof Error, 'err is instanceof Error')
t.equals(err.message, 'dbIncorrectPatchLevel', 'err.message is dbIncorrectPatchLevel')
t.end()
// defer to allow node-tap to finish its work
process.nextTick(process.exit)
}
)
}
)

50
test/local/log-stats.js Normal file
Просмотреть файл

@ -0,0 +1,50 @@
require('ass')
var test = require('../ptaptest')
var P = require('../../promise')
var error = require('../../error')
var config = require('../../config')
config.logLevel = 'info'
config.statInterval = 100
var log = require('../../log')(config.logLevel, 'db-api')
// monkeypatch log.stat to hook into db/mysql.js:statInterval
var dfd = P.defer()
log.stat = function(stats) {
dfd.resolve(stats)
}
var DB = require('../../db/mysql')(log, error)
DB.connect(config)
.then(
function (db) {
test(
'db/mysql logs stats periodically',
function (t) {
t.plan(4);
return dfd.promise
.then(
function(stats) {
t.type(stats, 'object', 'stats is an object')
t.equal(stats.stat, 'mysql', 'stats.stat is mysql')
t.equal(stats.errors, 0, 'have no errors')
t.equal(stats.connections, 1, 'have one connection')
},
function(err) {
t.fail('this should never happen ' + err)
}
)
}
)
test(
'teardown',
function () {
return db.close()
}
)
}
)

165
test/local/mysql_tests.js Normal file
Просмотреть файл

@ -0,0 +1,165 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
require('ass')
var P = require('../../promise')
var test = require('../ptaptest')
var error = require('../../error')
var config = require('../../config')
var log = { trace: console.log, error: console.log }
var DB = require('../../db/mysql')(log, error)
DB.connect(config)
.then(
function (db) {
test(
'ping',
function (t) {
t.plan(1);
return db.ping()
.then(function(account) {
t.pass('Got the ping ok')
}, function(err) {
t.fail('Should not have arrived here')
})
}
)
test(
'a select on an unknown table should result in an error',
function (t) {
var query = 'SELECT mumble as id FROM mumble.mumble WHERE mumble = ?'
var param = 'mumble'
db.read(query, param)
.then(
function(result) {
t.plan(1)
t.fail('Should not have arrived here for an invalid select')
},
function(err) {
t.plan(5)
t.ok(err, 'we have an error')
t.equal(err.code, 500)
t.equal(err.errno, 1146)
t.equal(err.error, 'Internal Server Error')
t.equal(err.message, 'ER_NO_SUCH_TABLE')
}
)
}
)
test(
'an update to an unknown table should result in an error',
function (t) {
var query = 'UPDATE mumble.mumble SET mumble = ?'
var param = 'mumble'
db.write(query, param)
.then(
function(result) {
t.plan(1)
t.fail('Should not have arrived here for an invalid update')
},
function(err) {
t.plan(5)
t.ok(err, 'we have an error')
t.equal(err.code, 500)
t.equal(err.errno, 1146)
t.equal(err.error, 'Internal Server Error')
t.equal(err.message, 'ER_NO_SUCH_TABLE')
}
)
}
)
test(
'an transaction to update an unknown table should result in an error',
function (t) {
var sql = 'UPDATE mumble.mumble SET mumble = ?'
var param = 'mumble'
function query(connection, sql, params) {
var d = P.defer()
connection.query(
sql,
params || [],
function (err, results) {
if (err) { return d.reject(err) }
d.resolve(results)
}
)
return d.promise
}
db.transaction(
function (connection) {
return query(connection, sql, param)
})
.then(
function(result) {
t.plan(1)
t.fail('Should not have arrived here for an invalid update')
},
function(err) {
t.plan(5)
t.ok(err, 'we have an error')
t.equal(err.code, 500)
t.equal(err.errno, 1146)
t.equal(err.error, 'Internal Server Error')
t.equal(err.message, 'ER_NO_SUCH_TABLE')
}
)
}
)
test(
'retryable does retry when the errno is matched',
function (t) {
var query = 'UPDATE mumble.mumble SET mumble = ?'
var param = 'mumble'
var callCount = 0
var writer = function() {
++callCount
return db.write(query, param)
.then(
function(result) {
t.fail('this query should never succeed!')
},
function(err) {
t.ok(true, 'we got an error')
t.equal(err.code, 500)
t.equal(err.errno, 1146)
t.equal(err.error, 'Internal Server Error')
t.equal(err.message, 'ER_NO_SUCH_TABLE')
throw err
}
)
}
db.retryable_(writer, [ 1146 ])
.then(
function(result) {
t.fail('This should never happen, even with a retry ' + callCount)
t.end()
},
function(err) {
t.equal(callCount, 2, 'the function was retried')
t.end()
}
)
}
)
test(
'teardown',
function (t) {
return db.close()
}
)
}
)

37
test/local/ping.js Normal file
Просмотреть файл

@ -0,0 +1,37 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
require('ass')
var test = require('../ptaptest')
var error = require('../../error')
var config = require('../../config')
var log = { trace: console.log, error: console.log }
var DB = require('../../db/mysql')(log, error)
DB.connect(config)
.then(
function (db) {
test(
'ping',
function (t) {
t.plan(1);
return db.ping()
.then(function(account) {
t.pass('Got the ping ok')
}, function(err) {
t.fail('Should not have arrived here')
})
}
)
test(
'teardown',
function (t) {
return db.close()
}
)
}
)

54
test/ptaptest.js Normal file
Просмотреть файл

@ -0,0 +1,54 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/*
* A promise-ified version of tap.test.
*
* This module provides a 'test' function that operates just like tap.test, but
* will properly close a promise if the test returns one. This makes it easier
* to ensure that any unhandled errors cause the test to fail. Use like so:
*
* var test = require('./ptap')
*
* test(
* 'an example test',
* function (t) {
* return someAPI.thingThatReturnsPromise()
* .then(function(result) {
* t.assertEqual(result, 42)
* })
* }
* )
*
* Because the test function returns a promise, we get the following for free:
*
* * wait for the promise to resolve, and call t.end() when it does
* * check for unhandled errors and fail the test if they occur
*
*/
// support code coverage
require('ass');
var tap = require('tap')
module.exports = function(name, testfunc) {
var wrappedtestfunc = function(t) {
var res = testfunc(t)
if (typeof res !== 'undefined') {
if (typeof res.done === 'function') {
res.done(
function() {
t.end()
},
function(err) {
t.fail(err.message || err.error || err)
t.end()
}
)
}
}
}
return tap.test(name, wrappedtestfunc)
}

451
test/remote/account.js Normal file
Просмотреть файл

@ -0,0 +1,451 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
var uuid = require('uuid')
var restify = require('restify')
var test = require('tap').test
var fake = require('../fake')
var TestServer = require('../test_server')
var config = require('../../config')
var clientThen = require('../client-then')
function emailToHex(email) {
return Buffer(email).toString('hex')
}
var cfg = {
port: 8000
}
var testServer = new TestServer(cfg)
var client = clientThen({ url : 'http://127.0.0.1:' + cfg.port })
test(
'startup',
function (t) {
t.plan(2)
testServer.start(function (err) {
t.type(testServer.server, 'object', 'test server was started')
t.equal(err, null, 'no errors were returned')
t.end()
})
}
)
function respOk(t, r) {
t.equal(r.res.statusCode, 200, 'returns a 200')
t.equal(r.res.headers['content-type'], 'application/json', 'json is returned')
}
function respOkEmpty(t, r) {
t.equal(r.res.statusCode, 200, 'returns a 200')
t.equal(r.res.headers['content-type'], 'application/json', 'json is returned')
t.deepEqual(r.obj, {}, 'Returned object is empty')
}
function testNotFound(t, err) {
t.equal(err.statusCode, 404, 'returns a 404')
t.deepEqual(err.body, { message : 'Not Found' }, 'Object contains no other fields')
}
test(
'account not found',
function (t) {
t.plan(2)
client.getThen('/account/hello-world')
.then(function(r) {
t.fail('This request should have failed (instead it suceeded)')
t.end()
}, function(err) {
testNotFound(t, err)
t.end()
})
}
)
test(
'add account, retrieve it, delete it',
function (t) {
t.plan(31)
var user = fake.newUserDataHex()
client.putThen('/account/' + user.accountId, user.account)
.then(function(r) {
respOkEmpty(t, r)
return client.getThen('/account/' + user.accountId)
})
.then(function(r) {
respOk(t, r)
var account = r.obj
var fields = 'accountId,email,emailCode,kA,verifierVersion,verifyHash,authSalt'.split(',')
fields.forEach(function(f) {
t.equal(user.account[f], account[f], 'Both Fields ' + f + ' are the same')
})
t.equal(user.account.emailVerified, !!account.emailVerified, 'Both fields emailVerified are the same')
}, function(err) {
t.fail('Error for some reason:' + err)
})
.then(function() {
return client.headThen('/emailRecord/' + emailToHex(user.account.email))
})
.then(function(r) {
respOkEmpty(t, r)
return client.getThen('/emailRecord/' + emailToHex(user.account.email))
})
.then(function(r) {
respOk(t, r)
var account = r.obj
var fields = 'accountId,email,emailCode,kA,verifierVersion,verifyHash,authSalt'.split(',')
fields.forEach(function(f) {
t.equal(user.account[f], account[f], 'Both Fields ' + f + ' are the same')
})
t.equal(user.account.emailVerified, !!account.emailVerified, 'Both fields emailVerified are the same')
})
.then(function() {
return client.delThen('/account/' + user.accountId)
})
.then(function(r) {
respOk(t, r)
// now make sure this record no longer exists
return client.headThen('/emailRecord/' + emailToHex(user.account.email))
})
.then(function(r) {
t.fail('Should not be here, since this account no longer exists')
}, function(err) {
t.equal(err.toString(), 'NotFoundError', 'Account not found (no body due to being a HEAD request')
t.deepEqual(err.body, {}, 'Body contains nothing since this is a HEAD request')
t.deepEqual(err.statusCode, 404, 'Status Code is 404')
})
.done(function() {
t.end()
}, function(err) {
t.fail(err)
t.end()
})
}
)
test(
'session token handling',
function (t) {
t.plan(14)
var user = fake.newUserDataHex()
client.putThen('/account/' + user.accountId, user.account)
.then(function() {
return client.getThen('/sessionToken/' + user.sessionTokenId)
})
.then(function(r) {
t.fail('A non-existant session token should not have returned anything')
}, function(err) {
t.pass('No session token exists yet')
return client.putThen('/sessionToken/' + user.sessionTokenId, user.sessionToken)
})
.then(function(r) {
respOk(t, r)
return client.getThen('/sessionToken/' + user.sessionTokenId)
})
.then(function(r) {
var token = r.obj
// tokenId is not returned from db.sessionToken()
t.deepEqual(token.tokenData, user.sessionToken.data, 'token data matches')
t.deepEqual(token.uid, user.accountId, 'token belongs to this account')
t.ok(token.createdAt, 'Got a createdAt')
t.equal(!!token.emailVerified, user.account.emailVerified)
t.equal(token.email, user.account.email)
t.deepEqual(token.emailCode, user.account.emailCode)
t.ok(token.verifierSetAt, 'verifierSetAt is set to a truthy value')
// now delete it
return client.delThen('/sessionToken/' + user.sessionTokenId)
})
.then(function(r) {
respOk(t, r)
// now make sure the token no longer exists
return client.getThen('/sessionToken/' + user.sessionTokenId)
})
.then(function(r) {
t.fail('Fetching the non-existant sessionToken should have failed')
t.end()
}, function(err) {
testNotFound(t, err)
t.end()
})
}
)
test(
'key fetch token handling',
function (t) {
t.plan(13)
var user = fake.newUserDataHex()
client.putThen('/account/' + user.accountId, user.account)
.then(function() {
return client.getThen('/keyFetchToken/' + user.keyFetchTokenId)
})
.then(function(r) {
t.fail('A non-existant session token should not have returned anything')
}, function(err) {
t.pass('No session token exists yet')
return client.putThen('/keyFetchToken/' + user.keyFetchTokenId, user.keyFetchToken)
})
.then(function(r) {
respOk(t, r)
return client.getThen('/keyFetchToken/' + user.keyFetchTokenId)
})
.then(function(r) {
var token = r.obj
// tokenId is not returned from db.keyFetchToken()
t.deepEqual(token.uid, user.accountId, 'token belongs to this account')
t.deepEqual(token.authKey, user.keyFetchToken.authKey, 'authKey matches')
t.deepEqual(token.keyBundle, user.keyFetchToken.keyBundle, 'keyBundle matches')
t.ok(token.createdAt, 'Got a createdAt')
t.equal(!!token.emailVerified, user.account.emailVerified)
t.ok(token.verifierSetAt, 'verifierSetAt is set to a truthy value')
// now delete it
return client.delThen('/keyFetchToken/' + user.keyFetchTokenId)
})
.then(function(r) {
respOk(t, r)
// now make sure the token no longer exists
return client.getThen('/keyFetchToken/' + user.keyFetchTokenId)
})
.then(function(r) {
t.fail('Fetching the non-existant keyFetchToken should have failed')
t.end()
}, function(err) {
testNotFound(t, err)
t.end()
})
}
)
test(
'account reset token handling',
function (t) {
t.plan(11)
var user = fake.newUserDataHex()
client.putThen('/account/' + user.accountId, user.account)
.then(function() {
return client.getThen('/accountResetToken/' + user.accountResetTokenId)
})
.then(function(r) {
t.fail('A non-existant session token should not have returned anything')
}, function(err) {
t.pass('No session token exists yet')
return client.putThen('/accountResetToken/' + user.accountResetTokenId, user.accountResetToken)
})
.then(function(r) {
respOk(t, r)
return client.getThen('/accountResetToken/' + user.accountResetTokenId)
})
.then(function(r) {
var token = r.obj
// tokenId is not returned from db.accountResetToken()
t.deepEqual(token.uid, user.accountId, 'token belongs to this account')
t.deepEqual(token.tokenData, user.accountResetToken.data, 'token data matches')
t.ok(token.createdAt, 'Got a createdAt')
t.ok(token.verifierSetAt, 'verifierSetAt is set to a truthy value')
// now delete it
return client.delThen('/accountResetToken/' + user.accountResetTokenId)
})
.then(function(r) {
respOk(t, r)
// now make sure the token no longer exists
return client.getThen('/accountResetToken/' + user.accountResetTokenId)
})
.then(function(r) {
t.fail('Fetching the non-existant accountResetToken should have failed')
t.end()
}, function(err) {
testNotFound(t, err)
t.end()
})
}
)
test(
'password change token handling',
function (t) {
t.plan(11)
var user = fake.newUserDataHex()
client.putThen('/account/' + user.accountId, user.account)
.then(function() {
return client.getThen('/passwordChangeToken/' + user.passwordChangeTokenId)
})
.then(function(r) {
t.fail('A non-existant session token should not have returned anything')
}, function(err) {
t.pass('No session token exists yet')
return client.putThen('/passwordChangeToken/' + user.passwordChangeTokenId, user.passwordChangeToken)
})
.then(function(r) {
respOk(t, r)
return client.getThen('/passwordChangeToken/' + user.passwordChangeTokenId)
})
.then(function(r) {
var token = r.obj
// tokenId is not returned from db.passwordChangeToken()
t.deepEqual(token.tokenData, user.passwordChangeToken.data, 'token data matches')
t.deepEqual(token.uid, user.accountId, 'token belongs to this account')
t.ok(token.createdAt, 'Got a createdAt')
t.ok(token.verifierSetAt, 'verifierSetAt is set to a truthy value')
// now delete it
return client.delThen('/passwordChangeToken/' + user.passwordChangeTokenId)
})
.then(function(r) {
respOk(t, r)
// now make sure the token no longer exists
return client.getThen('/passwordChangeToken/' + user.passwordChangeTokenId)
})
.then(function(r) {
t.fail('Fetching the non-existant passwordChangeToken should have failed')
t.end()
}, function(err) {
testNotFound(t, err)
t.end()
})
}
)
test(
'password forgot token handling',
function (t) {
t.plan(19)
var user = fake.newUserDataHex()
client.putThen('/account/' + user.accountId, user.account)
.then(function() {
return client.getThen('/passwordForgotToken/' + user.passwordForgotTokenId)
})
.then(function(r) {
t.fail('A non-existant session token should not have returned anything')
}, function(err) {
t.pass('No session token exists yet')
return client.putThen('/passwordForgotToken/' + user.passwordForgotTokenId, user.passwordForgotToken)
})
.then(function(r) {
respOk(t, r)
return client.getThen('/passwordForgotToken/' + user.passwordForgotTokenId)
})
.then(function(r) {
var token = r.obj
// tokenId is not returned from db.passwordForgotToken()
t.deepEqual(token.tokenData, user.passwordForgotToken.data, 'token data matches')
t.deepEqual(token.uid, user.accountId, 'token belongs to this account')
t.ok(token.createdAt, 'Got a createdAt')
t.deepEqual(token.passCode, user.passwordForgotToken.passCode)
t.equal(token.tries, user.passwordForgotToken.tries, 'Tries is correct')
t.equal(token.email, user.account.email)
t.ok(token.verifierSetAt, 'verifierSetAt is set to a truthy value')
// now update this token (with extra tries)
user.passwordForgotToken.tries += 1
return client.postThen('/passwordForgotToken/' + user.passwordForgotTokenId + '/update', user.passwordForgotToken)
})
.then(function(r) {
respOk(t, r)
// re-fetch this token
return client.getThen('/passwordForgotToken/' + user.passwordForgotTokenId)
})
.then(function(r) {
var token = r.obj
// tokenId is not returned from db.passwordForgotToken()
t.deepEqual(token.tokenData, user.passwordForgotToken.data, 'token data matches')
t.deepEqual(token.uid, user.accountId, 'token belongs to this account')
t.ok(token.createdAt, 'Got a createdAt')
t.deepEqual(token.passCode, user.passwordForgotToken.passCode)
t.equal(token.tries, user.passwordForgotToken.tries, 'Tries is correct (now incremented)')
t.equal(token.email, user.account.email)
t.ok(token.verifierSetAt, 'verifierSetAt is set to a truthy value')
// now delete it
return client.delThen('/passwordForgotToken/' + user.passwordForgotTokenId)
})
.then(function(r) {
respOk(t, r)
// now make sure the token no longer exists
return client.getThen('/passwordForgotToken/' + user.passwordForgotTokenId)
})
.then(function(r) {
t.fail('Fetching the non-existant passwordForgotToken should have failed')
t.end()
}, function(err) {
testNotFound(t, err)
t.end()
})
}
)
test(
'password forgot token verified',
function (t) {
t.plan(16)
var user = fake.newUserDataHex()
client.putThen('/account/' + user.accountId, user.account)
.then(function(r) {
respOk(t, r)
return client.putThen('/passwordForgotToken/' + user.passwordForgotTokenId, user.passwordForgotToken)
})
.then(function(r) {
respOk(t, r)
// now, verify the password (which inserts the accountResetToken)
user.accountResetToken.tokenId = user.accountResetTokenId
return client.postThen('/passwordForgotToken/' + user.passwordForgotTokenId + '/verified', user.accountResetToken)
})
.then(function(r) {
respOk(t, r)
// check the accountResetToken exists
return client.getThen('/accountResetToken/' + user.accountResetTokenId)
})
.then(function(r) {
var token = r.obj
// tokenId is not returned from db.accountResetToken()
t.deepEqual(token.uid, user.accountId, 'token belongs to this account')
t.deepEqual(token.tokenData, user.accountResetToken.data, 'token data matches')
t.ok(token.createdAt, 'Got a createdAt')
t.ok(token.verifierSetAt, 'verifierSetAt is set to a truthy value')
// make sure then passwordForgotToken no longer exists
return client.getThen('/passwordForgotToken/' + user.passwordForgotTokenId)
})
.then(function(r) {
t.fail('Fetching the non-existant passwordForgotToken should have failed')
}, function(err) {
testNotFound(t, err)
// and check that the account has been verified
return client.getThen('/emailRecord/' + emailToHex(user.account.email))
})
.then(function(r) {
respOk(t, r)
var account = r.obj
t.equal(true, !!account.emailVerified, 'emailVerified is now true')
})
.then(function(r) {
t.pass('All password forgot token verified tests passed')
t.end()
}, function(err) {
t.fail(err)
t.end()
})
}
)
test(
'teardown',
function (t) {
t.plan(1)
testServer.stop()
t.equal(testServer.server.killed, true, 'test server has been killed')
t.end()
}
)

59
test/remote/basic.js Normal file
Просмотреть файл

@ -0,0 +1,59 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
var test = require('tap').test
var restify = require('restify')
var TestServer = require('../test_server')
var pkg = require('../../package.json')
var config = require('../../config')
var clientThen = require('../client-then')
var cfg = {
port: 8000
}
var testServer = new TestServer(cfg)
var client = clientThen({ url : 'http://127.0.0.1:' + cfg.port })
test(
'startup',
function (t) {
testServer.start(function (err) {
t.type(testServer.server, 'object', 'test server was started')
t.equal(err, null, 'no errors were returned')
t.end()
})
}
)
test(
'top level info',
function (t) {
client.getThen('/')
.then(function(r) {
t.equal(r.res.statusCode, 200, 'returns a 200')
t.equal(r.obj.version, pkg.version, 'Version reported is the same a package.json')
t.deepEqual(r.obj, { version : pkg.version }, 'Object contains no other fields')
t.end()
})
}
)
test(
'heartbeat',
function (t) {
client.getThen('/__heartbeat__')
.then(function (r) {
t.deepEqual(r.obj, {}, 'Heartbeat contains an empty object and nothing unexpected')
t.end()
})
}
)
test(
'teardown',
function (t) {
testServer.stop()
t.equal(testServer.server.killed, true, 'test server has been killed')
t.end()
}
)

60
test/test_server.js Normal file
Просмотреть файл

@ -0,0 +1,60 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
var cp = require('child_process')
var request = require('request')
function TestServer(config) {
this.url = 'http://127.0.0.1:' + config.port
this.server = null
}
function waitLoop(testServer, url, cb) {
request(
url + '/',
function (err, res, body) {
if (err) {
if (err.errno !== 'ECONNREFUSED') {
console.log('ERROR: unexpected result from ' + url)
console.log(err)
return cb(err)
}
return setTimeout(waitLoop.bind(null, testServer, url, cb), 100)
}
if (res.statusCode !== 200) {
console.log('ERROR: bad status code: ' + res.statusCode)
return cb(res.statusCode)
}
return cb()
}
)
}
TestServer.prototype.start = function (cb) {
if (!this.server) {
this.server = cp.spawn(
'node',
['./db_server_stub'],
{
cwd: __dirname,
stdio: 'ignore'
}
)
}
waitLoop(this, this.url, function (err) {
if (err) {
cb(err)
} else {
cb(null)
}
})
}
TestServer.prototype.stop = function () {
if (this.server) {
this.server.kill('SIGINT')
}
}
module.exports = TestServer