I have added a command getCSRFToken that is used by other commands to get the CSRF token for making requests to my app:
Cypress.Commands.add("getCSRFToken", () => {
cy.getCookie('XSRF-TOKEN').then((cookie) => {
if (!cookie) {
return cy.request('HEAD', '/')
.its('headers')
.then((headers) => {
const token = headers['x-xsrf-token'];
if (!token) {
throw new Error('XSRF token not found');
}
return cy.setCookie('XSRF-TOKEN', token)
.then(() => token);
});
}
return cookie.value;
});
});
The portion that makes a HEAD request is for usage of this function when no pages have yet been visited in the test, for example when making POST requests to create test data.
AFAICT this looks like it should work to me, however it seems subsequent calls to getCookie doesn't actually retrieve anything:
I thought returning the setCookie promise and getCookie promise might make a difference but it does not seem like that is the case.
By default, Cypress clears up all cookies before every test is run. They have an api to keep a cookie for the next test execution which is Cypress.Cookies.preserveOnce
Back to your use case, you can call Cypress.Cookies.preserveOnce('XSRF-TOKEN') in the suite-level beforeEach in every suite where you want to get the token. If you don't want to repeat the call, you can move it inside your getCSRFToken command.
Cypress.Commands.add("getCSRFToken", () => {
Cypress.Cookies.preserveOnce('XSRF-TOKEN')
cy.getCookie('XSRF-TOKEN').then((cookie) => {
.....
});
});
Related
Next JS. I am trying to set some cookies in my /api/tokencheck endpoint. Here is a very simplified version of the code:
import { serialize } from 'cookie';
export default (req, res) => {
/* I change this manually to simulate if a cookie is already set */
let cookieexists = 'no';
async function getToken() {
const response = await fetch('https://getthetokenurl');
const data = await response.json();
return data.token;
}
if (cookieexists === 'no') {
getToken().then((token) => {
res.setHeader('Set-Cookie', serialize('token', token, { path: '/' }));
});
return res.status(200).end();
} else {
return res.status(200).end();
}
};
I have tried a ton of variations as to where to put my return.res.status... code, and tried many different ways to return a success code, but depending on where I put the code I variously end up with either of the following errors:
"API resolved without sending a response for /api/checkguestytoken, this may result in stalled requests."
or
"unhandledRejection: Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client"
I seem to have some gap in my knowledge about how the API works in Next JS because I cannot figure out how to just run the async function, get a result, set a couple of cookies and then exit with a 200. Could someone please tell me what I'm doing wrong?
I am trying to call a graphql and get the data from cookies, it runs well in postman app. However when I trying to run this postman collection on the command line with Newman
In terminal:
newman run postman_collection.json -e environment.json
then it gave me the error
[UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block,
or by rejecting a promise which was not handled with .catch().
The promise rejected with the reason "TypeError: CookieJar.getAll() requires a callback function".]
{
code: 'ERR_UNHANDLED_REJECTION'
}
And the Test script code is like this
pm.test("Get a test data", function () {
const jsonData = pm.response.json();
pm.expect(jsonData.data.createTest.success).to.eql(true);
});
pm.test("Test data cookies set", async function () {
const cookieJar = pm.cookies.jar();
const url = pm.environment.get("service-url");
const cookies = await cookieJar.getAll(url);
const cookieNames = cookies.map(cookie => cookie.name);
pm.expect(cookieNames).to.include("test-token");
pm.expect(cookieNames).to.include("legacy-test-token");
});
So I assume the error is because getAll() requires a callback function, Do you know what I'm doing wrong? How can I improve it, Can you help me solve this? Many thanks
'it runs well in postman app' --> I doubt it. I tried and it always passed.
I added a callback, also changed a setting Whitelist Domain in Postman GUI.
pm.test("Test data cookies set", function () {
const cookieJar = pm.cookies.jar();
const url = pm.environment.get("service-url");
cookieJar.getAll(url, (error, cookies)=> {
if(error) console.log(error);
const cookieNames = cookies.map(cookie => cookie.name);
pm.expect(cookieNames).to.include("test-token");
pm.expect(cookieNames).to.include("legacy-test-token");
});
});
I want to save/persist/preserve a cookie or localStorage token that is set by a cy.request(), so that I don't have to use a custom command to login on every test. This should work for tokens like jwt (json web tokens) that are stored in the client's localStorage.
To update this thread, there is already a better solution available for preserving cookies (by #bkucera); but now there is a workaround available now to save and restore local storage between the tests (in case needed). I recently faced this issue; and found this solution working.
This solution is by using helper commands and consuming them inside the tests,
Inside - cypress/support/<some_command>.js
let LOCAL_STORAGE_MEMORY = {};
Cypress.Commands.add("saveLocalStorage", () => {
Object.keys(localStorage).forEach(key => {
LOCAL_STORAGE_MEMORY[key] = localStorage[key];
});
});
Cypress.Commands.add("restoreLocalStorage", () => {
Object.keys(LOCAL_STORAGE_MEMORY).forEach(key => {
localStorage.setItem(key, LOCAL_STORAGE_MEMORY[key]);
});
});
Then in test,
beforeEach(() => {
cy.restoreLocalStorage();
});
afterEach(() => {
cy.saveLocalStorage();
});
Reference: https://github.com/cypress-io/cypress/issues/461#issuecomment-392070888
From the Cypress docs
For persisting cookies: By default, Cypress automatically clears all cookies before each test to prevent state from building up.
You can configure specific cookies to be preserved across tests using the Cypress.Cookies api:
// now any cookie with the name 'session_id' will
// not be cleared before each test runs
Cypress.Cookies.defaults({
preserve: "session_id"
})
NOTE: Before Cypress v5.0 the configuration key is "whitelist", not "preserve".
For persisting localStorage: It's not built in ATM, but you can achieve it manually right now because the method thats clear local storage is publicly exposed as Cypress.LocalStorage.clear.
You can backup this method and override it based on the keys sent in.
const clear = Cypress.LocalStorage.clear
Cypress.LocalStorage.clear = function (keys, ls, rs) {
// do something with the keys here
if (keys) {
return clear.apply(this, arguments)
}
}
You can add your own login command to Cypress, and use the cypress-localstorage-commands package to persist localStorage between tests.
In support/commands:
import "cypress-localstorage-commands";
Cypress.Commands.add('loginAs', (UserEmail, UserPwd) => {
cy.request({
method: 'POST',
url: "/loginWithToken",
body: {
user: {
email: UserEmail,
password: UserPwd,
}
}
})
.its('body')
.then((body) => {
cy.setLocalStorage("accessToken", body.accessToken);
cy.setLocalStorage("refreshToken", body.refreshToken);
});
});
Inside your tests:
describe("when user FOO is logged in", ()=> {
before(() => {
cy.loginAs("foo#foo.com", "fooPassword");
cy.saveLocalStorage();
});
beforeEach(() => {
cy.visit("/your-private-page");
cy.restoreLocalStorage();
});
it('should exist accessToken in localStorage', () => {
cy.getLocalStorage("accessToken").should("exist");
});
it('should exist refreshToken in localStorage', () => {
cy.getLocalStorage("refreshToken").should("exist");
});
});
Here is the solution that worked for me:
Cypress.LocalStorage.clear = function (keys, ls, rs) {
return;
before(() => {
LocalStorage.clear();
Login();
})
Control of cookie clearing is supported by Cypress: https://docs.cypress.io/api/cypress-api/cookies.html
I'm not sure about local storage, but for cookies, I ended up doing the following to store all cookies between tests once.
beforeEach(function () {
cy.getCookies().then(cookies => {
const namesOfCookies = cookies.map(c => c.name)
Cypress.Cookies.preserveOnce(...namesOfCookies)
})
})
According to the documentation, Cypress.Cookies.defaults will maintain the changes for every test run after that. In my opinion, this is not ideal as this increases test suite coupling.
I added a more robust response in this Cypress issue: https://github.com/cypress-io/cypress/issues/959#issuecomment-828077512
I know this is an old question but wanted to share my solution either way in case someone needs it.
For keeping a google token cookie, there is a library called
cypress-social-login. It seems to have other OAuth providers as a milestone.
It's recommended by the cypress team and can be found on the cypress plugin page.
https://github.com/lirantal/cypress-social-logins
This Cypress library makes it possible to perform third-party logins
(think oauth) for services such as GitHub, Google or Facebook.
It does so by delegating the login process to a puppeteer flow that
performs the login and returns the cookies for the application under
test so they can be set by the calling Cypress flow for the duration
of the test.
I can see suggestions to use whitelist. But it does not seem to work during cypress run.
Tried below methods in before() and beforeEach() respectively:
Cypress.Cookies.defaults({
whitelist: "token"
})
and
Cypress.Cookies.preserveOnce('token');
But none seemed to work. But either method working fine while cypress open i.e. GUI mode. Any ideas where I am coming short?
2023 Updated on Cypress v12 or more:
Since Cypress Version 12 you can use the new cy.session()
it cache and restore cookies, localStorage, and sessionStorage (i.e. session data) in order to recreate a consistent browser context between tests.
Here's how to use it
// Caching session when logging in via page visit
cy.session(name, () => {
cy.visit('/login')
cy.get('[data-test=name]').type(name)
cy.get('[data-test=password]').type('s3cr3t')
cy.get('form').contains('Log In').click()
cy.url().should('contain', '/login-successful')
})
My backend is a REST API served up by Django-Rest-Framework. I am using VueJS for the front end and trying to figure out what is the best practice for doing authentication/login. This is probably terrible code, but it works (in a component called Login.vue):
methods: {
login () {
axios.post('/api-token-auth/login/', {
username: this.username,
password: this.pwd1
}).then(response => {
localStorage.setItem('token', response.data.token)
}).catch(error => {
console.log("Error login")
console.log(error)
})
this.dialog = false
}
}
Does it make sense to use localStorage this way? Also, I'm wondering when the user wants to sign out, and I call /api-token-auth/logout, do I still need to remove the token from localStorage? It's not actually clear to me what goes on with the tokens either on Django's end or the browser/Vue.
Application-wide data, such as authentication and user information, should go into centralized state. You can use Vuex or a simple shared state. Vuex is awesome but it does add complication so you have to count the cost. If you use Vuex, you can use Vuex persisted state to keep it in localStorage. This should be much faster to access than localStorage. In my experience, localStorage is not reliable and can cause problems. State is the way to go.
For example, modifying your current code to send it to Vuex:
methods: {
login () {
axios.post('/api-token-auth/login/', {
username: this.username,
password: this.pwd1
}).then(response => {
that.$store.commit('LOGIN_SUCCESS', response)
}).catch(error => {
console.log("Error login")
console.log(error)
})
this.dialog = false
}
}
Then over in Vuex (like /store/modules/user.js if using modules):
Vue.use(Vuex)
const state = { token: null}
const mutations = {
LOGIN_SUCCESS(state, response) {
state.token = response.token
}
export default {
state,
mutations
}
And call the token either by a Getter or directly:
this.$store.state.user.token
This assumes store is used by Vue. For example, in main.js you would have:
import store from './store/index.js'
new Vue({
el: '#app',
store
})
I have a web app that store token/refresh token inside Vuex and load data from localStorage only when the store is init. It work well until our users report that they keep got 403 error. Found out they was using 2 (or more) browser tabs open. After the refresh token is fetch our new token is saved to state and local storage, but the other tab is not notice of data change, so they use the old token/refresh token to fetch, and fails :'(
It took me several hours of re-produce and debugging, now I will never put token inside Vuex again
My UserManager service automatically fires a $http POST every hour to refresh the user access token.
I'm trying to mock that call to verify that the token is being refreshed, but when I try to flush the $httpbackend I get an error saying 'No pending requests to flush' even though I know that the refresh function has been called (added a console.log just to verify).
Either the fact that the function is being called through a setTimeOut is effecting the $httpbackend or I'm missing something else.
Code attached bellow:
describe("UserManager Service Testing", function() {
beforeEach(angular.mock.module('WebApp'));
beforeEach(inject(function($httpBackend) {
window.apiUrls = jQuery.parseJSON('{"cover_art": "http://myrockpack.com/ws/cover_art/?locale=en-us", "channel_search_terms": "http://myrockpack.com/ws/complete/channels/?locale=en-us", "register": "http://myrockpack.com/ws/register/", "categories": "http://myrockpack.com/ws/categories/?locale=en-us", "reset_password": "http://myrockpack.com/ws/reset-password/", "share_url": "http://myrockpack.com/ws/share/link/", "video_search": "http://myrockpack.com/ws/search/videos/?locale=en-us", "channel_search": "http://myrockpack.com/ws/search/channels/?locale=en-us", "video_search_terms": "http://myrockpack.com/ws/complete/videos/?locale=en-us", "popular_channels": "http://myrockpack.com/ws/channels/?locale=en-us", "popular_videos": "http://myrockpack.com/ws/videos/?locale=en-us", "login": "http://myrockpack.com/ws/login/", "login_register_external": "http://myrockpack.com/ws/login/external/", "refresh_token": "http://myrockpack.com/ws/token/"}');
var mockLogin = {"token_type":"Bearer","user_id":"oCRwcy5MRIiWmsJjvbFbHA","access_token":"752a4f939662846a787a1474ad17ffddcd816dc7AAFB1G7HvgH-0qAkcHMuTESIlprCY72xWxyiuhySCpFJxKVYqOx9W7Gt","resource_url":"http://myrockpack.com/ws/oCRwcy5MRIiWmsJjvbFbHA/","expires_in":2,"refresh_token":"fa2f47f3590240e4bdfdbde03bf8042d"}
var refreshToken = {"token_type":"Bearer","user_id":"CeGfSz6dQW2ga2P2tKb3Bg","access_token":"ef235de46dba53ba69ed049f57496ec902da5d28AAFB1HdeTE-2vwnhn0s-nUFtoGtj9rSm9waiuhySCpFJxKVYqOx9W7Gt","resource_url":"http://myrockpack.com/ws/CeGfSz6dQW2ga2P2tKb3Bg/","expires_in":3600,"refresh_token":"873e06747d964a0d80f79181c98aceac"};
$httpBackend.when('POST', window.apiUrls.refresh_token).respond(refreshToken);
$httpBackend.when('POST', window.apiUrls.login).respond(mockLogin);
}));
it('UserManager should refresh the token after 2 seconds', inject(function(UserManager, $httpBackend) {
UserManager.oauth.Login('gtest','qweqwe');
$httpBackend.flush();
waits(4000);
$httpBackend.flush();
expect(UserManager.oauth.credentials.access_token).toEqual('ef235de46dba53ba69ed049f57496ec902da5d28AAFB1HdeTE-2vwnhn0s-nUFtoGtj9rSm9waiuhySCpFJxKVYqOx9W7Gt');
}));
});
There is a discussion about this here. "Since promises are async you need to do $rootScope.$digest() in your tests to get them going"
add this before your $httpBackend.flush():
if(!$rootScope.$$phase) {
$rootScope.$apply();
}
so your code becomes:
it('UserManager should refresh the token after 2 seconds', inject(function(UserManager, $httpBackend) {
UserManager.oauth.Login('gtest','qweqwe');
if(!$rootScope.$$phase) {
$rootScope.$apply();
}
$httpBackend.flush();
expect(UserManager.oauth.credentials.access_token).toEqual('ef235de46dba53ba69ed049f57496ec902da5d28AAFB1HdeTE-2vwnhn0s-nUFtoGtj9rSm9waiuhySCpFJxKVYqOx9W7Gt');
}));
I guess you have to wrap the code below the waits inside a runs(...) block. Otherwise your code is executed immediately before the waiting has been finished
waits(4000);
runs(function() {
$httpBackend.flush();
expect(UserManager.oauth.credentials.access_token).toEqual('ef235de46dba53ba69ed049f57496ec902 da5d28AAFB1HdeTE-2vwnhn0s-nUFtoGtj9rSm9waiuhySCpFJxKVYqOx9W7Gt');
});
It's described in the jasmine docs as well: https://github.com/pivotal/jasmine/wiki/Asynchronous-specs
Your first $httpBackend.flush() flushes all the pending requests that you defined in your beforeEach().
When you call $httpBackend.flush() a second time, there are no pending requests, so you get the message.
You need to add another event(s) after the first flush.
Untested, but that's my theory.
So with my jasmine tests all the time sometimes I have had to use regex to make the url a little less specific, most of this has been with angular wanting to strip trailing slashes or other stupid little things. Such as the request url wouldn't find the expect request but the regex below would.
https://qa.com/sessions/5cdec5bde6a242dca2cf5dd0ff7be2c9
/(qa.com\/sessions\/5cdec5bde6a242dca2cf5dd0ff7be2c9)/g