From 887b87231b9f923a0330aaa4cb4077012d1169c1 Mon Sep 17 00:00:00 2001 From: Keith Irwin Date: Fri, 19 Jan 2018 21:23:43 +0000 Subject: [PATCH] #68 Fixed tests up to login/-out --- config/models.js | 117 ++++-------------- config/passport.js | 7 +- config/routes/auth.js | 13 +- config/routes/index.js | 0 config/routes/settings.js | 2 +- config/test.js | 8 ++ package-lock.json | 69 ++++++++--- package.json | 1 + test/auth.js | 254 ++++++++++++++++++++++++-------------- 9 files changed, 261 insertions(+), 210 deletions(-) mode change 100644 => 100755 config/models.js mode change 100644 => 100755 config/passport.js mode change 100644 => 100755 config/routes/auth.js mode change 100644 => 100755 config/routes/index.js mode change 100644 => 100755 config/routes/settings.js create mode 100755 config/test.js mode change 100644 => 100755 package-lock.json mode change 100644 => 100755 package.json diff --git a/config/models.js b/config/models.js old mode 100644 new mode 100755 index 9b31d31..762b0cb --- a/config/models.js +++ b/config/models.js @@ -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 = { diff --git a/config/passport.js b/config/passport.js old mode 100644 new mode 100755 index 33dde56..8be8f9a --- a/config/passport.js +++ b/config/passport.js @@ -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 diff --git a/config/routes/auth.js b/config/routes/auth.js old mode 100644 new mode 100755 index 18aff04..007d006 --- a/config/routes/auth.js +++ b/config/routes/auth.js @@ -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 diff --git a/config/routes/index.js b/config/routes/index.js old mode 100644 new mode 100755 diff --git a/config/routes/settings.js b/config/routes/settings.js old mode 100644 new mode 100755 index 18fa718..5d9a958 --- a/config/routes/settings.js +++ b/config/routes/settings.js @@ -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 diff --git a/config/test.js b/config/test.js new file mode 100755 index 0000000..377c86a --- /dev/null +++ b/config/test.js @@ -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, +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json old mode 100644 new mode 100755 index 5ea9cf1..a963d02 --- a/package-lock.json +++ b/package-lock.json @@ -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" } } } diff --git a/package.json b/package.json old mode 100644 new mode 100755 index 75fb7e4..a7ea8d4 --- a/package.json +++ b/package.json @@ -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": { diff --git a/test/auth.js b/test/auth.js index 6b1d25c..4894d13 100755 --- a/test/auth.js +++ b/test/auth.js @@ -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 () => { - - // }) - - }) - })