and so our story begins
This commit is contained in:
Коммит
9a932731ee
|
@ -0,0 +1,2 @@
|
|||
node_modules
|
||||
coverage.html
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
)
|
|
@ -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
|
||||
}
|
||||
}
|
||||
)
|
|
@ -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';
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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')
|
|
@ -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);
|
||||
}
|
||||
});
|
|
@ -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
|
||||
}
|
|
@ -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')
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
)
|
|
@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -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()
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
|
@ -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()
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
)
|
|
@ -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()
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
)
|
|
@ -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)
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
)
|
|
@ -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()
|
||||
}
|
||||
)
|
|
@ -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
|
Загрузка…
Ссылка в новой задаче