Cypress blocks cookies sent from third party response when the same site attribute is not set. How to store such blocked cookie values and use them - https

We are trying to test a third party sign in (Single Sign On) flow using Cypress.
Cypress blocks cookies sent in third party response when the SameSite attribute is not set when tested with Electron V-94.
How to store such blocked cookie values and use them?
The same works when tested from normal browser with third party cookie even with no SameSite attribute set.
Is there a way to prevent cypress from blocking it or can the cookies received from third party can be stored and used? If so, how?

I've just been fighting the same issue, I resolved it by intercepting all requests, checking if they had a set-cookie header(s) and rewriting the SameSite attribute. There's probably a neater way to do it, as this does clutter up the cypress dashboard a little. You can add this as a command for easy reuse:
In your commands file:
declare namespace Cypress {
interface Chainable<Subject> {
disableSameSiteCookieRestrictions(): void;
}
}
Cypress.Commands.add('disableSameSiteCookieRestrictions', () => {
cy.intercept('*', (req) => {
req.on('response', (res) => {
if (!res.headers['set-cookie']) {
return;
}
const disableSameSite = (headerContent: string): string => {
return headerContent.replace(/samesite=(lax|strict)/ig, 'samesite=none');
}
if (Array.isArray(res.headers['set-cookie'])) {
res.headers['set-cookie'] = res.headers['set-cookie'].map(disableSameSite);
} else {
res.headers['set-cookie'] = disableSameSite(res.headers['set-cookie']);
}
})
});
});
Usage:
it('should login using third party idp', () => {
cy.disableSameSiteCookieRestrictions();
//add test body here
});
or alteratively, run it before each test:
beforeEach(() => cy.disableSameSiteCookieRestrictions());

Related

How do I log a specific field in API requests within Cypress

I want to cypress.log() out a specific field in the request header whenever my webapp makes requests that way when it fails and adds screenshots/logs I can grab that that requestId that failed.
Is there a way to setup cypress so that for all network requests it checks for this field and log it?
I can add a cy.intercept within each individual file but I want a more generic way to handle this.
Cypress.log is the synchronous version of cy.log().
Add middleware: true to the intercept to pass request to other intercepts.
cy.intercept({ url: '*', middleware: true }, (req) => {
const headerValue = req.headers?['x-custom-headers']
if (headerValue) {
Cypress.log({
name: 'x-custom-header',
message: headerValue
})
}
})
You'll get an Cypress promise error if you try to use cy.log() to log out every request header in an cy.intercept() within a routeHandler callback. This would also make it kind of tough to log to a CI terminal as well.
Instead you can console.log to dev tools. To make it apply to all tests, you can wrap it in a beforeEach() and place it in the support/index.js file.
// support/index.js
beforeEach(() => {
cy.intercept('*', (req) => {
req.continue((res) => {
console.log(JSON.stringify(req.headers))
})
})
})

Can you configure Cypress to log requests and responses to a file?

When I run my Cypress tests I randomly get HTTP 405 Method not allowed code when I submit the form, but the HTTP method is a correct one - POST. If I open the Developer Tools to see the outgoing request the HTTP 405 is never returned, in other words the error only happens when Developer Tools are closed. No combination of cy.pause(), cy.wait() alleviates the problem.
Question: Can you configure Cypress so it logs all the outgoing requests and responses to a file so I don't have to open DevTools?
Should be possible with cy.intercept() functional handler.
General info here Using the routeHandler function
cy.intercept(url, (req) => {
cy.writeFile('my-log', req, { flag: 'a' }) // append
req.continue((res) => {
cy.writeFile('my-log', res, { flag: 'a' }) // append
})
})
Utilizing an interceptor solely for logging purposes is not very efficient.
You can generate a HAR file that includes detailed information about the network activity during the execution of your Cypress tests.
Especially for this purpose, you can use the cypress-har-generator.
describe('my tests', () => {
before(() => {
// start recording
cy.recordHar();
});
after(() => {
// save the HAR file
cy.saveHar({ waitForIdle: true });
});
it('does something', () => {
cy.visit('https://example.com');
// ...
});
});

Sapper/svelte3 session not synchronizing without page reload

I'm having trouble getting Sapper to synchronize session changes made in my server-side routes without a pageload. My example scenario is that I load my app with no user in the session, my server-side login route sets the user to the session, and I use goto to got to the dashboard.
The problem is that the session argument in the dashboard's preload function isn't populated. If I use window.location.href = '/dashboard', it is, because it's running through Sapper's page_handler. But if I do a client-only redirect, Sapper isn't sending the updated session to the client.
Any way around this? Am I using my tools wrong?
Note: I'm using connect-pg-simple and express-session, setting up sapper like this: sapper.middleware({session: (req, res) => req.session.public}).
I found my answer in the Sapper docs
session contains whatever data was seeded on the server. It is a writable store, meaning you can update it with new data (for example, after the user logs in) and your app will be refreshed.
Reading between the lines, this indicates that your app has to manually synchronize your session data.
The solution here is to manually sync the session data to the client, either with a webhook connection, a response header, or a key in the response data.
I've got a decorator I use to create a server route handler, in which I add the session data to the response. Here's a simplified version:
const createHandler = getData => (req, res) => {
res.status(200).json({data: getData(req.body), session: req.session.public})
}
Obviously there's more to it than that, e.g. error handling, but you get the idea. On the client, I wrap fetch in a helper function that I always use anyway to get my json, set the correct headers, etc. In it, I look at the response, and if there's a session property, I set that to the session store so that it's available in my preloads.
import {stores} from "#sapper/app"
const myFetch = (...args) => fetch(...args).then(r => r.json()).then(body => {
if (body.session) stores().session.set(body.session)
return body.data
})
To put it simply, after your session status changes from the front end (user just logged in, or you just invalidated his login), you should update the session store on the front end.
<script>
import { goto, stores } from '#sapper/app';
const { session } = stores();
const loginBtnHandler = () => {
const req = await fetch('/api/login', {
method: 'POST',
credentials: 'same-origin', // (im using cookies in this example)
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ ........ })
});
if (req.ok) {
// here is where you refresh the session on the client right after you log in
$session.loggedIn = true; // or your token or whatever
// next page will properly read the session
goto('/');
return;
}
...
}
</script>

Can't intercept Cypress API call

I have stuck with Cypress fixtures. Can't intercept an XHR request with SSR and navigation routing.
cypress/integration/page.js:
const fetch = require("unfetch")
describe("/about", () => {
beforeEach(() => {
cy.visit("/", { // Visit home page to trigger SSR
onBeforeLoad (win) {
win.fetch = fetch // replace fetch with xhr implementation
},
})
})
it("Has a correct title", () => {
cy.server()
cy.fixture("about").then(about => {
// about object is correct here, like {title: "About+"}
cy.route("GET", "http://localhost:8080/api/documents/url", about) // Not sure where .route should be
cy.get(".main > :nth-child(1) > a").click() // Navigate to the /about page
cy.route("GET", "http://localhost:8080/api/documents/url", about) // Tried both ways
// This hits my server API without stubbing, getting {title: "About"}
cy.title().should("eq", "About+") // About != About+
})
})
})
cypress/fixtures/about.json:
{"title": "About+"}
I see an XHR request (type=xhr) in Dev Tools and it doesn't use the above about stub object but hits real API instead. Why? Double checked URL and method – 100% the same. Can it be that route is coupled to visit and ignores click-based routing?!
Rechecking this once again, I've found a solution. Let me share the details for everyone interested:
1) I use Next.js which is an excellent tool for SSR but it doesn't allow you to disable server-side rendering (yet) according to this and this issues.
2) You can use Cypress with SSR pages but, in this way, you're limited to testing real HTML. Which means you have to either couple tests to real data (not good in most cases) or stub the database itself (slow). In general, you want to stub HTTP requests.
3) Cypress can't stub fetch requests and mocking fetch with XHR-based implementation was trickier than I thought.
First you need to:
// cypress/integration/your-test.js
Cypress.on('window:before:load', (win) => {
delete win.fetch
})
Then:
// pages/your-page.js
Entry.getInitialProps = async function() {
window.fetch = require("unfetch").default
...
}
Other combinations of delete & update code lines I tried didn't yield positive results. For example, when I had window.fetch = line in the test file it didn't work and fetch.toString() gave "native code". Not sure why, no time to explore further.
Axios solves the above but I don't like to bloat my bundle with extra stuff. You can inject XHR-based fetch for tests only.
4) The most important missing piece. You need to wait for route.
it("Has a correct title", () => {
cy.visit("/")
cy.server()
cy.route("GET", "http://localhost:8080/api/documents/url/about", {title: "About+"}).as("about")
cy.get("[href='/about']").click()
cy.wait("#about") // !!!
cy.get("h1").contains("About+")
})

Save local storage across tests to avoid re-authentication

I'm wondering if it is possible to save the state of localStorage across tests.
Mainly because I want to avoid re-authentication on each test. I realize that I can create a command that sends an API request to our backend to avoid going through the auth flow but for various reasons this won't work in my situation.
I am asking if it possible to have a workflow like this:
Go to login page: Authenticate get back response and save session to local storage
Persist local storage somehow...
Go through other tests as authenticated user
You can use the cypress-localstorage-commands package to persist localStorage between tests, so you'll be able to do login only once:
In support/commands.js:
import "cypress-localstorage-commands";
In your tests:
before(() => {
// Do your login stuff here
cy.saveLocalStorage();
});
beforeEach(() => {
cy.restoreLocalStorage();
});
Here's what I ended up doing:
Go to login page: Authenticate
At this point we have data we want to persist between tests in localStorage but we are not allowed to whitelist localStorage.
However, we are allow to whitelist cookies
I have some code like this inside my support/commands.js that act as helpers
const sessionKeys = {
authTokens: 'auth.tokens',
sessionConfig: 'session.config',
};
// The concatenation of username and cid will be the key to set the session
Cypress.Commands.add('persistSession', (key) => {
const authTokens = localStorage.getItem(key);
cy.setCookie(key, authTokens);
});
Cypress.Commands.add('restoreSession', (key) => {
cy.getCookie(key).then(authTokens => {
localStorage.setItem(key, authTokens.value);
});
});
So we call cy.persistSession(key) after we login, which means we have all the authentication saved as cookies which are whitelisted inside of support/index.js with code.
Like this:
Cypress.Cookies.defaults({
whitelist: function(cookie){
// Persist auth stuff
const reAuthTokens = new RegExp('.*auth\.tokens');
if(reAuthTokens.test(cookie.name)){
return true;
}
return false;
}
});
Now anytime we need our auth tokens inside our other tests before running them we cy.restoreSession(key) and we should be good!
Here is the useful link that solves my problem like yours: Preserve cookies through multiple tests
my code like:
const login = () => {
cy.visit('http://0.0.0.0:8080/#/login');
cy.get('#username').type('username');
cy.get('#password').type('1234password$');
cy.get('#login-button').click();
}
describe('UI', () => {
// beforeEach(login);
beforeEach(() => {
login();
Cypress.Cookies.preserveOnce('session_id', 'remember_token');
});
});
hope can help you.
Anything you can do in JS you can do in a cypress test. If you have some way to store creds (auth token, etc.) in local storage, I see no reason why you can't do that. If cypress is clearing out your local storage between tests, you will have to write a beforeEach hook that saves an authenticated token (hard-coded by you) to local storage before each test.

Resources