cypress testing - server sent events mocking - cypress

I am trying to setup a E2E cypress test
And for the same, trying(but FAILED) to get the events from the SSE connection(mocked) and for the same emitting the push event before setting the SSE connection
Q: Can you please help to sort this out, as I might be doing the whole thing wrong or missing something
Note: as per this PR, cypress supports SSE - https://github.com/cypress-io/cypress/pull/2054
Not able to find any reference on cypress for SSE support - https://docs.cypress.io/api/commands/route.html
const EEmitter = new EventEmitter();
cy.route({
method: 'GET',
url: `**/documentprocessing/startprocess`,
status:200,
response: {
"uniqueId": "abcd12345677",
},
})
.as(`startprocess`)
.route({
method: 'GET',
status:200,
url: '**/documentprocessing/getSSEStatus/**',
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
},
onResponse: () => {
EEmitter.on('push', function(event, data) {
response.write(
'event: ' +
String(event) +
'data: ' +
String(data) +,
);
});
}
})
.as(`sseStatus`);
In the below code, after 1st API call(#startprocess), emitting push events
Then trying to get push events in response (in #sseStatus call above)
cy.route(`#startprocess`);
setTimeout(function() {
EEmitter.emit('push','message', { 'uniqueId':'abcd12345677' ,'uploadStatus':'Started'});
}, 1000);
setTimeout(function() {
EEmitter.emit('push','message', { 'uniqueId':'abcd12345677' ,'uploadStatus':'Complete'});
}, 3000);
cy.wait(3000)
cy.wait(`#sseStatus`);

I had a similar issue where I needed to mock a api call with text/event-stream as the content type.
I have mocked api calls using json fixtures in the past easy stuff you just do something like
cy.intercept('GET', '/endpoint?*', { fixture: 'folder/my-json.json' }).as('my-api-call');
but with text/event-stream I had to do it a little different (keep in mind this is what I did, it doesn't mean it is the best way to do it, since I couldn't find anything in the oficial documentation)
import json from 'path/to/myjson.json'
cy.intercept('GET', '/endpoint*', (req) =>
req.reply(`data: ${JSON.stringify(json)} \n\n`,
{
'content-type': 'text/event-stream'
}
)).as('my-api-call');

Related

How to return HTTP response body from Cypress custom command?

I am trying to write a custom Cypress command that sends a POST request to an endpoint, & I then want to store the response body in my test.
Here is what the response body looks like in Postman:
Here is my custom command in cypress/support/commands.js, for simplicity, I've removed the request body values:
Cypress.Commands.add('createStudent', (email) => {
cy.request({
method: `POST`,
url: `myUrl`,
body: {}
}).then((resp) => {
return resp
});
});
Here is the code in my spec file:
let response = cy.createStudent(email);
cy.log(response)
However, when I run the code I get back the below object rather than the response body:
Can someone please tell me where I am going wrong, & what changes are required to return the actual HTTP response body?
If you look at the console message, there's a type $Chainer shown which is a wrapper object around the result you actually want (response).
The Chainer is fundamental to Cypress being able to retry queries that fail initially but may succeed within a timeout period (usually 4 seconds).
But it means you can't use the return value. Instead you need to "unwrap" the value using .then().
Cypress.Commands.add('createStudent', (email) => {
cy.request({
method: 'POST',
url: 'myUrl',
body: {...}
})
// The response is put on the "chain" upon exit of the custom command
// You need nothing else here to get the raw response
})
cy.createStudent().then(response => {
cy.log(response)
});
You can add a step to extract details from the response, like
Cypress.Commands.add('createStudent', (email) => {
cy.request({
method: 'POST',
url: 'myUrl',
body: {...}
})
.then(response => {
expect(response.success).to.eq(true) // check expected response is good
return response.body.id // pass on just the id
})
})
cy.createStudent().then(id => {
cy.log(id)
});
If you'll only ever be using the value in a Cypress chain, you could simply alias the command.
Cypress.Commands.add('createStudent', (email) => {
cy.request({
method: `POST`,
url: `myUrl`,
body: {}
}).as('student');
});
...
cy.createStudent();
cy.get('#student').then((response) => {
cy.log(response.body) // assuming you'd want to log the response body.
});
// OR
cy.get('#student').its('body').should('eq', { foo: 'bar' });
// the above example doesn't work with logging, but I'm guessing you don't _just_ want to log the response
If you may need the variable at other times outside of a Cypress chain, you could always stash the variable in Cypress.env().
Cypress.Commands.add('createStudent', (email) => {
cy.request({
method: `POST`,
url: `myUrl`,
body: {}
}).then((res) => {
Cypress.env('student', res);
});
});
...
cy.createStudent().then(() => {
cy.get('foo').should('have.text', Cypress.env('student').body.foo);
});
// key point is referencing the entire response by `Cypress.env('student')`

How can I stop external js script from stopping my fetch POST request?

My app generates a custom product page on a Shopify store. I use Vue3 for the frontend. There are other apps running js on the page, e.g. live chat, push notification pop-up, GDPR cookie bar, etc. They are injected by the platform and I can't remove them. (Btw, these js are minified and hard to read)
My app has an add bundle to cart button on the floating footer to send a POST request to my server with Fetch API. But it's blocked by these irrelevant apps. I think these apps are monitoring if a POST / GET request is sent. They assume they are working on standard product pages but not custom one like mine.
I tried to implement a block list with yett. But this passive way is not good enough. It's just a fix after the issue happens. Any way I can protect my fetch request without interfering by other js scripts?
let request = new Request('/create_new_product/', {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
let vm1 = this;
fetch(request)
.then(response => response.json())
.then(data => {
console.log('Success creating variant:', data);
console.log('variant_id:', data.variant_id);
// stopped here by other apps :-(
if (data.variant_id) {
vm1.addNewVariantToCart(this.variants, data.variant_id);
vm1.$emit('clearall');
setTimeout(function(){ vm1.isLoading = false; }, 2000);
}
else if (data.Error) {
alert(data.Error);
vm1.isLoading = false;
}
})
.catch((error) => {
console.error('Error:', error);
vm1.isLoading = false;
});

Tradier API websockets stream node.js example is not receiving any events

I have the following nodejs code per this and this:
const WebSocket = require('ws');
const ws = new WebSocket('wss://ws.tradier.com/v1/markets/events');
request({
method: 'post',
url: 'https://api.tradier.com/v1/markets/events/session',
form: {
},
headers: {
'Authorization': 'Bearer MY_API_KEY_NOT_SHOWN',
'Accept': 'application/json'
}
}, (error, response, body) => {
console.log(response.statusCode);
console.log(body);
let data = JSON.parse(body)
let sessionId = data.stream.sessionid
streamPrice(sessionId)
});
function streamPrice(sessionId){
console.log(sessionId)
ws.on('open', function open() {
console.log('Connected, sending subscription commands...');
ws.send(`{"symbols": ["TSLA"], "sessionid": "${sessionId}", "linebreak": true}`);
});
ws.on('message', function incoming(data) {
console.log(data);
});
ws.on('error', function error(data) {
console.log(data);
});
}
I get a 200 OK back from the API request to create the web sockets session, and I have a valid session ID:
200
{"stream":{"url":"https:\/\/stream.tradier.com\/v1\/markets\/events","sessionid":"6ba4158d-8ff8-46c3-b005-***********"}}
6ba4158d-8ff8-46c3-b005-***********
However, the ws.on() events never fire. I am not getting any errors. The session does close after a period of time, presumably due to inactivity. But it's not inactivity on my code's part...
Is there something wrong in my code / something I'm missing to make this work?
I was able to identify the issue myself. The problem is I was opening the websocket too early.
I moved the following line inside of streamingPrice scope instead of the global scope to resolve.
const ws = new WebSocket('wss://ws.tradier.com/v1/markets/events');

Mock Graphql server with multiple stubs in Cypress

Problem:
I’m using cypress with angular and apollo graphQl. I’m trying to mock the graph server so I write my tests using custom responses. The issue here is that all graph calls go on a single endpoint and that cypress doesn’t have default full network support yet to distinguish between these calls.
An example scenario would be:
access /accounts/account123
when the api is hit two graph calls are sent out - a query getAccountDetails and another one with getVehicles
Tried:
Using one stub of the graph endpoint per test. Not working as it stubs with the same stub all calls.
Changing the app such that the query is appended 'on the go' to the url where I can intercept it in cypress and therefore have a unique url for each query. Not possible to change the app.
My only bet seems to be intercepting the XHR call and using this, but I don't seem to be able to get it working Tried all options using XHR outlined here but to no luck (it picks only the stub declared last and uses that for all calls) https://github.com/cypress-io/cypress-documentation/issues/122.
The answer from this question uses Fetch and therefore doesn't apply:
Mock specific graphql request in cypress when running e2e tests
Anyone got any ideas?
With cypress 6.0 route and route2 are deprecated, suggesting the use of intercept. As written in the docs (https://docs.cypress.io/api/commands/intercept.html#Aliasing-individual-GraphQL-requests) you can mock the GraphQL requests in this way:
cy.intercept('POST', '/api', (req) => {
if (req.body.operationName === 'operationName') {
req.reply({ fixture: 'mockData.json'});
}
For anyone else hitting this issue, there is a working solution with the new cypress release using cy.route2()
The requests are sent to the server but the responses are stubbed/ altered on return.
Later Edit:
Noticed that the code version below doesn't alter the status code. If you need this, I'd recommend the version I left as a comment below.
Example code:
describe('account details', () => {
it('should display the account details correctly', () => {
cy.route2(graphEndpoint, (req) => {
let body = req.body;
if (body == getAccountDetailsQuery) {
req.reply((res) => {
res.body = getAccountDetailsResponse,
res.status = 200
});
} else if (body == getVehiclesQuery) {
req.reply((res) => {
res.body = getVehiclesResponse,
res.status = 200
});
}
}).as('accountStub');
cy.visit('/accounts/account123').wait('#accountStub');
});
});
Both your query and response should be in string format.
This is the cy command I'm using:
import * as hash from 'object-hash';
Cypress.Commands.add('stubRequest', ({ request, response, alias }) => {
const previousInteceptions = Cypress.config('interceptions');
const expectedKey = hash(
JSON.parse(
JSON.stringify({
query: request.query,
variables: request.variables,
}),
),
);
if (!(previousInteceptions || {})[expectedKey]) {
Cypress.config('interceptions', {
...(previousInteceptions || {}),
[expectedKey]: { alias, response },
});
}
cy.intercept('POST', '/api', (req) => {
const interceptions = Cypress.config('interceptions');
const receivedKey = hash(
JSON.parse(
JSON.stringify({
query: req.body.query,
variables: { ...req.body.variables },
}),
),
);
const match = interceptions[receivedKey];
if (match) {
req.alias = match.alias;
req.reply({ body: match.response });
}
});
});
With that is posible to stub exact request queries and variables:
import { MUTATION_LOGIN } from 'src/services/Auth';
...
cy.stubRequest({
request: {
query: MUTATION_LOGIN,
variables: {
loginInput: { email: 'test#user.com', password: 'test#user.com' },
},
},
response: {
data: {
login: {
accessToken: 'Bearer FakeToken',
user: {
username: 'Fake Username',
email: 'test#user.com',
},
},
},
});
...
Cypress.config is what make it possible, it is kind of a global key/val getter/setter in tests which I'm using to store interceptions with expected requests hash and fake responses
This helped me https://www.autoscripts.net/stubbing-in-cypress/
But I'm not sure where the original source is
A "fix" that I use is to create multiple aliases, with different names, on the same route, with wait on the alias between the different names, as many as requests you have.
I guess you can use aliases as already suggested in Answer by #Luis above like this. This is given in documentation too. Only thing you need to use here is multiple aliases as you have multiple calls and have to manage the sequence between them . Please correct me if i understood you question in other way ??
cy.route({
method: 'POST',
url: 'abc/*',
status: 200.
response: {whatever response is needed in mock }
}).as('mockAPI')
// HERE YOU SHOULD WAIT till the mockAPI is resolved.
cy.wait('#mockAPI')

I can't use json to make a Post request to my web api using react

I created a webapi in ASP.NET Core, and I need to consume it using React, the web api works normally, if I use curl or postman among others, it works normally. The problem starts when I'm going to use React, when I try to make any requests for my API with js from the problem.
To complicate matters further, when I make the request for other APIs it works normally, this led me to believe that the problem was in my API, but as I said it works with others only with the react that it does not. I've tried it in many ways.
The API is running on an IIS on my local network
Attempted Ways
Using Ajax
$ .ajax ({
method: "POST",
url: 'http://192.168.0.19:5200/api/token',
beforeSend: function (xhr) {
xhr.setRequestHeader ("Content-type", "application / json");
},
date: {
name: 'name',
password: 'password'
},
success: function (message) {
console.log (message);
},
error: function (error) {
/ * if (error.responseJSON.modelState)
showValidationMessages (error.responseJSON.modelState); * /
console.log (error);
}
});
Using Fetch
const headers = new Headers ();
headers.append ('Content-Type', 'application / json');
const options = {
method: 'POST',
headers,
body: JSON.stringify (login),
mode: 'cors' // I tried with cors and no-cors
}
const request = new Request ('http://192.168.0.19:5200/api/token', options);
const response = await fetch (request);
const status = await response.status;
console.log (response); * /
// POST adds a random id to the object sent
fetch ('http://192.168.0.19:5200/api/token', {
method: 'POST',
body: JSON.stringify ({
name: 'name',
password: 'password'
}),
headers: {
"Content-type": "application / json; charset = UTF-8"
},
credentials: 'same-origin'
})
.then (response => response.json ())
.then (json => console.log (json))
Using Request
var request = new XMLHttpRequest ();
request.open ('POST', 'http://192.168.0.19:5200/api/token', true);
request.setRequestHeader ('Content-Type', 'application / json; charset = UTF-8');
request.send (login);
ERRORS
Console
Network tab
When I do this without being change the content type to JSON it works
because the API returns saying that it is not a valid type.
Apart from allowing CORS in you .NET configuration. You also need to return 200 OK for all OPTION requests.
Not sure how it's done in .NET but just create a middleware that detects the METHOD of the request, and if it's OPTIONS, the finish the request right there with 200 status.
Well I had the same issue and it seems that you need to add the action to the HttpPost attribute in the controller.
Here is an example.
[HttpPost("[action]")]
public void SubmitTransaction([FromBody] SubmitTransactionIn request)
{
Ok();
}
Try like this
public void ConfigureServices(IServiceCollection services)
{
services.AddCors();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseCors(option => option.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod().AllowCredentials());
app.UseAuthentication();
app.UseMvc();
}

Resources