User Signup

This section will cover creating a user from a form submission. When committing user submitted data to the database it is import to verify that the content is valid, and return an appropriate error message to the user if it is not. To perform the validation we will you be using node-validator.

node-validator - String validation and sanitization in JavaScript

Validating user content with node-validator

  1. Add node-validator and express-validator to your package.json

    package.json

     "express-validator": "0.3.x",
     "validator": "0.4.x"
    
  2. Updated your dependencies

     $ npm install
    
  3. Add the express-validator middleware:

    app.js

     //... imports
     var expressValidator = require('express-validator');
    
     var app = exports.app = express();
    
     app.configure(function () {
       //... existing settings
       // add the express-validator middleware.
       // Important: This must come before the app.router middleware
       app.use(expressValidator);
       app.use(app.router);
       app.use(express.static(path.join(__dirname, 'public')));
     });
    
     //... additional code
    
  4. Write a test for the form submission:

    test/users/routes.js

     'use strict';
    
     // import the moongoose helper utilities
     var utils = require('../utils');
     var request = require('supertest');
     var should = require('should');
     var app = require('../../app').app;
    
     describe('Users: routes', function () {
       describe('POST /signup', function () {
         it('should redirect to "/account" if the form is valid', function (done) {
           var post = {
             givenName: 'Barrack',
             familyName: 'Obama',
             email: 'berry@example.com',
             password: 'secret'
           };
           request(app)
             .post('/signup')
             .send(post)
             .expect(302)
             .end(function (err, res) {
               should.not.exist(err);
               // confirm the redirect
               res.header.location.should.include('/account');
               done();
             });
         });
       });
     });
    

    Check the test runner -- you will see:

     ✖ 1 of 7 tests failed:
    
     1) Users: routes POST /signup should redirect to "/account" if the form is valid:
        AssertionError: expected [Error: expected 302 "Moved Temporarily", got 404 "Not Found"] to not exist
    
  5. Write the code to make the test pass:

    user/routes.js

     'use strict';
    
     var User = require('./models').User;
    
     // POST /users
     exports.signup = function (req, res) {
       res.redirect('/account');
     };
    

    Now that we have the handler we need to map the POST /signup route to it. Edit your app.js file:

    app.js

     //... imports
     var users = require('./users/routes');
    
     //... routes
     app.post('/signup', users.signup);
    

    Check the test runner -- you will see:

     Users: routes
       POST /signup
         ◦ should redirect to "/account" if the form is valid: POST /signup 302 2ms - 58
         ✓ should redirect to "/account" if the form is valid
    
     ✔ 7 tests complete (111 ms)
    
  6. Write tests to check validation:

    test/users/routes.js

     //... previous tests
     it('should redirect to "/login" if the form is invalid', function (done) {
       var post = {
         givenName: 'Barrack',
         familyName: '',
         email: 'fakeemail',
         password: 'se'
       };
       request(app)
         .post('/signup')
         .send(post)
         .expect(302)
         .end(function (err, res) {
           should.not.exist(err);
           // confirm the redirect
           res.header.location.should.include('/signup');
           done();
         });
     });
    

    Test should fail:

     ✖ 1 of 8 tests failed:
    
     1) Users: routes POST /signup should redirect to "/login" if the form is invalid:
        AssertionError: expected '//127.0.0.1:3461/account' to include '/signup'
    
  7. Write code to make it pass:

    users/routes.js

     exports.signup = function (req, res) {
       req.onValidationError(function (msg) {
         //Redirect to `/signup` if validation fails
         return res.redirect('/signup');
       });
       req.check('email', 'Please enter a valid email').len(1).isEmail();
       req.check('password', 'Please enter a password with a length between 4 and 34 digits').len(4, 34);
       req.check('givenName', 'Please enter your first name').len(1);
       req.check('familyName', 'Please enter your last name').len(1);
       res.redirect('/account');
     };
    

    Tests are now passing:

     Users: routes
       POST /signup
         ◦ should redirect to "/account" if the form is valid: POST /signup 302 2ms - 58
         ✓ should redirect to "/account" if the form is valid
         ◦ should redirect to "/login" if the form is invalid: POST /signup 302 2ms - 57
         ✓ should redirect to "/login" if the form is invalid
    
     ✔ 8 tests complete (186 ms)
    
  8. Ok, so our application is redirecting correctly, but is it actually creating a user? Let's create a test to check:

    test/users/routes.js

     //... previous imports
     var User = require('../../users/models').User;
    
     describe('Users: routes', function () {
       describe('POST /signup', function () {
         //... previous tests
         it('should create a new User if the form is valid', function (done) {
           var post = {
             givenName: 'Barrack',
             familyName: 'Obama',
             email: 'berry@example.com',
             password: 'secret'
           };
           request(app)
             .post('/signup')
             .send(post)
             .expect(302)
             .end(function (err, res) {
               should.not.exist(err);
               User.find(function (err, users) {
                 users.length.should.equal(1);
                 var u = users[0];
                 // Make sure the user values match up.
                 u.name.givenName.should.equal(post.givenName);
                 u.name.familyName.should.equal(post.familyName);
                 u.emails[0].value.should.equal(post.email);
                 should.exist(u.passwordHash);
                 done();
               });
             });
         });
       });
     });
    

    It appears not.

     ✖ 1 of 9 tests failed:
    
     1) Users: routes POST /signup should create a new User if the form is valid:
        AssertionError: expected 0 to equal 1
    
  9. So lets make the test pass. Update user/routes.js so that it looks like this:

    user/routes.js

     'use strict';
    
     var User = require('./models').User;
    
     // POST /signup
    
     exports.signup = function (req, res) {
       req.onValidationError(function (msg) {
         //Redirect to `/signup` if validation fails
         return res.redirect('/signup');
       });
       req.check('email', 'Please enter a valid email').len(1).isEmail();
       req.check('password', 'Please enter a password with a length between 4 and 34 digits').len(4, 34);
       req.check('givenName', 'Please enter your first name').len(1);
       req.check('familyName', 'Please enter your last name').len(1);
       // If the form is valid create a new user
       var newUser = {
         name: {
           givenName: req.body.givenName,
           familyName: req.body.familyName
         },
         emails: [
           {
             value: req.body.email
           }
         ]
       };
       // hash password
       User.hashPassword(req.body.password, function (err, passwordHash) {
         // update attributes
         newUser.passwordHash = passwordHash;
         // Create new user
         User.create(newUser, function (err, user) {
           return res.redirect('/account');
         });
       });
     };
    

    Add a passwordHash String to our userSchema:

    users/models.js

     //... previous code
     // define the userSchema
     var userSchema = new Schema({
       name  : {
         givenName   : String,
         familyName  : String
       },
       emails: [emailSchema],
       passwordHash: String
     });
     //... previous code
    

    All test are now passing:

     addition
       ✓ should add 1+1 correctly
       ◦ should return 2 given the url /add/1/1: GET /add/1/1 200 1ms - 1
       ✓ should return 2 given the url /add/1/1
    
     Users: models
       #create()
         ✓ should create a new User
       #hashPassoword()
         ✓ should return a hashed password asynchronously
       #comparePasswordAndHash()
         ✓ should return true if password is valid
         ✓ should return false if password is invalid
    
     Users: routes
       POST /signup
         ◦ should redirect to "/account" if the form is valid: POST /signup 302 4ms - 58
         ✓ should redirect to "/account" if the form is valid
         ◦ should redirect to "/login" if the form is invalid: POST /signup 302 3ms - 57
         ✓ should redirect to "/login" if the form is invalid
         ◦ should create a new User if the form is valid: POST /signup 302 2ms - 58
         ✓ should create a new User if the form is valid
    
     ✔ 9 tests complete (287 ms)
    

Extra credit:

While our current solution works -- we have a lot of logic the could probably be moved to the model. How would you refactor the signup function and the userSchema to transfer that logic?

Hint: mongoose statics

Resources

Comments