diff --git a/.circleci/config.yml b/.circleci/config.yml index bf01d7daf5..64ac505008 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -59,6 +59,14 @@ jobs: - prep_environment - run: "bundle exec fastlane main_tests" + ringcon-only: + executor: mac-executor + + steps: + - prep_environment + - prep_bundler_carthage + - run: "bundle exec fastlane ringcon-only" + ui-and-analyze-tests: executor: mac-executor @@ -123,6 +131,7 @@ jobs: - run: "./stripe3ds2-support/ci_scripts/run_analyzer.sh" workflows: + version: 2 build-and-test: jobs: - ci-builds @@ -142,3 +151,13 @@ workflows: - linting-tests - install-tests - threeds2-tests + e2e-only: + triggers: + - schedule: + cron: "56 * * * *" # at 56 minutes every hour + filters: + branches: + only: + - private + jobs: + - ringcon-only diff --git a/Stripe.xcodeproj/project.pbxproj b/Stripe.xcodeproj/project.pbxproj index f515b7ace2..1bfb030e5b 100644 --- a/Stripe.xcodeproj/project.pbxproj +++ b/Stripe.xcodeproj/project.pbxproj @@ -241,6 +241,7 @@ 3185126C252FE67E008C0C57 /* StripeTests-Prefix.pch in Headers */ = {isa = PBXBuildFile; fileRef = 3185126B252FE67E008C0C57 /* StripeTests-Prefix.pch */; }; 31851272252FE770008C0C57 /* STPBlocks.h in Headers */ = {isa = PBXBuildFile; fileRef = 31851271252FE770008C0C57 /* STPBlocks.h */; }; 3186371D25D1B51B00B31CF6 /* STPPaymentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 317ABDE725117C9E00CC59EF /* STPPaymentHandler.swift */; }; + 31875B5C25DC63C800884BE0 /* STPE2ETest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31875B5B25DC63C800884BE0 /* STPE2ETest.swift */; }; 319490592513CC6200AD8F0B /* STPImageLibrary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 317ABDD725117C9D00CC59EF /* STPImageLibrary.swift */; }; 319490612514041300AD8F0B /* STPBankSelectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 317ABD9825117C9900CC59EF /* STPBankSelectionViewController.swift */; }; 3194906625140BEF00AD8F0B /* STPCoreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 317ABDC725117C9C00CC59EF /* STPCoreViewController.swift */; }; @@ -931,6 +932,7 @@ 318321E525C8910E00D96469 /* STPPaymentCardTextField+SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "STPPaymentCardTextField+SwiftUI.swift"; sourceTree = ""; }; 3185126B252FE67E008C0C57 /* StripeTests-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "StripeTests-Prefix.pch"; sourceTree = ""; }; 31851271252FE770008C0C57 /* STPBlocks.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPBlocks.h; sourceTree = ""; }; + 31875B5B25DC63C800884BE0 /* STPE2ETest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPE2ETest.swift; sourceTree = ""; }; 3194CF5B2314869400E1940F /* STPPaymentMethodFPXTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPPaymentMethodFPXTest.m; sourceTree = ""; }; 3194CF5D231487A100E1940F /* STPFPXBankBrandTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPFPXBankBrandTest.m; sourceTree = ""; }; 31C26219255F887A000C5B50 /* Stripe-umbrella.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Stripe-umbrella.h"; sourceTree = SOURCE_ROOT; }; @@ -1831,6 +1833,7 @@ 3111C3BF252BC79400207E32 /* STPPushProvisioningDetailsFunctionalTest.swift */, B640DB1922C69C01003C8810 /* STPSetupIntentFunctionalTest.m */, C1D7B5241E36C70D002181F5 /* STPSourceFunctionalTest.m */, + 31875B5B25DC63C800884BE0 /* STPE2ETest.swift */, ); name = Functional; sourceTree = ""; @@ -2314,6 +2317,7 @@ 3111C5B7252BF66500207E32 /* STPColorUtilsTest.swift in Sources */, C17D24EE1E37DBAC005CB188 /* STPSourceTest.m in Sources */, 3111C5FA252D1DC000207E32 /* STPShippingMethodsViewControllerLocalizationTests.swift in Sources */, + 31875B5C25DC63C800884BE0 /* STPE2ETest.swift in Sources */, B69CABB9246DCB620081B1EF /* STPPaymentHandlerFunctionalTest.m in Sources */, 31C5B87C252E859300A481A7 /* STPPaymentConfigurationTest.m in Sources */, 31C5B87E252E869D00A481A7 /* StripeErrorTest.swift in Sources */, diff --git a/Tests/Tests/STPE2ETest.swift b/Tests/Tests/STPE2ETest.swift new file mode 100644 index 0000000000..1c9971f413 --- /dev/null +++ b/Tests/Tests/STPE2ETest.swift @@ -0,0 +1,118 @@ +// +// STPE2ETest.swift +// StripeiOS Tests +// +// Created by David Estes on 2/16/21. +// Copyright © 2021 Stripe, Inc. All rights reserved. +// + +import XCTest +import Stripe + +class STPE2ETest: XCTestCase { + let E2ETestTimeout : TimeInterval = 120 + + struct E2EExpectation { + var amount: Int + var currency: String + var accountID: String + } + + class E2EBackend { + static let backendAPIURL = URL(string: "https://stp-e2e.glitch.me")! + + func createPaymentIntent(completion: @escaping (STPPaymentIntentParams, E2EExpectation) -> Void) { + requestAPI("create_pi", method: "POST") { (json) in + let paymentIntentClientSecret = json["paymentIntent"] as! String + let expectedAmount = json["expectedAmount"] as! Int + let expectedCurrency = json["expectedCurrency"] as! String + let expectedAccountID = json["expectedAccountID"] as! String + let publishableKey = json["publishableKey"] as! String + STPAPIClient.shared.publishableKey = publishableKey + completion(STPPaymentIntentParams(clientSecret: paymentIntentClientSecret), E2EExpectation(amount: expectedAmount, currency: expectedCurrency, accountID: expectedAccountID)) + } + } + + func fetchPaymentIntent(id: String, completion: @escaping (E2EExpectation) -> Void) { + requestAPI("fetch_pi", queryItems: [URLQueryItem(name: "pi", value: id)]) { (json) in + let resultAmount = json["amount"] as! Int + let resultCurrency = json["currency"] as! String + let resultAccountID = json["on_behalf_of"] as! String + completion(E2EExpectation(amount: resultAmount, currency: resultCurrency, accountID: resultAccountID)) + } + } + + private func requestAPI(_ resource: String, method: String = "GET", queryItems: [URLQueryItem] = [], completion: @escaping ([String : Any]) -> Void) { + var url = URLComponents(url: Self.backendAPIURL.appendingPathComponent(resource), resolvingAgainstBaseURL: false)! + url.queryItems = queryItems + var request = URLRequest(url: url.url!) + request.httpMethod = method + let task = URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in + guard let data = data, + let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String : Any] else { + XCTFail("Did not receive valid JSON response from E2E server. \(String(describing: error))") + return + } + DispatchQueue.main.async { + completion(json) + } + }) + task.resume() + } + } + + static let TestPM: STPPaymentMethodParams = { + let testCard = STPPaymentMethodCardParams() + testCard.number = "4242424242424242" + testCard.expYear = 2050 + testCard.expMonth = 12 + testCard.cvc = "123" + return STPPaymentMethodParams(card: testCard, billingDetails: nil, metadata: nil) + }() + + // MARK: LOG.04.01c + // In this test, a PaymentIntent object is created from an example merchant backend, + // confirmed by the iOS SDK, and then retrieved to validate that the original amount, + // currency, and merchant are the same as the original inputs. + func testE2E() throws { + continueAfterFailure = false + let backend = E2EBackend() + let createPI = XCTestExpectation(description: "Create PaymentIntent") + let fetchPIBackend = XCTestExpectation(description: "Fetch and check PaymentIntent via backend") + let fetchPIClient = XCTestExpectation(description: "Fetch and check PaymentIntent via client") + let confirmPI = XCTestExpectation(description: "Confirm PaymentIntent") + + // Create a PaymentIntent + backend.createPaymentIntent { (pip, expected) in + createPI.fulfill() + + // Confirm the PaymentIntent using a test card + pip.paymentMethodParams = STPE2ETest.TestPM + STPAPIClient.shared.confirmPaymentIntent(with: pip) { (confirmedPI, confirmError) in + confirmPI.fulfill() + XCTAssertNotNil(confirmedPI) + XCTAssertNil(confirmError) + + // Check the PI information using the backend + backend.fetchPaymentIntent(id: pip.stripeId!) { (expectationResult) in + XCTAssertEqual(expectationResult.amount, expected.amount) + XCTAssertEqual(expectationResult.accountID, expected.accountID) + XCTAssertEqual(expectationResult.currency, expected.currency) + fetchPIBackend.fulfill() + } + + // Check the PI information using the client + STPAPIClient.shared.retrievePaymentIntent(withClientSecret: pip.clientSecret) { (fetchedPI, fetchError) in + XCTAssertNil(fetchError) + let fetchedPI = fetchedPI! + XCTAssertEqual(fetchedPI.status, .succeeded) + XCTAssertEqual(fetchedPI.amount, expected.amount) + XCTAssertEqual(fetchedPI.currency, expected.currency) + // The client can't check the "on_behalf_of" field, so we check it via the merchant test above. + fetchPIClient.fulfill() + } + } + } + wait(for: [createPI, confirmPI, fetchPIBackend, fetchPIClient], timeout: E2ETestTimeout) + } +} diff --git a/ci_scripts/e2e_test.sh b/ci_scripts/e2e_test.sh new file mode 100755 index 0000000000..21c6273c64 --- /dev/null +++ b/ci_scripts/e2e_test.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +function info { + echo "[$(basename "${0}")] [INFO] ${1}" +} + +function die { + echo "[$(basename "${0}")] [ERROR] ${1}" + exit 1 +} + +# Verify xcpretty is installed +if ! command -v xcpretty > /dev/null; then + if [[ "${CI}" != "true" ]]; then + die "Please install xcpretty: https://github.com/supermarin/xcpretty#installation" + fi + + info "Installing xcpretty..." + gem install xcpretty --no-document || die "Executing \`gem install xcpretty\` failed" +fi + +# Install test dependencies +info "Installing test dependencies..." + +carthage bootstrap --platform iOS --configuration Release --no-use-binaries --cache-builds --use-xcframeworks +carthage_exit_code="$?" + +if [[ "${carthage_exit_code}" != 0 ]]; then + die "Executing carthage failed with status code: ${carthage_exit_code}" +fi + +info "Executing E2E tests (iPhone 8 @ iOS 13.7)..." + +xcodebuild test \ + -workspace "Stripe.xcworkspace" \ + -scheme "StripeiOS" \ + -configuration "Debug" \ + -sdk "iphonesimulator" \ + -destination "platform=iOS Simulator,name=iPhone 8,OS=13.7" \ + -only-testing:"StripeiOS Tests/STPE2ETest" \ + | xcpretty +exit_code="${PIPESTATUS[0]}" + +if [[ "${exit_code}" != 0 ]]; then + die "xcodebuild exited with non-zero status code: ${exit_code}" +fi + +info "All good!" diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 34f4f7c87c..cd6dcb6e82 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -98,6 +98,12 @@ platform :ios do end end + lane :e2e_only do + Dir.chdir("..") do + sh("./ci_scripts/e2e_test.sh") + end + end + lane :analyze do Dir.chdir("..") do sh("./ci_scripts/run_analyzer.sh")