Nock isn't matching HTTPS call inside middleware? - node.js

I have a piece of middleware that performs authentication with a third-party service via request. I do this request with superagent. Obviously I want to mock this in my tests since it slows them down quite a lot and also is dependant on the third-parties server.
When using nock, it doesn't seem to find the request at all. I even tried using the recorder and it only picks up the actual requests of my local endpoints. (Although it uses an unfamiliar IP and port?).
The request inside my middleware;
export default async (req, res, next) => {
const user = await superagent
.get(`https://example.com/session/`)
.query({ session })
.set('Api-Key', '1234');
}
My Nock Instance;
nock('https://example.com/session/')
.persist()
.get('/session/')
.reply(200, {
success: true,
username: 'testuser',
})
.log(console.log);

You have 2 issues here.
First off, you define twice /session/:
nock('https://example.com/session/')
.get('/session/')
Choose:
nock('https://example.com').get('/session/')
nock('https://example.com/session/').get('/')
Second issue, you're adding a query string to your call (.query({ session })), but you don't tell that to Nock using .query(true).
In the end, you should have something like:
nock('https://example.com/session/')
.persist()
.get('/') // rewrote here
.query(true) // added here
.reply(200, {
success: true,
username: 'testuser',
})
.log(console.log);

Related

Cypress: cy.task vs cy.request for interacting with backend by API?

I want to set the app in a preconditioned state (before testing) by making HTTP requests to the backend by exposed API. I need to make a series of HTTP requests like creating an account, uploading a file, executing some processes, etc. Some of the requests return some data in the response body which I need to pass to the next requests. I need the requests to be run sequentially so one request needs to be finished in order to execute the next one.
Should I use cy.request() for that or maybe cy.task() and an external tool like axios to make HTTP requests?
I found an article: https://spin.atomicobject.com/2021/07/30/cypress-tasks-vs-commands/
where there is written:
If you need to run a promise or interact with your backend, go with a task. Remember: Commands are not promises. If you are interacting with the DOM and making assertions, go with a command.
So according to it, it's advised to go with cy.task when interacting with backend. But why?
What are the benefits of making API calls using cy.tasks instead of making them by cy.request?
I tried to approach this by cy.task() and axios, which let me assign response data to variables and pass them to the next requests but it made debugging the HTTP requests harder because the requests are being executed in a separate node process outside of the cypress browser context so I need to console.log() the requests to see if they are correct or to see the response message.
The article is only partially correct.
You cannot await cy.request(), but it's not necessary to use cy.task() to handle axios async/await.
Try this in your test:
import axios from 'axios';
it('handles axios calls in the browser', async () => {
const result = await axios.request({
method: 'GET',
url: 'https://jsonplaceholder.typicode.com/todos/1'
});
const expectedObject = {userId: 1, id: 1, title: 'delectus aut autem', completed: false}
expect(result.status).to.eq(200);
expect(result.data).to.deep.eq(expectedObject) // ✅ passes
})
Intercepting
Even better, you can remove the async/await and use the cy.intercept() API with axios calls
import axios from 'axios';
it('uses cy.intercept() with axios calls', () => {
cy.intercept('https://jsonplaceholder.typicode.com/todos/1').as('fetch')
axios.request({
method: 'GET',
url: 'https://jsonplaceholder.typicode.com/todos/1'
});
const expectedObject = {userId: 1, id: 1, title: 'delectus aut autem', completed: false}
cy.wait('#fetch')
.its('response.body')
.should('deep.eq', expectedObject) // ✅ passes
})
which is something you cannot do with cy.request()
it('intercepts cy.request() calls', () => {
cy.intercept('https://jsonplaceholder.typicode.com/todos/1').as('fetch')
cy.request({
method: 'GET',
url: 'https://jsonplaceholder.typicode.com/todos/1'
});
const expectedObject = {userId: 1, id: 1, title: 'delectus aut autem', completed: false}
cy.wait('#fetch')
.its('response.body')
.should('deep.eq', expectedObject) // ❌ fails
})
In Summary
cy.request() is designed not to interact with the cy.intercept() API, so you can issue requests from the test and not interfere with code that should only be monitoring requests issuing from the app under test.
If you use axios to make the requests, you run the risk of unexpected results from your cy.intercept() network listeners.

Authentication with passport.js and HttpOnly cookies with supertest is not working

and thanks for the tool 😊 .
I'm working on a Nestjs (v8.x) application relying on Passport.js with a JWT strategy through HttpOnly cookies. For testing the different endpoints of my app, I'm using supertest (v6.1.x).
In order to reach certain endpoints, I need to get an authentication cookie set after submitting credentials. When playing with the UI, everything is working correctly and I can get the data I want. However, when trying to create an automated test based on that, it does not work (at all).
My tests looks like following:
it('gives the current user when the token is valid', async () => {
const cookieToken = await authenticate(app);
const { body: user } = await request(app.getHttpServer())
.get('/users/me')
.set('Cookie', cookieToken);
expect(user.email).toEqual('joe.doe#gmail.com');
expect(user.fullname).toEqual('Joe Doe');
expect(user.uuid).toBeTruthy();
expect(user.password).toBeFalsy();
});
The authenticate function is a helper that looks like:
export const authenticate = async (
app: INestApplication,
username = 'joe.doe#gmail.com',
password = 'password',
) => {
const res = await request(app.getHttpServer()).post('/auth/login').send({
username,
password,
});
// This cookie resolves correctly
const cookieWithToken = res.headers['set-cookie'];
return cookieWithToken;
};
This tests fails and I got a 401 response from my JWT strategy. The code for this strategy looks like:
#Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromExtractors([
(request: Request) => {
let data = request?.cookies?.['auth-cookie'];
// In the test scenario, request.cookies is always undefined
if (!data) {
throw new UnauthorizedException();
}
return data.access_token;
},
]),
ignoreExpiration: false,
secretOrKey: jwtConstants.secret,
});
}
async validate(payload: any) {
// Not important for this issue
}
}
During the test run, request?.cookies is undefined and never set, thus throws the 401 error to the caller.
I don't know what is going but it looks like something is wrong on the way I set it.
I've tried the following approaches:
Adding withCredentials (cuz, who knows...)
Nesting the second call inside the callback resolving the login call in case there's a shared context between the requests
Relying on the same agent to make both the calls instead of directly calling superagent
But still, without success :(
Do you have any ideas?

NodeJS API creates two sessions because of "CORS preflight request"

I am currently developing an API in NodeJS and a WebClient in VueJS.
I want to create a simple login/logout mechanism. The Webclient should send the request to the API and the API should handle the sessions of the different users and serving the data from its mongoDB.
Recently I came across a strange problem. When I want to login via WebClient, the browser shows me that it sends two different headers to the API. One "OPTIONS" header and one "POST" header. The POST header is sent due to my POST-Request (WebClient), which is clear. Due to Mozillas explenation I also understand the OPTION header part since the browser wants to know if the API's CORS-configuration has been configured contrary for the WebClient or not (or something like this).
But the problem now is the following:
Due to the two different header-methods, my API creates two session-IDs with just one login-post action (via WebClient), whereas one of these two sessions gets detached from the WebClient, unnecessarily consuming valuable space. This only happens through the WebClient. Using PostMan does not show this behaviour, only one session will be created since only one header is sent.
What I want to know is:
Since there is a reason for why the OPTIONS-header is sent, I want to know how I can prevent my API to create the second session via the WebClient.
Since this problem happened after testing my WebClient, it is clear to me that the WebClient is not configured or written properly, but I cannot tell where or how to prevent this since WebDev at this level is new to me. Like: Do I have to configure my WebClient or the API?
If more code is needed just tell me what you need and I will edit this post and attach the neede code.
//////////////////// Code:
//// API:
// src/main.js:
const corsOptions = {
origin: "http://localhost:8080",
credentials:true,
methods: "GET,HEAD,POST",
preflightContinue: false,
optionsSuccessStatus: 204
};
// src/routes/LoginRoute.js:
router.post("/login", function(req,res,next){
console.log("// Routes/Login");
if(!req.user){
console.log("---- Info: User is not logged in");
passport.authenticate("local", (err, user, info) => {
if (err) {
return next(err);
}
if (!user) {
return res.status(401).json({success:false,errors:["User not found"]});
}
req.login(user, (err) => {
if(err){
return next(err);
}
console.log("---- Info: Routing success");
return res.status(200).json({success:true});
});
})(req, res, next);
}else{
console.log("---- Info: User is already logged in");
return res.status(403).json({success:false,errors:["Already logged in"]});
}
});
//// VueJS
// src/store/index.js
actions:{
authenticate({commit},formData){
console.log("---- Info: Vuex/Store/Action/Authenticate - Trying to log in");
var url = "http://localhost:3000/api/login";
console.log(formData);
return Vue.axios.post(url,formData)
.then((res)=>{
console.log("---- Info: Vuex/Store/Action/Authenticate - Success");
commit('login',res.data);
return res.data;
})
.catch((err)=>{
console.log("---- Error: Vuex/Store/Action/Authenticate");
console.log(err);
// commit('logout');
return err;
});
}
}
//////////////////// FireFox Network Analysis:
I would believe that the cors package you apparently use would resolve this problem.
In any case this is not a problem with your frontend, it's handled by your backend and it's typical that the browser creates problems that aren't present with Postman. Postman is built to not care about browser CORS issues. You can however set a session in Postman for testing: https://blog.postman.com/sessions-faq/
Back to the problem: One approach is a middleware function for all or specific routes to filter out requests that already contain a session.
app.use((req, res, next) => {
if (req.session) {
// bypass new session creation, re-route or other solution
}
next()
})
Another and more flexible approach is to target the OPTIONS header directly. I solved a similar problem in a serverless proxy function with a request handler that targets the OPTIONS header specifically. It filters such requests out and returns an "OK signal" and generous headers to tell the browser it can go ahead with the real request, the POST.
You could try something like this as a general middleware or add it as a response to certain endpoints (code not tested, just freestyling the Express syntax here):
const optionsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type'
}
exports.reqHandler = async(req, res) => {
if (req.method === 'OPTIONS') {
return {
res.set(optionsHeaders)
res.status(200)
}
}
req.session.user = {}
req.session.user.id = uuid.v4()
// etc. ...
return res.status(200).json({
success: true,
data: req.session.user
msg: 'Session ID set'
})
}
So I figured out what to do in order to prevent the browser to send two headers to the API server.
I had to configure the axios.post() overgive a header option to the funciton:
const axiosHeaders = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
};
Vue.axios.post(url,QueryString.stringify({username:username}),axiosHeaders)
.then(..)
[...]
Setting the "Configure-Type" as "application/x-www-form-urlencoded" will do the work, but the form-data has to be wrapped with the QueryString.stringify(..) function.
With this, the browser stops to send multiple headers with one post-request and in my case, it stops to create multiple sessions on my API.
I hope this will be usefull for anyone else too.

Migrating Node JS code to Apollo server

I am setting up Apollo Server on my Node app and wondered about moving the functionality over to Apollo.
I have business logic like this:
router.post(
'/login',
(req, res, next) => {
if (!req.body.email || !req.body.password) {
return 'You must send the username and the password.';
}
Users.findOne({ email: req.body.email })
.then(user => {
bcrypt.compare(req.body.password, user.password, (err, success) => {
req.user = user;
next();
});
})
},
auth.createToken,
auth.createRefreshToken,
auth.logUserActivity,
(req, res) => {
res.status(201).send({
success: true,
authToken: req.authToken,
refreshToken: req.refreshToken
});
}
);
It follows Node router architecture where I add the found user object to req object, which passes the user to the next functions - createToken etc.. using the next() function. This was ok for my server before trying to introduce GraphQL/Apollo, but now I want all this logic to be easily accessible to the Apollo resolvers.
I often hear that people are having an easy time turning their server from REST/non-GraphQL into a GraphQL server, but at the moment it's looking like it's going to be a bit of a job to go through all the logic and separate everything in to their own functions which take parameters directly rather than using the req object.
Is this a correct assumption? Or am I missing something?
Thanks!
Migrating the code you have shown above would be a very easy task. Once you build your graphql server and create your schema, etc. Then all you need to do is create login mutation. Then your resolver would handle the logic you have shown above. Then, instead of pulling the values from from req.body they would be function parameters.
A good pattern I am currently following is creating a login method on the model itself. Then the resolver calls the method on the schema (Here is an example of a project I'm doing it on now: Login method. Then here is an example of what the resolver looks like: Resolver
Hopefully that helped!

how to debug passport

I can't for the life of me seem to figure out how to debug a passport strategy. Am I just conceptually doing something horribly wrong? I've been banging my head at this for about 10 hours now, and haven't gotten a single bit closer.
This is my first time using passport. In this particular scenario, I'm using passport-jwt. I know that the object keys aren't right, but I'm just trying to trace the path through the server using console.log() so I understand how things work. I'm not even reaching the passport.use( new JwtStrategy(..)).
I've left out unnecessary code. The connection to my mongodb is fine and my mongoose schema's are fine.
I'm testing using a test route server.get('/fakelogin', ...) that does a request-promise POST to /api/login. I've also tried using a curl -X POST and modifying the post route to url query parameter. I just constantly get an "Unauthorized" error without the passport strategy code console.log ever firing.
Server
var server = express();
server.use(passport.initialize());
let opts = {
jwtFromRequest: ExtractJwt.fromBodyField('token'),
secretOrKey: config.apiKey,
algorithms: [ 'HS256', 'HS384' ],
ignoreExpiration: true
};
passport.use(new JwtStrategy(opts, function( jwt_payload, done ) {
// mongoose users query against JWT content
return done(null, true); // EDIT: test to finish flow
}));
server.use('/api', routes);
server.listen(8000);
Routes
let routes = express.Router();
routes.post('/securedroute', passport.authenticate('jwt', { session: false }),
( req, res ) => {
res.send('success');
}
);
routes.get('/testsecure', ( req, res ) => { // EDIT: mock request with JWT
let options = {
method: 'POST',
uri: 'http://localhost:8000/api/authentication/securedroute',
body: {
token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiZW1haWwiOiJhQGEuY29tIiwiYWRtaW4iOnRydWV9.afjyUmC81heavGBk7l9g7gAF5E6_eZeYSeE7FNmksp8'
},
json: true
};
rp(options)
.then( ( info ) => res.json({ info }) )
.catch( ( err ) => res.json({ err }) );
});
export default routes;
Made a couple edits to the code above to finish the flow.
Totally understand that this is super n00b, but hopefully it'll help a beginner trying to understand auth.
So completely missed the fact that you need to send a JWT in the request. The JWT used in the request needs to use the same secret as what you defined in your passport-jwt strategy opts.secretOrKey. If they don't match, you will get an unauthorized and never reach the passport.use strategy block.
Generated a HMAC
http://www.freeformatter.com/hmac-generator.html
Created a test JWT
https://jwt.io/#debugger
Greate guide, i eventually found
http://jonathanmh.com/express-passport-json-web-token-jwt-authentication-beginners/

Resources