Jest spyOn.mockImplementation calls actual method - node.js

I have a middleware function:
// middlewares/auth.middleware.ts
export async function verifyUser(
req: Request,
res: Response,
next: NextFunction
): Promise<void> {
try {
// verify token logic
req.user = user; // I add the verified user details to req
next();
} catch (error) {
console.log(error);
res
.status(401)
.json({ error: 'You are not authorized to make this request' });
}
}
In my test, I'm doing the following:
import * as middleware from '../../middlewares/auth.middleware';
const spy = jest.spyOn(middleware, 'verifyUser');
describe('POST /api/users', () => {
test('when the user has ONLY been created in external auth', async () => {
spy.mockImplementation(
jest.fn(async (req, _res, next) => {
req.user = {
id: '1',
email: 'user1#email.com',
fullName: null,
};
next();
})
);
const res = await request(app)
.post('/api/users')
.send({ fullName: 'Jimmy Neutron' });
expect(res.status).toEqual(200);
spy.mockRestore();
});
});
However this calls the original verifyUser function, instead of the what I added.
But, if I add this to the test file then my test passes:
jest.mock('../../middlewares/auth.middleware', () => ({
verifyUser: jest.fn((req, _res, next) => {
req.user = {
id: '1',
email: 'user1#email.com',
fullName: null
};
next();
}),
}));
How can I make my code work so that I can change the implementation per test?

If you look at the jest.spyOn docs you can see this:
Note: By default, jest.spyOn also calls the spied method. This is different behavior from most other test libraries. If you want to overwrite the original function, you can use jest.spyOn(object, methodName).mockImplementation(() => customImplementation) or object[methodName] = jest.fn(() => customImplementation);
So this explains why adding a mock makes your tests pass.
Depending on what version of jest you are running, there is a very useful helper method that you can now use, jest.mocked:
jest.mocked<T>(item: T, deep = false)
In your case you could:
Add a mockImplementation to your spyOn:
const spy = jest.spyOn(middleware, 'verifyUser').mockImplementation(jest.fn((req, _res, next) => {
req.user = {
id: '1',
email: 'user1#email.com',
fullName: null
};
next();
});
// ...
Remove the spyOn entirely and refactor as follows:
import * as middleware from '../../middlewares/auth.middleware';
jest.mock('../../middlewares/auth.middleware');
const mockMiddleware = jest.mocked(middleware);
const mockVerifyUser = jest.fn(async (req, _res, next) => {
req.user = {
id: '1',
email: 'user1#email.com',
fullName: null,
};
next();
});
describe('POST /api/users', () => {
test('when the user has ONLY been created in external auth', async () => {
mockMiddleware.mockImplementation(() => ({
verifyUser: mockVerifyUser,
});
// ...
But the two approaches are equivalent.

Related

Correct Async/Await usage with Axios

I'm working with axios to post user responses to a database. I'm using this set up shown below to handle many posts back to back. I'm wanting to make sure that this is the correct set up to avoid backing up requests.
Is this the correct way to use async and await when using Axios?
// Frontend React Code
// Posting conclusionInput to Mongodb
const postConclusion = async () => {
await axios({
method: "POST",
data: {
conclusion: conclusionInput,
},
withCredentials: true,
url: "http://localhost:4000/conclusion",
}).then((res) => console.log(res));
};
//Backend Node / Express code
app.post("/conclusion", (req, res) => {
console.log("Attempting to post the conclusion");
User.findOne({ username: req.user.username }, async (err, user) => {
if (err) throw err;
if (user) {
(user.conclusion = req.body.conclusion), await user.save();
res.send();
}
});
});
Frontend
In an async function use await and try/catch. Any .then calls can be rolled out into const x = await y variables.
Return values from promise functions, in case you want to use them.
const postConclusion = async () => {
const res = await axios({
method: "POST",
data: {
conclusion: conclusionInput,
},
withCredentials: true,
url: "http://localhost:4000/conclusion",
})
console.log(res)
return res
};
Backend
Again, if you are going with async use that API consistently.
Mongoose provides a promise API, so use that too.
app.post("/conclusion", async (req, res) => {
try {
console.log("Attempting to post the conclusion");
const user = await User.findOne({ username: req.user.username })
if (!user) {
return res.send('not found')
}
user.conclusion = req.body.conclusion
await user.save()
return res.send('saved')
}
catch (error) {
console.error(error)
return res.send('error')
}
});
When using async await, setting an await call to a variable is equal to the parameter in a .then callback
// Frontend React Code
// Posting conclusionInput to Mongodb
const postConclusion = async () => {
// Set the await call to a variable.
const res = await axios({
method: "POST",
data: {
conclusion: conclusionInput,
},
withCredentials: true,
url: "http://localhost:4000/conclusion",
})
// Now no need for .then()!! This is why async/await is so nice.
console.log(res)
};
//Backend Node / Express code
app.post("/conclusion", (req, res) => {
console.log("Attempting to post the conclusion");
User.findOne({ username: req.user.username }, async (err, user) => {
// You need to send the error to the request. Otherwise, the
// request will keep waiting for a response until it times out.
if (err) {
console.error(err)
res.status(500).send(err)
}
if (user) {
// Side note: these should all be on separate lines:
user.conclusion = req.body.conclusion
await user.save();
// You should also send a status code and a response message
res.status(200).send({status: "Success}");
}
});
});
I recommended have a folder called "services" and inside it has yours services by backend.
services/
getData.js
import axios from "axios";
export const getData = () => {
axios.post("http://localhost:4000/conclusion");
};
App.js
import { useEffect, useState } from "react";
import { getData } from "./services/getData";
export default function App() {
const [data, setData] = useState([]); // save the value of service
useEffect(() => {
try {
getData().then((res) => {
setData(res?.data);
});
} catch (e) {
console.error(e);
}
}, []); // execute once
return <div className="App">{data}</div>;
}

Returning value of function containing a Promise

I want to be informed about a user's login status. For example, if a user attempts to sign up using an existing email in the database, I want to get a return value of STATUS_DUPLICATE_EMAIL (a string). Another possibility is if a user attempts to sign in with an email with a previously logged in social media account, I want to get a return value of STATUS_SNS_EMAIL.
Code snippet:
app.post("/api/signup", (req, res) => {
const status = checkEmail(req, res);
// some sign up logic...
}
...
checkEmail = (req, res) => {
Database.findOne({
where: {
email: req.body.email
}
}).then((user) => {
if (user) {
res.status(400).send({ message: "Email already used." });
}
return STATUS_DUPLICATE_EMAIL;
}
When I try to console.log status, it is undefined, and I'm not sure why so. What is the general way to get a return value from a function like checkEmail?
The value of status is the value the function checkEmail() returns - which is currently nothing.
What you should do is return your promise (Database.findOne) from your checkEmail.
Now the value of status will be the promise your returned.
Now you should call it's method then again to get the value.
The promise is resolved but the Javascript engine reads your code before it is. Therefore, the method then of a promise returns a promise as well.
app.post("/api/signup", (req, res) => {
checkEmail(req, res).then(desired_value => {
const status = desired_value;
}
}
...
checkEmail = (req, res) => {
return Database.findOne({
where: {
email: req.body.email
}
}).then((user) => {
if (user) {
res.status(400).send({ message: "Email already used." });
}
return STATUS_DUPLICATE_EMAIL;
}
}
Alternatively
You can use async/await pattern. When declaring a function as async you can prefix promises inside it with await which will block the code execution until it's resolved.
app.post("/api/signup", async (req, res) => {
const status = await checkEmail(req, res);
}
...
checkEmail = (req, res) => {
return Database.findOne({
where: {
email: req.body.email
}
}).then((user) => {
if (user) {
res.status(400).send({ message: "Email already used." });
}
return STATUS_DUPLICATE_EMAIL;
}
}
You should return Database.findOne().
Also use async await to get the result of checkMail().
app.post("/api/signup", async (req, res) => {
const status = await checkEmail(req, res);
// some sign up logic...
}
...
app.post("/api/signup", (req, res) => {
const status = checkEmail(req, res);
// some sign up logic...
}
...
checkEmail = (req, res) => {
return Database.findOne({
where: {
email: req.body.email
}
}).then((user) => {
if (user) {
res.status(400).send({ message: "Email already used." });
}
return STATUS_DUPLICATE_EMAIL;
}
}

Simple Mock Function of Passport using Jest

I am currently unit testing all my routes, including some that are using a custom passport authentication function. I am trying to mock the passport function to test error handling, but I keep getting the error:
TypeError: _passport.default.authenticate(...) is not a function
Here is the actual code that runs in /controllers/users.js:
export const persistentLogin = (req, res, next) => {
// Authenicate the cookie sent on the req object.
passport.authenticate('jwt', { session: false }, async (authErr, user) => {
// If there is an system error, send 500 error
if (authErr) return res.sendStatus(500);
// If no user is returned, send response showing failure.
if (!user) {
return res.status(200).json({
success: 'false',
});
}
})(req, res, next);
};
Here is the testing code in /tests/controllers/users.js:
import passport from 'passport';
import { persistentLogin } from '../../controllers/users';
beforeEach(() => {
mockResponse = () => {
const response = {};
response.status = jest.fn().mockReturnValue(response);
response.json = jest.fn().mockReturnValue(response);
response.sendStatus = jest.fn().mockReturnValue(response);
response.clearCookie = jest.fn().mockReturnValue(response);
response.cookie = jest.fn().mockReturnValue(response);
return response;
};
});
/**
* persistentLogin Tests
*/
describe('Persistent Login Controller', () => {
beforeEach(() => {
req = {};
res = mockResponse();
validateLoginForm.mockClear();
bcrypt.compare.mockClear();
});
// Passport authenication error
test('Should show passport authenication error', async () => {
passport.authenticate = jest.fn((authType, options, callback) => callback('This is an error', null));
await persistentLogin(req, res);
expect(passport.authenticate).toHaveBeenCalledTimes(1);
expect(res.sendStatus).toHaveBeenCalledWith(500);
});
});
If I had to guess, I would say it has something to do with how the (req, res, next) objects are passed into the live function after the fact. But since we are just mocking the function, I am not sure if it actually needs access to those objects.
EDIT #1:
Per the comment from #jakemingolla, I am now thinking it may be because Jest is not running my app.js file which defines my custom JWT strategy.
Here is the code from the /app.js file:
import passport from 'passport';
import passportJWTStrategy from './utils/auth/passport';
app.use(passport.initialize());
passportJWTStrategy(passport);
And the code from the /utils/auth/passport.js file:
import { Strategy } from 'passport-jwt';
/**
* Verifies JWT payload
*
* #param passport The instance of passport module.
*/
export default (passport) => {
const JWTStrategy = Strategy;
// Setup Options Object
const opts = {};
opts.jwtFromRequest = req => req.cookies.jwt;
opts.secretOrKey = process.env.PASSPORT_SECRET;
passport.use(
new JWTStrategy(opts, (jwtPayload, done) => {
if (Date.now() > jwtPayload.expire_date) {
return done('jwt expired');
}
return done(null, jwtPayload);
}),
);
};
You just need a small change:
Your mock for passport.authenticate just needs to return a function:
passport.authenticate = jest.fn((authType, options, callback) => () => { callback('This is an error', null); });
In the question you mock passport.authenticate, but in this case verify function of your strategy is not called. If you want to run this function as well or mock specific strategy then try something like this:
sinon
.stub(passport._strategies.google, 'authenticate')
.callsFake(function verified() {
const self = this;
this._verify(
null,
null,
{
_json: { email: faker.internet.email() },
name: {
givenName: faker.name.firstName(),
familyName: faker.name.lastName(),
},
},
(err, user, info) => {
if (err) {
return self.error(err);
}
if (!user) {
return self.fail(info);
}
return self.success(user, info);
}
);
});
const response = await supertest(app)
.get('/google/callback?code=123');

Axios get request resolving after, therefore my variable doesnt get assigned

On my user controller , i want to send a get request to receive a json response.
When the response comes i want to assign the value to a variable calles embed, however the rendering part:
res.render('user', {
user,
title: user.name,
embed: embed.html,
});
Happens before the axis function is finished... leaving me with an empty object.
What do i have to do in order to wait for the response... and then render the template?
Note that console log 2 happens before console log 1 in this code:
exports.getUserBySlug = async (req, res, next) => {
const user = await User.findOne({ slug: req.params.slug })
let embed = {}
if (!user) return next();
axios.get(`https://soundcloud.com/oembed?format=json&url=${user.musicLink}`)
.then(response => {
embed = response.data
console.log('1: ', embed)
})
.catch(error => {
console.log(error);
})
console.log('2: ', embed)
res.render('user', {
user,
title: user.name,
embed: embed.html,
});
};
Request is asynchronous. You need to add one await more
exports.getUserBySlug = async (req, res, next) => {
const user = await User.findOne({ slug: req.params.slug })
let embed = {}
if (!user) return next();
await axios.get(`https://soundcloud.com/oembed?format=json&url=${user.musicLink}`)
.then(response => {
embed = response.data
console.log('1: ', embed)
})
.catch(error => {
console.log(error);
})
console.log('2: ', embed)
res.render('user', {
user,
title: user.name,
embed: embed.html,
});
};
Also I would recommend NOT to mix async/await with .then calls.
exports.getUserBySlug = async (req, res, next) => {
const user = await User.findOne({ slug: req.params.slug })
let embed = {}
if (!user) return next();
try {
const response = await axios.get(`https://soundcloud.com/oembed?format=json&url=${user.musicLink}`)
embed = response.data
} catch (e) {
console.error(e)
}
res.render('user', {
user,
title: user.name,
embed: embed.html,
});
};

Problem with async/await and http client requests

Not sure what I'm missing here but the console.log() line prints "Promise { }" instead of the JSON body from the response.
I believe that I'm doing something wrong with async/await.
My code (Express):
async function axiosPost(url, payload) {
try {
const res = await axios.post(url, payload);
const data = await res.data;
return data;
} catch (error) {
console.error(error);
}
}
app.get('/data', (req, res) => {
data = axiosPost('http://localhost:8080', {
userpass: 'XXX',
method: 'getdata'
});
console.log(data)
res.status(200).send({
message: data
})
});
Any help is appreciated.
replace your router with this. you were not using await while making an API call. Hope it helps.
app.get('/data', async (req, res) => {
let data = await axiosPost('http://localhost:8080', {
userpass: 'XXX',
method: 'getdata'
});
console.log(data)
res.status(200).send({
message: data
})
});
You get that result because you didn't resolve the call to axiosPost() which is asynchronous. This can be solved in 2 ways, one by appending .then() to the axiosPost() call or simply awaiting it using the await keyword. See below:
async function axiosPost(url, payload) {
try {
const res = await axios.post(url, payload);
const data = await res.data; // this is not required but you can leave as is
return data;
} catch (error) {
console.error(error);
}
}
// I converted the callback to an async function and
// also awaited the result from the call to axiosPost(),
// since that is an async function
app.get('/data', async (req, res) => {
data = await axiosPost('http://localhost:8080', {
userpass: 'XXX',
method: 'getdata'
});
console.log(data)
res.status(200).send({
message: data
})
});
// OR using `then()`
app.get('/data', (req, res) => {
axiosPost('http://localhost:8080', {
userpass: 'XXX',
method: 'getdata'
}).then((data) => {
console.log(data);
res.status(200).send({
message: data
});
});
})

Resources