Testing Stripe Checkout w/ Cypress - javascript

I have to call stripe.redirectToCheckout (https://stripe.com/docs/js/checkout/redirect_to_checkout) in js to take a customer to their stripe checkout page.
I want to use cypress to test the checkout process, but it is not able to handle the stripe redirect as the cypress frame is lost when stripe.redirectToCheckout navigates to the page on Stripe's domain.
I also want to test that Stripe redirects us back to the success or error URL.
Is there any way to force cypress to "reattach" to the page once we've navigated to the Stripe checkout
-or-
Is there any way to get the URL for the Stripe checkout page so we can redirect manually or just know that it was at least called with the right parameters.
I know that testing external sites is considered an "anti-pattern" by the people at cypress (https://github.com/cypress-io/cypress/issues/1496). But how can a very standard web process, checkout, be tested (with a very popular and standard payment service, I will add) in that case? I don't buy that this is an "anti-pattern". This is an important step of the end-to-end test, and Stripe specifically gives us a testing sandbox for this kind of thing.

One common way to e2e test application with external dependencies like stripe is to make a simple mock version of it, that is then applied in e2e testing. The mock can also be applied during development, to speed things up.

Would this help?
it(`check stripe redirection`, () => {
cy.get('#payButton').click();
cy.location('host', { timeout: 20 * 1000 }).should('eq', STRIPE_REDIRECT_URL);
// do some payment stuff here
// ...
// after paying return back to local
cy.location({ timeout: 20 * 1000 }).should((location) => {
expect(location.host).to.eq('localhost:8080')
expect(location.pathname).to.eq('/')
})
})
I've used this method to test keyCloak login. This is the actual code that worked.
describe('authentication', () => {
beforeEach(() => cy.kcLogout());
it('should login with correct credentials', () => {
cy.visit('/');
cy.location('host', { timeout: 20 * 1000 }).should('eq', 'keycloak.dev.mysite.com:8443');
// This happens on https://keycloak.dev.mysite.com:8443/auth/dealm/...
cy.fixture('userCredentials').then((user) => {
cy.get('#username').type(user.email);
cy.get('#password').type(user.password);
cy.get('#kc-login').click();
cy.location({ timeout: 20 * 1000 }).should((location) => {
expect(location.host).to.eq('localhost:8080')
expect(location.pathname).to.eq('/')
})
})
});

I'm giving it a try to use stripe elements instead since it does not redirect and gives me much more control over it.

Related

Cypress | How to create a variable from network response body for OTP on login test

Disclaimer: I'm very new to using cypress, javascript and coding in general so please be as clear as possible as I'm still picking up some basic concepts.(I have a lot to learn and I appreciate you!).
Problem: I'm writing a login test that requires a OTP. In the test environment, the OTP is listed as a key value in the network response body after entering UN/PW and clicking a continue button. The response looks something similar to this:
{"status":"VALID_USER","otp":"123456","maskedEmailAddress":"email#email.com","maskedPhoneNumber":"--****","otacDeliveryPreference":"SMS","pinRequired":true}
so the UI flow is to enter a UN/PW > click continue (this action triggers the request) > a new page loads that asks for the one time pw listed in the response as "otp":123456
I am trying to get cy.intercept to work so I can pull out that 6 digit pin (it's unique every time), turn it into a variable to then plug into my test to successfully login
I have read so many tutorials and have kind of frankensteined together the following test, but unsurprisingly keep getting errors.
context('QA login', () => {
beforeEach(() => {
cy.visit("https://QAlogin.com/login")
})
it('should login to QA', () => {
cy.get(usernameField)
.type(username)
.should('have.value', username); //enters username
cy.get(passwordField)
.type(password)
.should('have.value', password); //enters password
cy.intercept("POST", "/login").as("getLogin")
.get(loginButton)
.click()
cy.wait('#getLogin').then(xhr => {
cy.log(xhr.response.body)
const jsonResponseData = xhr.response.body
const pinNumber = jsonResponseData['OTP']
cy.get(accessCodeField).type(pinNumber)
cy.get(submitButton).click()
});
})
});
I was hoping that this would intercept the request after I made that first .click() and make a variable out of the response body, and then make a variable out of the otp key value in the body. I'm not sure if this is a good approach.
Edit: I moved the .intercept based on a helpful answer, but my test fails when trying to use the pin as a variable. error msg:
"cy.type() can only accept a string or number. You passed in: undefined"
How can I get the otp pin from the response body to save to a variable?
thank you in advance, any advice is appreciated - I'm clearly wading in waters too deep (but i'm trying!!)
Move the intercept listener to a position before the action that you want it to catch
// set up listener
cy.intercept("POST", "/login").as("getLogin")
// trigger the POST you want listener to catch
cy.get(loginButton).click()
// wait for interception
cy.wait('#getLogin')then(interception =>
Here's an alternative solution that might work better.
Note that I've used the cy.wrap(xhr.response.body.otp).as('pinNumber') instead of const variables, which I then retrieve using cy.get('#pinNumber').then(pinNumber => {}). This is more verbose, but it's the Cypress-recommended way of storing data from the browser for later reuse.
I'm also not chaining the login button click off the intercept command, as that's not how the intercept command is designed to be used.
it('should login to QA', () => {
cy
.get(usernameField)
.type(username)
.should('have.value', username); //enters username
cy
.get(passwordField)
.type(password)
.should('have.value', password); //enters password
cy
.intercept("POST", "/login").as("getLogin");
cy
.get(loginButton)
.click();
cy.wait('#getLogin').then(xhr => {
cy.log(xhr.response.body);
cy.wrap(xhr.response.body.otp).as('pinNumber');
});
cy.get('#pinNumber').then(pinNumber => {
cy
.get(accessCodeField)
.type(pinNumber);
});
cy
.get(submitButton)
.click();
});

Cypress wait for API after button click

I've made a React app, which all works perfectly and I'm now writing some end to end tests using Cypress.
The React app all works on the same url, it's not got any routes, and api calls from inside the app are handled through button clicks.
The basis of the app is the end user selects some options, then presses filter to view some graphs that are dependant on the selected options.
cy.get('button').contains('Filter').click()
When the button is pressed in cypress, it runs the 3 api calls which return as expected, but looking over the cypress docs there is no easy way unless I use inline cy.wait(15000) which isn't ideal, as sometimes they return a lot faster, and sometimes they return slower, depending on the selected options.
Edit 1
I've tried using server and route:
cy.server({ method: 'GET' });
cy.route('/endpoint1*').as('one')
cy.route('/endpoint2*').as('two')
cy.route('/endpoint3*').as('three')
cy.get('button').contains('Filter').click()
cy.wait(['#one', '#two', '#three'], { responseTimeout: 15000 })
Which gives me the error:
CypressError: Timed out retrying: cy.wait() timed out waiting 5000ms for the 1st request to the route: 'one'. No request ever occurred.
After further investigation
Changing from responseTimeout to just timeout fixed the error.
cy.server({ method: 'GET' });
cy.route('/endpoint1*').as('one')
cy.route('/endpoint2*').as('two')
cy.route('/endpoint3*').as('three')
cy.get('button').contains('Filter').click()
cy.wait(['#one', '#two', '#three'], { timeout: 15000 }).then(xhr => {
// Do what you want with the xhr object
})
Sounds like you'll want to wait for the routes. Something like this:
cy.server();
cy.route('GET', '/api/route1').as('route1');
cy.route('GET', '/api/route2').as('route2');
cy.route('GET', '/api/route3').as('route3');
cy.get('button').contains('Filter').click();
// setting timeout because you mentioned it can take up to 15 seconds.
cy.wait(['#route1', '#route2', 'route3'], { responseTimeout: 15000 });
// This won't execute until all three API calls have returned
cy.get('#something').click();
Rather than using a .wait you can use a timeout parameter. That way if it finished faster, you don't have to wait.
cy.get('button').contains('Filter', {timeout: 15000}).click()
This is mentioned as one of the options parameters in the official docs here.
cy.server() and cy.route() are deprecated in Cypress 6.0.0 In a future release, support for cy.server() and cy.route() will be removed. Consider using cy.intercept() instead.
this worked for me...
ex:-
see the screen shot
cy.intercept('GET', 'http://localhost:4001/meta').as('route');
cy.get(':nth-child(2) > .nav-link').click();
cy.contains('Filter');
cy.wait(['#route'], { responseTimeout: 15000 });
Try this:
cy.contains('button', 'Save').click();
cy.get('[data-ng-show="user.manage"]', { timeout: 10000 }).should('be.visible').then(() => {
`cy.get('[data-ng-show="user.manage"]').click();
})

How do I know that 3D Secure 2 authentication works after upgrading to stripe.js version 3

I have updated a site so it uses latest stripe-php (6.39.0) and it now loads stripe.js version 3. I’ve made all the necessary changes to my code so that my credit card fields are now displayed using Stripe Elements. Test transactions work and I have updated the live site and real payments are being excepted.
The reason I made this update was because I was informed by stripe that I needed to upgrade the site so that its stripe integration will work with Strong Customer Authentication (SCA) which is required in the EU by September 2019.
Stripe has different credit card test numbers you can use to test things that arise when processing payments. This numbers can be found here: https://stripe.com/docs/testing#cards
4000000000003220 simulates a transactions where 3D Secure 2 authentication must be completed. But when I use this code stripe turns down payment and returns the message:
"Your card was declined. This transaction requires authentication. Please check your card details and try again."
Does this mean that 3D Secure 2 is working or not?
In the real world it would open a window with an interface from the customer's card issuer. So I not sure wether my integration is working or not. As said before payments are being excepted butI need to ready when Strong Customer Authentication is required in September.
It seems you have an integration problem with the JS part. For a simple charge (the following example does not work for subscription), this is the way you have to implement it:
First you have to create a Payment intent (doc here: https://stripe.com/docs/api/payment_intents/create):
\Stripe\PaymentIntent::create([
"amount" => 2000,
"currency" => "usd",
"payment_method_types" => ["card"],
]);
Once your PaymentIntent response is returned, you will have a client_secret key (doc here : https://stripe.com/docs/api/payment_intents/object). You can see that your payment status is "requires_payment_method"
{
"id": "pi_1Dasb62eZvKYlo2CPsLtD0kn",
"object": "payment_intent",
"amount": 1000,
"amount_capturable": 0,
"amount_received": 0,
...
"client_secret": "pi_1Dasb62eZvKYlo2CPsLtD0kn_secret_6aR6iII8CYaFCrwygLBnJW8js",
...
"status": "requires_payment_method",
...
}
On your server side you have to save this object.
You can now show your payment form with the JS part with the previous client_secret key (doc here : https://stripe.com/docs/payments/payment-intents/verifying-status). The idea is that you have to call the Js function on click on the submit button but do not submit ! Wait for the response to submit.
With some jquery this should like this:
var $mySubmitButton = $('#my-submit-button'),
$myPaymentForm = $('#my-payment-form'),
clientSecret = $cardButton.data('client-secret'); // put your client-secret somewhere
$mySubmitButton.on('click', function(e) {
e.preventDefault();
// Disable button to disallow multiple click
$(this).attr("disabled", true);
stripe.handleCardPayment(
clientSecret,
{
payment_method: clientMethod, // if you have a clientMethod
}
).then(function(result) {
if (result.error) {
// show error message
// then enable button
$mySubmitButton.attr("disabled", false);
} else {
// Submit form
$myPaymentForm.submit();
}
});
});
If everything goes right, when you click your submit button, you will have a test 3D secure popin with the options to "success" or "fail" the security test. If you click on success button, your form is submitted and you have to wait for the webhook "charged.success" to confirm transaction.
Once received, change you server object status and notify user about the transaction.
In my case, once the form submitted, I show a loader and check with ajax call every second to see if my payment intent status have changed (through the webhook). For your test environnement, you can use http://requestbin.net and Postman.
Beware: some cards referenced on this page will not work properly (you can't add them) https://stripe.com/docs/testing#cards (section 3D Secure test card numbers and tokens). Confirmed with their support.
If you work with saved card, you can test only with these cards: https://stripe.com/docs/payments/cards/charging-saved-cards#testing

How to call a third party site from Cypress test to get the text of Captcha image?

I need to get the text of a 'captcha' image and calculate it and enter the value in a text field while submitting a form. I have found that a third party library which does that. My question is how to call the third party library ( https://somesite) in the Cypress test? Or is there any other easy way to get the captcha image using javascript, could someone please advise on how to achieve this?
describe('Check the submission of form', function() {
it.only('Verify the submission of form', function() {
const getCaptcha = () => {
// How to call the third party url here or some where ???
return text // these text retured are numbers and this look like '35+12 =?'
}
cy.visit("testUrl")
cy.wrap({ name: getCaptcha })
cy.get('input[name="textinput1"]').type('Mazda')
cy.get('input[name="textinput2"]').clear().type('Axela')
....// rest of the code
})
})
If I understand correctly, you want to visit a site you control that uses captcha, get the capture image, then send it to a 3rd party API to resolve the captcha, then continue to login to the site.
You can use cy.request to call a 3rd party API:
cy.request('POST', 'http://somesite', body).then((response) => {
// do something with response.body
})
To get the captcha image from your login screen you could use something like:
describe('my website', () => {
it('should accept typing after login', () => {
cy.visit('testURL')
cy.get('a selector to target your #captcha img').then((captchaImg) => {
// Now you will have the captcha image itself
const body = { captchaImg } // prepare the body to send to the 3rd party API
cy.request('POST', 'http://somesite', body).then((response) => {
// Process the response to extract the field that you are interested in
// For instance, you could pull out the string '55+20='
let captchaText = getCaptchaText(response.body)
let captchaAnswer = getCaptchaAnswer(captchaText)
cy.get('captcha text input field').type(captchaAnswer)
// You will probably need to click a submit button
// Test the site here, now that you have logged in
cy.get('input[name="textinput1"]').type('Mazda')
// assert something
cy.get('input[name="textinput2"]').clear().type('Axela')
// assert something else
})
})
})
})
Doing this extra request each test will slow down your tests considerably. So it is better to test the login flow once, and then try some alternative methods to bypass the login flow of your site for the rest of the tests. At the least, you could try putting the captcha related test logic into a before hook and then run a suite of tests.
Cypress recommend against visiting 3rd party sites in your tests for several reasons, documented in this answer. And they also recommend against testing sites which you don't control. But accessing a 3rd party API can be done with cy.request.

DDP Rate limiter on login attempts in meteor

I'm trying to put a DDP rate limiter on the number of login attempts coming in from the client to the server. I've gone through the official documentation but I'm unable to verify if any of it actually works.
I've added the package: ddp-rate-limiter
My server code is:
Meteor.startup(function() {
var preventBruteForeLogin= {
type: 'method',
name: 'Meteor.loginWithPassword'
}
DDPRateLimiter.addRule(preventBruteForeLogin, 1, 2000);
DDPRateLimiter.setErrorMessage("slow down");
});
My understanding with the above is that it has added a rate limiting rule on Meteor.loginWithPassword method that it only allows one attempt every 2 seconds. However, given the little information available in the documentation and elsewhere on the net, I'm unable to figure out if it's actually working or if I've done it wrong. I've also gone through MC's blog on this and frankly I don't understand the coffee script code.
Can someone guide me through this?
Firstly according to the Meteor docs
By default, there are rules added to the DDPRateLimiter that rate limit logins, new user registration and password reset calls to a limit of 5 requests per 10 seconds per session.
If you want to remove, or replace default limits you should call Accounts.removeDefaultRateLimit() somewhere in your server side code.
Next you should create method similar to this one below
NOTE: You should pass only hashed password from client side to server side
Meteor.methods({
'meteor.login' ({ username, password }) {
Meteor.loginWithPassword({ user: username, password })
}
})
Then on your server side you should limit just created method.
if (Meteor.isServer) {
DDPRateLimiter.setErrorMessage(({ timeToReset }) => {
const time = Math.ceil(timeToReset / 1000)
return 'Try again after ' + time + ' seconds.'
})
DDPRateLimiter.addRule({
type: 'method',
name: 'meteor.login',
connectionId () {
return true
},
numRequests: 1,
timeInterval: 10000
})
}
This one will limit meteor.login method to one call in 10 seconds using DDP connection id. When you call the method on your client side you can get remaining time using callback error object.
Personally, I do the rate limiting using a slightly changed method from themeteorchef guide. I suggest you to the same, because it is much easier to implement when you build app with more methods to limit and for me, it is more readable. It is written using ES6 syntax. I recommend to read a little bit about it and start using it(you don't have to install additional packages etc.). I am sure that you will quickly like it.
EDIT
We found that using wrapping Meteor.loginWithPassword() method in another method may cause security problems with sending password as plain text. Accounts package comes with Accounts._hashPassword(password) method which returns hashed version of our password. We should use it when we call our meteor.login method. It may be done like below
Meteor.call('meteor.login', username, Accounts._hashPassword(password), function (err) {
//asyncCallback
})
Meteor.loginWithPassword is client side... you can't call it at server side
easy solution from meteor official documentation.
// Define a rule that matches login attempts by non-admin users.
const loginRule = {
userId(userId) {
const user = Meteor.users.findOne(userId);
return user && user.type !== 'admin';
},
type: 'method',
name: 'login'
};
// Add the rule, allowing up to 5 messages every 1000 milliseconds.
DDPRateLimiter.addRule(loginRule, 5, 1000);

Categories

Resources