├── 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 | [](https://github.com/DiUS/pact-consumer-swift/actions?query=workflow%3ABuild)
4 | [](https://codecov.io/gh/DiUS/pact-consumer-swift)
5 | [](https://github.com/Carthage/Carthage)
6 | [][swift-package-manager]
7 | [](https://swift.org/)
8 | [](https://cocoadocs.org/docsets/PactConsumerSwift)
9 | [](https://cocoadocs.org/docsets/PactConsumerSwift)
10 | [](https://opensource.org/licenses/MIT)
11 | [](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 | 
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] [][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-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-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-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 |
--------------------------------------------------------------------------------