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.
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,
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.
Related
I'm trying to set up an email verification flow in my project, but I can't seem to get it right.
How my flow works now is the user enters their credentials (email and password), which are used to create a new firebase user. Then, once that promise is resolved, it sends an email verification link to the new user that was created. The code looks like this:
async createUser(email: string, password: string) {
try {
console.log("Creating user...");
const userCredentials = await createUserWithEmailAndPassword(
auth,
email,
password
);
console.log("Successfully created user");
const { user } = userCredentials;
console.log("Sending email verification link...");
await this.verifyEmail(user);
console.log("EMAIL VERIFICATION LINK SUCCESSFULLY SENT");
return user;
} catch (err) {
throw err;
}
}
async verifyEmail(user: User) {
try {
sendEmailVerification(user);
} catch (err) {
throw err;
}
}
The link is sent through fine, but once I press on it, I'm redirected to a page that says this:
Strangely, the user's email is verified after this, in spite of the error message displayed. Any idea why this is happening?
Update:
I managed to figure it out. The email provider I'm using is my university's, and it seems to be preventing the verification link from working properly. I did try with my personal email to see if that was the case, but I wasn't seeing the verification link appearing there. I eventually realized that it was because it was being stored in the spam folder. It's working on other email providers, though, ideally, I'd want it to work on my university's email provider (the emails that users sign up with are supposed to be exclusively student emails). Any ideas how I could resolve this?
I eventually figured out that the issue was with my email provider. I was using my student email, which the university provides, and I imagine they've placed rigorous measures in place to secure them as much as possible. I have no idea what was preventing it from working, but I managed to figure out a workaround.
In brief, I changed the action URL in the template (which can be found in the console for your Firebase project in the Authentication section, under the Templates tab) to a route on my website titled /authenticate. I created a module to handle email verification. Included in it is a function that parses the URL, extracting the mode (email verification, password reset, etc.), actionCode (this is the important one. It stores the id that Firebase decodes to determine if it's valid), continueURL (optional), and lang (optional).
export const parseUrl = (queryString: string) => {
const urlParams = new URLSearchParams(window.location.search);
const mode = urlParams.get("mode");
const actionCode = urlParams.get("oobCode");
const continueUrl = urlParams.get("continueUrl");
const lang = urlParams.get("lang") ?? "en";
return { mode, actionCode, continueUrl, lang };
};
I created another method that handles the email verification by applying the actionCode from the URL using Firebase's applyActionCode.
export const handleVerifyEmail = async (
actionCode: string,
continueUrl?: string,
lang?: string
) => {
try {
await applyActionCode(auth, actionCode);
return { alreadyVerified: false };
} catch (err) {
if (err instanceof FirebaseError) {
switch (err.code) {
case "auth/invalid-action-code": {
return { alreadyVerified: true };
}
}
}
throw err;
}
};
The auth/invalid-action-code error seems to be thrown when the user is already verified. I don't throw an error for it, because I handle this differently to other errors.
Once the user presses the verification link, they're redirected to the /authenticate page on my website. This page then handles the email verification by parsing the query appended to the route. The URL looks something like this http://localhost:3000/authenticate?mode=verifyEmail&oobCode=FLVl85S-ZI13_am0uwWeb4Jy8DUWC3E6kIiwN2LLFpUAAAGDUJHSwA&apiKey=AIzaSyA_V9nKEZeoTOECWaD7UXuzqCzcptmmHQI&lang=en
Of course, in production, the root path would be the name of the website instead of localhost. I have my development environment running on port 3000.
Once the user lands on the authentication page, I handle the email verification in a useEffect() hook (Note: I'm using Next.js, so if you're using a different framework you might have to handle changing the URL differently):
useEffect(() => {
verifyEmail();
async function verifyEmail() {
const { actionCode } = parseUrl(window.location.search);
if (!actionCode) return;
router.replace("/authenticate", undefined, { shallow: true });
setLoadingState(LoadingState.LOADING);
try {
const response = await handleVerifyEmail(actionCode!);
if (response.alreadyVerified) {
setEmailAlreadyVerified(true);
onEmailAlreadyVerified();
return;
}
setLoadingState(LoadingState.SUCCESS);
onSuccess();
} catch (err) {
console.error(err);
onFailure();
setLoadingState(LoadingState.ERROR);
}
}
}, []);
It first checks if there is an action code in the URL, in case a user tries to access the page manually.
The onSuccess, onFailure, and onEmailAlreadyVerified callbacks just display toasts. loadingState and emailAlreadyVerified are used to conditionally render different responses to the user.
Products like Loyalty lion offer "reward points" when a user follows your Instagram account. I am trying to add the same functionality to my website but cannot for the life of me figure out how they do it. In Loyalty Lion you can click a "follow us on Instagram" button on your website, a small window opens, asks you to sign in to Instagram if you are not already, then imitates the follow automatically and closes the window. While this behavior is ideal, I thought I would try a simpler approach... but none of them seem to work.
My first thought was to ask a user to follow my account then click a verify button to receive points. This would trigger a function that checks if that specific user has my account in their list of follows. According to this answer however, this solution is no longer viable (as of 2018).
My second through was to just record the follower count at the beginning of the process and check if it had increased when the user clicks confirms. I realize this is not very secure but it seemed like the most basic (and therefore most likely to be supported) method. I have tried three different implementations now to get my follower count and while some of them work on on my local machine, none work once deployed:
Option 1: Using axios. worked the very first time I ran it but seems to be blocked on all subsequent attempts. If I simply paste the link into the browser I get readable JSON but the response to my HTTP request is gibberish.
const axios = require("axios").default;
exports.getInstagramFollowerCount = async (req, res) => {
axios
.get("https://www.instagram.com/username/?__a=1")
.then((resp) => {
return res.status(200).json({ count: resp.data.graphql.user.edge_followed_by.count });
})
.catch((err) => {
return res.status(500).json({ msg: JSON.stringify(err) });
});
};
Option 2: using the instagram-followers package. This works locally but not when deployed to a firesbase server
const followers = require("instagram-followers");
exports.getInstagramFollowerCount = async (req, res) => {
followers(username)
.then((no) => {
return res.status(200).json({ count: Number(no) });
})
.catch((err) => {
return res.status(500).json({ msg: err });
});
};
Option 3: using the instagram-basic-data-scraper-with-username package. This works locally but not when deployed to a firesbase server
const instaObj = require("instagram-basic-data-scraper-with-username");
exports.getInstagramFollowerCount = async (req, res) => {
instaObj
.specificField(username, "followers")
.then(resp => {
return res.status(200).json({ count: Number(resp.data) });
})
.catch(err => {
return res.status(500).json({ msg: err });
});
};
Why is this incredibly rudimentary implementation not working when deployed? Does anybody know of a better way to confirm that a user has followed your account?
I'm building an API that I intend to use with my react native application. The problem which I'm facing right now is that I when I try to hit this particular route /api/auth/signup in Postman I get the Could not get any response error message.
This is the route:
//create user token
router.post(
"/signup",
[check("username").isEmail(), check("password").isLength({ min: 6 })],
async (req, res) => {
//validate input field on the backend
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(422).json({ errors: errors.array() });
}
const { username, password, firstName, lastName } = req.body;
try {
//search DB if there is an existing user
let user = await User.findOne({ username });
if (user) {
return res.status(400).json({ msg: "User already exists" });
}
const salt = await bcrypt.genSalt(10);
res.status(200).json({ data: salt });
} catch (error) {}
}
);
module.exports = router;
The strange thing is that if I remove the User.findOne function I get a response. I don't know why this keeps happening, as I had built a similar application following the same pattern without a problem.
NOTE: In the main app.js I have the app.use(express.json({extended:true}), I've also successfully linked the routes in the main file too. Any help would be greatly appreciated!
I think you have a typo in your app.use... statement which you've given in your comment as,
app.use(express.json({extented:true})
which needs to be corrected as, ('d' should come in place of 't')
app.use(express.json({extended:true})
Hope this helps!
So it turns out when I was requiring mongoose in one of my models I required it with an uppercase letter const mongoose = require('Mongoose') which isn't valid, therefore the findOne method won't run because there's no model to take it from. Strange that visual studio code didn't complain about the false import. Anyways, thanks to anyone willing to help me out :) !
I am attempting to add MFA for user authentication to an already existing solution (built in Angular) for device management within AWS Cognito.
I am having trouble figuring out how to handle this particular response well from a user-experience perspective. It actually feels broken, so would love if anyone else has experience pain points here.
See Use Case 23. for example implementation, mine is below:
authenticate(username: string, password: string): Observable<any> {
// init cognitoUser here
return new Observable((observer) => {
cognitoUser.authenticateUser(authenticationDetails, {
onSuccess: (result: any) => {},
onFailure: (err: Error) => {},
mfaRequired: (codeDeliveryDetails: any) => {
// SMS has just been sent automatically
// and it needs to be confirmed within this scope
// The example linked requests the code via `confirm()`
// which is awful UX...and since this is a service
// probably non-compliant with best practice
// However, without this `confirm` at this point in
// time, we have no confirmationCode below
cognitoUser.sendMFACode(confirmationCode, {
onSuccess: (result) => {
observer.next(result);
observer.complete();
}, onFailure: (err: Error) => {
observer.error(err);
observer.complete();
}
});
}
});
});
}
Expected:
If the user authenticates successfully but has not added this device through MFA, we can manage the redirect to appropriate confirmation code form page and trigger the sendMFACode function manually (perhaps through some sort of limited session?)
Issue/s:
we don't have a session, so we have no way of asking the user the MFA code sent automatically outside of this login screen...catch 22?
adding another show/hide field in the login form doesn't work as it would hit the sendMfaCode function multiple times, resulting in multiple SMS codes sent.
Has anyone had any luck stepping out of this flow?
Whilst I’m sure very talented people worked on the amazon-cognito-identity-js API, it is just straight up badly designed. Thus why it’s been depricated. My personal advise would be to migrate to Amplify, which makes me much less angry.
With Amplify you can do these ones.
import Amplify from 'aws-amplify'
import Auth from '#aws-amplify/auth'
let mfaRequired = false
Amplify.configure({
Auth: {
userPoolWebClientId: '',
userPoolId: ''
}
})
const logUserIn = (user) => {
// Go forth and be happy
}
// Run me on your login form's submit event
const login = async (username, password) => {
const user = await Auth.signIn(username, password)
if (user.challengeName === 'SMS_MFA') {
// Change UI to show MFA Code input
mfaRequired = true
return
}
return logUserIn(user)
}
// Run me when the user submits theire MFA code
const senfMfaCode = async (mfaCode) => {
const user = await Auth.confirmSignIn(mfaCode)
return logUserIn(user)
}
BUT if for some sad reason you need to keep using amazon-cognito-identity-js don’t worry. I got you.
Just keep the cognitoUser object stored outside the callback. The documentation is a little misleading because it only show’s self contained examples but there’s no reason that you can’t notify your UI when MFA is required and then call cognitoUser.sendMFACode() later.
Just remember that the documentation show’s the passing of this to sendMFACode() for scoping (which is terrible) but you can just declare your callbacks as a variable and share it between your authenticateUser() and sendMFACode() functions (or as many functions as you like).
import { CognitoUserPool, AuthenticationDetails, CognitoUser } from 'amazon-cognito-identity-js'
export let mfaRequired = false
export let cognitoUser = null
export const cognitoCallbacks = {
mfaRequired () {
// Implement you functionality to show UI for MFA form
mfaRequired = true
},
onSuccess (response) {
// Dance for joy the code gods be glorious.
},
onFailure () {
// Cry.
}
}
export const logUserIn = payload => {
cognitoUser = new CognitoUser({
Username: 'Matt Damon',
Pool: new CognitoUserPool({
UserPoolId: '',
ClientId: ''
})
})
return cognitoUser.authenticateUser(new AuthenticationDetails(payload), cognitoCallbacks)
}
export const sendMfaCode = MFACode => {
cognitoUser.sendMFACode(MFACode, cognitoCallbacks)
}
That’s a super basic implementation and on top of that you could,
Just overwrite the mfaRequired function in an external module to do whatever you want.
Wrap the whole thing in a pub/sub plugin and subscribe to events.
Hope that helps!
I know this is an old question, but I thought this answer might be helpful for anyone who is still using the amazon-cognito-identity-js API instead of Amplify. #stwilz's answer works somewhat, but there are a few complications that come when you stray too far away from the documentation's use cases (and might come about when doing TOTP MFA instead of SMS MFA). I've created a workaround to address situations where you might get errors like Invalid Access Token, Missing parameter Session, or Invalid session for the user.
If you need to use something like sendMFACodeoutside of the callbacks, it's not enough to just keep cognitoUser stored outside the callback. You actually have to call the authenticateUser function again, then call the sendMFACode within the callback. It gets more complicated with verifySoftwareToken for TOTP, where you actually have to store the Cognito user object and then reassign it when calling authenticateUser again.
If none of this makes sense, I've created a simple Github Gist that uses React and amazon-cognito-identity-js to show how such a flow would work. It's here: https://gist.github.com/harve27/807597824720d0919476c0262e30f587
We are currently struggling with a Uncaught Error: Can't set headers after they are sent. error message when trying to chain a user sign in into a test with chai-http.
The test signs in as a user which already exists in the database via our API and then should attempts to GET all items from an existing route. Our current test is below which mirrors very closely the example given on the Chai-HTTP documentation http://chaijs.com/plugins/chai-http/#retaining-cookies-with-each-request.
it('should return all notes on /api/notes GET', function (done) {
agent
.post('/users/register')
.send(user)
.then(function() {
return agent
.get('/api/notes')
.end(function (err, res) {
// expectations
done();
});
});
});
Our stack trace
Uncaught Error: Can't set headers after they are sent.
at ServerResponse.OutgoingMessage.setHeader (_http_outgoing.js:346:11)
at ServerResponse.header (node_modules/express/lib/response.js:718:10)
at ServerResponse.send (node_modules/express/lib/response.js:163:12)
at ServerResponse.json (node_modules/express/lib/response.js:249:15)
at app/routes/usersRouter.js:16:29
at node_modules/passport/lib/middleware/authenticate.js:236:29
at node_modules/passport/lib/http/request.js:51:48
at pass (node_modules/passport/lib/authenticator.js:287:14)
at Authenticator.serializeUser (node_modules/passport/lib/authenticator.js:289:5)
at IncomingMessage.req.login.req.logIn (node_modules/passport/lib/http/request.js:50:29)
at Strategy.strategy.success (node_modules/passport/lib/middleware/authenticate.js:235:13)
at verified (node_modules/passport-local/lib/strategy.js:83:10)
at InternalFieldObject.ondone (node_modules/passport-local-mongoose/lib/passport-local-mongoose.js:149:24)
This is the function being called on our users router which seems to be raising the error (not raised manually, just raised when using chai)
router.post('/register', function(req, res) {
User.register(new User({ username : req.body.username }), req.body.password, function(err, user) {
if (err) {
res.status(500).json({info: err});
}
passport.authenticate('local')(req, res, function () {
res.status(200).json({info: "success"});
});
});
});
Manually testing this functionality works correctly, the issue seems to purely be down to our test and how it is interacting with passport.
Does anyone have any suggestions or pointers which could be of help?
Is there an err object being passed in the User.register callback? If so, try putting return res.status(500).json({info: err}); so that the passport code will not run. The return will exit the function and will not attempt to set the headers twice.
This all comes back to two different pieces of code both answering the request (res.send or res.json).
Essentially there are two possibilities:
1. Passport is using the res object to answer the request, before you do. Check the docs on how this should be done.
2. You get an error and since you have a bug with a missing return in the error handler the res object is set twice.
This is a late answer but I was just having this exact problem.
Did you try putting your passport.authenticate function inside an else statement?
I found that fixed this problem for me.