Unsure of how to unit test password validation function that uses bCrypt.compare(), getting undefined - javascript

I am trying to write a jest unit test for my validateUser function, but when I call the function in my test I am always getting the value undefined returned.
additional context
I am using nestJS as my api framework, I have mocked my user findOne query function to return expectedUserObjHashed.
I am using bcrypt to handle hashing and comparing passwords, I am using hashSync and compare from the library in this test
(not sure if this was the proper way to validate) but I added log statements to verify that my test is making it into the if(result) block and not throwing an exception.
I am guessing this is some async problem but I've been trying to a few hours a day for a couple days and im not sure where its occuring.
// This is the set up for my test
const saltRounds = 10;
let hashedPassword: string;
let expectedUserObjHashed: any;
beforeAll(() => {
hashedPassword = hashSync('test123!', saltRounds);
expectedUserObjHashed = {
id: 1,
email: 'test#test.com',
first_name: 'foo',
last_name: 'barr',
password: hashedPassword,
};
});
it('should validate password', async () => {
expect(
await service.validateUser(
// expectedUserObjUnhashed is a duplicate of expectedUserObjHashed minus having the password property hashed
expectedUserObjUnhashed.email,
expectedUserObjUnhashed.password,
),
// validatedUserObj is the same as the other UserObj objects but the password property is removed
).toStrictEqual(validatedUserObj);
});
async validateUser(email: string, password: string): Promise<any> {
// findUserByEmail() is mocked in the test to return expectedUserObjHashed (noted above before code blocks)
const user = await this.userService.findUserByEmail(email);
if (user !== undefined) {
compare(password, user.password, function (err, result) {
if (result) {
const { password, ...userInfo } = user;
console.log(userInfo);
return userInfo;
} else {
throw new UnauthorizedException();
}
});
} else {
throw new BadRequestException();
}
}
Update: I wrote a controller to test the validateUser() function in postman and it seems that it is not getting any return value (which is probably why the test got undefined) but right in the bcrypt.compare() callback (right before the return userInfo line) I logged userInfo and it is defined, so now i'm unsure why the callback is not returning userInfo as instructed.
Update 2:
I played around with my implementation of bcrypt.compare() and I got it to work like this:
...
const isMatch = await compare(password, user.password);
if (isMatch) {
const { password, ...userInfo } = user;
return userInfo;
} else {
throw new UnauthorizedException();
}
...
I would still like to know why my original implementation did not work for my knowledge.

compare(password, user.password, function (err, result) {
if (result) {
const { password, ...userInfo } = user;
console.log(userInfo);
return userInfo;
} else {
throw new UnauthorizedException();
}
Function inside compare is a callback that works after compare is done. So compare is firing and it returns nothing and if it's done that function is running so console.log it's working but return don't have any effect. When you added await you actually waited for compare to be complete.

Related

Nestjs Testing in signup BadRequestException: email in use error

user.service.ts
async findWithMail(email:string):Promise<any> {
return this.userRepository.findOne({email});
}
auth.service.ts
async signup(email:string,password:string,name?:string,surname?:string,phone:string){
if(email) {
const users = await this.userService.findWithMail(email);
if(users) {
throw new BadRequestException('email in use');
}
}
if(!password) return {error:"password must be!"};
const salt = randomBytes(8).toString('hex');
const hash = (await scrypt(password,salt,32)) as Buffer;
const result = salt + '.' +hash.toString('hex');
password = result;
const user = await
this.userService.create(email,password,name,surname,phone);
return user;
}
auth.service.spec.ts
let service:AuthService;
let fakeUsersService: Partial<UserService>;
describe('Auth Service',()=>{
beforeEach(async() => {
fakeUsersService = {
findWithMail:() => Promise.resolve([]),
create:(email:string,password:string) => Promise.resolve({email,password} as User),
}
const module = await Test.createTestingModule({
providers:[AuthService,{
provide:UserService,
useValue:fakeUsersService
}],
}).compile();
service = module.get(AuthService);
});
it('can create an instance of auth service',async()=> {
expect(service).toBeDefined();
})
it('throws an error if user signs up with email that is in use', async () => {
await service.signup('asdf#asdf.com', 'asdf')
});
})
When ı try to run my test its give me error even this email is not in database its give error: BadRequestException: email in use. I couldnt figure out how to solve problem
You can use isExists method instead of findOne.
Also you can add extra check for your findWithMail method. Check the length of db request result. Like if (dbReqResult.length === 0) return false; else true
please put your attention on your mocked user service, especially on findWithEmail function, this part
beforeEach(async() => {
fakeUsersService = {
findWithMail:() => Promise.resolve([]),
create:(email:string,password:string) =>
Promise.resolve({email,password} as User),
}
...
try to resolve the promise to be null not [] (empty array) or change your if(users) on your auth.service to be if(users.length > 0), why? it because empty array means to be thruthy value so when run through this process on your auth.service
if(email) {
const users = await this.userService.findWithMail(email);
// on this part below
if(users) {
throw new BadRequestException('email in use');
}
}
the 'users' executed to be truthy value so it will invoke the error. I hope my explanation will help you, thank you

Code runs after catch statement catches and error and returns in react native firebase

I am having issue whenever I catch an error and return from a function, by code after the catch block still runs. Here is my two functions that I use:
usernameTaken: async (username) => {
const user = await firebase.firestore().collection("uniqueUsers").doc(username).get();
if (user.exists) {
alert("Username is taken. Try again with another username.");
throw new Error('Username is taken. Try again with another username.');
}
},
changeUsername: async (currentUsername, newUsername) => {
try {
var user = Firebase.getCurrentUser();
Firebase.usernameTaken(newUsername);
} catch (err) {
alert(err.message);
return;
}
await db.collection('uniqueUsers').doc(currentUsername).delete();
await db.collection("users").doc(user.uid).update({username: newUsername});
await db.collection("uniqueUsers").doc(newUsername).set({username: newUsername});
alert("Congratulations! You have successfully updated your username.");
}
I would greatly appreciate any help for this problem, as I have been struggling with it for over 2 days now and can't seem to find a solution.
In your original code, the usernameTaken() promise is floating, because you didn't use await. Because it was floating, your catch() handler will never catch it's error.
changeUsername: async (currentUsername, newUsername) => {
try {
const user = Firebase.getCurrentUser();
/* here -> */ await Firebase.usernameTaken(newUsername);
} catch (err) {
alert(err.message);
return;
}
/* ... other stuff ... */
}
Additional Points
usernameTaken should return a boolean
You should change usernameTaken to return a boolean. This is arguably better rather than using alert() (which blocks execution of your code) or throwing an error.
usernameTaken: async (username) => {
const usernameDoc = await firebase.firestore().collection("uniqueUsers").doc(username).get();
return usernameDoc.exists; // return a boolean whether the doc exists
}
Securely claim and release usernames
Based on your current code, you have no protections for someone coming along and just deleting any usernames in your database or claiming a username that was taken between the time you last checked it's availability and when you call set() for the new username. You should secure your database so that a user can only write to a username they own.
Add the owner's ID to the document:
"/uniqueUsers/{username}": {
username: "username",
uid: "someUserId"
}
This then allows you to lock edits/deletions to the user who owns that username.
service cloud.firestore {
match /databases/{database}/documents {
match /uniqueUsers/{username} {
// new docs must have { username: username, uid: currentUser.uid }
allow create: if request.auth != null
&& request.resource.data.username == username
&& request.resource.data.uid == request.auth.uid
&& request.resource.data.keys().hasOnly(["uid", "username"]);
// any logged in user can get this doc
allow read: if request.auth != null;
// only the linked user can delete this doc
allow delete: if request.auth != null
&& request.auth.uid == resource.data.uid;
// only the linked user can edit this doc, as long as username and uid are the same
allow update: if request.auth != null
&& request.auth.uid == resource.data.uid
&& request.resource.data.diff(resource.data).unchangedKeys().hasAll(["uid", "username"]) // make sure username and uid are unchanged
&& request.resource.data.diff(resource.data).changedKeys().size() == 0; // make sure no other data is added
}
}
}
Atomically update your database
You are modifying your database in a way that could corrupt it. You could delete the old username, then fail to update your current username which would mean that you never link your new username. To fix this, you should use a batched write to apply all these changes together. If any one were to fail, nothing is changed.
await db.collection("uniqueUsers").doc(currentUsername).delete();
await db.collection("users").doc(user.uid).update({username: newUsername});
await db.collection("uniqueUsers").doc(newUsername).set({username: newUsername});
becomes
const db = firebase.firestore();
const batch = db.batch();
batch.delete(db.collection("uniqueUsers").doc(currentUsername));
batch.update(db.collection("users").doc(user.uid), { username: newUsername });
batch.set(db.collection("uniqueUsers").doc(newUsername), { username: newUsername });
await batch.commit();
Usernames should be case-insensitive
Your current usernames are case-sensitive which is not recommended if you expect your users to type/write out their profile's URL. Consider how "example.com/MYUSERNAME", "example.com/myUsername" and "example.com/myusername" would all be different users. If someone scribbled out their username on a piece of paper, you'd want all of those to go to the same user's profile.
usernameTaken: async (username) => {
const usernameDoc = await firebase.firestore().collection("uniqueUsers").doc(username.toLowerCase()).get();
return usernameDoc.exists; // return a boolean whether the doc exists
},
changeUsername: async (currentUsername, newUsername) => {
const lowerCurrentUsername = currentUsername.toLowerCase();
const lowerNewUsername = newUsername.toLowerCase();
/* ... */
return lowerNewUsername; // return the new username to show success
}
The result
Combining this all together, gives:
usernameTaken: async (username) => {
const usernameDoc = await firebase.firestore().collection("uniqueUsers").doc(username).get();
return usernameDoc.exists; // return a boolean
},
changeUsername: async (currentUsername, newUsername) => {
const user = Firebase.getCurrentUser();
if (user === null) {
throw new Error("You must be signed in first!");
}
const taken = await Firebase.usernameTaken(newUsername);
if (taken) {
throw new Error("Sorry, that username is taken.");
}
const lowerCurrentUsername = currentUsername.toLowerCase();
const lowerNewUsername = newUsername.toLowerCase();
const db = firebase.firestore();
const batch = db.batch();
batch.delete(db.collection("uniqueUsers").doc(lowerCurrentUsername));
batch.update(db.collection("users").doc(user.uid), {
username: lowerNewUsername
});
batch.set(db.collection("uniqueUsers").doc(lowerNewUsername), {
username: lowerNewUsername,
uid: user.uid
});
await batch.commit();
return lowerNewUsername;
}
// elsewhere in your code
changeUsername("olduser", "newuser")
.then(
(username) => {
alert("Your username was successfully changed to #" + username + "!");
},
(error) => {
console.error(error);
alert("We couldn't update your username!");
}
);
Note: If you are using all of the above recommendations (like the security rules), one of the expected ways batch.commit() will fail is if someone takes the username before the current user. If you get a permissions error, assume that someone took the username before you.
Try this check if your values are empty or not defined throw some error in try block e.g.
cosnt user = Firebase.getCurrentUser();
const name = Firebase.usernameTaken(newUsername);
// throwing error
if(name == "") throw "is empty";
await db.collection('uniqueUsers').doc(currentUsername).delete();

Issues with scope in try/catch while using async/await

My issue is that (seemingly) things are going out of scope, or the scope is being polluted when I enter my catch block in the function below:
export const getOne = model => async (req, res, next) => {
let id = req.params.id
let userId = req.user
try {
let item = await model.findOne({ _id: id, createdBy: userId }).exec()
if (!item) {
throw new Error('Item not found!')
} else {
res.status(200).json({ data: item }) // works perfectly
}
} catch (e) {
res.status(400).json({ error: e }) // TypeError: res.status(...).json is not a function
// also TypeError: next is not a function
// next(e)
}
}
Interestingly enough, using res.status(...).end() in the catch block works just fine, but it bothers me that I am not able to send any detail back with the response. According to the Express Documentation for res.send() and res.json I should be able to chain off of .status(), which, also interestingly enough, works just fine in the try statement above if things are successful - res.status(200).json(...) works perfectly.
Also, I tried abstracting the error handling to middleware, as suggested on the Express documentation, and through closures, I should still have access to next in the catch statement, right? Why is that coming back as not a function?
Why does res.status(...).json(...) work in my try but not catch block?
Why is next no longer a function in the catch block?
Thanks in advance!
Edit
This is failing in unit tests, the following code produces the errors described above:
describe('getOne', async () => {
// this test passes
test('finds by authenticated user and id', async () => {
expect.assertions(2)
const user = mongoose.Types.ObjectId()
const list = await List.create({ name: 'list', createdBy: user })
const req = {
params: {
id: list._id
},
user: {
_id: user
}
}
const res = {
status(status) {
expect(status).toBe(200)
return this
},
json(result) {
expect(result.data._id.toString()).toBe(list._id.toString())
}
}
await getOne(List)(req, res)
})
// this test fails
test('400 if no doc was found', async () => {
expect.assertions(2)
const user = mongoose.Types.ObjectId()
const req = {
params: {
id: mongoose.Types.ObjectId()
},
user: {
_id: user
}
}
const res = {
status(status) {
expect(status).toBe(400)
return this
},
end() {
expect(true).toBe(true)
}
}
await getOne(List)(req, res)
})
})
Why does res.status(...).json(...) work in my try but not catch block?
Seems like you're passing a non-express object that only has status & end methods when running using the unit testing. That's why it fails to find the json method

How to get data passed to mongoose schema constructor

I am testing my application and need to verify that mongoose schema constructor is called with correct data.
let's say I do this:
const UserData = new User(user)
console.log(UserData.contructor.args)
I would expect log of the user object.
Probably the data is passed to constructor of mongoose schema?
Can some one please advise me how to access it?
Here is specific case I am trying to solve.
export const signup = async (req, res, next) => {
try {
//if user object is missing return error
if (!req.body.user)
return next(boom.unauthorized('No user data received.'))
//get user data
const user = req.body.user,
{ auth: { local: { password, password_2 } } } = user
//check if both passwords match
if (password !== password_2)
return next(boom.unauthorized('Passwords do not match.'))
//check if password is valid
if (!Password.validate(password)) {
const errorData = Password.validate(password, { list: true })
return next(boom.notAcceptable('Invalid password.', errorData))
}
//creates new mongo user
const UserData = new User(user)
//sets user password hash
UserData.setPassword(password)
//saves user to database
await UserData.save()
//returns new users authorization data
return res.json({ user: UserData.toAuthJSON() })
} catch(err) {
//if mongo validation error return callback with error
if(err.name === 'ValidationError') {
return next(boom.unauthorized(err.message))
}
// all other server errors
return next(boom.badImplementation('Something went wrong', err))
}
}
And part of test:
describe('Success', () => {
it('Should create new instance of User with request data', async () => {
const req = { body },
res = {},
local = { password: '1aaaBB', password_2: '1aaaBB'},
constructorStub = sandbox.stub(User.prototype, 'constructor')
req.body.user.auth.local = {...local}
await signup(req, res, next)
expect(constructorStub.calledOnceWith({...req.body.user})).to.be.true
})
})
EDIT: I can verify that is is called with expect(constructorStub.calledOnce).to.be.true
Just can't get to verify data passed.
Edit: After talking for some time sounds like what you need is to validate that you are creating a new user correctly.
My suggestion here is to create a new function createUserFromRequest that would take in request and return a new User.
You can then test this function easily as it's pure (no side effects, just input and output).
At this point, most of the logic in your handler is in this function so it would be probably not worth testing the handler itself, but you could still do it, for example by mocking the function above.
Example:
function createUserFromRequest(request) {
//get user data
const user = req.body.user,
{ auth: { local: { password, password_2 } } } = user
//check if both passwords match
if (password !== password_2)
return next(boom.unauthorized('Passwords do not match.'))
//check if password is valid
if (!Password.validate(password)) {
const errorData = Password.validate(password, { list: true })
return next(boom.notAcceptable('Invalid password.', errorData))
}
//creates new mongo user
const UserData = new User(user)
//sets user password hash
UserData.setPassword(password)
return UserData;
}
Please note: stubs and mocking are usually a code smell: there could either be a better way of testing, or it could be a sign of a need to refactor the code into something more easily testable. They usually point to tightly coupled or cluttered code.
Check out this great article on that topic: https://medium.com/javascript-scene/mocking-is-a-code-smell-944a70c90a6a

How to test Meteor.users with Mocha

I have a function as follows:
if(Meteor.isServer) {
Meteor.methods({
addUser: function (newUser) {
check(newUser, { email: String, password: String });
userId = Accounts.createUser(newUser);
return userId;
},
getUser: function (userID) {
check(userID, String);
return Meteor.users.find({_id: userID}).fetch();
}
});
And I am trying to test this function using Mocha:
if (Meteor.isServer) {
let testUser;
describe('Users', () => {
it("Add User", (done) => {
testUser = {email: 'test#test.test', password: 'test'};
try {
testUser._id = Meteor.call('addUser', testUser);
console.log(Accounts.users.find({_id: testUser._id}).fetch());
done();
} catch (err) {
assert.fail();
}
});
it("Get user", (done) => {
try {
Meteor.call('getUser', testUser._id);
done();
} catch (err) {
assert.fail();
}
});
});
And I know that the meteor call with 'addUser' works, because the console.log after that returns the user that I just made and the first test passes when I run it with "meteor test --driver-package practicalmeteor:mocha"
But then I come to the second testing part, where I try to get the user with the meteor call 'getUser', but then I get stuck:
'Cannot call method 'find' of undefined'
Now I know that the difference is that I use 'Meteor.users' instead of 'Account.users' to find the user, but I am totally in the dark what the difference is between these two. Should I replace all the Meteor.users method calls with Accounts.user method calls or not? How would you test this?
I just stumbled on this post, since I have dealt with the same issue some hours ago.
As I can see in your code, your testUser is defined in your first unit ( it("Add User"...){}). I advise you not to use the value from the first unit in the second unit.
You may rather use beforeEach and afterEach to have a clean setup for each unit and then create a new user in the second test unit. You should also clean up your db after each unit:
describe('Users', () => {
// use this for each test
let testUser;
beforeEach(() => {
// let's always create a new clean testUser object
testUser = {email: 'test#test.test', password: 'test'};
});
afterEach(() => {
// Remove the user to keep our db clean
if (testUser._id)
Meteor.users.remove(testUser._id);
});
it("Add User", (done) => {
testUser._id = Meteor.call('addUser', testUser);
const users = Meteor.users.find({_id: testUser._id}).fetch();
const user = users[0];
assert.isNotNull(user);
assert.isDefined(user);
assert.equal(user._id, testUser._id);
done();
});
it("Get user", (done) => {
// get a new id from our previously tested
// (and assume to be working) function
testUser._id = Meteor.call('addUser', testUser);
const user = Meteor.call('getUser', testUser._id);
assert.isNotNull(user);
assert.isDefined(user);
assert.equal(user._id, testUser._id);
done();
});
});
I also found, that your 'getUser' method returns an array, so I changed it to:
getUser: function (userID) {
check(userID, String);
const users = Meteor.users.find({_id: userID}).fetch();
return users[0];
}
All tested and running.

Categories

Resources