diff --git a/cypress/fixtures/cards.json b/cypress/fixtures/cards.json index aa4dcfa16..a23455250 100644 --- a/cypress/fixtures/cards.json +++ b/cypress/fixtures/cards.json @@ -1,35 +1,42 @@ { - "clientData": "eyJ2ZXJzaW9uIjoiMS4wLjAiLCJkZXZpY2VGaW5nZXJwcmludCI6InpabUlDMEVYOHcwMDMwMDAwMDAwMDAwMDAwS1piSVFqNmt6czAwNjA4MjQxOTBjVkI5NGlLekJHS1IybWJMc3JmckFKUEdCY2JHSG0wMDZpdmJTdVlkRzBSMDAwMDBZVnhFcjAwMDAwVk1yM0h5dHhQNmlaQ3FuSTRsc2s6NDAiLCJwZXJzaXN0ZW50Q29va2llIjpbXSwiY29tcG9uZW50cyI6eyJ1c2VyQWdlbnQiOiJkYzJjNDM2MThlOTM0YjY2NmIwOWIxNmEzZDE4NmViZCIsIndlYmRyaXZlciI6MCwibGFuZ3VhZ2UiOiJwbC1QTCIsImNvbG9yRGVwdGgiOjMwLCJkZXZpY2VNZW1vcnkiOjgsInBpeGVsUmF0aW8iOjIsImhhcmR3YXJlQ29uY3VycmVuY3kiOjEyLCJzY3JlZW5XaWR0aCI6MTc5Miwic2NyZWVuSGVpZ2h0IjoxMTIwLCJhdmFpbGFibGVTY3JlZW5XaWR0aCI6MTc5MiwiYXZhaWxhYmxlU2NyZWVuSGVpZ2h0IjoxMDMwLCJ0aW1lem9uZU9mZnNldCI6LTEyMCwidGltZXpvbmUiOiJFdXJvcGUvV2Fyc2F3Iiwic2Vzc2lvblN0b3JhZ2UiOjEsImxvY2FsU3RvcmFnZSI6MSwiaW5kZXhlZERiIjoxLCJhZGRCZWhhdmlvciI6MCwib3BlbkRhdGFiYXNlIjoxLCJwbGF0Zm9ybSI6Ik1hY0ludGVsIiwicGx1Z2lucyI6ImMxMzU2NDM4NzUxNzZkZWU5MTkyZTE5ZDRkOGM3YTkwIiwiY2FudmFzIjoiZDYxNjRlNzA3ZWQ4NDdlNTE4OGE1YjUyMDhmNDI1OGIiLCJ3ZWJnbCI6IjcwM2ZjZWMyM2MxNjhlYjAzNmM0MjA3MzA1ODYzZmNlIiwid2ViZ2xWZW5kb3JBbmRSZW5kZXJlciI6IkludGVsIEluYy5+SW50ZWwoUikgVUhEIEdyYXBoaWNzIDYzMCIsImFkQmxvY2siOjAsImhhc0xpZWRMYW5ndWFnZXMiOjAsImhhc0xpZWRSZXNvbHV0aW9uIjowLCJoYXNMaWVkT3MiOjAsImhhc0xpZWRCcm93c2VyIjowLCJmb250cyI6IjI5MmVhMmNjZWNjZDAyYjAxYzBjNGMxZDQxMzIxNzVlIiwiYXVkaW8iOiIzOTRmNjQwMGY0NDg5NDIwNDIzZGYzMzY1OGU1NGJlNiIsImVudW1lcmF0ZURldmljZXMiOiJmMjA4Yzc3MWExNjBiNjRmNDkzOWFmYmE1OTY2YTRhZiJ9fQ==", - "encryptedExpiryMonth": "test_03", - "encryptedExpiryYear": "test_2030", - "encryptedSecurityCodes": { - "unknown": "665", - "matches": "test_001" - }, - "cards": { - "simpleCard": { - "brand": "visa", - "encryptedCardNumber": "test_4111111145551142" + "adyen": { + "clientData": "eyJ2ZXJzaW9uIjoiMS4wLjAiLCJkZXZpY2VGaW5nZXJwcmludCI6InpabUlDMEVYOHcwMDMwMDAwMDAwMDAwMDAwS1piSVFqNmt6czAwNjA4MjQxOTBjVkI5NGlLekJHS1IybWJMc3JmckFKUEdCY2JHSG0wMDZpdmJTdVlkRzBSMDAwMDBZVnhFcjAwMDAwVk1yM0h5dHhQNmlaQ3FuSTRsc2s6NDAiLCJwZXJzaXN0ZW50Q29va2llIjpbXSwiY29tcG9uZW50cyI6eyJ1c2VyQWdlbnQiOiJkYzJjNDM2MThlOTM0YjY2NmIwOWIxNmEzZDE4NmViZCIsIndlYmRyaXZlciI6MCwibGFuZ3VhZ2UiOiJwbC1QTCIsImNvbG9yRGVwdGgiOjMwLCJkZXZpY2VNZW1vcnkiOjgsInBpeGVsUmF0aW8iOjIsImhhcmR3YXJlQ29uY3VycmVuY3kiOjEyLCJzY3JlZW5XaWR0aCI6MTc5Miwic2NyZWVuSGVpZ2h0IjoxMTIwLCJhdmFpbGFibGVTY3JlZW5XaWR0aCI6MTc5MiwiYXZhaWxhYmxlU2NyZWVuSGVpZ2h0IjoxMDMwLCJ0aW1lem9uZU9mZnNldCI6LTEyMCwidGltZXpvbmUiOiJFdXJvcGUvV2Fyc2F3Iiwic2Vzc2lvblN0b3JhZ2UiOjEsImxvY2FsU3RvcmFnZSI6MSwiaW5kZXhlZERiIjoxLCJhZGRCZWhhdmlvciI6MCwib3BlbkRhdGFiYXNlIjoxLCJwbGF0Zm9ybSI6Ik1hY0ludGVsIiwicGx1Z2lucyI6ImMxMzU2NDM4NzUxNzZkZWU5MTkyZTE5ZDRkOGM3YTkwIiwiY2FudmFzIjoiZDYxNjRlNzA3ZWQ4NDdlNTE4OGE1YjUyMDhmNDI1OGIiLCJ3ZWJnbCI6IjcwM2ZjZWMyM2MxNjhlYjAzNmM0MjA3MzA1ODYzZmNlIiwid2ViZ2xWZW5kb3JBbmRSZW5kZXJlciI6IkludGVsIEluYy5+SW50ZWwoUikgVUhEIEdyYXBoaWNzIDYzMCIsImFkQmxvY2siOjAsImhhc0xpZWRMYW5ndWFnZXMiOjAsImhhc0xpZWRSZXNvbHV0aW9uIjowLCJoYXNMaWVkT3MiOjAsImhhc0xpZWRCcm93c2VyIjowLCJmb250cyI6IjI5MmVhMmNjZWNjZDAyYjAxYzBjNGMxZDQxMzIxNzVlIiwiYXVkaW8iOiIzOTRmNjQwMGY0NDg5NDIwNDIzZGYzMzY1OGU1NGJlNiIsImVudW1lcmF0ZURldmljZXMiOiJmMjA4Yzc3MWExNjBiNjRmNDkzOWFmYmE1OTY2YTRhZiJ9fQ==", + "encryptedExpiryMonth": "test_03", + "encryptedExpiryYear": "test_2030", + "encryptedSecurityCodes": { + "unknown": "665", + "matches": "test_001" }, - "threeDSecureOneAuth": { - "brand": "visa", - "encryptedCardNumber": "test_4212345678901237" - }, - "threeDSecureTwoAuth": { - "encryptedCardNumber": "test_5454545454545454", - "brand": "mc" - }, - "errorCard": { - "brand": "visa", - "encryptedCardNumber": "test_5201282999005515" - }, - "closeAccount" : { - "brand": "visa", - "encryptedCardNumber" : "test_5454541580311093" - }, - "avs" : { - "brand": "visa", - "encryptedCardNumber" : "test_4400000000000008" + "cards": { + "simpleCard": { + "brand": "visa", + "encryptedCardNumber": "test_4111111145551142" + }, + "threeDSecureOneAuth": { + "brand": "visa", + "encryptedCardNumber": "test_4212345678901237" + }, + "threeDSecureTwoAuth": { + "encryptedCardNumber": "test_5454545454545454", + "brand": "mc" + }, + "errorCard": { + "brand": "visa", + "encryptedCardNumber": "test_5201282999005515" + }, + "closeAccount": { + "brand": "visa", + "encryptedCardNumber": "test_5454541580311093" + }, + "avs": { + "brand": "visa", + "encryptedCardNumber": "test_4400000000000008" + } } + }, + "stripe":{ + "simpleCardNumber": "5555555555554444", + "insufficientFundsCard": "4000000000009995", + "threeDSecureAuthCard": "4000002760003184" } } \ No newline at end of file diff --git a/cypress/fixtures/urlList.js b/cypress/fixtures/urlList.js index 272c62578..7da736735 100644 --- a/cypress/fixtures/urlList.js +++ b/cypress/fixtures/urlList.js @@ -24,6 +24,7 @@ export const urlList = { shippingMethods: "shipping/", siteSettings: "site-settings/", staffMembers: "staff/", + stripeApiPaymentMethods: "https://api.stripe.com/v1/payment_methods", translations: "translations/", vouchers: "discounts/vouchers/", warehouses: "warehouses/", @@ -70,3 +71,6 @@ export const productTypeDetailsUrl = productTypeId => export const giftCardDetailsUrl = giftCardId => `${urlList.giftCards}${giftCardId}`; + +export const stripeConfirmationUrl = id => + `https://api.stripe.com/v1/payment_intents/${id}/confirm`; diff --git a/cypress/integration/configuration/plugins/adyen.js b/cypress/integration/configuration/plugins/adyen.js index 5988d716f..31e8823aa 100644 --- a/cypress/integration/configuration/plugins/adyen.js +++ b/cypress/integration/configuration/plugins/adyen.js @@ -42,7 +42,7 @@ filterTests({ definedTags: ["stagedOnly"] }, () => { deleteProductsStartsWith(startsWith); deleteShippingStartsWith(startsWith); cy.fixture("cards").then(cardsResp => { - paymentCards = cardsResp; + paymentCards = cardsResp.adyen; cardData = { clientData: paymentCards.clientData, encryptedExpiryMonth: paymentCards.encryptedExpiryMonth, diff --git a/cypress/integration/configuration/plugins/stripe.js b/cypress/integration/configuration/plugins/stripe.js new file mode 100644 index 000000000..5c736d49d --- /dev/null +++ b/cypress/integration/configuration/plugins/stripe.js @@ -0,0 +1,140 @@ +/// +/// + +import { + addShippingMethod, + completeCheckout, + createCheckout +} from "../../../support/api/requests/Checkout"; +import { getOrder } from "../../../support/api/requests/Order"; +import { confirmThreeDSecure } from "../../../support/api/requests/stripe"; +import { addStripePaymentAndGetConfirmationData } from "../../../support/api/utils/ordersUtils"; +import { createProductWithShipping } from "../../../support/api/utils/products/productsUtils"; +import { deleteShippingStartsWith } from "../../../support/api/utils/shippingUtils"; +import filterTests from "../../../support/filterTests"; + +filterTests({ definedTags: ["stagedOnly"] }, () => { + describe("Stripe payments", () => { + const startsWith = "Stripe-"; + const email = `example@example.com`; + + let address; + let defaultChannel; + let shippingMethod; + let variantsList; + let checkout; + let paymentCards; + let cardData; + + before(() => { + cy.clearSessionData().loginUserViaRequest(); + deleteShippingStartsWith(startsWith); + cy.fixture("cards").then(({ stripe }) => { + paymentCards = stripe; + cardData = { + publicKey: paymentCards.publicApiKey, + cvc: 123, + expMonth: 10, + expYear: 50 + }; + }); + createProductWithShipping({ name: startsWith }).then(values => { + address = values.address; + defaultChannel = values.defaultChannel; + shippingMethod = values.shippingMethod; + variantsList = values.variantsList; + }); + }); + + beforeEach(() => { + cy.clearSessionData().loginUserViaRequest(); + createCheckout({ + channelSlug: defaultChannel.slug, + email, + variantsList, + address, + billingAddress: address, + auth: "token" + }) + .then(({ checkout: checkoutResp }) => { + checkout = checkoutResp; + addShippingMethod(checkout.id, shippingMethod.id); + }) + .then(({ checkout: checkoutResp }) => { + checkout = checkoutResp; + }); + }); + + it("should purchase products with simple card", () => { + const simpleCard = cardData; + simpleCard.cardNumber = paymentCards.simpleCardNumber; + addStripePaymentAndGetConfirmationData({ + card: simpleCard, + checkoutId: checkout.id, + amount: checkout.totalPrice.gross.amount + }) + .then(() => { + completeCheckout(checkout.id); + }) + .then(({ order }) => { + getOrder(order.id); + }) + .then(order => { + expect(order.paymentStatus).to.eq("FULLY_CHARGED"); + }); + }); + + it("should not purchase products with card with insufficient funds", () => { + const simpleCard = cardData; + simpleCard.cardNumber = paymentCards.insufficientFundsCard; + addStripePaymentAndGetConfirmationData({ + card: simpleCard, + checkoutId: checkout.id, + amount: checkout.totalPrice.gross.amount + }).then(resp => { + expect(resp.body.error.code).to.equal("card_declined"); + }); + }); + + it("should purchase products with 3D secure card", () => { + const threeDSecureCard = cardData; + threeDSecureCard.cardNumber = paymentCards.threeDSecureAuthCard; + addStripePaymentAndGetConfirmationData({ + card: threeDSecureCard, + checkoutId: checkout.id, + amount: checkout.totalPrice.gross.amount + }) + .then(resp => { + confirmThreeDSecure(resp.body.next_action.redirect_to_url.url); + }) + .then(() => { + completeCheckout(checkout.id); + }) + .then(({ order }) => { + getOrder(order.id); + }) + .then(order => { + expect(order.paymentStatus).to.eq("FULLY_CHARGED"); + }); + }); + + it("should not purchase product when 3D secure not pass", () => { + const threeDSecureCard = cardData; + threeDSecureCard.cardNumber = paymentCards.threeDSecureAuthCard; + addStripePaymentAndGetConfirmationData({ + card: threeDSecureCard, + checkoutId: checkout.id, + amount: checkout.totalPrice.gross.amount + }) + .then(resp => { + confirmThreeDSecure(resp.body.next_action.redirect_to_url.url, false); + }) + .then(() => { + completeCheckout(checkout.id); + }) + .then(({ order }) => { + expect(order).to.not.be.ok; + }); + }); + }); +}); diff --git a/cypress/integration/products/images.js b/cypress/integration/products/images.js index 77870e638..60a3d4ab1 100644 --- a/cypress/integration/products/images.js +++ b/cypress/integration/products/images.js @@ -67,6 +67,7 @@ filterTests({ definedTags: ["all"] }, () => { it("Should upload saved image", () => { const name = "CyImages"; + cy.clearSessionData().loginUserViaRequest(); loginDeleteProductsAndCreateNewOneWithNewDataAndDefaultChannel({ name }) .then(product => { cy.visit(productDetailsUrl(product.id)) diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js index 24e49f4ec..82ca68a36 100644 --- a/cypress/plugins/index.js +++ b/cypress/plugins/index.js @@ -26,6 +26,8 @@ module.exports = async (on, config) => { config.env.APP_MOUNT_URI = process.env.APP_MOUNT_URI; config.env.mailHogUrl = process.env.CYPRESS_MAILHOG; config.env.SHOP = await getShopInfo(process.env); + config.env.STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY; + config.env.STRIPE_PUBLIC_KEY = process.env.STRIPE_PUBLIC_KEY; on("before:browser:launch", (browser = {}, launchOptions) => { launchOptions.args.push("--proxy-bypass-list=<-loopback>"); diff --git a/cypress/support/api/requests/Checkout.js b/cypress/support/api/requests/Checkout.js index 6ba9f75d0..8ce36a91f 100644 --- a/cypress/support/api/requests/Checkout.js +++ b/cypress/support/api/requests/Checkout.js @@ -69,6 +69,7 @@ export function addShippingMethod(checkoutId, shippingMethodId) { field } checkout{ + id shippingMethod{ id name @@ -107,6 +108,7 @@ export function addPayment({ checkoutId, gateway, token, amount }) { gateway: "${gateway}" ${tokenLine} ${amountLine} + returnUrl: "https://qa.storefront.staging.saleor.cloud/checkout/payment-confirm" }){ paymentErrors{ field diff --git a/cypress/support/api/requests/stripe.js b/cypress/support/api/requests/stripe.js new file mode 100644 index 000000000..4c39c872e --- /dev/null +++ b/cypress/support/api/requests/stripe.js @@ -0,0 +1,106 @@ +import { stripeConfirmationUrl, urlList } from "../../../fixtures/urlList"; +import { getValueWithDefault } from "./utils/Utils"; + +const stripeAuthBearer = `Bearer ${Cypress.env("STRIPE_SECRET_KEY")}`; +const stripePublicKey = Cypress.env("STRIPE_PUBLIC_KEY"); + +export function getPaymentMethodStripeId({ + cardNumber, + cvc, + expMonth, + expYear +}) { + return cy.request({ + url: urlList.stripeApiPaymentMethods, + method: "POST", + form: true, + body: { + type: "card", + "card[number]": cardNumber, + "card[cvc]": cvc, + "card[exp_month]": expMonth, + "card[exp_year]": expYear, + pasted_fields: "number", + key: stripePublicKey + }, + headers: { + Authorization: stripeAuthBearer + } + }); +} + +export function sendConfirmationToStripe(paymentMethodId, confirmationId) { + return cy.request({ + method: "POST", + url: stripeConfirmationUrl(confirmationId), + form: true, + failOnStatusCode: false, + headers: { + Authorization: stripeAuthBearer + }, + body: { + payment_method: paymentMethodId, + return_url: Cypress.config().baseUrl, + webauthn_uvpa_available: "true", + spc_eligible: "false", + key: stripePublicKey + } + }); +} + +export function confirmThreeDSecure(nextActionUrl, withSuccess = true) { + let returnUrl; + const paRes = getValueWithDefault(withSuccess, "success", "failure"); + + return cy + .request(nextActionUrl) + .then(resp => { + const { body } = new DOMParser().parseFromString(resp.body, "text/html"); + const formUrl = body.querySelector('[id="form"]').getAttribute("action"); + const source = body + .querySelector('[name="source"]') + .getAttribute("value"); + returnUrl = body + .querySelector('[name="return_url"]') + .getAttribute("value"); + const amount = body + .querySelector('[name="amount"]') + .getAttribute("value"); + const currency = body + .querySelector('[name="currency"]') + .getAttribute("value"); + const usage = body.querySelector('[name="usage"]').getAttribute("value"); + + const url = `${formUrl}?source=${source}&livemode=false&type=three_d_secure&pass_through=&return_url=${returnUrl}&amount=${amount}¤cy= + ${currency}&usage=${usage}`; + cy.request(url); + }) + .then(() => { + cy.request({ + url: returnUrl, + method: "POST", + form: true, + body: { + PaRes: "success", + MD: "" + } + }); + }) + .then(resp => { + const { body } = new DOMParser().parseFromString(resp.body, "text/html"); + const formUrl = body.querySelector('[id="form"]').getAttribute("action"); + const merchant = body + .querySelector('[name="merchant"]') + .getAttribute("value"); + cy.request({ + url: formUrl, + method: "POST", + form: true, + body: { + PaRes: paRes, + MD: "", + Merchant: merchant + } + }); + }); +} diff --git a/cypress/support/api/utils/ordersUtils.js b/cypress/support/api/utils/ordersUtils.js index 4f1e0def2..ccf63ae99 100644 --- a/cypress/support/api/utils/ordersUtils.js +++ b/cypress/support/api/utils/ordersUtils.js @@ -1,5 +1,9 @@ import * as checkoutRequest from "../requests/Checkout"; import * as orderRequest from "../requests/Order"; +import { + getPaymentMethodStripeId, + sendConfirmationToStripe +} from "../requests/stripe"; import { createProductInChannel } from "./products/productsUtils"; export function createWaitingForCaptureOrder({ @@ -172,6 +176,14 @@ export function addAdyenPayment(checkoutId, amount) { amount }); } +export function addStripePayment(checkoutId, amount, token) { + return checkoutRequest.addPayment({ + checkoutId, + gateway: "saleor.payments.stripe", + amount, + token + }); +} export function createAndCompleteCheckoutWithoutShipping({ channelSlug, @@ -226,3 +238,25 @@ export function createOrderWithNewProduct({ }) .then(({ order, checkout }) => ({ order, checkout, variantsList })); } + +export function addStripePaymentAndGetConfirmationData({ + card, + checkoutId, + amount +}) { + let paymentMethodId; + + return getPaymentMethodStripeId(card) + .then(resp => { + paymentMethodId = resp.body.id; + addStripePayment(checkoutId, amount, resp.body.id); + }) + .then(() => { + checkoutRequest.completeCheckout(checkoutId); + }) + .then(resp => { + const confirmationData = JSON.parse(resp.confirmationData); + sendConfirmationToStripe(paymentMethodId, confirmationData.id, false); + }) + .then(resp => resp); +} diff --git a/cypress/support/api/utils/products/productsUtils.js b/cypress/support/api/utils/products/productsUtils.js index 545229c9a..a591a39b1 100644 --- a/cypress/support/api/utils/products/productsUtils.js +++ b/cypress/support/api/utils/products/productsUtils.js @@ -10,6 +10,7 @@ import { import { deleteAttributesStartsWith } from "../attributes/attributeUtils"; import { deleteCollectionsStartsWith } from "../catalog/collectionsUtils"; import { getDefaultChannel } from "../channelsUtils"; +import { createShipping } from "../shippingUtils"; export function createProductInChannel({ name, @@ -107,15 +108,15 @@ export function deleteProductsStartsWith(startsWith) { ); } -export function loginDeleteProductsAndCreateNewOneWithNewDataAndDefaultChannel({ +export function deleteProductsAndCreateNewOneWithNewDataAndDefaultChannel({ name, - description = name + description = name, + warehouseId }) { let defaultChannel; let collection; let attribute; - cy.clearSessionData().loginUserViaRequest(); deleteProductsStartsWith(name); deleteCollectionsStartsWith(name); return getDefaultChannel() @@ -136,8 +137,57 @@ export function loginDeleteProductsAndCreateNewOneWithNewDataAndDefaultChannel({ channelId: defaultChannel.id, name, collectionId: collection.id, - description + description, + warehouseId }); }) - .then(({ product: productResp }) => productResp); + .then(({ product, variantsList }) => ({ product, variantsList })); +} + +export function createProductWithShipping({ name }) { + let address; + let warehouse; + let shippingMethod; + let defaultChannel; + let shippingZone; + + return cy + .fixture("addresses") + .then(addresses => { + address = addresses.usAddress; + getDefaultChannel(); + }) + .then(channelResp => { + defaultChannel = channelResp; + createShipping({ + channelId: defaultChannel.id, + name, + address, + price: 10 + }); + }) + .then( + ({ + warehouse: warehouseResp, + shippingZone: shippingZoneResp, + shippingMethod: shippingMethodResp + }) => { + warehouse = warehouseResp; + shippingMethod = shippingMethodResp; + shippingZone = shippingZoneResp; + deleteProductsAndCreateNewOneWithNewDataAndDefaultChannel({ + name, + warehouseId: warehouse.id + }); + } + ) + .then(({ variantsList, product }) => ({ + variantsList, + product, + warehouse, + shippingZone, + defaultChannel, + shippingMethod, + address + })); }