0

I am currently trying to test my node api w/ mocha chai. I am running into a scenario where a test should actually fail but is passing. I have a repo up of the current API that I am building here if you want to play around with it: enter link description here. However, I am still going to walk through the code in this question.

I'm trying to test the controller with the following code:

import chai, { expect } from 'chai';
import chaiHttp from 'chai-http';
import server from '../../src/app';

chai.use(chaiHttp);

describe('Authentication Controller', () => {

  const testUser = {
    email_address: 'test@test.com',
    password: 'test'
  };

  describe('login success', () => {
    it('responds with status 200', done => {
      chai.request(server)
        .post('/api/auth/login')
        .send(testUser)
        .end((err, res) => {
          expect(res).to.have.status(200);
          done();
        });
    });
  });

  describe('login failure', () => {
    it('responds with status 401', done => {
      chai.request(server)
        .post('/api/auth/login')
        .send(testUser.email_address = 'fake@news.com')
        .end((err, res) => {
          expect(res).to.have.status(200);
          done();
        });
    });
  });

});

Obviously I want to test a successful login and a failed login attempt. However, both the response statuses from the server are 200 and this should not be the case. When testing in Postman the response status when an individual tries to login with an email address that doesn't exist or a password that doesn't match, it returns a status of 401. If I write a test

expect(1).to.equal(1) => test passes. expect(1).to.equal(2) => test fails.

Here is the controller function that handles the request for logging in:

export function login(req, res) {
  User.findOne({email: req.body.email})
    .then(user => {
      if (user && bcrypt.compareSync(req.body.password, user.password)) {
        generateToken(res, user);
      } else {
        res.status(401).json({
          success: false,
          message: 'Incorrect username or password.'
        });
      }
    })
    .catch(err => {
      res.json(err);
    });
}

The model that handles the request:

export function createUser(req) {
  return db('users').insert(Object.assign(req.body,{password: hashPassword(req.body.password)}))
    .then((id) => db('users').select().where('id', id).first());
}

As you can see I am using Knex.js. I have setup a test database and everything is connected appropriately, so I'm confused as to why my server is responding w/ a 200 response status when testing?

I just want to say thanks to anyone who takes the time to help me understand how mocha chai is working. I have very LITTLE experience with testing applications, but I want to start familiarizing myself w/ doing so because I believe it to be good practice.

Louis
  • 146,715
  • 28
  • 274
  • 320
Nappstir
  • 995
  • 2
  • 20
  • 38
  • Do you have some sort of requirement that ties you to Mocha/Chai? If not, I suggest you use Jest, 100x simpler to setup and better tests. – Baruch Oct 01 '17 at 22:59
  • I've rolled back your edit to remove the solution you added to your question. Please do not add solutions to your questions. The editorial practices for this site are that questions should *only* be questions, and answers should *only* be answers. I see you already have an answer. You can edit it if you need to add to it. – Louis Oct 02 '17 at 19:36
  • Thanks Louis for doing that! I'll be sure to keep that in mind moving forward. @Baruch No I dont have any tie to Mocha/Chai. I'll have to take a look at `Jest` as I'm not opposed at all to using something if it is more simple and easier to implement. – Nappstir Oct 02 '17 at 22:09

2 Answers2

2

I actually cloned your Github repo and tried running the test. From what I have seen, there are a couple of different issues in your code, as followed:

1. from the controller function you posted in the question:

```

export function login(req, res) {
    User.findOne({email: req.body.email})
    .then(user => {
      // [removed because of little relevancy]
    })
    .catch(err => {
      res.json(err);
    });
}

```

The issue is the line res.json(err) which actually responded with a 200 status code (even though it was an error in this case). This is because res.json does not automatically set the HTTP response status code for you when you "send an error object". This fooled the client (in this case, chai-http) into thinking it was a successful request. To properly respond with an error status code, you may use this instead: res.status(500).json(err)

It's also worth noticing that some of your other controller functions got into this issue too.


2. from your userModels.js file, line 10, which is:

```

return db('users').select().where(req).first();

```

You are using Knex API in an incorrect way. It should be ...where('email', req.email)... This was the initial reason why your requests failed.


3. you set up your unit tests in different manners:

Test no. 1 (login success):

```

chai.request(server)
        .post('/api/auth/login')
        .send(testUser)

```

Test no. 2 (login failure):

```

chai.request(server)
        .post('/api/auth/login')
        .send(testUser.email_address = 'fake@news.com')

```

So, what happened?

In the first test, you passed an object into .send(), whereas in the second test, you simply passed an expression. When done this way, the model handler, userModels.findOne(), received an object with keys email_address and password for the first test, but for the second test, it did not.

Also, in your 1st test case, you sent testUser.email_address, but in your controller function, you referenced req.body.email.

All these, in addition to the issue no. 1 as I mentioned earlier, further complicated your test suite, leading to your misunderstanding in the end.


Disclaimer:

All what I wrote above was based on the source code from your Github repo, so if you have fixed some issues since you pushed your code, and some (or all) of my points above are no longer valid, please disregard. Nevertheless, I wish you have found, or will soon find out why your code didn't behave as you expected!

Cheers,

Khang Dinh
  • 86
  • 1
  • 5
  • 1. Makes total sense! 2. The reason why I'm looking up a user by passing in `req` is because I want have a reusable function and not just one that can look up a user via email. This works just fine as long as I pass in the appropriate parameters. I dont see a huge issue with it. 3. I adjusted it so that I am now just passing in separate obj. Thanks for all your insight and help. I have edited previous question. – Nappstir Oct 02 '17 at 18:40
0

I just wanted to post an answer here that is specific to my experience and what helped me get all of this setup. The only thing that I really needed to change on the Repo Proj was the property email on the user object I was passing. I had email_address and was thus searching for that column in the database whilst it did not exist! So once I changed that I started down the right path.

I was then able to get my failed login test to pass. However, my successful login didn't pass. The reason was because I was seeding my database with a plain string password. Thus, when I performed the conditional statement of:

if (user && bcrypt.compareSync(req.body.password, user.password))

It wasn't passing because the bcrypt.comparSync was looking for a password that was hashed. In order to get this to work I needed to require babel-register in my knex file. This then allowed me to use es6 and perform my hashPassword function:

test/userSeed.js

import hashPassword from '../../src/helpers/hashPassword';

exports.seed = function(knex, Promise) {
  return knex('users').truncate()
    .then(() => {
      return knex('users')
        .then(() => {
          return Promise.all([
            // Insert seed entries
            knex('users').insert([
              {
                first_name: 'admin',
                last_name: 'admin',
                email: 'admin@admin.com',
                password: hashPassword('test')
              },
              {
                first_name: 'test',
                last_name: 'test',
                email: 'test@test.com',
                password: hashPassword('test')
              }
            ]),
          ]);
        });
    })
};

hashPassword.js

import bcrypt from 'bcrypt';

export default function(password) {
  const saltRounds = 10;
  let salt = bcrypt.genSaltSync(saltRounds);
  return bcrypt.hashSync(password, salt);
}

This resulted in the hashing of my users password when I seeded the DB. Tests all pass as they should and api works as intended using Postman.

Nappstir
  • 995
  • 2
  • 20
  • 38