├── Cartfile.private ├── Cartfile.resolved ├── certificates └── development.p12 ├── scripts ├── stop_server.sh ├── images │ └── xcode-scheme-test-pre-actions.png ├── start_server.sh ├── check_dependencies.sh ├── install_deps.sh ├── carthage_xcode12 ├── build_children.sh ├── swiftlint.sh ├── build.sh └── release.sh ├── PactConsumerSwift.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── PactConsumerSwift iOS.xcscheme │ ├── PactConsumerSwift tvOS.xcscheme │ └── PactConsumerSwift macOS.xcscheme ├── Sources ├── Reporting │ ├── ErrorReporter.swift │ └── ErrorReporterXCTest.swift ├── Supporting Files │ ├── PactConsumerSwift.h │ └── Info.plist ├── Matcher.swift ├── Interaction.swift ├── MockService.swift └── PactVerificationService.swift ├── Brewfile ├── Package.swift ├── PactConsumerObjCTests ├── OCAnimalServiceClient.h ├── OCAnimalServiceClient.m └── PactObjectiveCTests.m ├── .codecov.yml ├── Tests ├── Mocks │ ├── ErrorCapture.swift │ └── RubyPactMockServiceStub.swift ├── Supporting Files │ └── Info.plist ├── PactConsumerSwiftTests │ ├── PactSSLSpecs.swift │ ├── AnimalServiceClient.swift │ └── PactSpecs.swift ├── MatcherSpec.swift ├── InteractionSpec.swift └── MockServiceSpec.swift ├── .swiftlint.yml ├── .gitignore ├── LICENSE ├── PactConsumerSwift.podspec ├── CONTRIBUTING.md ├── .github └── workflows │ ├── pull_request.yml │ ├── pull_request_xcode11_6.yml │ └── build.yml ├── CHANGELOG.md └── README.md /Cartfile.private: -------------------------------------------------------------------------------- 1 | github "Quick/Nimble" ~> 9.0 2 | github "Quick/Quick" ~> 3.0 -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "Quick/Nimble" "v9.0.0" 2 | github "Quick/Quick" "v3.0.0" 3 | -------------------------------------------------------------------------------- /certificates/development.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiUS/pact-consumer-swift/HEAD/certificates/development.p12 -------------------------------------------------------------------------------- /scripts/stop_server.sh: -------------------------------------------------------------------------------- 1 | 2 | #pact-mock-service stop 3 | 4 | ps -ef | grep pact-mock-service | grep -v grep | awk '{print $2}' | xargs kill -------------------------------------------------------------------------------- /scripts/images/xcode-scheme-test-pre-actions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiUS/pact-consumer-swift/HEAD/scripts/images/xcode-scheme-test-pre-actions.png -------------------------------------------------------------------------------- /PactConsumerSwift.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Sources/Reporting/ErrorReporter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public typealias FileString = StaticString 4 | 5 | public protocol ErrorReporter { 6 | func reportFailure(_ message: String) 7 | func reportFailure(_ message: String, file: FileString, line: UInt) 8 | } 9 | -------------------------------------------------------------------------------- /Brewfile: -------------------------------------------------------------------------------- 1 | tap 'pact-foundation/pact-ruby-standalone', 'https://github.com/pact-foundation/homebrew-pact-ruby-standalone.git' 2 | tap 'thii/xcbeautify', 'https://github.com/thii/xcbeautify.git' 3 | brew 'carthage' 4 | brew 'swiftlint' 5 | brew 'pact-ruby-standalone' 6 | brew 'xcbeautify' 7 | -------------------------------------------------------------------------------- /PactConsumerSwift.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /scripts/start_server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir -p "${SRCROOT}/tmp" 4 | 5 | pact-mock-service start --pact-specification-version 2.0.0 --log "${SRCROOT}/tmp/pact.log" --pact-dir "${SRCROOT}/tmp/pacts" -p 1234 6 | pact-mock-service start --ssl --pact-specification-version 2.0.0 --log "${SRCROOT}/tmp/pact-ssl.log" --pact-dir "${SRCROOT}/tmp/pacts-ssl" -p 2345 7 | -------------------------------------------------------------------------------- /Sources/Reporting/ErrorReporterXCTest.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | 4 | public class ErrorReporterXCTest: ErrorReporter { 5 | 6 | public func reportFailure(_ message: String) { 7 | XCTFail(message, file: #file, line: #line) 8 | } 9 | 10 | public func reportFailure(_ message: String, file: FileString, line: UInt) { 11 | XCTFail(message, file: file, line: line) 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "PactConsumerSwift", 7 | platforms: [ 8 | .macOS(.v10_10), .iOS(.v9), .tvOS(.v9) 9 | ], 10 | products: [ 11 | .library(name: "PactConsumerSwift", targets: ["PactConsumerSwift"]) 12 | ], 13 | targets: [ 14 | .target( 15 | name: "PactConsumerSwift", 16 | path: "./Sources" 17 | ) 18 | ] 19 | ) 20 | -------------------------------------------------------------------------------- /PactConsumerObjCTests/OCAnimalServiceClient.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface Animal : NSObject 4 | @property (nonatomic, strong) NSString* name; 5 | @property (nonatomic, strong) NSString* dob; 6 | @property (nonatomic, strong) NSNumber* legs; 7 | @end 8 | 9 | @interface OCAnimalServiceClient : NSObject 10 | - (id)initWithBaseUrl:(NSString *)url; 11 | - (Animal *)getAlligator; 12 | - (NSArray *)findAnimalsLiving:(NSString *)living; 13 | @end 14 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | notify: 3 | require_ci_to_pass: yes 4 | 5 | coverage: 6 | precision: 2 7 | round: down 8 | range: "70...100" 9 | 10 | status: 11 | project: yes 12 | patch: yes 13 | changes: no 14 | 15 | ignore: 16 | - Tests/* 17 | 18 | parsers: 19 | gcov: 20 | branch_detection: 21 | conditional: yes 22 | loop: yes 23 | method: no 24 | macro: no 25 | 26 | comment: 27 | layout: "header, diff" 28 | behavior: default 29 | require_changes: no 30 | -------------------------------------------------------------------------------- /scripts/check_dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | PACT_MOCK_SERVICE=pact-mock-service 5 | 6 | echo "Test Dependencies check:" 7 | 8 | if which $PACT_MOCK_SERVICE >/dev/null; then 9 | echo "- $PACT_MOCK_SERVICE: installed" 10 | else 11 | echo "- $PACT_MOCK_SERVICE: not found!" 12 | echo "" 13 | echo "error: $PACT_MOCK_SERVICE is not installed!" 14 | echo "See https://github.com/pact-foundation/pact-ruby-standalone or use Homebrew tap \"pact-foundation/pact-ruby-standalone\"" 15 | exit 1 16 | fi -------------------------------------------------------------------------------- /Sources/Supporting Files/PactConsumerSwift.h: -------------------------------------------------------------------------------- 1 | // 2 | // PactConsumerSwift.h 3 | // PactConsumerSwift 4 | // 5 | // Created by Marko Justinek on 22/9/17. 6 | // 7 | 8 | #import 9 | 10 | //! Project version number for PactConsumerSwift. 11 | FOUNDATION_EXPORT double PactConsumerSwift_VersionNumber; 12 | 13 | //! Project version string for PactConsumerSwift. 14 | FOUNDATION_EXPORT const unsigned char PactConsumerSwift_VersionString[]; 15 | 16 | // In this header, you should import all the public headers of your framework using statements like #import 17 | 18 | 19 | -------------------------------------------------------------------------------- /Tests/Mocks/ErrorCapture.swift: -------------------------------------------------------------------------------- 1 | import PactConsumerSwift 2 | 3 | struct ErrorReceived { 4 | var message: String 5 | var file: FileString? 6 | var line: UInt? 7 | } 8 | 9 | class ErrorCapture: ErrorReporter { 10 | public var message: ErrorReceived? 11 | 12 | func reportFailure(_ message: String) { 13 | self.message = ErrorReceived(message: message, file: nil, line: nil) 14 | } 15 | func reportFailure(_ message: String, file: FileString, line: UInt) { 16 | self.message = ErrorReceived(message: message, file: file, line: line) 17 | } 18 | 19 | func clear() { 20 | self.message = nil 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /scripts/install_deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | # GitHub Hosted runners are complaining about re-installing Swiftlint using brew. 5 | if [[ "$CI" == true ]]; then 6 | # Explicitly install only pact-ruby-standalone and xcbeautify, other tools are already installed on the GitHub hosted runner 7 | brew tap 'pact-foundation/pact-ruby-standalone', 'https://github.com/pact-foundation/homebrew-pact-ruby-standalone.git' 8 | brew tap 'thii/xcbeautify', 'https://github.com/thii/xcbeautify.git' 9 | brew install 'pact-ruby-standalone' 10 | brew install 'xcbeautify' 11 | else 12 | # Install dependencies using Brewfile 13 | brew update && brew bundle 14 | carthage checkout 15 | fi 16 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | excluded: 2 | - scripts 3 | - Carthage 4 | - certificates 5 | - Products/ 6 | included: 7 | - Sources/ 8 | opt_in_rules: 9 | - empty_count 10 | force_cast: 11 | severity: 12 | warning 13 | force_try: 14 | severity: warning 15 | type_body_length: 16 | - 300 # warning 17 | - 400 # error 18 | line_length: 19 | warning: 128 20 | error: 156 21 | file_length: 22 | warning: 500 23 | error: 1200 24 | type_name: 25 | min_length: 4 26 | max_length: 27 | warning: 40 28 | error: 50 29 | identifier_name: 30 | min_length: 31 | error: 4 32 | excluded: 33 | - id 34 | - URL 35 | - url 36 | - min 37 | - max 38 | - key 39 | - GET 40 | - PUT 41 | - get 42 | - put 43 | reporter: "xcode" 44 | -------------------------------------------------------------------------------- /Tests/Supporting Files/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Sources/Matcher.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @objc 4 | open class Matcher: NSObject { 5 | 6 | @objc 7 | public class func term(matcher: String, generate: String) -> [String: Any] { 8 | return [ "json_class": "Pact::Term", 9 | "data": [ 10 | "generate": generate, 11 | "matcher": [ 12 | "json_class": "Regexp", 13 | "o": 0, 14 | "s": matcher] 15 | ] ] 16 | } 17 | 18 | @objc 19 | public class func somethingLike(_ value: Any) -> [String: Any] { 20 | return [ 21 | "json_class": "Pact::SomethingLike", 22 | "contents": value 23 | ] 24 | } 25 | 26 | @objc 27 | public class func eachLike(_ value: Any, min: Int = 1) -> [String: Any] { 28 | return [ 29 | "json_class": "Pact::ArrayLike", 30 | "contents": value, 31 | "min": min 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/Supporting Files/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | .idea 4 | # Xcode 5 | build/ 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata 15 | *.xccheckout 16 | profile 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | *.swp 22 | .build 23 | 24 | # Bundler 25 | .bundle 26 | Carthage 27 | 28 | #pact 29 | log 30 | tmp 31 | .pid 32 | 33 | # We recommend against adding the Pods directory to your .gitignore. However 34 | # you should judge for yourself, the pros and cons are mentioned at: 35 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 36 | # 37 | # Note: if you ignore the Pods directory, make sure to uncomment 38 | # `pod install` in .travis.yml 39 | # 40 | # Pods/ 41 | 42 | #scan 43 | test_output 44 | /.swiftpm 45 | /Brewfile.lock.json 46 | -------------------------------------------------------------------------------- /scripts/carthage_xcode12: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # carthage.sh 4 | # Usage example: ./carthage.sh build --platform iOS 5 | # 6 | # Sources: 7 | # - https://github.com/Carthage/Carthage/issues/3019#issuecomment-665136323 8 | # - https://github.com/Carthage/Carthage/issues/3019#issuecomment-734415287 9 | 10 | set -euo pipefail 11 | 12 | xcconfig=$(mktemp /tmp/static.xcconfig.XXXXXX) 13 | trap 'rm -f "$xcconfig"' INT TERM HUP EXIT 14 | 15 | # For Xcode 12 make sure EXCLUDED_ARCHS is set to arm architectures otherwise 16 | # the build will fail on lipo due to duplicate architectures. 17 | for simulator in iphonesimulator appletvsimulator; do 18 | echo "EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_${simulator}__NATIVE_ARCH_64_BIT_x86_64__XCODE_1200 = arm64 arm64e armv7 armv7s armv6 armv8" >> $xcconfig 19 | done 20 | echo 'EXCLUDED_ARCHS = $(inherited) $(EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_$(PLATFORM_NAME)__NATIVE_ARCH_64_BIT_$(NATIVE_ARCH_64_BIT)__XCODE_$(XCODE_VERSION_MAJOR))' >> $xcconfig 21 | 22 | export XCODE_XCCONFIG_FILE="$xcconfig" 23 | cat $XCODE_XCCONFIG_FILE 24 | carthage "$@" 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 DiUS 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /PactConsumerSwift.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "PactConsumerSwift" 3 | s.module_name = "PactConsumerSwift" 4 | s.version = "0.10.2" 5 | s.summary = "A Swift / ObjeciveC DSL for creating pacts." 6 | s.license = { :type => 'MIT' } 7 | 8 | s.description = <<-DESC 9 | This library provides a Swift / Objective C DSL for creating Consumer [Pacts](http://pact.io). 10 | 11 | Implements [Pact Specification v2](https://github.com/pact-foundation/pact-specification/tree/version-2), 12 | including [flexible matching](http://docs.pact.io/documentation/matching.html). 13 | DESC 14 | 15 | s.homepage = "https://github.com/DiUS/pact-consumer-swift" 16 | 17 | s.author = { "andrewspinks" => "andrewspinks@gmail.com", "markojustinek" => "mjustinek@dius.com.au" } 18 | 19 | s.ios.deployment_target = '9.0' 20 | s.tvos.deployment_target = '9.0' 21 | s.osx.deployment_target = '10.10' 22 | 23 | s.source = { :git => "https://github.com/DiUS/pact-consumer-swift.git", :tag => "v#{s.version}" } 24 | s.source_files = 'Sources/**/*.swift' 25 | s.resources = 'scripts/start_server.sh', 'scripts/stop_server.sh' 26 | s.requires_arc = true 27 | s.frameworks = 'XCTest' 28 | 29 | s.pod_target_xcconfig = { 30 | 'ENABLE_BITCODE' => 'NO' 31 | } 32 | 33 | s.swift_versions = ['4.2', '5.0'] 34 | end 35 | -------------------------------------------------------------------------------- /scripts/build_children.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eu 4 | set -o pipefail 5 | 6 | REPO=${GITHUB_REPOSITORY:-""} 7 | 8 | if [[ "${REPO}" != "DiUS/pact-consumer-swift" ]]; then 9 | echo "[INFO]: 👮 - Not the source repository DiUS/pact-consumer-swift... Skipping this step." 10 | exit 0 11 | fi 12 | 13 | TRAVISCI_AUTH_TOKEN=${AUTH_TOKEN:-"invalid_travis_ci_token"} 14 | GITHUB_AUTH_TOKEN=${GH_BUILD_CHILDREN_TOKEN:-"invalid_github_token"} 15 | COMMIT_MESSAGE=${COMMIT_MESSAGE:="repository dispatched"} 16 | CLEAN_MESSAGE=`echo "${COMMIT_MESSAGE}" | head -1` 17 | 18 | function triggerTravisCIBuild { 19 | curl -s -X POST --silent --show-error --fail \ 20 | -H "Content-Type: application/json" \ 21 | -H "Accept: application/json" \ 22 | -H "Travis-API-Version: 3" \ 23 | -H "Authorization: token ${TRAVISCI_AUTH_TOKEN}" \ 24 | -d "{\"request\": {\"branch\":\"master\"}}" \ 25 | https://api.travis-ci.org/repo/$1/requests 26 | } 27 | 28 | function triggerGitHubActionsBuild { 29 | curl -X POST --silent --show-error --fail \ 30 | https://api.github.com/repos/$1/dispatches \ 31 | -H "Accept: application/vnd.github.everest-preview+json" \ 32 | -H "Content-Type: application/json" \ 33 | -u ${GITHUB_AUTH_TOKEN} \ 34 | --data "{\"event_type\":\"triggered ${CLEAN_MESSAGE}\"}" 35 | } 36 | 37 | # GitHub Actions 38 | # - and must be lower-case! 39 | echo "Triggering GitHub Actions" 40 | triggerGitHubActionsBuild surpher/pactswiftpmexample 41 | triggerGitHubActionsBuild surpher/pactmacosexample 42 | 43 | # TravisCI 44 | echo "Triggering TravisCI builds" 45 | triggerTravisCIBuild andrewspinks%2FPactObjectiveCExample 46 | triggerTravisCIBuild andrewspinks%2FPactSwiftExample 47 | -------------------------------------------------------------------------------- /scripts/swiftlint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -eu 3 | 4 | # Set default swiftlint command and config file 5 | BINARYFILE=swiftlint 6 | CONFIGFILE="${SRCROOT}/.swiftlint.yml" 7 | 8 | WARNING="warning" 9 | ERROR="error" 10 | 11 | throw_not_found() { 12 | echo "$1: \"$2\" - not found. $3" 13 | if [ "$1" == "$WARNING" ]; then 14 | exit 0 15 | fi 16 | exit 1 17 | } 18 | 19 | display_usage() { 20 | echo "Usage:\n\nswiftlint.sh [arguments]\n" 21 | echo "Available arguments:" 22 | echo " -h, --help\t\t\t# Show this output" 23 | echo " -b, --binary=PATH_TO_BINARY\t# Path to Swiftlint binary file (default: swiftlint" 24 | echo " -c, --config=PATH_TO_FILE\t# Path to Swiftlint configuration file (default: \${SRCROOT}/.swiftlint.yml)" 25 | } 26 | 27 | # Get arguments for binary and configuration file 28 | while [ "$#" -gt 0 ]; do 29 | case "$1" in 30 | -b) BINARYFILE="$2"; shift 2;; 31 | -c) CONFIGFILE="$2"; shift 2;; 32 | -h) display_usage; exit 0;; 33 | 34 | --binary=*) BINARYFILE="${1#*=}"; shift 1;; 35 | --config=*) CONFIGFILE="${1#*=}"; shift 1;; 36 | --help) display_usage; exit0;; 37 | --binary|--config) echo "$1 requires an argument" >&2; exit 1;; 38 | 39 | -*) echo "unknown option: $1" >&2; exit 1;; 40 | *) handle_argument "$1"; shift 1;; 41 | esac 42 | done 43 | 44 | # Check whether Swiftlint binary exists 45 | if ! which $BINARYFILE &> /dev/null; then 46 | throw_not_found $WARNING $BINARYFILE "See https://github.com/realm/SwiftLint" 47 | fi 48 | 49 | # Check whether Swiftlint Config file exists 50 | if [ ! -f "${CONFIGFILE}" ]; then 51 | throw_not_found $ERROR "${CONFIGFILE}" "" 52 | fi 53 | 54 | # All hunky dory, run linting 55 | $BINARYFILE --config "${CONFIGFILE}" --strict 56 | 57 | # Finish the script 58 | exit 0 59 | -------------------------------------------------------------------------------- /Tests/PactConsumerSwiftTests/PactSSLSpecs.swift: -------------------------------------------------------------------------------- 1 | import Quick 2 | import Nimble 3 | import PactConsumerSwift 4 | 5 | class PactSwiftSSLSpec: QuickSpec { 6 | override func spec() { 7 | var animalMockService: MockService? 8 | var animalServiceClient: AnimalServiceClient? 9 | 10 | describe("tests fulfilling all expected interactions over HTTPS") { 11 | beforeEach { 12 | let pactVerificationService = PactVerificationService( 13 | url: "https://localhost", 14 | port: 2345, 15 | allowInsecureCertificates: true 16 | ) 17 | 18 | animalMockService = MockService( 19 | provider: "Animal Service", 20 | consumer: "Animal Consumer Swift", 21 | pactVerificationService: pactVerificationService 22 | ) 23 | 24 | animalServiceClient = AnimalServiceClient(baseUrl: animalMockService!.baseUrl) 25 | } 26 | 27 | it("gets an alligator") { 28 | animalMockService!.given("an alligator exists") 29 | .uponReceiving("a request for all alligators") 30 | .withRequest(method:.GET, path: "/alligators") 31 | .willRespondWith(status: 200, 32 | headers: ["Content-Type": "application/json"], 33 | body: [ ["name": "Mary", "type": "alligator"] ]) 34 | 35 | //Run the tests 36 | animalMockService!.run(timeout: 10000) { (testComplete) -> Void in 37 | animalServiceClient!.getAlligators( { (alligators) in 38 | expect(alligators[0].name).to(equal("Mary")) 39 | testComplete() 40 | }, failure: { (error) in 41 | testComplete() 42 | }) 43 | } 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | if [[ -z "${PROJECT_NAME}" ]]; then 5 | if [[ "$*" == "macos" ]] || [[ "$*" == "macOS" ]]; then 6 | PROJECT_NAME="PactConsumerSwift.xcodeproj"; 7 | DESTINATION="arch=x86_64"; 8 | SCHEME="PactConsumerSwift macOS"; 9 | CARTHAGE_PLATFORM="macos"; 10 | elif [[ "$*" == "tvos" ]] || [[ "$*" == "tvos" ]]; then 11 | PROJECT_NAME="PactConsumerSwift.xcodeproj"; 12 | DESTINATION="OS=14.3,name=Apple TV 4K (at 1080p)"; 13 | SCHEME="PactConsumerSwift tvOS"; 14 | CARTHAGE_PLATFORM="tvos"; 15 | else 16 | PROJECT_NAME="PactConsumerSwift.xcodeproj"; 17 | DESTINATION="OS=14.4,name=iPhone 12 Pro"; 18 | SCHEME="PactConsumerSwift iOS"; 19 | CARTHAGE_PLATFORM="iOS"; 20 | fi 21 | fi 22 | 23 | SCRIPTS_DIR="${BASH_SOURCE[0]%/*}" 24 | 25 | # Build Carthage dependencies 26 | $SCRIPTS_DIR/carthage_xcode12 update --platform $CARTHAGE_PLATFORM 27 | 28 | # Carthage - debug 29 | echo "#### Testing scheme: $SCHEME, with destination: $DESTINATION ####" 30 | echo "Running: \"set -o pipefail && xcodebuild clean test -project $PROJECT_NAME -scheme "$SCHEME" -destination "$DESTINATION" GCC_INSTRUMENT_PROGRAM_FLOW_ARCS=YES GCC_GENERATE_TEST_COVERAGE_FILES=YES | xcbeautify\"" 31 | set -o pipefail && xcodebuild clean test -project $PROJECT_NAME -scheme "$SCHEME" -destination "$DESTINATION" -configuration Debug GCC_INSTRUMENT_PROGRAM_FLOW_ARCS=YES GCC_GENERATE_TEST_COVERAGE_FILES=YES | xcbeautify 32 | 33 | # Carthage - release 34 | echo "#### Testing RELEASE configuration for scheme: $SCHEME, with destination: $DESTINATION ####" 35 | echo "Running: \"set -o pipefail && xcodebuild clean test -project $PROJECT_NAME -scheme "$SCHEME" -destination "$DESTINATION" -configuration Release GCC_INSTRUMENT_PROGRAM_FLOW_ARCS=YES ONLY_ACTIVE_ARCH=YES GCC_GENERATE_TEST_COVERAGE_FILES=YES ENABLE_TESTABILITY=YES | xcbeautify\"" 36 | set -o pipefail && xcodebuild clean test -project $PROJECT_NAME -scheme "$SCHEME" -destination "$DESTINATION" -configuration Release GCC_INSTRUMENT_PROGRAM_FLOW_ARCS=YES ONLY_ACTIVE_ARCH=YES GCC_GENERATE_TEST_COVERAGE_FILES=YES ENABLE_TESTABILITY=YES | xcbeautify -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Pact Consumer Swift project 2 | 3 | ### Prepare your development environment 4 | The Pact Consumer Swift library is using Carthage and Swift Package Manager to manage library dependencies. You should install [Carthage](https://github.com/Carthage/Carthage) using [Homebrew](https://brew.sh), then download and build the dependencies using `carthage bootstrap` (Carthage). 5 | 6 | Required build dependencies can be installed by running: 7 | ``` 8 | ./scripts/install_deps.sh 9 | ``` 10 | 11 | ### Running tests with default destination 12 | ``` 13 | ./scripts/build.sh 14 | ``` 15 | defaults to iOS 11 on iPhone 8 16 | 17 | ### Running specific platform tests 18 | iOS 10.3 on iPhone 7: 19 | ``` 20 | xcodebuild -project PactConsumerSwift.xcodeproj -scheme "PactConsumerSwift iOS" -destination "OS=10.3,name=iPhone 7" -configuration Debug ONLY_ACTIVE_ARCH=NO ENABLE_TESTABILITY=YES test | xcbeautify; 21 | ``` 22 | 23 | for macOS: 24 | ``` 25 | xcodebuild -project PactConsumerSwift.xcodeproj -scheme "PactConsumerSwift macOS" -destination "arch=x86_64" -configuration Debug ONLY_ACTIVE_ARCH=NO ENABLE_TESTABILITY=YES test | xcbeautify; 26 | ``` 27 | 28 | for tvOS: 29 | ``` 30 | xcodebuild -project PactConsumerSwift.xcodeproj -scheme PactConsumerSwift tvOS -destination OS=11.0,name=Apple TV 4K (at 1080p) -configuration Debug ONLY_ACTIVE_ARCH=NO ENABLE_TESTABILITY=YES test | xcbeautify; 31 | ``` 32 | 33 | #### Test CocoaPods 34 | ``` 35 | pod spec lint PactConsumerSwift.podspec --allow-warnings 36 | ``` 37 | 38 | Getting set up to work with [CocoaPods](https://guides.cocoapods.org/making/getting-setup-with-trunk.html). 39 | 40 | #### Test Carthage 41 | ``` 42 | carthage build --no-skip-current --platform iOS,macOS,tvOS 43 | ``` 44 | 45 | #### Test Swift Package Manager 46 | ``` 47 | ./scripts/start_server.sh && 48 | swift build -c debug | release && 49 | swift test && 50 | ./scripts/stop_server.sh 51 | ``` 52 | For more information, see the [.travis.yml](/.travis.yml) configuration. 53 | 54 | ### TravisCI 55 | Builds on [Travis CI](https://travis-ci.org/DiUS/pact-consumer-swift/) with pipeline configuration in [.travis.yml](/.travis.yml). 56 | 57 | ### Release 58 | [release.sh](/scripts/release.sh) script helps with updating the Changelog, tagging the commit with a release version, and publish to Cocoapods. 59 | ``` 60 | ./scripts/release.sh 0.7.0 "Bugfix Release" 61 | ``` 62 | -------------------------------------------------------------------------------- /Tests/MatcherSpec.swift: -------------------------------------------------------------------------------- 1 | import Quick 2 | import Nimble 3 | import PactConsumerSwift 4 | 5 | class MatcherSpec: QuickSpec { 6 | 7 | override func spec() { 8 | 9 | describe("regex matcher") { 10 | let regex = "\\d{16}" 11 | let placeholder = "1111222233334444" 12 | let subject = Matcher.term(matcher: regex, generate: placeholder) 13 | 14 | it("sets the json_class") { 15 | let className = subject["json_class"] as? String 16 | expect(className).to(equal("Pact::Term")) 17 | } 18 | 19 | it("sets the regular expression to match against") { 20 | let data = subject["data"] as? [String: AnyObject] 21 | let matcher = data?["matcher"] as? [String: AnyObject] 22 | let matcherRegex = matcher?["s"] as! String 23 | 24 | expect(matcherRegex).to(equal(regex)) 25 | } 26 | 27 | it("sets the default value to return") { 28 | let data = subject["data"] as? [String: AnyObject] 29 | let generate = data?["generate"] as! String 30 | 31 | expect(generate).to(equal(placeholder)) 32 | } 33 | } 34 | 35 | describe("type matcher") { 36 | let subject = Matcher.somethingLike(1234) 37 | 38 | it("sets the json_class") { 39 | let className = subject["json_class"] as? String 40 | expect(className).to(equal("Pact::SomethingLike")) 41 | } 42 | 43 | it("sets the regular expressiont to match against") { 44 | let likeThis = subject["contents"] as? Int 45 | expect(likeThis).to(equal(1234)) 46 | } 47 | } 48 | 49 | describe("eachLike matcher") { 50 | let arrayItem = ["blah": "blow"] 51 | var subject = Matcher.eachLike(arrayItem) 52 | 53 | it("sets the json_class") { 54 | let className = subject["json_class"] as? String 55 | expect(className).to(equal("Pact::ArrayLike")) 56 | } 57 | 58 | it("sets array content to match against") { 59 | let contents = subject["contents"] as? [String: String] 60 | expect(contents).to(equal(arrayItem)) 61 | } 62 | 63 | it("defaults the minimum required in array to 1") { 64 | let min = subject["min"] as? Int 65 | expect(min).to(equal(1)) 66 | } 67 | 68 | it("allows min to be specified") { 69 | subject = Matcher.eachLike(arrayItem, min: 4) 70 | 71 | let min = subject["min"] as? Int 72 | expect(min).to(equal(4)) 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Tests/InteractionSpec.swift: -------------------------------------------------------------------------------- 1 | import Quick 2 | import Nimble 3 | @testable import PactConsumerSwift 4 | 5 | class InteractionSpec: QuickSpec { 6 | override func spec() { 7 | var interaction: Interaction? 8 | beforeEach { interaction = Interaction() } 9 | 10 | describe("json payload"){ 11 | context("pact state") { 12 | it("includes provider state in the payload") { 13 | let payload = interaction!.given("state of awesomeness").uponReceiving("an important request is received").payload() 14 | 15 | expect(payload["providerState"] as! String?) == "state of awesomeness" 16 | expect(payload["description"] as! String?) == "an important request is received" 17 | } 18 | } 19 | 20 | context("no provider state") { 21 | it("doesn not include provider state when not included") { 22 | let payload = interaction!.uponReceiving("an important request is received").payload() 23 | 24 | expect(payload["providerState"]).to(beNil()) 25 | } 26 | } 27 | 28 | context("request") { 29 | let method: PactHTTPMethod = .PUT 30 | let path = "/path" 31 | let headers = ["header": "value"] 32 | let body = "blah" 33 | 34 | it("returns expected request with specific headers and body") { 35 | let payload = interaction!.withRequest(method: method, path: path, headers: headers, body: body).payload() 36 | 37 | let request = payload["request"] as! [String: AnyObject] 38 | expect(request["path"] as! String?) == path 39 | expect(request["method"] as! String?).to(equal("put")) 40 | expect(request["headers"] as! [String: String]?).to(equal(headers)) 41 | expect(request["body"] as! String?).to(equal(body)) 42 | } 43 | 44 | it("returns expected request without body and headers") { 45 | let payload = interaction!.withRequest(method:method, path: path).payload() 46 | 47 | let request = payload["request"] as! [String: AnyObject] 48 | expect(request["path"] as! String?) == path 49 | expect(request["method"] as! String?).to(equal("put")) 50 | expect(request["headers"] as! [String: String]?).to(beNil()) 51 | expect(request["body"] as! String?).to(beNil()) 52 | } 53 | } 54 | 55 | context("response") { 56 | let statusCode = 200 57 | let headers = ["header": "value"] 58 | let body = "body" 59 | 60 | it("returns expected response with specific headers and body") { 61 | let payload = interaction!.willRespondWith(status: statusCode, headers: headers, body: body).payload() 62 | 63 | let request = payload["response"] as! [String: AnyObject] 64 | expect(request["status"] as! Int?) == statusCode 65 | expect(request["headers"] as! [String: String]?).to(equal(headers)) 66 | expect(request["body"] as! String?).to(equal(body)) 67 | } 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request 2 | 3 | env: 4 | PROJECT_NAME: "PactConsumerSwift.xcodeproj" 5 | 6 | on: [pull_request, workflow_dispatch] 7 | 8 | jobs: 9 | test_xcodebuild: 10 | name: Run tests 11 | runs-on: macOS-10.15 12 | 13 | strategy: 14 | fail-fast: true 15 | matrix: 16 | include: 17 | - scheme: "PactConsumerSwift iOS" 18 | destination: "platform=iOS Simulator,name=iPhone 12 Pro" 19 | carthage_platform: ios 20 | - scheme: "PactConsumerSwift macOS" 21 | destination: "arch=x86_64" 22 | carthage_platform: macos 23 | - scheme: "PactConsumerSwift tvOS" 24 | destination: "OS=14.3,name=Apple TV 4K (at 1080p)" 25 | carthage_platform: tvos 26 | 27 | env: 28 | SCHEME: ${{ matrix.scheme }} 29 | DESTINATION: ${{ matrix.destination }} 30 | CARTHAGE_PLATFORM: ${{ matrix.carthage_platform }} 31 | 32 | steps: 33 | - name: Checkout repository 34 | uses: actions/checkout@v2 35 | with: 36 | submodules: recursive 37 | 38 | - name: "Cache dependencies" 39 | uses: actions/cache@v2 40 | with: 41 | path: Carthage/build 42 | key: ${{ runner.os }}-pact-${{ hashFiles('**/Cartfile.resolved') }} 43 | restore-keys: | 44 | ${{ runner.os }}-pact- 45 | ${{ runner.os }}- 46 | 47 | - name: Use Xcode 12.4 48 | run: sudo xcode-select -switch /Applications/Xcode_12.4.app 49 | 50 | - name: Prepare the tools 51 | run: | 52 | scripts/install_deps.sh 53 | 54 | - name: "Run tests" 55 | run: | 56 | scripts/build.sh 57 | 58 | test_spm: 59 | name: Test for SPM 60 | runs-on: macOS-10.15 61 | 62 | steps: 63 | - name: Checkout repository 64 | uses: actions/checkout@v2 65 | with: 66 | submodules: recursive 67 | 68 | - name: Swift build 69 | run: | 70 | swift build 71 | echo "⚠️ Skipping \"swift test\" as no test target is defined in \"Package.swift\" (https://github.com/DiUS/pact-consumer-swift/commit/229f35d63a547f492c7ba9e177ac8d7b685e7a7f)" 72 | 73 | test_carthage: 74 | name: "Test Carthage dependency" 75 | runs-on: macOS-10.15 76 | 77 | steps: 78 | - name: Checkout repository 79 | uses: actions/checkout@v2 80 | with: 81 | submodules: recursive 82 | 83 | - name: "Cache dependencies" 84 | uses: actions/cache@v2 85 | with: 86 | path: Carthage/build 87 | key: ${{ runner.os }}-pact-${{ hashFiles('**/Cartfile.resolved') }} 88 | restore-keys: | 89 | ${{ runner.os }}-pact- 90 | ${{ runner.os }}- 91 | 92 | - name: Use Xcode 12.4 93 | run: sudo xcode-select -switch /Applications/Xcode_12.4.app 94 | 95 | - name: Prepare Tools 96 | run: | 97 | scripts/install_deps.sh 98 | 99 | - name: Carthage build 100 | run: | 101 | scripts/carthage_xcode12 build --no-skip-current --platform "ios,macos,tvos" 102 | -------------------------------------------------------------------------------- /PactConsumerObjCTests/OCAnimalServiceClient.m: -------------------------------------------------------------------------------- 1 | #import "OCAnimalServiceClient.h" 2 | 3 | @implementation Animal 4 | 5 | @end 6 | 7 | @interface OCAnimalServiceClient () 8 | @property (nonatomic, strong) NSString *baseUrl; 9 | @end 10 | 11 | @implementation OCAnimalServiceClient 12 | 13 | - (id)initWithBaseUrl:(NSString *)url { 14 | if (self = [super init]) { 15 | self.baseUrl = url; 16 | } 17 | return self; 18 | } 19 | 20 | - (Animal *)getAlligator { 21 | NSString *url = [NSString stringWithFormat:@"%@/%@", self.baseUrl, @"alligator"]; 22 | NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url] 23 | cachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData 24 | timeoutInterval:10]; 25 | 26 | [request setHTTPMethod: @"GET"]; 27 | NSError *requestError; 28 | NSURLResponse *urlResponse = nil; 29 | 30 | NSData *response = [self sendSynchronousRequest:request returningResponse:&urlResponse error:&requestError]; 31 | 32 | 33 | 34 | NSError *error; 35 | NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:response 36 | options:kNilOptions 37 | error:&error]; 38 | 39 | Animal * animal = [[Animal alloc] init]; 40 | animal.name = dic[@"name"]; 41 | animal.dob = dic[@"dateOfBirth"]; 42 | animal.legs = dic[@"legs"]; 43 | 44 | return animal; 45 | } 46 | 47 | - (NSArray *)findAnimalsLiving:(NSString *)living { 48 | NSString *query = [NSString stringWithFormat:@"animals?live=%@", living]; 49 | NSString *url = [NSString stringWithFormat:@"%@/%@", self.baseUrl, query]; 50 | NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url] 51 | cachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData 52 | timeoutInterval:10]; 53 | 54 | [request setHTTPMethod: @"GET"]; 55 | NSError *requestError; 56 | NSURLResponse *urlResponse = nil; 57 | 58 | NSData *response = [self sendSynchronousRequest:request returningResponse:&urlResponse error:&requestError]; 59 | 60 | NSError *error; 61 | NSArray *array = [NSJSONSerialization JSONObjectWithData:response options:kNilOptions error:&error]; 62 | 63 | NSMutableArray *animals = [[NSMutableArray alloc] init]; 64 | 65 | for(NSDictionary *dic in array) { 66 | Animal * animal = [[Animal alloc] init]; 67 | animal.name = dic[@"name"]; 68 | animal.legs = dic[@"legs"]; 69 | [animals addObject:animal]; 70 | } 71 | 72 | return animals; 73 | } 74 | 75 | - (NSData *)sendSynchronousRequest:(NSURLRequest *)request returningResponse:(NSURLResponse **)response error:(NSError **)error { 76 | 77 | NSError __block *err = NULL; 78 | NSData __block *data; 79 | BOOL __block reqProcessed = false; 80 | NSURLResponse __block *resp; 81 | NSURLSession * sess = [NSURLSession sharedSession]; 82 | 83 | [[sess dataTaskWithRequest:request 84 | completionHandler:^(NSData * _Nullable _data, NSURLResponse * _Nullable _resp, NSError * _Nullable _err) { 85 | resp = _resp; 86 | err = _err; 87 | data = _data; 88 | reqProcessed = true; 89 | }] resume]; 90 | 91 | while (!reqProcessed) { [NSThread sleepForTimeInterval: 0]; } 92 | 93 | *response = resp; 94 | *error = err; 95 | return data; 96 | } 97 | 98 | @end 99 | -------------------------------------------------------------------------------- /.github/workflows/pull_request_xcode11_6.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request (Xcode 11.6) 2 | 3 | env: 4 | PROJECT_NAME: "PactConsumerSwift.xcodeproj" 5 | 6 | on: [pull_request, workflow_dispatch] 7 | 8 | jobs: 9 | test_xcodebuild: 10 | name: Run tests 11 | runs-on: macOS-10.15 12 | 13 | strategy: 14 | fail-fast: true 15 | matrix: 16 | include: 17 | - scheme: "PactConsumerSwift iOS" 18 | destination: "platform=iOS Simulator,name=iPhone 8,OS=13.6" 19 | carthage_platform: ios 20 | - scheme: "PactConsumerSwift macOS" 21 | destination: "arch=x86_64" 22 | carthage_platform: macos 23 | - scheme: "PactConsumerSwift tvOS" 24 | destination: "OS=13.4,name=Apple TV 4K (at 1080p)" 25 | carthage_platform: tvos 26 | 27 | env: 28 | SCHEME: ${{ matrix.scheme }} 29 | DESTINATION: ${{ matrix.destination }} 30 | CARTHAGE_PLATFORM: ${{ matrix.carthage_platform }} 31 | 32 | steps: 33 | - name: Checkout repository 34 | uses: actions/checkout@v2 35 | with: 36 | submodules: recursive 37 | 38 | - name: Use Xcode 11.6 39 | run: sudo xcode-select -switch /Applications/Xcode_11.6.app 40 | 41 | - name: "Cache dependencies" 42 | uses: actions/cache@v2 43 | with: 44 | path: Carthage/build 45 | key: ${{ runner.os }}-pact-${{ hashFiles('**/Cartfile.resolved') }} 46 | restore-keys: | 47 | ${{ runner.os }}-pact- 48 | ${{ runner.os }}- 49 | 50 | - name: Prepare the tools 51 | run: | 52 | scripts/install_deps.sh 53 | 54 | - name: "Run tests" 55 | run: | 56 | ./scripts/carthage_xcode12 update --platform $CARTHAGE_PLATFORM 57 | set -o pipefail && xcodebuild clean test -project $PROJECT_NAME -scheme "$SCHEME" -destination "$DESTINATION" -configuration Debug GCC_INSTRUMENT_PROGRAM_FLOW_ARCS=YES GCC_GENERATE_TEST_COVERAGE_FILES=YES 58 | 59 | - name: "Upload code coverage" 60 | run: | 61 | bash <(curl -s https://codecov.io/bash) -J 'PactConsumerSwift' 62 | 63 | test_spm: 64 | name: Test for SPM compatibility 65 | runs-on: macOS-10.15 66 | 67 | steps: 68 | - name: Checkout repository 69 | uses: actions/checkout@v2 70 | with: 71 | submodules: recursive 72 | 73 | - name: Use Xcode 11.6 74 | run: sudo xcode-select -switch /Applications/Xcode_11.6.app 75 | 76 | - name: Swift build 77 | run: | 78 | swift build 79 | echo "⚠️ Skipping \"swift test\" as no test target is defined in \"Package.swift\" (https://github.com/DiUS/pact-consumer-swift/commit/229f35d63a547f492c7ba9e177ac8d7b685e7a7f)" 80 | 81 | test_carthage: 82 | name: "Test Carthage dependency" 83 | runs-on: macOS-10.15 84 | 85 | steps: 86 | - name: Checkout repository 87 | uses: actions/checkout@v2 88 | with: 89 | submodules: recursive 90 | 91 | - name: Use Xcode 11.6 92 | run: sudo xcode-select -switch /Applications/Xcode_11.6.app 93 | 94 | - name: "Cache dependencies" 95 | uses: actions/cache@v2 96 | with: 97 | path: Carthage/build 98 | key: ${{ runner.os }}-pact-${{ hashFiles('**/Cartfile.resolved') }} 99 | restore-keys: | 100 | ${{ runner.os }}-pact- 101 | ${{ runner.os }}- 102 | 103 | - name: Prepare Tools 104 | run: | 105 | scripts/install_deps.sh 106 | 107 | - name: Carthage build 108 | run: | 109 | carthage build --no-skip-current --platform "ios,macos,tvos" 110 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | env: 4 | PROJECT_NAME: "PactConsumerSwift.xcodeproj" 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | - master 11 | workflow_dispatch: 12 | 13 | jobs: 14 | test_xcodebuild: 15 | name: Run tests 16 | runs-on: macOS-10.15 17 | 18 | strategy: 19 | fail-fast: true 20 | matrix: 21 | include: 22 | - scheme: "PactConsumerSwift iOS" 23 | destination: "platform=iOS Simulator,name=iPhone 12 Pro" 24 | carthage_platform: ios 25 | - scheme: "PactConsumerSwift macOS" 26 | destination: "arch=x86_64" 27 | carthage_platform: macos 28 | - scheme: "PactConsumerSwift tvOS" 29 | destination: "OS=14.3,name=Apple TV 4K (at 1080p)" 30 | carthage_platform: tvos 31 | 32 | env: 33 | SCHEME: ${{ matrix.scheme }} 34 | DESTINATION: ${{ matrix.destination }} 35 | CARTHAGE_PLATFORM: ${{ matrix.carthage_platform }} 36 | 37 | steps: 38 | - name: Checkout repository 39 | uses: actions/checkout@v2 40 | with: 41 | submodules: recursive 42 | 43 | - name: "Cache dependencies" 44 | uses: actions/cache@v2 45 | with: 46 | path: Carthage/build 47 | key: ${{ runner.os }}-pact-${{ hashFiles('**/Cartfile.resolved') }} 48 | restore-keys: | 49 | ${{ runner.os }}-pact- 50 | ${{ runner.os }}- 51 | 52 | - name: Use Xcode 12.4 53 | run: sudo xcode-select -switch /Applications/Xcode_12.4.app 54 | 55 | - name: Prepare the tools 56 | run: | 57 | scripts/install_deps.sh 58 | 59 | - name: "Run tests" 60 | run: | 61 | scripts/build.sh 62 | 63 | - name: "Upload code coverage" 64 | run: | 65 | bash <(curl -s https://codecov.io/bash) -J 'PactConsumerSwift' 66 | 67 | test_spm: 68 | name: Test for SPM 69 | runs-on: macOS-10.15 70 | 71 | steps: 72 | - name: Checkout repository 73 | uses: actions/checkout@v2 74 | with: 75 | submodules: recursive 76 | 77 | - name: Use Xcode 12.4 78 | run: sudo xcode-select -switch /Applications/Xcode_12.4.app 79 | 80 | - name: Swift build 81 | run: | 82 | swift build 83 | echo "⚠️ Skipping \"swift test\" as no test target is defined in \"Package.swift\" (https://github.com/DiUS/pact-consumer-swift/commit/229f35d63a547f492c7ba9e177ac8d7b685e7a7f)" 84 | 85 | test_carthage: 86 | name: "Test Carthage dependency" 87 | runs-on: macOS-10.15 88 | 89 | steps: 90 | - name: Checkout repository 91 | uses: actions/checkout@v2 92 | with: 93 | submodules: recursive 94 | 95 | - name: "Cache dependencies" 96 | uses: actions/cache@v2 97 | with: 98 | path: Carthage/build 99 | key: ${{ runner.os }}-pact-${{ hashFiles('**/Cartfile.resolved') }} 100 | restore-keys: | 101 | ${{ runner.os }}-pact- 102 | ${{ runner.os }}- 103 | 104 | - name: Use Xcode 12.4 105 | run: sudo xcode-select -switch /Applications/Xcode_12.4.app 106 | 107 | - name: Prepare Tools 108 | run: | 109 | scripts/install_deps.sh 110 | 111 | - name: Carthage build 112 | run: | 113 | scripts/carthage_xcode12 build --no-skip-current --platform "ios,macos,tvos" 114 | 115 | after_success: 116 | needs: [test_xcodebuild, test_spm, test_carthage] 117 | name: "Build example projects" 118 | runs-on: ubuntu-20.04 119 | 120 | steps: 121 | - name: Checkout repository 122 | uses: actions/checkout@v2 123 | with: 124 | submodules: recursive 125 | 126 | - name: Trigger Demo Project builds 127 | run: | 128 | scripts/build_children.sh 129 | env: 130 | AUTH_TOKEN: ${{ secrets.AUTH_TOKEN }} 131 | GH_BUILD_CHILDREN_TOKEN: ${{ secrets.GH_BUILD_CHILDREN_TOKEN }} 132 | COMMIT_MESSAGE: ${{ github.event.head_commit.message }} 133 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | REMOTE_BRANCH=master 3 | POD_NAME=PactConsumerSwift 4 | PODSPEC=PactConsumerSwift.podspec 5 | RELEASE_NOTES=CHANGELOG.md 6 | 7 | POD=${COCOAPODS:-"pod"} 8 | 9 | function help { 10 | echo "Usage: release VERSION RELEASE_NAME DRY_RUN" 11 | echo 12 | echo "VERSION should be the version to release, should not include the 'v' prefix" 13 | echo "RELEASE_NAME should be the type of release 'Bugfix Release / Maintenance Release'" 14 | echo 15 | echo "FLAGS" 16 | echo " -d Dry run, won't push anything or publish cocoapods" 17 | echo 18 | echo " Example: ./scripts/release.sh 1.0.0 'Bugfix Release'" 19 | echo 20 | exit 2 21 | } 22 | 23 | function die { 24 | echo "[ERROR] $@" 25 | echo 26 | exit 1 27 | } 28 | 29 | if [ $# -lt 2 ]; then 30 | help 31 | fi 32 | 33 | VERSION=$1 34 | RELEASE_NAME=$2 35 | DRY_RUN=$3 36 | VERSION_TAG="v$VERSION" 37 | 38 | echo "-> Verifying Local Directory for Release" 39 | 40 | if [ -z "`which $POD`" ]; then 41 | die "Cocoapods is required to produce a release. Aborting." 42 | fi 43 | echo " > Cocoapods is installed" 44 | 45 | echo " > Is this a reasonable tag?" 46 | 47 | echo $VERSION_TAG | grep -q "^vv" 48 | if [ $? -eq 0 ]; then 49 | die "This tag ($VERSION) is an incorrect format. You should remove the 'v' prefix." 50 | fi 51 | 52 | echo $VERSION_TAG | grep -q -E "^v\d+\.\d+\.\d+(-\w+(\.\d)?)?\$" 53 | if [ $? -ne 0 ]; then 54 | die "This tag ($VERSION) is an incorrect format. It should be in 'v{MAJOR}.{MINOR}.{PATCH}(-{PRERELEASE_NAME}.{PRERELEASE_VERSION})' form." 55 | fi 56 | 57 | echo " > Is this version ($VERSION) unique?" 58 | git describe --exact-match "$VERSION_TAG" > /dev/null 2>&1 59 | if [ $? -eq 0 ]; then 60 | die "This tag ($VERSION) already exists. Aborting." 61 | else 62 | echo " > Yes, tag is unique" 63 | fi 64 | 65 | echo " > Generating release notes to $RELEASE_NOTES" 66 | cp $RELEASE_NOTES ${RELEASE_NOTES}.backup 67 | echo "# ${VERSION} - ${RELEASE_NAME}\n" > ${RELEASE_NOTES}.next 68 | LATEST_TAG=`git describe --match "v[0-9].*" --tags --abbrev=0 HEAD` 69 | git log --pretty='* %h - %s (%an, %ad)' ${LATEST_TAG}..HEAD . >> ${RELEASE_NOTES}.next 70 | cat $RELEASE_NOTES.next | cat - ${RELEASE_NOTES}.backup > ${RELEASE_NOTES} 71 | rm ${RELEASE_NOTES}.next 72 | rm ${RELEASE_NOTES}.backup 73 | git add $RELEASE_NOTES || { die "Failed to add ${RELEASE_NOTES} to INDEX"; } 74 | 75 | if [ ! -f "$PODSPEC" ]; then 76 | die "Cannot find podspec: $PODSPEC. Aborting." 77 | fi 78 | echo " > Podspec exists" 79 | 80 | # Verify cocoapods trunk ownership 81 | pod trunk me | grep -q "$POD_NAME" || die "You do not have access to pod repository $POD_NAME. Aborting." 82 | echo " > Verified ownership to $POD_NAME pod" 83 | 84 | 85 | echo "--- Releasing version $VERSION (tag: $VERSION_TAG)..." 86 | 87 | function restore_podspec { 88 | if [ -f "${PODSPEC}.backup" ]; then 89 | mv -f ${PODSPEC}{.backup,} 90 | fi 91 | } 92 | 93 | echo "-> Ensuring no differences to origin/$REMOTE_BRANCH" 94 | git fetch origin || die "Failed to fetch origin" 95 | git diff --quiet HEAD "origin/$REMOTE_BRANCH" || die "HEAD is not aligned to origin/$REMOTE_BRANCH. Cannot update version safely" 96 | 97 | 98 | echo "-> Setting podspec version" 99 | cat "$PODSPEC" | grep 's.version' | grep -q "\"$VERSION\"" 100 | SET_PODSPEC_VERSION=$? 101 | if [ $SET_PODSPEC_VERSION -eq 0 ]; then 102 | echo " > Podspec already set to $VERSION. Skipping." 103 | else 104 | sed -i.backup "s/s.version *= *\".*\"/s.version = \"$VERSION\"/g" "$PODSPEC" || { 105 | restore_podspec 106 | die "Failed to update version in podspec" 107 | } 108 | 109 | git add ${PODSPEC} || { restore_podspec; die "Failed to add ${PODSPEC} to INDEX"; } 110 | git commit -m "chore: Bumping version to $VERSION" || { restore_podspec; die "Failed to push updated version: $VERSION"; } 111 | fi 112 | 113 | echo "-> Tagging version" 114 | git tag "$VERSION_TAG" -F "$RELEASE_NOTES" || die "Failed to tag version" 115 | 116 | if [ -z "$DRY_RUN" ]; then 117 | echo "-> Pushing tag to origin" 118 | git push origin "$VERSION_TAG" || die "Failed to push tag '$VERSION_TAG' to origin" 119 | 120 | if [ $SET_PODSPEC_VERSION -ne 0 ]; then 121 | git push origin "$REMOTE_BRANCH" || die "Failed to push to origin" 122 | echo " > Pushed version to origin" 123 | fi 124 | 125 | echo 126 | echo "---------------- Released as $VERSION_TAG ----------------" 127 | echo 128 | 129 | echo 130 | echo "Pushing to pod trunk..." 131 | 132 | $POD trunk push "$PODSPEC" --allow-warnings 133 | else 134 | echo "-> Dry run specified, skipping push of new version" 135 | $POD spec lint "$PODSPEC" --allow-warnings 136 | fi 137 | 138 | rm ${PODSPEC}.backup -------------------------------------------------------------------------------- /Sources/Interaction.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @objc 4 | public enum PactHTTPMethod: Int { 5 | case OPTIONS, GET, HEAD, POST, PUT, PATCH, DELETE, TRACE, CONNECT 6 | } 7 | 8 | @objc 9 | public class Interaction: NSObject { 10 | private var providerState: String? 11 | private var testDescription: String = "" 12 | private var request: [String: Any] = [:] 13 | private var response: [String: Any] = [:] 14 | 15 | /// 16 | /// Define the providers state 17 | /// 18 | /// Use this method in the `Arrange` step of your Pact test. 19 | /// 20 | /// myMockService.given("a user exists") 21 | /// 22 | /// - Parameter providerState: A description of providers state 23 | /// - Returns: An `Interaction` object 24 | /// 25 | @discardableResult 26 | public func given(_ providerState: String) -> Interaction { 27 | self.providerState = providerState 28 | return self 29 | } 30 | 31 | /// 32 | /// Describe the request your provider will receive 33 | /// 34 | /// Use this method in the `Arrange` step of your Pact test. 35 | /// 36 | /// myMockService.given("a user exists") 37 | /// .uponReceiving("a request for users") 38 | /// 39 | /// - Parameter testDescription: A description of the request to the provider 40 | /// - Returns: An `Interaction` object 41 | /// 42 | @objc 43 | @discardableResult 44 | public func uponReceiving(_ testDescription: String) -> Interaction { 45 | self.testDescription = testDescription 46 | return self 47 | } 48 | 49 | /// 50 | /// Describe the request your consumer will send to your provider 51 | /// 52 | /// Use this method in the `Arrange` step of your Pact test. 53 | /// 54 | /// myMockService.given("a user exists") 55 | /// .uponReceiving("a request for users") 56 | /// .withRequest(method:.GET, path: "/users") 57 | /// 58 | /// - Parameter method: Enum of available HTTP methods 59 | /// - Parameter path: an object representing url path component 60 | /// - Parameter query: an object representing url query components 61 | /// - Parameter headers: Dictionary representing any headers in network request 62 | /// - Parameter body: An object representing the body of your network request 63 | /// - Returns: An `Interaction` object 64 | /// 65 | @objc(withRequestHTTPMethod: path: query: headers: body:) 66 | @discardableResult 67 | public func withRequest(method: PactHTTPMethod, 68 | path: Any, 69 | query: Any? = nil, 70 | headers: [String: Any]? = nil, 71 | body: Any? = nil) -> Interaction { 72 | request = ["method": httpMethod(method), "path": path] 73 | if let headersValue = headers { 74 | request["headers"] = headersValue 75 | } 76 | if let bodyValue = body { 77 | request["body"] = bodyValue 78 | } 79 | if let queryValue = query { 80 | request["query"] = queryValue 81 | } 82 | return self 83 | } 84 | 85 | /// 86 | /// Describe the response of your provider 87 | /// 88 | /// Use this method in the `Arrange` step of your Pact test. 89 | /// 90 | /// myMockService.given("a user exists") 91 | /// .uponReceiving("a request for users") 92 | /// .withRequest(method:.GET, path: "/users") 93 | /// .willRespondWith(status: 200, 94 | /// headers: [ /* ... */ ], 95 | /// body: [ /* ...DSL... */ ]) 96 | /// 97 | /// - Parameter status: The status code of your provider's response 98 | /// - Parameter headers: A Dictionary representing the return headers 99 | /// - Parameter body: An object representing the body of your Provider's response 100 | /// - Returns: An `Interaction` object 101 | /// 102 | @objc(willRespondWithHTTPStatus: headers: body:) 103 | @discardableResult 104 | public func willRespondWith(status: Int, 105 | headers: [String: Any]? = nil, 106 | body: Any? = nil) -> Interaction { 107 | response = ["status": status] 108 | if let headersValue = headers { 109 | response["headers"] = headersValue 110 | } 111 | if let bodyValue = body { 112 | response["body"] = bodyValue 113 | } 114 | return self 115 | } 116 | 117 | /// 118 | @objc 119 | func payload() -> [String: Any] { 120 | var payload: [String: Any] = ["description": testDescription, 121 | "request": request, 122 | "response": response ] 123 | if let providerState = providerState { 124 | payload["providerState"] = providerState 125 | } 126 | return payload 127 | } 128 | 129 | // MARK: - Private 130 | 131 | private func httpMethod(_ method: PactHTTPMethod) -> String { 132 | switch method { 133 | case .GET: 134 | return "get" 135 | case .HEAD: 136 | return "head" 137 | case .POST: 138 | return "post" 139 | case .PUT: 140 | return "put" 141 | case .PATCH: 142 | return "patch" 143 | case .DELETE: 144 | return "delete" 145 | case .TRACE: 146 | return "trace" 147 | case .CONNECT: 148 | return "connect" 149 | default: 150 | return "get" 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /Tests/Mocks/RubyPactMockServiceStub.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | @testable import PactConsumerSwift 3 | 4 | class VerifiableHttpStub { 5 | public var requestExecuted = false 6 | public var requestBody: String? 7 | 8 | public var responseBody: Data = Data() 9 | public var responseCode: Int = 0 10 | 11 | public init() { } 12 | 13 | public init(responseCode: Int, response: String) { 14 | self.responseCode = responseCode 15 | self.responseBody = response.data(using: .utf8) ?? Data() 16 | } 17 | } 18 | 19 | enum RubyMockServiceRequest: CaseIterable { 20 | case cleanInteractions 21 | case setupInteractions 22 | case verifyInteractions 23 | case writePact 24 | 25 | init?(request: URLRequest) { 26 | guard let mockRequest = RubyMockServiceRequest.allCases.filter({ 27 | request.httpMethod?.uppercased() == $0.route.method.uppercased() 28 | && request.url?.path == $0.route.path 29 | }).first else { 30 | return nil 31 | } 32 | self = mockRequest 33 | } 34 | 35 | private var route: PactVerificationService.Router { 36 | switch self { 37 | case .cleanInteractions: 38 | return .clean 39 | case .setupInteractions: 40 | return .setup([:]) 41 | case .verifyInteractions: 42 | return .verify 43 | case .writePact: 44 | return .write([:]) 45 | } 46 | } 47 | } 48 | 49 | class StubProtocol: URLProtocol { 50 | static var stubs: [RubyMockServiceRequest:VerifiableHttpStub] = [:] 51 | 52 | override class func canInit(with request: URLRequest) -> Bool { 53 | return true 54 | } 55 | 56 | override func startLoading() { 57 | guard let url = request.url else { fatalError("A request should always have an URL") } 58 | 59 | guard 60 | let mockRequest = RubyMockServiceRequest(request: request), 61 | let stub: VerifiableHttpStub = StubProtocol.stubs[mockRequest] else { 62 | // Nothing registered, just return a 200 63 | let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)! 64 | client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) 65 | client?.urlProtocolDidFinishLoading(self) 66 | return 67 | } 68 | 69 | StubProtocol.stubs[mockRequest]?.requestExecuted = true 70 | StubProtocol.stubs[mockRequest]?.requestBody = request.body 71 | 72 | let response = HTTPURLResponse(url: url, statusCode: stub.responseCode, httpVersion: nil, headerFields: nil)! 73 | client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) 74 | client?.urlProtocol(self, didLoad: stub.responseBody) 75 | client?.urlProtocolDidFinishLoading(self) 76 | } 77 | 78 | override func stopLoading() { } 79 | 80 | override class func canonicalRequest(for request: URLRequest) -> URLRequest { return request } 81 | 82 | } 83 | 84 | struct RubyPactMockServiceStub { 85 | var cleanStub: VerifiableHttpStub { StubProtocol.stubs[.cleanInteractions] ?? .init() } 86 | var setupInteractionsStub: VerifiableHttpStub { StubProtocol.stubs[.setupInteractions] ?? .init() } 87 | var verifyInteractionsStub: VerifiableHttpStub { StubProtocol.stubs[.verifyInteractions] ?? .init() } 88 | var writePactStub: VerifiableHttpStub { StubProtocol.stubs[.writePact] ?? .init() } 89 | 90 | let session: URLSession 91 | 92 | init() { 93 | let configuration = URLSessionConfiguration.ephemeral 94 | configuration.protocolClasses = [StubProtocol.self] 95 | self.session = URLSession.init(configuration: configuration) 96 | } 97 | 98 | @discardableResult 99 | func clean(responseCode: Int, 100 | response: String) -> RubyPactMockServiceStub { 101 | StubProtocol.stubs[.cleanInteractions] = .init(responseCode: responseCode, response: response) 102 | return self 103 | } 104 | 105 | @discardableResult 106 | func setupInteractions(responseCode: Int, 107 | response: String) -> RubyPactMockServiceStub { 108 | StubProtocol.stubs[.setupInteractions] = .init(responseCode: responseCode, response: response) 109 | return self 110 | } 111 | 112 | @discardableResult 113 | func verifyInteractions(responseCode: Int, 114 | response: String) -> RubyPactMockServiceStub { 115 | StubProtocol.stubs[.verifyInteractions] = .init(responseCode: responseCode, response: response) 116 | return self 117 | } 118 | 119 | @discardableResult 120 | func writePact(responseCode: Int, 121 | response: String) -> RubyPactMockServiceStub { 122 | StubProtocol.stubs[.writePact] = .init(responseCode: responseCode, response: response) 123 | return self 124 | } 125 | 126 | func reset() { 127 | StubProtocol.stubs = [:] 128 | } 129 | } 130 | 131 | extension URLRequest { 132 | var body: String? { 133 | guard let input = httpBodyStream else { return nil } 134 | 135 | var data = Data() 136 | 137 | input.open() 138 | defer { 139 | input.close() 140 | } 141 | 142 | let bufferSize = 1024 143 | let buffer = UnsafeMutablePointer.allocate(capacity: bufferSize) 144 | defer { 145 | buffer.deallocate() 146 | } 147 | while input.hasBytesAvailable { 148 | let read = input.read(buffer, maxLength: bufferSize) 149 | if read <= 0 { 150 | break 151 | } 152 | data.append(buffer, count: read) 153 | } 154 | 155 | return String(data: data, encoding: .utf8) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /PactConsumerSwift.xcodeproj/xcshareddata/xcschemes/PactConsumerSwift iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 37 | 38 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 52 | 55 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 70 | 76 | 77 | 78 | 79 | 80 | 86 | 87 | 88 | 89 | 90 | 91 | 101 | 102 | 108 | 109 | 110 | 111 | 112 | 113 | 119 | 120 | 126 | 127 | 128 | 129 | 131 | 132 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /PactConsumerSwift.xcodeproj/xcshareddata/xcschemes/PactConsumerSwift tvOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 37 | 38 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 52 | 55 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 70 | 76 | 77 | 78 | 79 | 80 | 86 | 87 | 88 | 89 | 90 | 91 | 101 | 102 | 108 | 109 | 110 | 111 | 112 | 113 | 119 | 120 | 126 | 127 | 128 | 129 | 131 | 132 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /PactConsumerSwift.xcodeproj/xcshareddata/xcschemes/PactConsumerSwift macOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 37 | 38 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 52 | 55 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 70 | 76 | 77 | 78 | 79 | 80 | 86 | 87 | 88 | 89 | 90 | 91 | 101 | 102 | 108 | 109 | 110 | 111 | 112 | 113 | 119 | 120 | 126 | 127 | 128 | 129 | 131 | 132 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /Tests/PactConsumerSwiftTests/AnimalServiceClient.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct Animal: Decodable { 4 | public let name: String 5 | public let type: String 6 | public let dob: String? 7 | public let legs: Int? 8 | 9 | enum CodingKeys: String, CodingKey { 10 | case name 11 | case type 12 | case dob = "dateOfBirth" 13 | case legs 14 | } 15 | } 16 | 17 | open class AnimalServiceClient: NSObject, URLSessionDelegate { 18 | fileprivate let baseUrl: String 19 | 20 | public init(baseUrl : String) { 21 | self.baseUrl = baseUrl 22 | } 23 | 24 | // MARK: - 25 | 26 | open func getAlligators(_ success: @escaping (Array) -> Void, failure: @escaping (NSError?) -> Void) { 27 | self.performRequest("\(baseUrl)/alligators", decoder: decodeAnimals) { animals, nsError in 28 | if let animals = animals { 29 | success(animals) 30 | } else { 31 | if let error = nsError { 32 | failure(error) 33 | } else { 34 | failure(NSError(domain: "", code: 42, userInfo: nil)) 35 | } 36 | } 37 | } 38 | } 39 | 40 | open func getSecureAlligators(authToken: String, success: @escaping (Array) -> Void, failure: @escaping (NSError?) -> Void) { 41 | self.performRequest("\(baseUrl)/alligators", headers: ["Authorization": authToken], decoder: decodeAnimals) { animals, nsError in 42 | if let animals = animals { 43 | success(animals) 44 | } else { 45 | if let error = nsError { 46 | failure(error) 47 | } else { 48 | failure(NSError(domain: "", code: 42, userInfo: nil)) 49 | } 50 | } 51 | } 52 | } 53 | 54 | open func getAlligator(_ id: Int, success: @escaping (Animal) -> Void, failure: @escaping (NSError?) -> Void) { 55 | self.performRequest("\(baseUrl)/alligators/\(id)", decoder: decodeAnimal) { animal, nsError in 56 | if let animal = animal { 57 | success(animal) 58 | } else { 59 | if let error = nsError { 60 | failure(error) 61 | } else { 62 | failure(NSError(domain: "", code: 42, userInfo: nil)) 63 | } 64 | } 65 | } 66 | } 67 | 68 | open func findAnimals(live: String, response: @escaping ([Animal]) -> Void) { 69 | let liveEncoded = live.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)! 70 | self.performRequest("\(baseUrl)/animals?live=\(liveEncoded)", decoder: decodeAnimals) { animals, nsError in 71 | if let animals = animals { 72 | response(animals) 73 | } 74 | } 75 | } 76 | 77 | open func eat(animal: String, success: @escaping () -> Void, error: @escaping (Int) -> Void) { 78 | self.performRequest("\(baseUrl)/alligator/eat", method: "patch", parameters: ["type": animal], decoder: decodeString) { string, nsError in 79 | if let localErr = nsError { 80 | error(localErr.code) 81 | } else { 82 | success() 83 | } 84 | } 85 | } 86 | 87 | open func wontEat(animal: String, success: @escaping () -> Void, error: @escaping (Int) -> Void) { 88 | self.performRequest("\(baseUrl)/alligator/eat", method: "delete", parameters: ["type": animal], decoder: decodeAnimals) { animals, nsError in 89 | if let localErr = nsError { 90 | error(localErr.code) 91 | } else { 92 | success() 93 | } 94 | } 95 | } 96 | 97 | open func eats(_ success: @escaping ([Animal]) -> Void) { 98 | self.performRequest("\(baseUrl)/alligator/eat", decoder: decodeAnimals) { animals, nsError in 99 | if let animals = animals { 100 | success(animals) 101 | } 102 | } 103 | } 104 | 105 | // MARK: - URLSessionDelegate 106 | 107 | public func urlSession( 108 | _ session: URLSession, 109 | didReceive challenge: URLAuthenticationChallenge, 110 | completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void 111 | ) { 112 | guard 113 | challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, 114 | challenge.protectionSpace.host.contains("localhost"), 115 | let serverTrust = challenge.protectionSpace.serverTrust 116 | else { 117 | completionHandler(.performDefaultHandling, nil) 118 | return 119 | } 120 | 121 | let credential = URLCredential(trust: serverTrust) 122 | completionHandler(.useCredential, credential) 123 | } 124 | 125 | // MARK: - Networking and Decoding 126 | 127 | private lazy var session = { 128 | URLSession(configuration: .ephemeral, delegate: self, delegateQueue: .main) 129 | }() 130 | 131 | private func performRequest(_ urlString: String, 132 | headers: [String: String]? = nil, 133 | method: String = "get", 134 | parameters: [String: String]? = nil, 135 | decoder: @escaping (_ data: Data) throws -> T, 136 | completionHandler: @escaping (_ response: T?, _ error: NSError?) -> Void 137 | ) { 138 | var request = URLRequest(url: URL(string: urlString)!) 139 | request.httpMethod = method 140 | if let headers = headers { 141 | request.allHTTPHeaderFields = headers 142 | } 143 | if let parameters = parameters, 144 | let data = try? JSONSerialization.data(withJSONObject: parameters, options: []) { 145 | request.addValue("application/json", forHTTPHeaderField: "Content-Type") 146 | request.httpBody = data 147 | } 148 | 149 | let task = self.session.dataTask(with: request) { data, response, error in 150 | if let error = error { 151 | completionHandler(nil, error as NSError) 152 | return 153 | } 154 | 155 | if let data = data, 156 | let response = response as? HTTPURLResponse, 157 | (200..<300).contains(response.statusCode) { 158 | do { 159 | let result = try decoder(data) 160 | completionHandler(result, nil) 161 | } catch { 162 | completionHandler(nil, error as NSError) 163 | } 164 | } else { 165 | completionHandler(nil, NSError(domain: "", code: 41, userInfo: nil)) 166 | } 167 | } 168 | 169 | task.resume() 170 | } 171 | 172 | private func decodeAnimal(_ data: Data) throws -> Animal { 173 | let decoder = JSONDecoder() 174 | return try decoder.decode(Animal.self, from: data) 175 | } 176 | 177 | private func decodeAnimals(_ data: Data) throws -> [Animal] { 178 | let decoder = JSONDecoder() 179 | return try decoder.decode([Animal].self, from: data) 180 | } 181 | 182 | private func decodeString(_ data: Data) throws -> String { 183 | guard let result = String(data: data, encoding: .utf8) else { 184 | throw NSError(domain: "", code: 63, userInfo: nil) 185 | } 186 | return result 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /PactConsumerObjCTests/PactObjectiveCTests.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "OCAnimalServiceClient.h" 4 | 5 | #if TARGET_OS_IOS 6 | #import "PactConsumerSwift_iOSTests-Swift.h" 7 | #elif TARGET_OS_TV 8 | #import "PactConsumerSwift_tvOSTests-Swift.h" 9 | #elif TARGET_OS_MAC 10 | #import "PactConsumerSwift_macOSTests-Swift.h" 11 | #endif 12 | 13 | @import PactConsumerSwift; 14 | 15 | @interface PactObjectiveCTests : XCTestCase 16 | @property (strong, nonatomic) MockService *animalMockService; 17 | @property (strong, nonatomic) OCAnimalServiceClient *animalServiceClient; 18 | @end 19 | 20 | @implementation PactObjectiveCTests 21 | 22 | - (void)setUp { 23 | [super setUp]; 24 | self.animalMockService = [[MockService alloc] initWithProvider:@"Animal Provider" 25 | consumer:@"Animal Service Client Objective-C"]; 26 | self.animalServiceClient = [[OCAnimalServiceClient alloc] initWithBaseUrl:self.animalMockService.baseUrl]; 27 | } 28 | 29 | - (void)tearDown { 30 | [super tearDown]; 31 | } 32 | 33 | - (void)testGetAlligator { 34 | typedef void (^CompleteBlock)(void); 35 | 36 | [[[[self.animalMockService given:@"an alligator exists"] 37 | uponReceiving:@"ObjC - a request for an alligator"] 38 | withRequestHTTPMethod:PactHTTPMethodGET 39 | path:@"/alligator" 40 | query:nil headers:nil body:nil] 41 | willRespondWithHTTPStatus:200 42 | headers:@{@"Content-Type": @"application/json"} 43 | body: @"{ \"name\": \"Mary\"}" ]; 44 | 45 | [self.animalMockService run:^(CompleteBlock testComplete) { 46 | Animal *animal = [self.animalServiceClient getAlligator]; 47 | XCTAssertEqualObjects(animal.name, @"Mary"); 48 | testComplete(); 49 | } withTimeout: 10]; 50 | } 51 | 52 | - (void)testWithQueryParams { 53 | typedef void (^CompleteBlock)(void); 54 | 55 | [[[[self.animalMockService given:@"an alligator exists"] 56 | uponReceiving:@"ObjC - a request for animals living in water"] 57 | withRequestHTTPMethod:PactHTTPMethodGET 58 | path:@"/animals" 59 | query: @{ @"live" : @"water" } 60 | headers:nil body: nil] 61 | willRespondWithHTTPStatus:200 62 | headers:@{@"Content-Type": @"application/json"} 63 | body: @[ @{ @"name": [Matcher somethingLike:@"Mary"] } ] ]; 64 | 65 | [self.animalMockService run:^(CompleteBlock testComplete) { 66 | NSArray *animals = [self.animalServiceClient findAnimalsLiving:@"water"]; 67 | 68 | XCTAssertEqual(animals.count, 1); 69 | Animal *animal = animals[0]; 70 | XCTAssertEqualObjects(animal.name, @"Mary"); 71 | testComplete(); 72 | }]; 73 | } 74 | 75 | #pragma mark - Mather tests 76 | 77 | - (void)testMatchingRegex { 78 | typedef void (^CompleteBlock)(void); 79 | 80 | [[[[self.animalMockService given:@"an alligator exists with a birthdate"] 81 | uponReceiving:@"ObjC - a request for alligator with birthdate"] 82 | withRequestHTTPMethod:PactHTTPMethodGET 83 | path:@"/alligator" 84 | query: nil headers:nil body: nil] 85 | willRespondWithHTTPStatus:200 86 | headers:@{@"Content-Type": @"application/json"} 87 | body: @{ 88 | @"name": @"Mary", 89 | @"dateOfBirth": [Matcher termWithMatcher:@"\\d{2}\\/\\d{2}\\/\\d{4}" generate:@"02/02/1999"] 90 | }]; 91 | 92 | [self.animalMockService run:^(CompleteBlock testComplete) { 93 | Animal *animal = [self.animalServiceClient getAlligator]; 94 | XCTAssertEqualObjects(animal.name, @"Mary"); 95 | XCTAssertEqualObjects(animal.dob, @"02/02/1999"); 96 | testComplete(); 97 | }]; 98 | } 99 | 100 | - (void)testMatchingType { 101 | typedef void (^CompleteBlock)(void); 102 | 103 | [[[[self.animalMockService given:@"an alligator exists with legs"] 104 | uponReceiving:@"ObjC - a request for alligator with legs"] 105 | withRequestHTTPMethod:PactHTTPMethodGET 106 | path:@"/alligator" 107 | query: nil headers:nil body: nil] 108 | willRespondWithHTTPStatus:200 109 | headers:@{@"Content-Type": @"application/json"} 110 | body: @{ 111 | @"name": @"Mary", 112 | @"legs": [Matcher somethingLike:@4] 113 | }]; 114 | 115 | [self.animalMockService run:^(CompleteBlock testComplete) { 116 | Animal *animal = [self.animalServiceClient getAlligator]; 117 | XCTAssertEqualObjects(animal.name, @"Mary"); 118 | XCTAssertEqualObjects(animal.legs, @4); 119 | testComplete(); 120 | }]; 121 | } 122 | 123 | 124 | - (void)testMatchingVariableLengthArray { 125 | typedef void (^CompleteBlock)(void); 126 | 127 | [[[[self.animalMockService given:@"multiple land based animals exist"] 128 | uponReceiving:@"ObjC - a request for animals living on land"] 129 | withRequestHTTPMethod:PactHTTPMethodGET 130 | path:@"/animals" 131 | query: @{ @"live" : @"land" } 132 | headers:nil body: nil] 133 | willRespondWithHTTPStatus:200 134 | headers:@{@"Content-Type": @"application/json"} 135 | body: [Matcher eachLike:@{ @"name": @"Bruce", @"legs": @4 } min:1]]; 136 | 137 | [self.animalMockService run:^(CompleteBlock testComplete) { 138 | NSArray *animals = [self.animalServiceClient findAnimalsLiving:@"land"]; 139 | 140 | XCTAssertEqual(animals.count, 1); 141 | Animal *animal = animals[0]; 142 | XCTAssertEqualObjects(animal.name, @"Bruce"); 143 | XCTAssertEqualObjects(animal.legs, @4); 144 | testComplete(); 145 | }]; 146 | } 147 | 148 | - (void)testAsyncCall { 149 | typedef void (^CompleteBlock)(void); 150 | 151 | [[[self.animalMockService uponReceiving:@"an async request"] 152 | withRequestHTTPMethod:PactHTTPMethodGET 153 | path:@"/path" 154 | query:nil 155 | headers:nil 156 | body: nil] 157 | willRespondWithHTTPStatus:200 158 | headers:@{@"Content-Type": @"application/json"} 159 | body: @{}]; 160 | 161 | [self.animalMockService run:^(CompleteBlock testComplete) { 162 | NSString *dataUrl = @"http://localhost:1234/path"; 163 | NSURL *url = [NSURL URLWithString:dataUrl]; 164 | NSURLSessionDataTask *asyncTask = [[NSURLSession sharedSession] 165 | dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { 166 | testComplete(); 167 | }]; 168 | [asyncTask resume]; 169 | } 170 | ]; 171 | } 172 | 173 | @end 174 | -------------------------------------------------------------------------------- /Sources/MockService.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | 4 | @objc 5 | open class MockService: NSObject { 6 | private let provider: String 7 | private let consumer: String 8 | private let pactVerificationService: PactVerificationService 9 | private var interactions: [Interaction] = [] 10 | private let errorReporter: ErrorReporter 11 | 12 | /// The baseUrl of Pact Mock Service 13 | @objc 14 | public var baseUrl: String { 15 | return pactVerificationService.baseUrl 16 | } 17 | 18 | /// 19 | /// Initializer 20 | /// 21 | /// - parameter provider: Name of your provider (eg: Calculator API) 22 | /// - parameter consumer: Name of your consumer (eg: Calculator.app) 23 | /// - parameter pactVerificationService: Your customised `PactVerificationService` 24 | /// - parameter errorReporter: Your customised `ErrorReporter` 25 | /// 26 | public init( 27 | provider: String, 28 | consumer: String, 29 | pactVerificationService: PactVerificationService, 30 | errorReporter: ErrorReporter 31 | ) { 32 | self.provider = provider 33 | self.consumer = consumer 34 | self.pactVerificationService = pactVerificationService 35 | self.errorReporter = errorReporter 36 | } 37 | 38 | /// 39 | /// Convenience Initializer 40 | /// 41 | /// - parameter provider: Name of your provider (eg: Calculator API) 42 | /// - parameter consumer: Name of your consumer (eg: Calculator.app) 43 | /// - parameter pactVerificationService: Your customised `PactVerificationService` 44 | /// 45 | /// Use this initialiser to use the default XCodeErrorReporter 46 | /// 47 | @objc(initWithProvider: consumer: andVerificationService:) 48 | public convenience init(provider: String, consumer: String, pactVerificationService: PactVerificationService) { 49 | self.init(provider: provider, 50 | consumer: consumer, 51 | pactVerificationService: pactVerificationService, 52 | errorReporter: ErrorReporterXCTest()) 53 | } 54 | 55 | /// 56 | /// Convenience Initializer 57 | /// 58 | /// - parameter provider: Name of your provider (eg: Calculator API) 59 | /// - parameter consumer: Name of your consumer (eg: Calculator.app) 60 | /// 61 | /// Use this initialiser to use the default PactVerificationService and ErrorReporter 62 | /// 63 | @objc(initWithProvider: consumer:) 64 | public convenience init(provider: String, consumer: String) { 65 | self.init(provider: provider, 66 | consumer: consumer, 67 | pactVerificationService: PactVerificationService(), 68 | errorReporter: ErrorReporterXCTest()) 69 | } 70 | 71 | /// 72 | /// Define the providers state 73 | /// 74 | /// Use this method in the `Arrange` step of your Pact test. 75 | /// 76 | /// myMockService.given("a user exists") 77 | /// 78 | /// - Parameter providerState: A description of providers state 79 | /// - Returns: An `Interaction` object 80 | /// 81 | @objc 82 | public func given(_ providerState: String) -> Interaction { 83 | let interaction = Interaction().given(providerState) 84 | interactions.append(interaction) 85 | return interaction 86 | } 87 | 88 | /// 89 | /// Describe the request your provider will receive 90 | /// 91 | /// This is the entry point if not using a provider state i.e.: 92 | /// 93 | /// myMockService.uponReceiving("a request for users") 94 | /// 95 | /// - Parameter description: Describing the request to the provider 96 | /// - Returns: An `Interaction` object 97 | /// 98 | @objc(uponReceiving:) 99 | public func uponReceiving(_ description: String) -> Interaction { 100 | let interaction = Interaction().uponReceiving(description) 101 | interactions.append(interaction) 102 | return interaction 103 | } 104 | 105 | /// 106 | /// Runs the provided test function with 30 second timeout 107 | /// 108 | /// Use this method in the `Act` step of your Pact test. 109 | /// (eg. Testing your `serviceClientUnderTest!.getUsers(...)` method) 110 | /// 111 | /// [self.mockService run:^(CompleteBlock testComplete) { 112 | /// [self. serviceClientUnderTest getUsers] 113 | /// testComplete(); 114 | /// }]; 115 | /// 116 | /// Make sure you call `testComplete()` after your `Assert` step in your test 117 | /// 118 | /// - Parameter testFunction: The function making the network request you are testing 119 | /// 120 | @objc(run:) 121 | public func objcRun(_ testFunction: @escaping (_ testComplete: @escaping () -> Void) -> Void) { 122 | self.run(nil, line: nil, timeout: 30, testFunction: testFunction) 123 | } 124 | 125 | /// 126 | /// Runs the provided test function by specifying timeout in seconds 127 | /// 128 | /// Use this method in the `Act` step of your Pact test. 129 | /// (eg. Testing your `serviceClientUnderTest!.getUsers(...)` method) 130 | /// 131 | /// [self.mockService run:^(CompleteBlock testComplete) { 132 | /// [self. serviceClientUnderTest getUsers] 133 | /// testComplete(); 134 | /// } withTimeout: 10]; 135 | /// 136 | /// Make sure you call `testComplete()` after your `Assert` step in your test 137 | /// 138 | /// - Parameter testFunction: The function making the network request you are testing 139 | /// - Parameter timeout: Time to wait for the `testComplete()` else it fails the test 140 | /// 141 | @objc(run: withTimeout:) 142 | public func objcRun( 143 | _ testFunction: @escaping (_ testComplete: @escaping () -> Void) -> Void, 144 | timeout: TimeInterval 145 | ) { 146 | self.run(nil, line: nil, timeout: timeout, testFunction: testFunction) 147 | } 148 | 149 | /// 150 | /// Runs the provided test function 151 | /// 152 | /// Use this method in the `Act` step of your Pact test. 153 | /// (eg. Testing your `serviceClientUnderTest!.getUsers(...)` method) 154 | /// 155 | /// myMockService!.run(timeout: 10) { (testComplete) -> Void in 156 | /// serviceClientUnderTest!.getUsers( /* ... */ ) 157 | /// } 158 | /// 159 | /// Make sure you call `testComplete()` after your `Assert` step in your test 160 | /// 161 | /// - Parameter timeout: Number of seconds how long to wait for `testComplete()` before marking the test as failed. 162 | /// - Parameter testFunction: The function making the network request you are testing 163 | /// 164 | public func run( 165 | _ file: FileString? = #file, 166 | line: UInt? = #line, 167 | timeout: TimeInterval = 30, 168 | testFunction: @escaping (_ testComplete: @escaping () -> Void) throws -> Void 169 | ) { 170 | waitUntilWithLocation(timeout: timeout, file: file, line: line) { done in 171 | self 172 | .pactVerificationService 173 | .setup(self.interactions) { result in 174 | switch result { 175 | case .success: 176 | do { 177 | try testFunction { done() } 178 | } catch { 179 | self.failWithLocation( 180 | "Error thrown in test function (check build log): \(error.localizedDescription)", 181 | file: file, 182 | line: line 183 | ) 184 | done() 185 | } 186 | case .failure(let error): 187 | self.failWithLocation("Error setting up pact: \(error.localizedDescription)", file: file, line: line) 188 | done() 189 | } 190 | } 191 | } 192 | 193 | waitUntilWithLocation(timeout: timeout, file: file, line: line) { done in 194 | self 195 | .pactVerificationService 196 | .verify(provider: self.provider, consumer: self.consumer) { result in 197 | switch result { 198 | case .success: 199 | done() 200 | case .failure(let error): 201 | self.failWithLocation("Verification error (check build log for mismatches): \(error.localizedDescription)", 202 | file: file, 203 | line: line) 204 | done() 205 | } 206 | } 207 | } 208 | } 209 | 210 | // MARK: - Helper methods 211 | 212 | private func failWithLocation( 213 | _ message: String, 214 | file: FileString?, 215 | line: UInt? 216 | ) { 217 | if let fileName = file, let lineNumber = line { 218 | self.errorReporter.reportFailure(message, file: fileName, line: lineNumber) 219 | } else { 220 | self.errorReporter.reportFailure(message) 221 | } 222 | } 223 | 224 | private func waitUntilWithLocation( 225 | timeout: TimeInterval, 226 | file: FileString?, 227 | line: UInt?, 228 | action: @escaping (@escaping () -> Void) -> Void 229 | ) { 230 | let expectation = XCTestExpectation(description: "waitUntilWithLocation") 231 | action { expectation.fulfill() } 232 | 233 | let result = XCTWaiter().wait(for: [expectation], timeout: timeout) 234 | if result != .completed { 235 | let message = "test did not complete within \(timeout) second timeout" 236 | if let fileName = file, let lineNumber = line { 237 | errorReporter.reportFailure(message, file: fileName, line: lineNumber) 238 | } else { 239 | errorReporter.reportFailure(message) 240 | } 241 | } 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /Tests/MockServiceSpec.swift: -------------------------------------------------------------------------------- 1 | import Quick 2 | import Nimble 3 | @testable import PactConsumerSwift 4 | 5 | class MockServiceSpec: QuickSpec { 6 | override func spec() { 7 | var pactServicePactStub: RubyPactMockServiceStub! 8 | var mockService: MockService! 9 | var errorCapturer: ErrorCapture! 10 | 11 | beforeEach { 12 | pactServicePactStub = RubyPactMockServiceStub() 13 | errorCapturer = ErrorCapture() 14 | mockService = MockService(provider: "ABC Service", 15 | consumer: "unit tests", 16 | pactVerificationService: PactVerificationService(session: pactServicePactStub.session), 17 | errorReporter: errorCapturer!) 18 | 19 | mockService 20 | .uponReceiving("test request") 21 | .withRequest(method: .GET, 22 | path: "/widgets") 23 | .willRespondWith(status: 200, 24 | headers: ["Content-Type": "application/json"], 25 | body: ["name": "test response"]) 26 | } 27 | 28 | afterEach { 29 | pactServicePactStub.reset() 30 | } 31 | 32 | describe("pact verification succeeds") { 33 | beforeEach { 34 | pactServicePactStub 35 | .clean(responseCode: 200, response: "Cleaned OK") 36 | .setupInteractions(responseCode: 200, response: "Setup succeeded") 37 | .verifyInteractions(responseCode: 200, response: "Verify succeeded") 38 | .writePact(responseCode: 200, response: "Writing pact succeeded") 39 | } 40 | 41 | it("creates expected interactions in mock service") { 42 | mockService.run() { (testComplete) -> Void in 43 | testComplete() 44 | } 45 | expect(pactServicePactStub.setupInteractionsStub.requestExecuted).to(equal(true)) 46 | expect(pactServicePactStub.setupInteractionsStub.requestBody).to(contain( 47 | "\"description\":\"test request\"" 48 | )) 49 | } 50 | 51 | it("calls test function") { 52 | var calledTestFunction = false 53 | mockService.run() { (testComplete) -> Void in 54 | calledTestFunction = true 55 | testComplete() 56 | } 57 | expect(calledTestFunction).to(equal(true)) 58 | } 59 | 60 | it("writes pact for provider / consumer combination") { 61 | mockService.run() { (testComplete) -> Void in 62 | testComplete() 63 | } 64 | expect(pactServicePactStub.writePactStub.requestExecuted).to(equal(true)) 65 | expect(pactServicePactStub.writePactStub.requestBody).to(contain( 66 | "\"provider\":{\"name\":\"ABC Service\"" 67 | )) 68 | expect(pactServicePactStub.writePactStub.requestBody).to(contain( 69 | "\"consumer\":{\"name\":\"unit tests\"" 70 | )) 71 | } 72 | } 73 | 74 | context("when cleaning previous interactions fails") { 75 | beforeEach { 76 | pactServicePactStub.clean(responseCode: 500, response: "Error cleaning interactions") 77 | } 78 | 79 | it("does not call test function") { 80 | var calledTestFunction = false 81 | mockService.run() { (testComplete) -> Void in 82 | calledTestFunction = true 83 | testComplete() 84 | } 85 | expect(calledTestFunction).to(equal(false)) 86 | } 87 | 88 | it("returns error message from mock service") { 89 | mockService.run() { (testComplete) -> Void in 90 | testComplete() 91 | } 92 | expect(errorCapturer!.message!.message).to(contain("Error setting up pact: Error cleaning interactions")) 93 | } 94 | 95 | it("does not attempt to setup interactions or write pact") { 96 | mockService.run() { (testComplete) -> Void in 97 | testComplete() 98 | } 99 | expect(pactServicePactStub.cleanStub.requestExecuted).to(equal(true)) 100 | expect(pactServicePactStub.setupInteractionsStub.requestExecuted).to(equal(false)) 101 | expect(pactServicePactStub.verifyInteractionsStub.requestExecuted).to(equal(false)) 102 | expect(pactServicePactStub.writePactStub.requestExecuted).to(equal(false)) 103 | } 104 | } 105 | 106 | describe("pact setup fails") { 107 | beforeEach { 108 | pactServicePactStub 109 | .clean(responseCode: 200, response: "Cleaned OK") 110 | .setupInteractions(responseCode: 500, response: "Error setting up interactions") 111 | } 112 | 113 | it("does not call test function") { 114 | var calledTestFunction = false 115 | mockService.run() { (testComplete) -> Void in 116 | calledTestFunction = true 117 | testComplete() 118 | } 119 | expect(calledTestFunction).to(equal(false)) 120 | } 121 | 122 | it("returns error message from mock service") { 123 | mockService.run() { (testComplete) -> Void in 124 | testComplete() 125 | } 126 | expect(errorCapturer!.message!.message).to(contain("Error setting up pact: Error setting up interactions")) 127 | } 128 | 129 | it("does not attempt to verify or write pact") { 130 | mockService.run() { (testComplete) -> Void in 131 | testComplete() 132 | } 133 | expect(pactServicePactStub.cleanStub.requestExecuted).to(equal(true)) 134 | expect(pactServicePactStub.setupInteractionsStub.requestExecuted).to(equal(true)) 135 | expect(pactServicePactStub.verifyInteractionsStub.requestExecuted).to(equal(false)) 136 | expect(pactServicePactStub.writePactStub.requestExecuted).to(equal(false)) 137 | } 138 | } 139 | 140 | describe("pact verification fails") { 141 | beforeEach { 142 | pactServicePactStub 143 | .clean(responseCode: 200, response: "Cleaned OK") 144 | .setupInteractions(responseCode: 200, response: "Setup succeeded") 145 | .verifyInteractions(responseCode: 500, response: "Error running verification") 146 | } 147 | 148 | it("calls test function") { 149 | var calledTestFunction = false 150 | mockService.run() { (testComplete) -> Void in 151 | calledTestFunction = true 152 | testComplete() 153 | } 154 | expect(calledTestFunction).to(equal(true)) 155 | } 156 | 157 | it("returns error message from mock service") { 158 | mockService.run() { (testComplete) -> Void in 159 | testComplete() 160 | } 161 | expect(errorCapturer!.message!.message).to(contain( 162 | "Verification error (check build log for mismatches): Error running verification" 163 | )) 164 | } 165 | 166 | it("does not attempt to write pact") { 167 | mockService.run() { (testComplete) -> Void in 168 | testComplete() 169 | } 170 | expect(pactServicePactStub.cleanStub.requestExecuted).to(equal(true)) 171 | expect(pactServicePactStub.setupInteractionsStub.requestExecuted).to(equal(true)) 172 | expect(pactServicePactStub.verifyInteractionsStub.requestExecuted).to(equal(true)) 173 | expect(pactServicePactStub.writePactStub.requestExecuted).to(equal(false)) 174 | } 175 | } 176 | 177 | describe("writing pact fails") { 178 | beforeEach { 179 | pactServicePactStub 180 | .clean(responseCode: 200, response: "Cleaned OK") 181 | .setupInteractions(responseCode: 200, response: "Setup succeeded") 182 | .verifyInteractions(responseCode: 200, response: "Verify succeeded") 183 | .writePact(responseCode: 500, response: "Error writing pact") 184 | } 185 | 186 | it("calls test function") { 187 | var calledTestFunction = false 188 | mockService.run() { (testComplete) -> Void in 189 | calledTestFunction = true 190 | testComplete() 191 | } 192 | expect(calledTestFunction).to(equal(true)) 193 | } 194 | 195 | it("returns error message from mock service") { 196 | mockService.run() { (testComplete) -> Void in 197 | testComplete() 198 | } 199 | expect(errorCapturer!.message!.message).to(contain( 200 | "Verification error (check build log for mismatches): Error writing pact" 201 | )) 202 | } 203 | 204 | it("executes all expected requests") { 205 | mockService.run() { (testComplete) -> Void in 206 | testComplete() 207 | } 208 | expect(pactServicePactStub.cleanStub.requestExecuted).to(equal(true)) 209 | expect(pactServicePactStub.setupInteractionsStub.requestExecuted).to(equal(true)) 210 | expect(pactServicePactStub.verifyInteractionsStub.requestExecuted).to(equal(true)) 211 | expect(pactServicePactStub.writePactStub.requestExecuted).to(equal(true)) 212 | } 213 | } 214 | 215 | describe("when test function throws error") { 216 | beforeEach { 217 | pactServicePactStub 218 | .clean(responseCode: 200, response: "Cleaned OK") 219 | .setupInteractions(responseCode: 200, response: "Setup succeeded") 220 | } 221 | 222 | enum MockError: Error { 223 | case problem 224 | } 225 | 226 | it("returns message from thrown error") { 227 | mockService.run() { _ -> Void in 228 | throw MockError.problem 229 | } 230 | expect(errorCapturer!.message!.message).to(contain( 231 | "Error thrown in test function (check build log):" 232 | )) 233 | } 234 | } 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /Sources/PactVerificationService.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @objc 4 | open class PactVerificationService: NSObject { 5 | 6 | typealias VoidHandler = (Result) -> Void 7 | typealias StringHandler = (Result) -> Void 8 | 9 | @objc public let url: String 10 | @objc public let port: Int 11 | @objc public let allowInsecureCertificates: Bool 12 | 13 | open var baseUrl: String { 14 | return "\(url):\(port)" 15 | } 16 | 17 | private lazy var session = { 18 | return URLSession(configuration: .ephemeral, delegate: self, delegateQueue: .main) 19 | }() 20 | 21 | enum Router { 22 | static var baseURLString = "http://example.com" 23 | 24 | case clean 25 | case setup([String: Any]) 26 | case verify 27 | case write([String: [String: String]]) 28 | 29 | var method: String { 30 | switch self { 31 | case .clean: 32 | return "delete" 33 | case .setup: 34 | return "put" 35 | case .verify: 36 | return "get" 37 | case .write: 38 | return "post" 39 | } 40 | } 41 | 42 | var path: String { 43 | switch self { 44 | case .clean, 45 | .setup: 46 | return "/interactions" 47 | case .verify: 48 | return "/interactions/verification" 49 | case .write: 50 | return "/pact" 51 | } 52 | } 53 | 54 | // MARK: URLRequestConvertible 55 | func asURLRequest() throws -> URLRequest { 56 | guard let url = URL(string: Router.baseURLString) else { throw NSError(domain: "", code: 1, userInfo: nil) } 57 | var urlRequest = URLRequest(url: url.appendingPathComponent(path)) 58 | urlRequest.httpMethod = method 59 | urlRequest.setValue("true", forHTTPHeaderField: "X-Pact-Mock-Service") 60 | 61 | switch self { 62 | case .setup(let parameters): 63 | return try jsonEncode(urlRequest, with: parameters) 64 | case .write(let parameters): 65 | return try jsonEncode(urlRequest, with: parameters) 66 | default: 67 | return urlRequest 68 | } 69 | } 70 | 71 | private func jsonEncode(_ request: URLRequest, with parameters: [String: Any]) throws -> URLRequest { 72 | var urlRequest = request 73 | let data = try JSONSerialization.data(withJSONObject: parameters, options: []) 74 | 75 | urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") 76 | 77 | urlRequest.httpBody = data 78 | return urlRequest 79 | } 80 | } 81 | 82 | @objc(initWithUrl: port: allowInsecureCertificate:) 83 | public init(url: String, port: Int, allowInsecureCertificates: Bool) { 84 | self.url = url 85 | self.port = port 86 | self.allowInsecureCertificates = allowInsecureCertificates 87 | 88 | super.init() 89 | 90 | Router.baseURLString = baseUrl 91 | } 92 | 93 | @objc(initWithUrl: port:) 94 | public convenience init(url: String = "http://localhost", port: Int = 1234) { 95 | self.init(url: url, port: port, allowInsecureCertificates: false) 96 | } 97 | 98 | @objc(initWithUrl: port: session:) 99 | public convenience init(url: String = "http://localhost", port: Int = 1234, session: URLSession) { 100 | self.init(url: url, port: port, allowInsecureCertificates: false) 101 | self.session = session 102 | } 103 | 104 | // MARK: - Interface 105 | 106 | func setup(_ interactions: [Interaction], completion: @escaping VoidHandler) { 107 | clean { result in 108 | switch result { 109 | case .success: 110 | self.setupInteractions(interactions) { result in 111 | self.handleResponse(result: result, completion: completion) 112 | } 113 | case .failure(let error): 114 | completion(.failure(error)) 115 | } 116 | } 117 | } 118 | 119 | func verify(provider: String, consumer: String, completion: @escaping VoidHandler) { 120 | verifyInteractions { result in 121 | switch result { 122 | case .success: 123 | self.write(provider: provider, consumer: consumer) { result in 124 | self.handleResponse(result: result, completion: completion) 125 | } 126 | case .failure(let error): 127 | completion(.failure(error)) 128 | } 129 | } 130 | } 131 | 132 | } 133 | 134 | // MARK: - Private 135 | 136 | private extension PactVerificationService { 137 | 138 | func clean(completion: @escaping VoidHandler) { 139 | performNetworkRequest(for: Router.clean) { result in 140 | self.handleResponse(result: result, completion: completion) 141 | } 142 | } 143 | 144 | func setupInteractions (_ interactions: [Interaction], completion: @escaping StringHandler) { 145 | let payload: [String: Any] = [ 146 | "interactions": interactions.map({ $0.payload() }), 147 | "example_description": "description" 148 | ] 149 | 150 | performNetworkRequest(for: Router.setup(payload)) { result in 151 | self.handleResponse(result: result, completion: completion) 152 | } 153 | } 154 | 155 | func verifyInteractions(completion: @escaping VoidHandler) { 156 | performNetworkRequest(for: Router.verify) { result in 157 | self.handleResponse(result: result, completion: completion) 158 | } 159 | } 160 | 161 | func write(provider: String, consumer: String, completion: @escaping StringHandler) { 162 | let payload = [ 163 | "consumer": ["name": consumer], 164 | "provider": ["name": provider] 165 | ] 166 | 167 | performNetworkRequest(for: Router.write(payload)) { result in 168 | self.handleResponse(result: result, completion: completion) 169 | } 170 | } 171 | 172 | } 173 | 174 | // MARK: - Result handlers 175 | 176 | private extension PactVerificationService { 177 | 178 | func handleResponse(result: Result, completion: @escaping VoidHandler) { 179 | switch result { 180 | case .success: 181 | completion(.success(())) 182 | case .failure(let error): 183 | completion(.failure(error)) 184 | } 185 | } 186 | 187 | func handleResponse(result: Result, completion: @escaping VoidHandler) { 188 | switch result { 189 | case .success: 190 | completion(.success(())) 191 | case .failure(let error): 192 | completion(.failure(NSError.prepareWith(message: error.localizedDescription))) 193 | } 194 | } 195 | 196 | func handleResponse(result: Result, completion: @escaping StringHandler) { 197 | switch result { 198 | case .success(let resultString): 199 | completion(.success(resultString)) 200 | case .failure(let error): 201 | completion(.failure(NSError.prepareWith(message: error.localizedDescription))) 202 | } 203 | } 204 | 205 | } 206 | 207 | // MARK: - Network request handler 208 | 209 | extension PactVerificationService: URLSessionDelegate { 210 | 211 | public func urlSession( 212 | _ session: URLSession, 213 | didReceive challenge: URLAuthenticationChallenge, 214 | completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void 215 | ) { 216 | guard 217 | allowInsecureCertificates, 218 | challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, 219 | ["localhost", "127.0.0.1", "0.0.0.0"].contains(where: challenge.protectionSpace.host.contains), 220 | let serverTrust = challenge.protectionSpace.serverTrust 221 | else { 222 | completionHandler(.performDefaultHandling, nil) 223 | return 224 | } 225 | let credential = URLCredential(trust: serverTrust) 226 | completionHandler(.useCredential, credential) 227 | } 228 | 229 | fileprivate func performNetworkRequest( 230 | for router: Router, 231 | completion: @escaping (Result) -> Void 232 | ) { 233 | do { 234 | let dataTask = try session.dataTask(with: router.asURLRequest()) { result in 235 | switch result { 236 | case .success(let (response, data)): 237 | guard let statusCode = (response as? HTTPURLResponse)?.statusCode, 200..<299 ~= statusCode else { 238 | completion(.failure(.invalidResponse(NSError.prepareWith(data: data)))) 239 | return 240 | } 241 | guard let responseString = String(data: data, encoding: .utf8) else { 242 | completion(.failure(.noData)) 243 | return 244 | } 245 | completion(.success(responseString)) 246 | case .failure(let error): 247 | completion(.failure(.apiError(error))) 248 | } 249 | } 250 | dataTask.resume() 251 | } catch let error { 252 | completion(.failure(.dataTaskError(error))) 253 | } 254 | } 255 | 256 | } 257 | 258 | // MARK: - Type Extensions 259 | 260 | private extension NSError { 261 | 262 | static func prepareWith(userInfo: [String: Any]) -> NSError { 263 | return NSError(domain: "error", code: 0, userInfo: userInfo) 264 | } 265 | 266 | static func prepareWith(message: String) -> NSError { 267 | return NSError(domain: "error", code: 0, userInfo: [NSLocalizedDescriptionKey: NSLocalizedString("Error", value: message, comment: "")]) // swiftlint:disable:this line_length 268 | } 269 | 270 | static func prepareWith(data: Data) -> NSError { 271 | return NSError(domain: "error", code: 0, userInfo: [NSLocalizedDescriptionKey: NSLocalizedString("Error", value: "\(String(data: data, encoding: .utf8) ?? "Failed to cast response Data into String")", comment: "")]) // swiftlint:disable:this line_length 272 | } 273 | 274 | } 275 | 276 | private extension URLSession { 277 | 278 | enum APIServiceError: Error { 279 | case apiError(Error) 280 | case dataTaskError(Error) 281 | case decodeError 282 | case invalidEndpoint 283 | case invalidResponse(Error) 284 | case noData 285 | } 286 | 287 | func dataTask(with url: URLRequest, result: @escaping (Result<(URLResponse, Data), Error>) -> Void) -> URLSessionDataTask { 288 | return dataTask(with: url) { (data, response, error) in 289 | guard error == nil else { 290 | result(.failure(error!)) 291 | return 292 | } 293 | 294 | guard let response = response, let data = data else { 295 | let error = NSLocalizedString("Error", value: "No response or missing expected data", comment: "") 296 | result(.failure(NSError.prepareWith(userInfo: [NSLocalizedDescriptionKey: error]))) 297 | return 298 | } 299 | 300 | result(.success((response, data))) 301 | } 302 | } 303 | 304 | } 305 | 306 | extension URLSession.APIServiceError: LocalizedError { 307 | 308 | public var localizedDescription: String { 309 | switch self { 310 | case .apiError(let error), 311 | .dataTaskError(let error), 312 | .invalidResponse(let error): 313 | return error.localizedDescription 314 | case .decodeError: 315 | return URLSession.APIServiceError.decodeError.localizedDescription 316 | case .invalidEndpoint: 317 | return URLSession.APIServiceError.invalidEndpoint.localizedDescription 318 | case .noData: 319 | return URLSession.APIServiceError.noData.localizedDescription 320 | } 321 | } 322 | 323 | } 324 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.10.2 - Bugfix Release 2 | 3 | * d750d5b - Fix changelog generation (Andrew Spinks, Mon Sep 6 13:45:00 2021 +0900) 4 | * 7aba867 - Update Package.swift to set minimum supported iOS version to 9 (alexbasson, Thu May 13 14:29:01 2021 -0400) 5 | * 3ab783c - Move swiftlint configuration to root (Seb Skuse, Wed Mar 31 16:57:49 2021 +0100) 6 | # 0.10.2 - Bugfix Release 7 | 8 | * d750d5b - Fix changelog generation (Andrew Spinks, Mon Sep 6 13:45:00 2021 +0900) 9 | * 7aba867 - Update Package.swift to set minimum supported iOS version to 9 (alexbasson, Thu May 13 14:29:01 2021 -0400) 10 | * 3ab783c - Move swiftlint configuration to root (Seb Skuse, Wed Mar 31 16:57:49 2021 +0100) 11 | # 0.10.1 - Bugfix Release 12 | 13 | * c5e8324 - chore: Allow non-secure connections on 127.0.0.1 (Marko Justinek, Thu Feb 4 15:43:49 2021 +1100) 14 | * c1acc77 - fix(project): Reconfigure tvOS test target (Marko Justinek, Thu Feb 4 14:13:44 2021 +1100) 15 | * a10e546 - chore: Migrate CI pipeline to GitHub Actions (#105) (#107) (Marko Justinek, Sun Dec 20 00:03:33 2020 +1100) 16 | * cd18167 - doc: Fix links in README.md (#106) (Marko Justinek, Mon Dec 14 12:41:21 2020 +1100) 17 | 18 | # 0.10.0 - Bugfix Release 19 | 20 | * a3a1cae - fix: remove Nimble from cocoapods dependencies so it is not required by clients. (Andrew Spinks, Fri Oct 30 21:06:16 2020 +0900) 21 | * 229f35d - fix: Remove Nimble from Carthage / SPM dependencies so it is not required by clients. (Antoine Piellard, Thu Oct 29 02:57:09 2020 +0100) 22 | * 5f6479a - maintenance: Update Quick and Nimble versions (#102) (Marko Justinek, Wed Oct 21 09:54:39 2020 +1100) 23 | * a02f765 - Fixes the url for Request response matchers (#99) (Amitoj Duggal, Thu Jul 23 04:57:37 2020 +0200) 24 | 25 | # 0.9.0 - Maintenance Release 26 | 27 | * 3459f6b - build: ensure lint errors are included in build output (Marko Justinek, Wed Jun 3 08:46:35 2020 +1000) 28 | * c02537f - maintenance: removed Nimble dependency (#87) (Ruben Cagnie, Sun May 24 00:30:41 2020 -0400) 29 | * 8ccd7d9 - doc: add details of new pact-swift project to readme. (Marko Justinek, Fri May 22 21:03:30 2020 +1000) 30 | 31 | # 0.8.1 - Bugfix Release 32 | 33 | * 61191a6 - fix(build) Enable testing search path for Xcode 11.4 (#93) (Marko Justinek, Mon Apr 6 20:46:55 2020 +1000) 34 | 35 | # 0.8.0 - Maintenance Release 36 | 37 | * e8b5e27 - build: fix building when path has spaces in it. (Michał Myśliwiec, Sun Mar 29 13:18:57 2020 +0200) 38 | * 82a8a72 - feat: make it so `testFunction` can throw errors (#86) (Huw Rowlands, Sat Feb 22 15:51:33 2020 +1100) 39 | * 99f830c - maintenance: Remove BrightFutures dependency (#80) (Marko Justinek, Tue Feb 4 18:00:15 2020 +1100) 40 | * 4279255 - feat: allow self signed ssl certificates (#74) (Mihai Georgescu, Fri Jan 17 10:20:45 2020 +0000) 41 | * d086a7a - feat: add support for macCatalyst (#78) (Tomáš Linhart, Wed Nov 27 21:18:41 2019 +1100) 42 | 43 | # 0.7.1 - Bugfix Release 44 | 45 | * 01effee - style: add convenience init method to MockService which defaults error reporting but allows configuration of the verifier. (#77) (alan.nichols@outlook.com, Tue Nov 12 22:22:16 2019 +0000) 46 | * f5e34d5 - chore: Change minimum swift version to 4.2 due Nimble requirements. Also fix cocoapods configuration. (Andrew Spinks, Sat Aug 31 14:55:31 2019 +0900) 47 | 48 | # 0.7.0 - Bugfix Release 49 | 50 | * 16bbf44 - chore: Change minimum swift version to 4.2 due Nimble requirements. Also fix cocoapods configuration. (Andrew Spinks, Sat Aug 31 14:55:31 2019 +0900) 51 | * c91360d - Revert "chore: Bumping version to 0.6.2" due to cocoapods issues. (Andrew Spinks, Sat Aug 31 14:50:40 2019 +0900) 52 | * 58775a2 - chore: Bumping version to 0.6.2 (Andrew Spinks, Sat Aug 31 14:11:12 2019 +0900) 53 | * 8f2c76b - fix: Bump dependencies to support swift 5. Also removes obsolete references to Result.framework. (Marko Justinek, Tue Aug 27 00:50:46 2019 +0200) 54 | * 5b1e1bf - chore: convert release script to be a bash script #46 (Andrew Spinks, Sun Jul 28 10:32:30 2019 +0900) 55 | 56 | # 0.6.1 - Bugfix Release 57 | 58 | * 57ccbd6 - fix(objc): add escaping modifier to allow for testComplete function to be called… (#71) (Matt Arturi, Sat Jul 27 09:43:09 2019 -0400) 59 | * a611dce - chore: Only run coverage report when tests pass (Andrew Spinks, Sat Jul 27 20:48:05 2019 +0900) 60 | * cacde03 - chore: Fix test failures not failing travis build. (Andrew Spinks, Sat Jul 27 20:47:52 2019 +0900) 61 | * cc66ee5 - test: Add test around pact mock service to make refactors easier. (Andrew Spinks, Wed Jul 3 19:29:27 2019 +0900) 62 | * 31fdfc7 - fix: Revert "Refactor pact mock server network calling code and upgrade to swift 5 (#53)" which introduced a bug where messages from the mock server were no longer being returned. (Andrew Spinks, Thu Jul 18 17:35:45 2019 +0900) 63 | * 8a42f89 - fix(lint): Parameter alignment (Marko Justinek, Tue Jul 2 06:29:45 2019 +1000) 64 | * 217b5e2 - chore(lint): moved linting into a portable script (#66) (Marko Justinek, Mon Jul 1 21:00:51 2019 +1000) 65 | * aa25c8f - Cleanup access controls on classes to hide internal concepts. (Andrew Spinks, Sat Jun 29 16:50:33 2019 +0900) 66 | * 1665942 - Remove unused .ruby-version file (#65) (Marko Justinek, Fri Jun 28 17:32:07 2019 +1000) 67 | * feaa5d5 - Removes watchOS target and scheme (#64) (Marko Justinek, Fri Jun 28 17:30:39 2019 +1000) 68 | 69 | 70 | # 0.6.0 - Maintenance Release 71 | 72 | * 9605227 - Cleanup build settings so ci build can be more easily reproduced locally. (#59) (andrewspinks, Tue Jun 25 17:32:54 2019 +0900) 73 | * cdecfbc - Fixes bitcode related issue when used from CocoaPods. (Kyle Hammond, Mon Jun 24 10:36:16 2019 -0500) 74 | * 848f4c5 - Remove quick dependency from requirements. It is only used for internal tests. (#58) (andrewspinks, Sun Jun 23 11:19:26 2019 +0900) 75 | * 8688fdc - Refactor pact mock server network calling code and upgrade to swift 5 (#53) (Marko Justinek, Wed Jun 19 17:32:45 2019 +1000) 76 | * 23296ce - Completely removed Alamofire in favor of simple networking calls. (#51) (Kyle Hammond, Wed Jun 12 18:53:11 2019 -0500) 77 | * 3ed97f1 - Update dependencies (Mihai Georgescu, Mon Mar 25 23:12:14 2019 +0000) 78 | * 28e1588 - Fix cocoa pods release version number and release script. (Andrew Spinks, Thu Jan 24 20:17:51 2019 +0900) 79 | 80 | # 0.5.3 - Bugfix Release 81 | 82 | * a1cd000 - Update cocoapods dependencies. (Andrew Spinks, Thu Jan 24 19:34:14 2019 +0900) 83 | * 5ebc062 - Update pact-mock-service versions (Andrew Spinks, Thu Jan 24 17:29:40 2019 +0900) 84 | * 2e77868 - Update Cartfile.resolved for use with bootstrap on CI (Marko Justinek, Thu Jan 24 09:06:45 2019 +1100) 85 | * f8899f8 - Update Quick dependency to use “master” (Marko Justinek, Thu Jan 24 08:38:31 2019 +1100) 86 | * 596b6b8 - Update travis CI env matrix (Marko Justinek, Thu Jan 24 08:26:12 2019 +1100) 87 | * 8c4d3cf - Bumps up versions of all dependencies (Marko Justinek, Wed Jan 23 23:58:20 2019 +1100) 88 | * 4198e38 - Bump up Alamofire version (Marko Justinek, Wed Jan 23 23:47:04 2019 +1100) 89 | * 5897e28 - Updates travis pipeline to run tests on iOS 12.1 and 11.3 (Marko Justinek, Wed Jan 23 23:30:14 2019 +1100) 90 | * 0e7212b - Project updates for Swift 4.2 (Marko Justinek, Wed Jan 23 23:21:23 2019 +1100) 91 | * 6ca5c37 - Update BrightFutures dependency version (Marko Justinek, Wed Jan 23 23:12:39 2019 +1100) 92 | * d639fe7 - Update readme (Marko Justinek, Wed Jan 23 23:12:23 2019 +1100) 93 | * e1c4596 - Removed reference to non-standalone pact mock service (Marko Justinek, Fri May 18 11:00:12 2018 +1000) 94 | * 2a667c2 - Updates README to encourage usage of pact-ruby-standalone (Marko Justinek, Thu May 17 08:27:07 2018 +1000) 95 | * 5ec6eb3 - bump cocoapods version to 0.5.2 (Andrew Spinks, Tue May 8 17:22:01 2018 +0900) 96 | 97 | # 0.5.2 - Bugfix Release 98 | 99 | * 52cf980 - Update release script to work better with cocoa pods (Andrew Spinks, Tue May 8 17:09:25 2018 +0900) 100 | * 2bc59b5 - Update gems (Andrew Spinks, Tue May 8 16:26:54 2018 +0900) 101 | * 6749fdc - Merge pull request #38 from stitchfix/master (andrewspinks, Tue May 1 11:46:08 2018 +0900) 102 | * 26cab90 - Update BrightFutures (Robbin Voortman, Wed Apr 25 08:51:25 2018 +0200) 103 | * b538409 - wait on verify to complete before allowing tests to continue to prevent race condition (Eric Vennaro, Tue Apr 24 15:31:03 2018 -0700) 104 | * 3e1edc3 - move to brew update (Stephen, Thu Jan 4 16:42:57 2018 -0800) 105 | * db7abe5 - move to brew bundle (Stephen, Thu Jan 4 15:37:59 2018 -0800) 106 | * 8bc8fc0 - Create Brewfile (Stephen, Thu Jan 4 15:37:26 2018 -0800) 107 | * 9c5678b - Update cocopods version number (Andrew Spinks, Thu Oct 26 19:19:37 2017 +0900) 108 | 109 | # 0.5.1 - Bugfix Release 110 | 111 | * 1b6ba70 - Fixed issue preventing the pact writing (Angel G. Olloqui, Wed Oct 25 12:30:38 2017 +0200) 112 | * 44146ba - Updates documentation (Marko Justinek, Wed Oct 4 10:41:17 2017 +1100) 113 | * a5d24a0 - Build script improvements (Marko Justinek, Wed Oct 4 10:29:54 2017 +1100) 114 | * ff6939f - Fixes links (Marko Justinek, Wed Oct 4 10:09:08 2017 +1100) 115 | * 0aa3bd6 - Fixes links (Marko Justinek, Wed Oct 4 10:09:08 2017 +1100) 116 | * 10ddac3 - Updates README (Marko Justinek, Wed Oct 4 08:35:12 2017 +1100) 117 | * c9806df - Updates README (Marko Justinek, Wed Oct 4 08:16:21 2017 +1100) 118 | * 44b087f - Release script runs pod spec lint before versioning and tagging (Marko Justinek, Wed Oct 4 08:07:39 2017 +1100) 119 | * d8c22b0 - Updates readme and contributing. (Marko Justinek, Wed Oct 4 08:07:03 2017 +1100) 120 | 121 | # 0.5.0 - macOS and SwiftPM support 122 | 123 | * 51b567a - Adds support for macOS and tvOS targets using Carthage and SwiftPM (#32) (Marko, Mon Oct 2 12:46:52 2017 +1100) 124 | * 5248ba4 - Fix swiftlint upgrade. (#30) (andrewspinks, Sat Sep 23 15:03:52 2017 +0900) 125 | * c74eaae - Swift 4 compatible framework (#27) (Marko Justinek, Sat Sep 23 15:14:42 2017 +1000) 126 | * 2dd464d - Allow matchers to be used with headers, and add specs around query parameter matchers. (Andrew Spinks, Thu Jun 1 14:21:06 2017 +0900) 127 | * 2d7eb6f - Fix travisci badge. (Andrew Spinks, Thu Jun 1 07:54:11 2017 +0900) 128 | * 414159d - Fix cocoapods publish. (Andrew Spinks, Wed May 31 15:30:19 2017 +0900) 129 | * 878b70b - bump version to 0.4.3 (Andrew Spinks, Wed May 31 15:29:41 2017 +0900) 130 | 131 | # 0.4.2 - Bugfix Release 132 | 133 | * 891920f - bump version to 0.4.2 (Andrew Spinks, Wed May 31 14:55:31 2017 +0900) 134 | * 45edc9f - Add release script to make a consistent process. (Andrew Spinks, Wed May 31 14:52:31 2017 +0900) 135 | * 9e440a3 - Improve output of errors when a mismatch occurs. (Andrew Spinks, Wed May 31 12:19:02 2017 +0900) 136 | * ce528bc - Update dependencies. (Andrew Spinks, Wed May 31 11:45:15 2017 +0900) 137 | * b29de5f - Remove unused workspace (Andrew Spinks, Wed May 31 11:45:03 2017 +0900) 138 | * 23d3bf4 - Fix fastlane build command. (Andrew Spinks, Tue May 30 15:49:16 2017 +0900) 139 | * c93c410 - Update mock server version (Andrew Spinks, Tue May 30 15:48:55 2017 +0900) 140 | * e6e6e19 - Ignore swap files (Andrew Spinks, Tue May 30 14:54:30 2017 +0900) 141 | * fbe2b46 - Fixes codecov.yml blank line (Marko Justinek, Tue May 30 10:08:24 2017 +1000) 142 | * f99d8b5 - Adds codecov badge (Marko Justinek, Tue May 30 10:01:45 2017 +1000) 143 | * 7fc985a - Updates build steps (Marko Justinek, Tue May 30 09:45:27 2017 +1000) 144 | * 1dc353a - Changes codecov in travisci steps (Marko Justinek, Tue May 30 09:28:25 2017 +1000) 145 | * cfba6cd - Adds codecov.yml settings file (Marko Justinek, Tue May 30 08:41:43 2017 +1000) 146 | * f3de735 - Sends coverage data to codecov.io (Marko Justinek, Tue May 30 08:23:15 2017 +1000) 147 | * 1852edf - Enables code coverage in scheme (Marko Justinek, Tue May 30 08:17:27 2017 +1000) 148 | * 6425cb8 - Runs brew upgrade swiftlint (Marko Justinek, Mon May 29 12:48:17 2017 +1000) 149 | * 90f72b9 - Forces brew update on TravisCI (Marko Justinek, Mon May 29 12:43:03 2017 +1000) 150 | * 9d2cfe0 - Cleans up swiftlint.yml (Marko Justinek, Mon May 29 12:38:38 2017 +1000) 151 | * a3dbadb - Removes script that installs SwiftLint (Marko Justinek, Mon May 29 12:30:51 2017 +1000) 152 | * 0d66d39 - Cleans up code after default SwiftLint install (Marko Justinek, Mon May 29 12:27:53 2017 +1000) 153 | * acf722f - Installs swiftlint using brew (Marko Justinek, Mon May 29 12:20:37 2017 +1000) 154 | * 47914c6 - Reverts minValue to min to avoid breaking existing (Marko Justinek, Mon May 29 12:16:42 2017 +1000) 155 | * d066976 - Reverts failing swiftlint test (Marko Justinek, Mon May 29 11:12:09 2017 +1000) 156 | * bae0cf3 - Updates swiftlint with opt_in rules and tests failing scenario (Marko Justinek, Mon May 29 10:50:40 2017 +1000) 157 | * 1726a06 - testing swiftlint throws error (Marko Justinek, Mon May 29 10:34:42 2017 +1000) 158 | * fd4949b - testing swiftlint on travisCI (Marko Justinek, Mon May 29 10:00:17 2017 +1000) 159 | * d3ac542 - Fixes deprecated sendSynchronousRequest:returningResponse:error method (Marko Justinek, Mon May 29 09:29:47 2017 +1000) 160 | * 2277452 - Changes script order (Marko Justinek, Mon May 29 09:09:19 2017 +1000) 161 | * 2e3ce09 - Changes TravisCI build order (Marko Justinek, Sat May 27 06:32:35 2017 +1000) 162 | * bfc7e71 - Updates TravisCI build script to include SwiftLint (Marko Justinek, Fri May 26 20:21:20 2017 +1000) 163 | * 0820504 - Updates Gemfile.lock (Marko, Fri May 26 13:39:43 2017 +1000) 164 | * 6153b38 - Merge branch 'master' into swiftlint (Marko, Fri May 26 13:34:04 2017 +1000) 165 | * e08bf36 - Update ruby dependencies which was referencing a library which seems to have been removed. Also update scan to version 1.0. (Andrew Spinks, Fri May 26 12:27:01 2017 +0900) 166 | * a09cdd3 - Fixes build issues due changed method parameters (Marko, Fri May 26 12:22:14 2017 +1000) 167 | * 8109eab - Removes spaces after "[" and before "]" (Marko, Fri May 26 12:20:42 2017 +1000) 168 | * 9318dd0 - Introduces swiftlint (Marko, Thu May 25 18:53:35 2017 +1000) 169 | * 34c5dac - Update dependencies (Andrew Spinks, Fri Dec 16 16:06:26 2016 +0900) 170 | * 2d9a23d - Default timeout to 30 seconds. Add documentation. (Christi Viets, Thu Dec 15 07:57:41 2016 -0500) 171 | 172 | 173 | -------------------------------------------------------------------------------- /Tests/PactConsumerSwiftTests/PactSpecs.swift: -------------------------------------------------------------------------------- 1 | import Quick 2 | import Nimble 3 | import PactConsumerSwift 4 | 5 | class PactSwiftSpec: QuickSpec { 6 | override func spec() { 7 | var animalMockService: MockService? 8 | var animalServiceClient: AnimalServiceClient? 9 | 10 | describe("tests fulfilling all expected interactions") { 11 | beforeEach { 12 | animalMockService = MockService(provider: "Animal Service", consumer: "Animal Consumer Swift") 13 | animalServiceClient = AnimalServiceClient(baseUrl: animalMockService!.baseUrl) 14 | } 15 | 16 | it("gets an alligator") { 17 | animalMockService!.given("an alligator exists") 18 | .uponReceiving("a request for all alligators") 19 | .withRequest(method:.GET, path: "/alligators") 20 | .willRespondWith(status: 200, 21 | headers: ["Content-Type": "application/json"], 22 | body: [ ["name": "Mary", "type": "alligator"] ]) 23 | 24 | //Run the tests 25 | animalMockService!.run(timeout: 10000) { (testComplete) -> Void in 26 | animalServiceClient!.getAlligators( { (alligators) in 27 | expect(alligators[0].name).to(equal("Mary")) 28 | testComplete() 29 | }, failure: { (error) in 30 | testComplete() 31 | }) 32 | } 33 | } 34 | 35 | it("gets an alligator with path matcher") { 36 | let pathMatcher = Matcher.term(matcher: "^\\/alligators\\/[0-9]{4}", 37 | generate: "/alligators/1234") 38 | 39 | animalMockService!.given("an alligator exists") 40 | .uponReceiving("a request for an alligator with path matcher") 41 | .withRequest(method:.GET, path: pathMatcher) 42 | .willRespondWith(status: 200, 43 | headers: ["Content-Type": "application/json"], 44 | body: ["name": "Mary", "type": "alligator"]) 45 | 46 | //Run the tests 47 | animalMockService!.run { (testComplete) -> Void in 48 | animalServiceClient!.getAlligator(1234, success: { (alligator) in 49 | expect(alligator.name).to(equal("Mary")) 50 | testComplete() 51 | }, failure: { (error) in 52 | testComplete() 53 | }) 54 | } 55 | } 56 | 57 | describe("With query params") { 58 | it("should return animals living in water") { 59 | animalMockService!.given("an alligator exists") 60 | .uponReceiving("a request for animals living in water") 61 | .withRequest(method:.GET, path: "/animals", query: ["live": "water"]) 62 | .willRespondWith(status: 200, 63 | headers: ["Content-Type": "application/json"], 64 | body: [ ["name": "Mary", "type": "alligator"] ] ) 65 | 66 | //Run the tests 67 | animalMockService!.run { (testComplete) -> Void in 68 | animalServiceClient!.findAnimals(live: "water", response: { 69 | (response) in 70 | expect(response.count).to(equal(1)) 71 | let name = response[0].name 72 | expect(name).to(equal("Mary")) 73 | testComplete() 74 | }) 75 | } 76 | } 77 | 78 | it("should return animals living in water using dictionary matcher") { 79 | animalMockService!.given("an alligator exists") 80 | .uponReceiving("a request for animals living in water with dictionary matcher") 81 | .withRequest(method:.GET, path: "/animals", query: ["live": Matcher.somethingLike("water")]) 82 | .willRespondWith(status: 200, 83 | headers: ["Content-Type": "application/json"], 84 | body: [ ["name": "Mary", "type": "alligator"] ] ) 85 | 86 | //Run the tests 87 | animalMockService!.run { (testComplete) -> Void in 88 | animalServiceClient!.findAnimals(live: "water", response: { 89 | (response) in 90 | expect(response.count).to(equal(1)) 91 | let name = response[0].name 92 | expect(name).to(equal("Mary")) 93 | testComplete() 94 | }) 95 | } 96 | } 97 | 98 | it("should return animals living in water using matcher") { 99 | let queryMatcher = Matcher.term(matcher: "live=*", generate: "live=water") 100 | 101 | animalMockService!.given("an alligator exists") 102 | .uponReceiving("a request for animals living in water with matcher") 103 | .withRequest(method:.GET, path: "/animals", query: queryMatcher) 104 | .willRespondWith(status: 200, 105 | headers: ["Content-Type": "application/json"], 106 | body: [ ["name": "Mary", "type": "alligator"] ] ) 107 | 108 | //Run the tests 109 | animalMockService!.run { (testComplete) -> Void in 110 | animalServiceClient!.findAnimals(live: "water", response: { 111 | (response) in 112 | expect(response.count).to(equal(1)) 113 | let name = response[0].name 114 | expect(name).to(equal("Mary")) 115 | testComplete() 116 | }) 117 | } 118 | } 119 | } 120 | 121 | describe("With Header matches") { 122 | it("gets a secure alligator with auth header matcher") { 123 | animalMockService!.given("an alligator exists") 124 | .uponReceiving("a request for an alligator with header matcher") 125 | .withRequest(method: .GET, 126 | path: "/alligators", 127 | headers: ["Authorization": Matcher.somethingLike("OIOIUOIU")]) 128 | .willRespondWith(status: 200, 129 | headers: ["Content-Type": "application/json", "Etag": Matcher.somethingLike("x234")], 130 | body: ["name": "Mary", "type": "alligator"]) 131 | 132 | //Run the tests 133 | animalMockService!.run { (testComplete) -> Void in 134 | animalServiceClient!.getSecureAlligators(authToken: "OIOIUOIU", success: { (alligators) in 135 | expect(alligators[0].name).to(equal("Mary")) 136 | testComplete() 137 | }, failure: { (error) in 138 | testComplete() 139 | } 140 | ) 141 | } 142 | } 143 | } 144 | 145 | describe("PATCH request") { 146 | it("should unfriend me") { 147 | animalMockService!.given("Alligators and pidgeons exist") 148 | .uponReceiving("a request eat a pidgeon") 149 | .withRequest(method:.PATCH, path: "/alligator/eat", body: [ "type": "pidgeon" ]) 150 | .willRespondWith(status: 204, headers: ["Content-Type": "application/json"]) 151 | 152 | //Run the tests 153 | animalMockService!.run{ (testComplete) -> Void in 154 | animalServiceClient!.eat(animal: "pidgeon", success: { () in 155 | testComplete() 156 | }, error: { (error) in 157 | expect(true).to(equal(false)) 158 | testComplete() 159 | }) 160 | } 161 | } 162 | } 163 | 164 | 165 | describe("Expecting an error response") { 166 | it("returns an error") { 167 | animalMockService!.given("Alligators don't eat pidgeons") 168 | .uponReceiving("a request to no longer eat pidgeons") 169 | .withRequest(method:.DELETE, path: "/alligator/eat", body: [ "type": "pidgeon" ]) 170 | .willRespondWith(status:404, body: "No relationship") 171 | 172 | //Run the tests 173 | animalMockService!.run { (testComplete) -> Void in 174 | animalServiceClient!.wontEat(animal: "pidgeon", success: { () in 175 | // We are expecting this test to fail - the error handler should be called 176 | expect(true).to(equal(false)) 177 | testComplete() 178 | }, error: { (error) in 179 | testComplete() 180 | }) 181 | } 182 | } 183 | } 184 | 185 | describe("multiple interactions") { 186 | it("should allow multiple interactions in test setup") { 187 | animalMockService!.given("alligators don't each pidgeons") 188 | .uponReceiving("a request to eat") 189 | .withRequest(method:.PATCH, path: "/alligator/eat", body: ["type": "pidgeon"]) 190 | .willRespondWith(status: 204, headers: ["Content-Type": "application/json"]) 191 | animalMockService!.uponReceiving("what alligators eat") 192 | .withRequest(method:.GET, path: "/alligator/eat") 193 | .willRespondWith(status:200, headers: ["Content-Type": "application/json"], body: [ ["name": "Joseph", "type": Matcher.somethingLike("pidgeon")]]) 194 | 195 | //Run the tests 196 | animalMockService!.run { (testComplete) -> Void in 197 | animalServiceClient!.eat(animal: "pidgeon", success: { () in 198 | animalServiceClient!.eats { (response) in 199 | expect(response.count).to(equal(1)) 200 | let name = response[0].name 201 | let type = response[0].type 202 | expect(name).to(equal("Joseph")) 203 | expect(type).to(equal("pidgeon")) 204 | testComplete() 205 | } 206 | }, error: { (error) in 207 | expect(true).to(equal(false)) 208 | testComplete() 209 | }) 210 | } 211 | } 212 | } 213 | 214 | describe("Matchers") { 215 | it("Can match date based on regex") { 216 | animalMockService!.given("an alligator exists with a birthdate") 217 | .uponReceiving("a request for alligator with birthdate") 218 | .withRequest(method:.GET, path: "/alligators/123") 219 | .willRespondWith( 220 | status: 200, 221 | headers: ["Content-Type": "application/json"], 222 | body: [ 223 | "name": "Mary", 224 | "type": "alligator", 225 | "dateOfBirth": Matcher.term( 226 | matcher: "\\d{2}\\/\\d{2}\\/\\d{4}", 227 | generate: "02/02/1999" 228 | ) 229 | ]) 230 | 231 | //Run the tests 232 | animalMockService!.run { (testComplete) -> Void in 233 | animalServiceClient!.getAlligator(123, success: { (alligator) in 234 | expect(alligator.name).to(equal("Mary")) 235 | expect(alligator.dob).to(equal("02/02/1999")) 236 | testComplete() 237 | }, failure: { (error) in 238 | expect(true).to(equal(false)) 239 | testComplete() 240 | }) 241 | } 242 | } 243 | 244 | it("Can match legs based on type") { 245 | animalMockService!.given("an alligator exists with legs") 246 | .uponReceiving("a request for alligator with legs") 247 | .withRequest(method:.GET, path: "/alligators/1") 248 | .willRespondWith( 249 | status: 200, 250 | headers: ["Content-Type": "application/json"], 251 | body: [ 252 | "name": "Mary", 253 | "type": "alligator", 254 | "legs": Matcher.somethingLike(4) 255 | ]) 256 | 257 | //Run the tests 258 | animalMockService!.run { (testComplete) -> Void in 259 | animalServiceClient!.getAlligator(1, success: { (alligator) in 260 | expect(alligator.legs).to(equal(4)) 261 | testComplete() 262 | }, failure: { (error) in 263 | expect(true).to(equal(false)) 264 | testComplete() 265 | }) 266 | } 267 | } 268 | 269 | it("Can match based on flexible length array") { 270 | animalMockService!.given("multiple land based animals exist") 271 | .uponReceiving("a request for animals living on land") 272 | .withRequest( 273 | method:.GET, 274 | path: "/animals", 275 | query: ["live": "land"]) 276 | .willRespondWith( 277 | status: 200, 278 | headers: ["Content-Type": "application/json"], 279 | body: Matcher.eachLike(["name": "Bruce", "type": "wombat"])) 280 | 281 | //Run the tests 282 | animalMockService!.run { (testComplete) -> Void in 283 | animalServiceClient!.findAnimals(live: "land", response: { 284 | (response) in 285 | expect(response.count).to(equal(1)) 286 | expect(response[0].name).to(equal("Bruce")) 287 | testComplete() 288 | }) 289 | } 290 | } 291 | } 292 | } 293 | 294 | context("when defined interactions are not received") { 295 | let errorCapturer = ErrorCapture() 296 | 297 | beforeEach { 298 | animalMockService = MockService( 299 | provider: "Animal Service", 300 | consumer: "Animal Consumer Swift", 301 | pactVerificationService: PactVerificationService(), 302 | errorReporter: errorCapturer 303 | ) 304 | } 305 | 306 | describe("but specified HTTP request was not received by mock service") { 307 | it("returns error message from mock service") { 308 | animalMockService?.given("an alligator exists") 309 | .uponReceiving("a request for all alligators") 310 | .withRequest(method:.GET, path: "/alligators") 311 | .willRespondWith(status: 200, 312 | headers: ["Content-Type": "application/json"], 313 | body: [ ["name": "Mary", "type": "alligator"] ]) 314 | 315 | animalMockService?.run() { (testComplete) -> Void in 316 | testComplete() 317 | } 318 | expect(errorCapturer.message?.message).to(contain("Actual interactions do not match expected interactions for mock")) 319 | } 320 | 321 | it("specifies origin of test error to line where .run() method is called") { 322 | animalMockService?.given("an alligator exists") 323 | .uponReceiving("a request for all alligators") 324 | .withRequest(method:.GET, path: "/alligators") 325 | .willRespondWith(status: 200, 326 | headers: ["Content-Type": "application/json"], 327 | body: [ ["name": "Mary", "type": "alligator"] ]) 328 | 329 | let thisFile: String = #file 330 | let thisLine: UInt = #line 331 | animalMockService?.run() { (testComplete) -> Void in 332 | testComplete() 333 | } 334 | expect(errorCapturer.message?.file?.description) == thisFile 335 | expect(errorCapturer.message?.line) == thisLine + 1 336 | } 337 | } 338 | } 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pact Consumer Swift 2 | 3 | [![Build](https://github.com/DiUS/pact-consumer-swift/workflows/Build/badge.svg)](https://github.com/DiUS/pact-consumer-swift/actions?query=workflow%3ABuild) 4 | [![Codecov](https://codecov.io/gh/DiUS/pact-consumer-swift/branch/master/graph/badge.svg)](https://codecov.io/gh/DiUS/pact-consumer-swift) 5 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 6 | [![Swift Package Manager](https://img.shields.io/badge/Swift_Package_Manager-compatible-brightgreen.svg)][swift-package-manager] 7 | [![Swift](https://img.shields.io/badge/Swift-5-orange.svg?style=flat)](https://swift.org/) 8 | [![Badge w/ CocoaPod Version](https://cocoapod-badges.herokuapp.com/v/PactConsumerSwift/badge.png)](https://cocoadocs.org/docsets/PactConsumerSwift) 9 | [![Badge w/ Supported Platforms](https://cocoapod-badges.herokuapp.com/p/PactConsumerSwift/badge.svg)](https://cocoadocs.org/docsets/PactConsumerSwift) 10 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 11 | [![Twitter](https://img.shields.io/badge/twitter-@pact__up-blue.svg?style=flat)](http://twitter.com/pact_up) 12 | 13 | > ℹ 14 | > A new version featuring [Pact Specification v3][pact-spec-v3], a simplified installation and better management of the mock server processes is in active development and can be found at [PactSwift][pact-swift]. We are currently looking for people to try it out and provide feedback. 15 | 16 | This library provides a Swift / Objective C DSL for creating Consumer [Pacts](http://pact.io). It provides support for **[Consumer Driven Contract Testing][pact-microservices]** between dependent systems where the integration is based on HTTP (or message queues for some of the implementations). 17 | 18 | _But why?_ To test communication boundaries between your app and services. 19 | You can view a presentation on how Pact can work in a mobile context here: [Yow! Connected 2016 Andrew Spinks - Increasing The Confidence In Your Service Integrations](https://www.youtube.com/watch?v=UQkMr4bKYp4). 20 | 21 | Implements [Pact Specification v2][pact-spec-v2], 22 | including [flexible matching][pact-flexible-matching]. 23 | 24 | This DSL relies on the Ruby [pact-ruby-standalone][pact-ruby-standalone] ([brew tap][pact-ruby-standalone-homebrew]) to provide the mock service for the tests. 25 | 26 | ## Installation 27 | 28 | Note: see [Upgrading][upgrading] for notes on upgrading from 0.2 to 0.3 29 | 30 | ### Install Pact Mock Service 31 | 32 | #### Homebrew 33 | 34 | brew tap pact-foundation/pact-ruby-standalone 35 | brew install pact-ruby-standalone 36 | 37 | This will install the following tools: 38 | 39 | pact 40 | pact-broker 41 | pact-message 42 | pact-mock-service 43 | pact-provider-verifier 44 | pact-publish 45 | pact-stub-service 46 | 47 | #### Manually 48 | 49 | Alternatively you can download and install the [pact-ruby-standalone][pact-ruby-standalone] archives for your platform and install as per installation instructions written in [Pact Ruby Standalone release notes][pact-ruby-standalone-releases]. 50 | 51 | #### Xcode Setup 52 | 53 | In Xcode, edit your scheme and add _pre-_ and _post-actions_ to `Test` to start and stop `pact-mock-service`. Make sure you select your target in _Provide build settings from_ the drop down menu. 54 | 55 | ``` 56 | # Pre-actions 57 | PATH=/path/to/your/standalone/pact/bin:$PATH 58 | pact-mock-service start --pact-specification-version 2.0.0 --log "${SRCROOT}/tmp/pact.log" --pact-dir "${SRCROOT}/tmp/pacts" -p 1234 59 | 60 | # Post-actions 61 | PATH=/path/to/your/standalone/pact/bin:$PATH 62 | pact-mock-service stop 63 | ``` 64 | Note: your generated Pact files will be dropped into `"${SRCROOT}/tmp/pacts"` folder. 65 | 66 | ![Xcode Scheme Test Pre-actions](scripts/images/xcode-scheme-test-pre-actions.png) 67 | 68 | ### Add the PactConsumerSwift library to your project 69 | 70 | #### Using [Carthage](https://github.com/Carthage/Carthage) 71 | 72 | - See the [PactSwiftExample][pact-carthage-ios-example] [![Swift, Carthage Example - Build Status](https://travis-ci.org/andrewspinks/PactSwiftExample.svg?branch=master)][build-carthage-ios-example] for an example project using `pact-consumer-swift` with Carthage for an iOS target. 73 | - See the [PactMacOSExample][pact-carthage-macos-example] [![Build](https://github.com/surpher/PactMacOSExample/workflows/Build/badge.svg)][build-carthage-macos-example] for an example project using `pact-consumer-swift` through Carthage for a macOS target. 74 | 75 | #### Using [CocoaPods](https://cocoapods.org/pods/PactConsumerSwift) 76 | 77 | - See the [PactObjectiveCExample][pact-objc-example] [![Build Status](https://travis-ci.org/andrewspinks/PactObjectiveCExample.svg?branch=master)][build-objc-example] for an example project using `pact-consumer-swift` with CocoaPods for an iOS target. 78 | 79 | #### Using [Swift Package Manager][swift-package-manager] 80 | 81 | - See the [PactSwiftPMExample][pact-swiftpm-example] [![Build](https://github.com/surpher/PactSwiftPMExample/workflows/Build/badge.svg)][build-swiftpm-example] for an example project using `pact-consumer-swift` library through Swift Package Manager for an executable that runs in terminal. 82 | 83 | ## Writing Pact Tests 84 | 85 | ### Testing with Swift 86 | 87 | Write a Unit test similar to the following (NB: this example is using the [Quick](https://github.com/Quick/Quick) test framework) 88 | 89 | ```swift 90 | import PactConsumerSwift 91 | 92 | ... 93 | beforeEach { 94 | animalMockService = MockService(provider: "Animal Service", consumer: "Animal Consumer Swift") 95 | animalServiceClient = AnimalServiceClient(baseUrl: animalMockService!.baseUrl) 96 | } 97 | 98 | it("gets an alligator") { 99 | animalMockService!.given("an alligator exists") 100 | .uponReceiving("a request for an alligator") 101 | .withRequest(method:.GET, path: "/alligator") 102 | .willRespondWith(status:200, 103 | headers: ["Content-Type": "application/json"], 104 | body: ["name": "Mary"]) 105 | 106 | //Run the tests 107 | animalMockService!.run { (testComplete) -> Void in 108 | animalServiceClient!.getAlligator { (alligator) in 109 | expect(alligator.name).to(equal("Mary")) 110 | testComplete() 111 | } 112 | } 113 | } 114 | ``` 115 | 116 | An optional `timeout` (seconds) parameter can be included on the run function. This defaults to 30 seconds. 117 | 118 | ```swift 119 | ... 120 | animalMockService!.run(timeout: 60) { (testComplete) -> Void in 121 | animalServiceClient!.getAlligator { (alligator) in 122 | expect(alligator.name).to(equal("Mary")) 123 | testComplete() 124 | } 125 | } 126 | ``` 127 | 128 | ### Testing with Objective-C 129 | 130 | Write a Unit test similar to the following 131 | 132 | ```objc 133 | @import PactConsumerSwift; 134 | ... 135 | - (void)setUp { 136 | [super setUp]; 137 | self.animalMockService = [[MockService alloc] initWithProvider:@"Animal Provider" 138 | consumer:@"Animal Service Client Objective-C"]; 139 | self.animalServiceClient = [[OCAnimalServiceClient alloc] initWithBaseUrl:self.animalMockService.baseUrl]; 140 | } 141 | 142 | - (void)testGetAlligator { 143 | typedef void (^CompleteBlock)(); 144 | 145 | [[[[self.animalMockService given:@"an alligator exists"] 146 | uponReceiving:@"oc a request for an alligator"] 147 | withRequestHTTPMethod:PactHTTPMethodGET 148 | path:@"/alligator" 149 | query:nil headers:nil body:nil] 150 | willRespondWithHTTPStatus:200 151 | headers:@{@"Content-Type": @"application/json"} 152 | body: @"{ \"name\": \"Mary\"}" ]; 153 | 154 | [self.animalMockService run:^(CompleteBlock testComplete) { 155 | Animal *animal = [self.animalServiceClient getAlligator]; 156 | XCTAssertEqualObjects(animal.name, @"Mary"); 157 | testComplete(); 158 | }]; 159 | } 160 | ``` 161 | 162 | An optional `timeout` (seconds) parameter can be included on the run function. This defaults to 30 seconds. 163 | 164 | ```objc 165 | ... 166 | [self.animalMockService run:^(CompleteBlock testComplete) { 167 | Animal *animal = [self.animalServiceClient getAlligator]; 168 | XCTAssertEqualObjects(animal.name, @"Mary"); 169 | testComplete(); 170 | } timeout:60]; 171 | } 172 | ``` 173 | 174 | ### Testing with XCTest 175 | 176 | Write a Unit Test similar to the following: 177 | 178 | ```swift 179 | import PactConsumerSwift 180 | ... 181 | var animalMockService: MockService? 182 | var animalServiceClient: AnimalServiceClient? 183 | 184 | override func setUp() { 185 | super.setUp() 186 | 187 | animalMockService = MockService(provider: "Animal Provider", consumer: "Animal Service Client") 188 | animalServiceClient = AnimalServiceClient(baseUrl: animalMockService!.baseUrl) 189 | } 190 | 191 | func testItGetsAlligator() { 192 | // Prepare the expecated behaviour using pact's MockService 193 | animalMockService! 194 | .given("an alligator exists") 195 | .uponReceiving("a request for alligator") 196 | .withRequest(method: .GET, path: "/alligator") 197 | .willRespondWith(status: 200, 198 | headers: ["Content-Type": "application/json"], 199 | body: [ "name": "Mary" ]) 200 | 201 | // Run the test 202 | animalMockService!.run(timeout: 60) { (testComplete) -> Void in 203 | self.animalServiceClient!.getAlligator { (response) -> in 204 | XCTAssertEqual(response.name, "Mary") 205 | testComplete() 206 | } 207 | } 208 | } 209 | ... 210 | ``` 211 | 212 | An optional `timeout` (seconds) parameter can be included on the run function. Defaults to 30 seconds. 213 | 214 | ```swift 215 | ... 216 | // Run the test 217 | animalMockService!.run(timeout: 60) { (testComplete) -> Void in 218 | self.animalServiceClient!.getAlligator { (response) -> in 219 | XCTAssertEqual(response.name, "Mary") 220 | testComplete() 221 | } 222 | } 223 | ``` 224 | 225 | For an example on how to test over `https` see [PactSSLSpec.swift][pact-ssl-spec]. 226 | 227 | ### Matching 228 | 229 | In addition to verbatim value matching, you have 3 useful matching functions 230 | in the `Matcher` class that can increase expressiveness and reduce brittle test 231 | cases. 232 | 233 | * `Matcher.term(matcher, generate)` - tells Pact that the value should match using 234 | a given regular expression, using `generate` in mock responses. `generate` must be 235 | a string. 236 | * `Matcher.somethingLike(content)` - tells Pact that the value itself is not important, as long 237 | as the element _type_ (valid JSON number, string, object etc.) itself matches. 238 | * `Matcher.eachLike(content, min)` - tells Pact that the value should be an array type, 239 | consisting of elements like those passed in. `min` must be >= 1. `content` may 240 | be a valid JSON value: e.g. strings, numbers and objects. 241 | 242 | *NOTE*: One caveat to note, is that you will need to use valid Ruby [regular expressions][regular-expressions] and double escape backslashes. 243 | 244 | See the `PactSpecs.swift`, `PactObjectiveCTests.m` for examples on how to expect error responses, how to use query params, and Matchers. 245 | 246 | For more on request / response matching, see [Matching][getting_started/matching]. 247 | 248 | ### Using in your CI 249 | 250 | Xcode's _pre-actions_ and _post-actions_ do not honour non-zero script exits and therefore would not fail your build if publishing to a Pact Broker would fail. If you would like to upload your Pact files to a Pact Broker as part of your CI, we would suggest that you create a separate step in your CI workflow with that responsibility. 251 | 252 | See [pact-ruby-standalone][pact-ruby-standalone-releases] page for installation instructions and how to use `pact-broker` client. 253 | 254 | ### Verifying your client against the service you are integrating with 255 | 256 | If your setup is correct and your tests run against the pack mock server, then you should see a log file here: 257 | `$YOUR_PROJECT/tmp/pact.log` 258 | And the generated pacts here: 259 | `$YOUR_PROJECT/tmp/pacts/...` 260 | 261 | [Publish][pact-publish-to-broker] your generated pact file(s) to your [Pact Broker][pact-broker] or a [Hosted Pact Broker][pactflow] so your _API provider_ can always retrieve them from one location, even when pacts change. Or even just by simply sending the pact file to your API provider devs so they can used them in their tests of their API responses. See [Verifying pacts][pact-verifying] for more information. 262 | For an end-to-end example with a ruby back end service, have a look at the [KatKit example][pact-katkit-example]. 263 | 264 | Also, check out this article on [using a dockerized Node.js service][pact-dockerized-example] that uses provider states. 265 | 266 | ## More reading 267 | 268 | - The Pact website [Pact](http://pact.io) 269 | - The pact mock server that the Swift library uses under the hood [Pact mock service][pact-mock-service] 270 | - A pact broker for managing the generated pact files (so you don't have to manually copy them around!) [Pact broker][pact-broker] 271 | 272 | ## Contributing 273 | 274 | Please read [CONTRIBUTING.md](/CONTRIBUTING.md) 275 | 276 | [upgrading]: https://github.com/DiUS/pact-consumer-swift/wiki/Upgrading 277 | [pact-broker]: https://github.com/pact-foundation/pact_broker 278 | [pact-readme]: https://github.com/realestate-com-au/pact 279 | [pact-verifying]: https://docs.pact.io/getting_started/verifying_pacts 280 | [pact-spec-v2]: https://github.com/pact-foundation/pact-specification/tree/version-2 281 | [pact-spec-v3]: https://github.com/pact-foundation/pact-specification/tree/version-3 282 | [pact-swift]: https://github.com/surpher/PactSwift 283 | [pact-flexible-matching]: https://docs.pact.io/getting_started/matching 284 | [pact-publish-to-broker]: https://github.com/pact-foundation/pact_broker/wiki/Publishing-and-retrieving-pacts 285 | [pact-katkit-example]: https://github.com/andrewspinks/pact-mobile-preso 286 | [pact-dockerized-example]: https://medium.com/@rajatvig/ios-docker-and-consumer-driven-contract-testing-with-pact-d99b6bf4b09e#.ozcbbktzk 287 | [pact-mock-service]: https://github.com/bethesque/pact-mock_service 288 | [pact-ruby-standalone]: https://github.com/pact-foundation/pact-ruby-standalone 289 | [pact-ruby-standalone-releases]: https://github.com/pact-foundation/pact-ruby-standalone/releases 290 | [pact-ruby-standalone-homebrew]: https://github.com/pact-foundation/homebrew-pact-ruby-standalone 291 | [pact-mock-service-without-ruby]: https://github.com/DiUS/pact-consumer-js-dsl/wiki/Using-the-Pact-Mock-Service-without-Ruby 292 | [pact-ssl-spec]: Tests/PactConsumerSwiftTests/PactSSLSpecs.swift 293 | [regular-expressions]: http://ruby-doc.org/core-2.1.5/Regexp.html 294 | [matching]: http://docs.pact.io/documentation/matching.html 295 | [pact-swiftpm-example]: http://github.com/surpher/PactSwiftPMExample 296 | [build-swiftpm-example]: https://github.com/surpher/PactSwiftPMExample/actions?query=workflow%3ABuild 297 | [pact-objc-example]: https://github.com/andrewspinks/PactObjectiveCExample 298 | [build-objc-example]: https://travis-ci.org/andrewspinks/PactObjectiveCExample 299 | [pact-carthage-macos-example]: https://github.com/surpher/PactMacOSExample 300 | [build-carthage-macos-example]: https://github.com/surpher/PactMacOSExample/actions?query=workflow%3ABuild 301 | [pact-carthage-ios-example]: https://github.com/andrewspinks/PactSwiftExample 302 | [build-carthage-ios-example]: https://travis-ci.org/andrewspinks/PactSwiftExample 303 | [pact-microservices]: https://dius.com.au/2016/02/03/pact-101-getting-started-with-pact-and-consumer-driven-contract-testing/ 304 | [swift-package-manager]: https://swift.org/package-manager/ 305 | [pactflow]: https://pactflow.io 306 | --------------------------------------------------------------------------------