#68 Fixed tests up to login/-out

master
Keith Irwin 2018-01-19 21:23:43 +00:00
parent 3b60b2fc86
commit 887b87231b
No known key found for this signature in database
GPG Key ID: 378933C743E2BBC0
9 changed files with 261 additions and 210 deletions

117
config/models.js Normal file → Executable file
View File

@ -47,33 +47,17 @@ const userSchema = new mongoose.Schema({
}).plugin(unique)
/* User methods */
// Do not replace with arrow functions!
// https://stackoverflow.com/a/37875212
// Create email confirmation token
userSchema.methods.createEmailToken = function (next) {
debug('user.createEmailToken() called')
var user = this
userSchema.methods.createEmailToken = function () {
debug(`user.createEmailToken() called for ${user.id}`)
let user = this
// Callback next(err, token)
if (typeof next === 'function') {
return new Promise((resolve, reject) => {
crypto.randomBytes(16, (err, buf) => {
if (err) return next(err)
if (buf) {
debug(`Buffer ${buf.toString('hex')} created`)
user.emailToken = buf.toString('hex')
user.save()
.then(() => {
return next(null, user.emailToken)
})
.catch((err) => {
return next(err)
})
}
})
// Promise
} else return new Promise((resolve, reject) => {
crypto.randomBytes(16, (err, buf) => {
if (err) reject(err)
if (err) return reject(err)
if (buf) {
debug(`Buffer ${buf.toString('hex')} created`)
user.emailToken = buf.toString('hex')
@ -89,49 +73,11 @@ userSchema.methods.createEmailToken = function (next) {
}
// Create password reset token
userSchema.methods.createPassToken = function (next) {
debug('user.createPassToken() called')
var user = this
userSchema.methods.createPassToken = function () {
let user = this
debug(`user.createPassToken() called for ${user.id}`)
// Callback next(err, token, expires)
if (typeof next === 'function') {
// Reuse old token, resetting clock
if (user.auth.passTokenExpires >= Date.now()) {
debug(`Reusing old password token...`)
user.auth.passTokenExpires = Date.now() + 3600000 // 1 hour
user.save()
.then(() => {
return next(null, user.auth.passToken, user.auth.passTokenExpires)
})
.catch((err) => {
return next(err)
})
// Create new token
} else {
debug(`Creating new password token...`)
crypto.randomBytes(16, (err, buf) => {
if (err) return next(err)
if (buf) {
user.auth.passToken = buf.toString('hex')
user.auth.passTokenExpires = Date.now() + 3600000 // 1 hour
user.save()
.then(() => {
debug('successfully saved user in createPassToken')
return next(null, user.auth.passToken, user.auth.passTokenExpires)
})
.catch((err) => {
debug('error saving user in createPassToken')
return next(err)
})
}
})
}
// Promise
} else return new Promise((resolve, reject) => {
return new Promise((resolve, reject) => {
// Reuse old token, resetting clock
if (user.auth.passTokenExpires >= Date.now()) {
@ -147,7 +93,7 @@ userSchema.methods.createPassToken = function (next) {
} else {
debug(`Creating new password token...`)
crypto.randomBytes(16, (err, buf) => {
if (err) return next(err)
if (err) return reject(err)
if (buf) {
user.auth.passToken = buf.toString('hex')
user.auth.passTokenExpires = Date.now() + 3600000 // 1 hour
@ -163,32 +109,20 @@ userSchema.methods.createPassToken = function (next) {
}
})
}
})
}
// Generate hash for new password and save it to the database
userSchema.methods.generateHashedPassword = function (password, next) {
userSchema.methods.generateHashedPassword = function (password) {
debug(`user.generateHashedPassword() called for ${this.id}`)
// Delete token
this.auth.passToken = undefined
this.auth.passTokenExpires = undefined
// Callback next(err, token, expires)
if (typeof next === 'function') {
// Generate hash
bcrypt.genSalt(8, (err, salt) => {
if (err) return next(err)
bcrypt.hash(password, salt, (err, hash) => {
if (err) return next(err)
this.auth.password = hash
this.save()
.then(() => { next(); })
.catch((err) => next(err) )
})
})
} else return new Promise((resolve, reject) => {
return new Promise((resolve, reject) => {
// Generate hash
bcrypt.genSalt(8, (err, salt) => {
@ -206,17 +140,12 @@ userSchema.methods.generateHashedPassword = function (password, next) {
}
// Check for valid password
userSchema.methods.validPassword = function (password, next) {
// Callback next(err, res)
if (typeof next === 'function') bcrypt.compare(password, this.auth.password, next)
else return new Promise( (resolve, reject) => {
bcrypt.compare(password, this.auth.password)
.then( (result) => {
if (result===true) resolve()
else reject(new Error('Passwords don\'t match'))
})
.catch( (err) => reject(err) )
})
userSchema.methods.validPassword = function (password) {
let user = this
debug(`user.validPassword() called for ${user.id}`)
return bcrypt.compare(password, user.auth.password)
}
module.exports = {

7
config/passport.js Normal file → Executable file
View File

@ -13,6 +13,7 @@ const mw = require('./middleware.js')
const User = require('./models.js').user
module.exports = (passport) => {
// Serialize/deserialize users
passport.serializeUser((user, done) => {
done(null, user.id)
@ -33,13 +34,16 @@ module.exports = (passport) => {
debug(`Perfoming local login for ${email}`)
User.findOne({'email': email})
.then((user) => {
// No user with that email
if (!user) {
debug(`No user with that email`)
req.session.next = undefined
return done(null, false, req.flash('warning', 'Incorrect email or password.'))
// User exists
} else {
debug(`User exists. Checking password...`)
// Check password
user.validPassword(password)
@ -47,6 +51,7 @@ module.exports = (passport) => {
// Password incorrect
if (!res) {
debug(`Incorrect password`)
req.session.next = undefined
return done(null, false, req.flash('warning', 'Incorrect email or password.'))
@ -71,7 +76,7 @@ module.exports = (passport) => {
))
// Social login
function socialLogin (req, service, profileId, done) {
function socialLogin(req, service, profileId, done) {
debug(`socialLogin() called for ${service} account ${profileId}`)
let query = {}
query['auth.' + service] = profileId

13
config/routes/auth.js Normal file → Executable file
View File

@ -10,6 +10,7 @@ const debug = require('debug')('tracman-routes-auth')
const env = require('../env/env.js')
module.exports = (app, passport) => {
// Methods for success and failure
const loginOutcome = {
failureRedirect: '/login',
@ -55,7 +56,7 @@ module.exports = (app, passport) => {
.post((req, res, next) => {
// Send token and alert user
function sendToken (user) {
function sendToken(user) {
debug(`sendToken() called for user ${user.id}`)
// Create a new password token
@ -119,7 +120,7 @@ module.exports = (app, passport) => {
mw.throwErr(err, req)
res.redirect('/login#signup')
}
} })
})
.catch( (err) => {
@ -127,9 +128,9 @@ module.exports = (app, passport) => {
mw.throwErr(err, req)
res.redirect('/login#signup')
})
}
// Validate email
req.checkBody('email', 'Please enter a valid email address.').isEmail()
@ -225,6 +226,7 @@ module.exports = (app, passport) => {
// Save user and send the token by email
Promise.all([slug, sk32])
//.then(() => { user.save() })
.then(() => { sendToken(user) })
.catch((err) => {
debug('Failed to save user after creating slug and sk32!')
@ -273,6 +275,7 @@ module.exports = (app, passport) => {
// User with that email does exist
} else {
// Create reset token
user.createPassToken()
.then( (token) => {
@ -319,7 +322,7 @@ module.exports = (app, passport) => {
mw.throwErr(err, req)
res.redirect('/login/forgot')
})
})
// Android

0
config/routes/index.js Normal file → Executable file
View File

2
config/routes/settings.js Normal file → Executable file
View File

@ -8,7 +8,7 @@ const mw = require('../middleware.js')
const User = require('../models.js').user
const mail = require('../mail.js')
const env = require('../env/env.js')
const debug = require('debug')('tracman-settings')
const debug = require('debug')('tracman-routes-settings')
const router = require('express').Router()
// Settings form

8
config/test.js Executable file
View File

@ -0,0 +1,8 @@
module.exports = {
FAKE_EMAIL: 'nobody@tracman.org',
TEST_EMAIL: 'test@tracman.org',
TEST_PASSWORD: 'mDAQYe2VYE',
BAD_PASSWORD: 'password123',
FUZZED_EMAIL_TRIES: 3,
FUZZED_PASSWORD_TRIES: 10,
}

69
package-lock.json generated Normal file → Executable file
View File

@ -827,6 +827,43 @@
"methods": "1.1.2",
"qs": "6.5.1",
"superagent": "2.3.0"
},
"dependencies": {
"async": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
"integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=",
"dev": true
},
"form-data": {
"version": "1.0.0-rc4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.0-rc4.tgz",
"integrity": "sha1-BaxrwiIntD5EYfSIFhVUaZ1Pi14=",
"dev": true,
"requires": {
"async": "1.5.2",
"combined-stream": "1.0.5",
"mime-types": "2.1.17"
}
},
"superagent": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/superagent/-/superagent-2.3.0.tgz",
"integrity": "sha1-cDUpoHFOV+EjlZ3e+84ZOy5Q0RU=",
"dev": true,
"requires": {
"component-emitter": "1.2.1",
"cookiejar": "2.0.6",
"debug": "2.6.9",
"extend": "3.0.1",
"form-data": "1.0.0-rc4",
"formidable": "1.1.1",
"methods": "1.1.2",
"mime": "1.4.1",
"qs": "6.5.1",
"readable-stream": "2.3.3"
}
}
}
},
"chalk": {
@ -5983,16 +6020,16 @@
}
},
"superagent": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/superagent/-/superagent-2.3.0.tgz",
"integrity": "sha1-cDUpoHFOV+EjlZ3e+84ZOy5Q0RU=",
"version": "3.8.2",
"resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.2.tgz",
"integrity": "sha512-gVH4QfYHcY3P0f/BZzavLreHW3T1v7hG9B+hpMQotGQqurOvhv87GcMCd6LWySmBuf+BDR44TQd0aISjVHLeNQ==",
"dev": true,
"requires": {
"component-emitter": "1.2.1",
"cookiejar": "2.0.6",
"debug": "2.6.9",
"cookiejar": "2.1.1",
"debug": "3.1.0",
"extend": "3.0.1",
"form-data": "1.0.0-rc4",
"form-data": "2.3.1",
"formidable": "1.1.1",
"methods": "1.1.2",
"mime": "1.4.1",
@ -6000,21 +6037,19 @@
"readable-stream": "2.3.3"
},
"dependencies": {
"async": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
"integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=",
"cookiejar": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.1.tgz",
"integrity": "sha1-Qa1XsbVVlR7BcUEqgZQrHoIA00o=",
"dev": true
},
"form-data": {
"version": "1.0.0-rc4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.0-rc4.tgz",
"integrity": "sha1-BaxrwiIntD5EYfSIFhVUaZ1Pi14=",
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"dev": true,
"requires": {
"async": "1.5.2",
"combined-stream": "1.0.5",
"mime-types": "2.1.17"
"ms": "2.0.0"
}
}
}

1
package.json Normal file → Executable file
View File

@ -45,6 +45,7 @@
"mocha": "^4.0.1",
"nodemon": "^1.11.0",
"standard": "^10.0.3",
"superagent": "^3.8.2",
"supertest": "^3.0.0"
},
"scripts": {

View File

@ -1,18 +1,19 @@
'use strict'
const chai = require('chai')
const app = require('../server')
const User = require('../config/models').user
const request = require('supertest')
.agent(require('../server'))
// const superagent = require('superagent').agent()
const request = require('supertest').agent(app)
chai.use(
require('chai-http')
)
// Constants for dummy accounts
const FAKE_EMAIL = 'nobody@tracman.org'
const TEST_EMAIL = 'test@tracman.org'
const TEST_PASSWORD = 'mDAQYe2VYE'
const BAD_PASSWORD = 'password123'
// Import test config by object destructuring
const { FAKE_EMAIL, TEST_EMAIL,
TEST_PASSWORD, BAD_PASSWORD,
FUZZED_EMAIL_TRIES, FUZZED_PASSWORD_TRIES,
} = require('../config/test.js')
describe('Authentication', () => {
@ -20,9 +21,131 @@ describe('Authentication', () => {
let passwordless_user
// Make sure test user doesn't exist
before( async () => {
let existing_test_user = await User.findOne({'email':TEST_EMAIL})
if (existing_test_user) existing_test_user.remove()
before( (done) => {
User.findOne({'email':TEST_EMAIL})
.then( (user) => {
if (!user) done()
else {
user.remove()
.then( (user) => { done() })
.catch(console.error)
}
}).catch(console.error)
})
// These tests require the test user to have been created
after( () => {
console.log('running after tests')
describe('Logged out', () => {
it('Fails to log in with bad password', async () => {
// Confirm redirect
chai.expect( await request.post('/login')
.type('form').send({
'email': TEST_EMAIL,
'password': BAD_PASSWORD
})
).to.redirectTo('/login') // Hey! Incorrect email or password.
})
// TODO: Implement fuzzer
it.skip(`Fails to log in with ${FUZZED_PASSWORD_TRIES} fuzzed passwords`, () => {
// Fuzz passwords
// loop with let fuzzed_password
// Confirm redirect
// chai.expect( await request.post('/login')
// .type('form').send({
// 'email': TEST_EMAIL,
// 'password': fuzzed_password
// })
// ).to.redirectTo('/login') // Hey! Incorrect email or password.
})
it('Logs in with password', async () => {
let res = await request.post('/login')
.type('form').send({
email: TEST_EMAIL,
password: TEST_PASSWORD
})
request.saveCookies(res)
chai.expect(res).to.redirectTo('/map')
})
// it('Forgets password', async () => {
// })
// it('Changes forgotten password', async () => {
// })
})
describe('Logged in', () => {
it('Logs out', async () => {
let res = request.get('/logout')
request.attachCookies(res)
chai.expect(res).to.redirectTo('/')
})
// it('Changes email address', async () => {
// })
// it('Changes password', async () => {
// })
// it('Changes settings', async () => {
// })
// it('Connects a Google account', async () => {
// })
// it('Connects a Facebook account', async () => {
// })
// it('Connects a Twitter account', async () => {
// })
// it('Logs in with Google', async () => {
// })
// it('Logs in with Facebook', async () => {
// })
// it('Logs in with Twitter', async () => {
// })
// it('Disconnects a Google account', async () => {
// })
// it('Disconnects a Facebook account', async () => {
// })
// it('Disconnects a Twitter account', async () => {
// })
})
})
it('Fails to create an account with a fake email', async () => {
@ -32,10 +155,11 @@ describe('Authentication', () => {
.type('form').send({ 'email':FAKE_EMAIL })
).to.redirectTo('/login#signup')
// Ensure user was deleted after email failed to send
// Users with bad emails are removed asynchronously and may happen after
// the response was recieved. Ensure it's happened in a kludgy way by
// waiting 2 seconds before asserting that the user doesn't exist
/* Ensure user was deleted after email failed to send
/* Users with bad emails are removed asynchronously and may happen after
/* the response was recieved. Ensure it's happened in a kludgy way by
/* waiting 2 seconds before asserting that the user doesn't exist
*/
setTimeout( async () => {
chai.assert.isNull( await User.findOne({
'email': FAKE_EMAIL
@ -44,6 +168,30 @@ describe('Authentication', () => {
})
// TODO: Implement fuzzer
it.skip(`Fails to create accounts with ${FUZZED_EMAIL_TRIES} fuzzed emails`, () => {
// Fuzz emails
// loop with let fuzzed_email
// Confirm redirect
// chai.expect( await request.post('/signup')
// .type('form').send({ 'email':fuzzed_email })
// ).to.redirectTo('/login#signup')
/* Ensure user was deleted after email failed to send
/* Users with bad emails are removed asynchronously and may happen after
/* the response was recieved. Ensure it's happened in a kludgy way by
/* waiting 2 seconds before asserting that the user doesn't exist
*/
// setTimeout( async () => {
// chai.assert.isNull( await User.findOne({
// 'email': FAKE_EMAIL
// }), 'Account with fake email was created')
// }, 2000)
})
it('Creates an account with a valid email', async () => {
// Set email address
@ -96,82 +244,4 @@ describe('Authentication', () => {
})
describe('Account usage', () => {
// Create account to play with
before( () => {
// Create user
// Set password
})
// it('Logs in', async () => {
// })
// it('Logs out', async () => {
// })
// it('Forgets password', async () => {
// })
// it('Changes forgotten password', async () => {
// })
// it('Logs back in', async () => {
// })
// it('Changes email address', async () => {
// })
// it('Changes password', async () => {
// })
// it('Changes settings', async () => {
// })
// it('Connects a Google account', async () => {
// })
// it('Connects a Facebook account', async () => {
// })
// it('Connects a Twitter account', async () => {
// })
// it('Logs in with Google', async () => {
// })
// it('Logs in with Facebook', async () => {
// })
// it('Logs in with Twitter', async () => {
// })
// it('Disconnects a Google account', async () => {
// })
// it('Disconnects a Facebook account', async () => {
// })
// it('Disconnects a Twitter account', async () => {
// })
})
})