diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md new file mode 100644 index 00000000..8ea80862 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -0,0 +1,50 @@ +--- +name: Bug report +about: Create a bug report to help us improve. +title: 'Bug: ...' +labels: "bug" +assignees: '' +--- +# 🌎 Environment + + - Xcode: [e.g. 11.4] + - Platform: [e.g. iOS] + - Version/Release: [e.g. 1.0.2] + - Dependency manager: [e.g. Carthage 0.38] + +# 💬 Description + + + +# 🦶 Reproduction Steps + + + +Steps to reproduce the behavior: +1. Do this... +2. Run this `xcrun ...` +3. Etc. + +# 🤔 Expected Results + + + +# 😲 Actual Results + + + +## 🌳 Logs + +``` +Include any logs or command output if applicable... +``` + +## 📄 Stack Traces + +``` +Include a stack trace if applicable... +``` + +# 🤝 Relationships + +- Related PRs or Issues: #xxx, #yyy diff --git a/.github/ISSUE_TEMPLATE/feature.md b/.github/ISSUE_TEMPLATE/feature.md new file mode 100644 index 00000000..d9de8654 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.md @@ -0,0 +1,36 @@ +--- +name: Feature Request +about: Create a feature request to enhance the project. +title: '' +labels: 'enhancement' +assignees: '' + +--- + +# 🗣 Context + + + +# 💬 Narrative + +When ... +I want ... +So that ... + +# 📝 Notes + + + +# 🏗 Design + + + +# ✅ Acceptance Criteria + +**GIVEN** ... +**WHEN** ... +**THEN** ... + +# 🚫 Out of Scope + + diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 00000000..9d666884 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,19 @@ +--- +name: Question +about: Just looking for some information about a thing +title: 'Question: ...' +labels: "question" +assignees: '' +--- +# ❔ Question + + + +# 💬 Context + + + + +# 🤝 Relationships + +- Other Related Issues: #xxx, #yyy diff --git a/.github/ISSUE_TEMPLATE/task.md b/.github/ISSUE_TEMPLATE/task.md new file mode 100644 index 00000000..949f4065 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/task.md @@ -0,0 +1,22 @@ +--- +name: Task +about: Create a technical task that needs doing. +title: 'Task: ...' +labels: "task" +assignees: '' +--- +# ❕ Problem Statement + + + +# 💬 Task Description + + + +# 👩‍🔧 Technical Design Notes + + + +# 🤝 Relationships + +- Other Related Issues: #xxx, #yyy diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..e2ec1ec6 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,26 @@ +- ☝️ Provide a short but descriptive title for this PR. +- 🏷 Remember to label your pull request appropriately and select appropriate code reviewers. + +_Provide a more descriptive summary of the changes in this PR here._ + +# 📝 Summary of Changes + +Changes proposed in this pull request: + +- Resolves issue #99999 by doing the thing with another thing +- Add more details here... +- ... + +# ⚠️ Items of Note + +_Document anything here that you think the reviewer(s) of this PR may need to know, or would be of specific interest._ + +# 🧐🗒 Reviewer Notes + +## 💁 Example + +_Add an example of using the changes you've implemented (eg, a screenshot or text dump of the output)._ + +## 🔨 How To Test + +_Provide instructions on how to test your changes._ \ No newline at end of file diff --git a/.github/codecov.yml b/.github/codecov.yml new file mode 100644 index 00000000..285fb950 --- /dev/null +++ b/.github/codecov.yml @@ -0,0 +1,29 @@ +codecov: + notify: + require_ci_to_pass: yes + +coverage: + precision: 2 + round: down + range: "70...90" + + status: + project: yes + patch: yes + changes: no + + ignore: + - "Tests/**" + +parsers: + gcov: + branch_detection: + conditional: yes + loop: yes + method: no + macro: no + +comment: + layout: "header, diff" + behavior: default + require_changes: no diff --git a/.github/config.yml b/.github/config.yml new file mode 100644 index 00000000..521c667f --- /dev/null +++ b/.github/config.yml @@ -0,0 +1,27 @@ +updateDocsWhiteList: + - BUG + - Chore + - minor + +updateDocsComment: > + Thanks for opening this pull request! The maintainers of this repository would appreciate it if you would update some of our documentation based on your changes. +requestInfoReplyComment: > + We would appreciate it if you could provide us with more info about this issue/pr! +requestInfoLabelToAdd: request-more-info + +newPRWelcomeComment: > + :tada: Thanks so much for opening your first PR in this project! We really do appreciate your help! :heart: +firstPRMergeComment: > + Congrats on merging your first pull request in this project! :tada: You're awesome! :metal: +newIssueWelcomeComment: > + Thanks for opening this issue, a maintainer will get back to you shortly! cc/ @surpher +sentimentBotToxicityThreshold: .7 + +sentimentBotReplyComment: > + Please be sure to review the code of conduct and be respectful of other users. cc/ @surpher +lockThreads: + toxicityThreshold: .7 + numComments: 2 + setTimeInHours: 72 + replyComment: > + This thread is being locked due to exceeding the toxicity minimums. cc/ @surpher diff --git a/.github/semantic.yml b/.github/semantic.yml new file mode 100644 index 00000000..1bf7a02e --- /dev/null +++ b/.github/semantic.yml @@ -0,0 +1,20 @@ +# Always validate the PR title and commits (this is important because release notes are automated and we rely on semantic commit titles) +titleAndCommits: true + +# By default types specified in commitizen/conventional-commit-types is used. +# See: https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json +# You can override the valid types +types: + - feat + - feature + - fix + - docs + - style + - refactor + - perf + - test + - build + - ci + - chore + - revert + - tech diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..3b7a87bd --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,65 @@ +name: Build + +on: + pull_request: + types: + - opened + - synchronize + - reopened + - ready_for_review + workflow_dispatch: + +env: + RUST_TARGET_PATH: pact-reference + +jobs: + test_ios: + name: "🤖 Test iOS" + runs-on: ${{ matrix.host }} + + strategy: + fail-fast: true + matrix: + host: [macos-13, macos-14] + platform: [ios, macos] + include: + - platform: ios + scheme: "PactSwift-iOS" + destination: "platform=iOS Simulator,name=iPhone 15 Pro" + - platform: macos + scheme: "PactSwift-macOS" + destination: "arch=x86_64" + - host: macos-13 + xcode: 14.3.1 + - host: macos-14 + xcode: 15.3 + + env: + SCHEME: "PactSwift-iOS" + DESTINATION: ${{ matrix.destination }} + + concurrency: + group: test_${{ matrix.host }}_${{ matrix.xcode }}_iOS_${{ github.ref }} + cancel-in-progress: true + + steps: + - name: "🧑‍💻 Checkout repository" + uses: actions/checkout@v3 + + - name: "🏭 Use Xcode ${{ matrix.xcode }}" + run: sudo xcode-select -switch /Applications/Xcode_${{ matrix.xcode }}.app + + - name: "🧪 Run tests (xcodebuild)" + run: | + set -o pipefail && \ + xcodebuild -resolvePackageDependencies && \ + xcodebuild test \ + -project PactSwift.xcodeproj \ + -scheme "$SCHEME"\ + -destination "$DESTINATION" \ + | xcbeautify + + # - name: "⚗️ Run tests (swift)" + # run: | + # swift build + # swift test -c release diff --git a/.github/workflows/build_pr.yml b/.github/workflows/build_pr.yml new file mode 100644 index 00000000..d55b28dd --- /dev/null +++ b/.github/workflows/build_pr.yml @@ -0,0 +1,86 @@ +name: Build Pull Request + +on: + pull_request: + branches: + - '!main' + +jobs: + test_macos: + name: "🤖 Test macOS" + runs-on: macos-13 + + env: + XCODE_VERSION: 14.3.1 + + concurrency: + group: test_macos13_darwin_$SCHEME_${{ github.ref }} + cancel-in-progress: true + + steps: + - name: "🧑‍💻 Checkout repository" + uses: actions/checkout@v3 + + - name: "⚙️ Use Xcode ${{ env.XCODE_VERSION }}" + run: sudo xcode-select -switch /Applications/Xcode_${{ env.XCODE_VERSION }}.app + + - name: "🧰 Prepare tools" + run: | + Scripts/prepare_build_tools + + - name: "🧪 xcodebuild clean test" + run: | + set -o pipefail && xcodebuild -resolvePackageDependencies | xcbeautify && xcodebuild clean test -project PactSwift.xcodeproj -scheme "PactSwift-macOS" -destination "arch=x86_64" | xcbeautify + + - name: "⚗️ swift test" + run: | + set -o pipefail && swift test -c release + + test_ios: + name: "🤖 Test iOS" + runs-on: macos-13 + needs: [test_macos] + + env: + SCHEME: "PactSwift-iOS" + DESTINATION: "platform=iOS Simulator,name=iPhone 14 Pro" + XCODE_VERSION: 14.3.1 + + concurrency: + group: test_macos13_ios_$SCHEME_${{ github.ref }} + cancel-in-progress: true + + steps: + - name: "🧑‍💻 Checkout repository" + uses: actions/checkout@v3 + + - name: "⚙️ Use Xcode ${{ env.XCODE_VERSION }}" + run: sudo xcode-select -switch /Applications/Xcode_${{ env.XCODE_VERSION }}.app + + - name: "🧰 Prepare tools" + run: | + Scripts/prepare_build_tools + + - name: "♘ Test for Carthage" + run: | + set -o pipefail && carthage build --no-skip-current --use-xcframeworks + + - name: "🧪 xcodebuild clean test" + run: | + set -o pipefail && xcodebuild -resolvePackageDependencies | xcbeautify && xcodebuild clean test -project PactSwift.xcodeproj -scheme "PactSwift-iOS" -destination "platform=iOS Simulator,name=iPhone 14 Pro" | xcbeautify + + - name: "⚗️ swift test" + run: | + swift test -c release + + after_success: + needs: [test_ios] + name: "🚚 Build demo projects" + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + + steps: + - name: "🚚 Build demo projects" + run: | + curl -X POST https://api.github.com/repos/surpher/pact-swift-examples/dispatches -H 'Accept: application/vnd.github.everest-preview+json' -u ${{ secrets.PACT_SWIFT_TOKEN }} --data '{"event_type":"PactSwift - ${{ github.event.head_commit.message }}"}' + \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..43a2b2ab --- /dev/null +++ b/.gitignore @@ -0,0 +1,108 @@ +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Xcode ### +# Xcode +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) + +## Xcode Patch +*.xcodeproj/* +!*.xcodeproj/project.pbxproj +!*.xcodeproj/xcshareddata/ +!*.xcworkspace/contents.xcworkspacedata +/*.gcno + +### Xcode Patch ### +**/xcshareddata/WorkspaceSettings.xcsettings + +### Swift ### +# Xcode +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## Build generated +build/ +DerivedData/ +tmp/ + +## Various settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata/ + +## Other +*.moved-aside +*.xccheckout +*.xcscmblueprint + +## Obj-C/Swift specific +*.hmap +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +###################### +## Package managers ## +###################### + +### SwiftPackageManager ### + +Packages/ +.build/ + +# Add this line if you want to avoid checking in Xcode SPM integration. +# .swiftpm/xcode + +### Carthage ### +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/ +.swiftpm* + +################### +## Binaries ## +################### +*.a diff --git a/.hound.yml b/.hound.yml new file mode 100644 index 00000000..6941f639 --- /dev/null +++ b/.hound.yml @@ -0,0 +1,2 @@ +swiftlint: + config_file: .swiftlint.yml diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 00000000..7e1fdcd0 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,78 @@ +disabled_rules: + - force_cast + - identifier_name + - large_tuple + - operator_whitespace + - unused_declaration + - unused_import + +opt_in_rules: + - closure_body_length + - closure_end_indentation + - closure_parameter_position + - closure_spacing + - collection_alignment + - contains_over_filter_count + - contains_over_filter_is_empty + - contains_over_first_not_nil + - contains_over_range_nil_comparison + - convenience_type + - discouraged_object_literal + - empty_collection_literal + - empty_count + - empty_string + - empty_xctest_method + - explicit_init + - first_where + - flatmap_over_map_reduce + - identical_operands + - implicit_return + - joined_default_parameter + - last_where + - legacy_multiple + - legacy_random + - literal_expression_end_indentation + - lower_acl_than_parent + - modifier_order + - number_separator + - operator_usage_whitespace + - overridden_super_call + - override_in_extension + - private_action + - prohibited_super_call + - reduce_into + - redundant_nil_coalescing + - redundant_type_annotation + - sorted_first_last + - sorted_imports + - static_operator + - toggle_bool + - trailing_whitespace + - unneeded_parentheses_in_closure_argument + - yoda_condition + - xct_specific_matcher + +analyzer_rules: + - unused_declaration + - unused_import + +line_length: + warning: 180 + ignores_function_declarations: true + ignores_comments: false + +modifier_order: + preferred_modifier_order: [acl, override] + +nesting: + type_level: 2 + +included: + - Sources + +excluded: + - Carthage + - Tests + +trailing_comma: + mandatory_comma: true diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..a01e1313 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,172 @@ +# 1.1.0 - v1.1.0 +* 8a30412 - Merge pull request #97 from huwr/huwr/sendable-done-function (Marko Justinek) +* 34c8056 - Make the `done` function Sendable (Huw Rowlands) +# 1.0.2 - v1.0.2 +* d511cc4 - chore: Bumps dependency version number (Marko Justinek) +# 1.0.1 - v1.0.1 +* c3aed20 - chore: Bumps mock server version to v0.4.2 (Marko Justinek) +* 2404e83 - fix: Resolves SomethingLike matcher ignoring nested matchers (#93) (#94) (Marko Justinek) +* b024615 - Chore: Update README (Marko Justinek) +# 1.0.0 - v1.0.0 +* d8d2e51 - docs: Update build status badge in README (Marko Justinek) +* 12d25bd - chore: Bump mock server version (Marko Justinek) +* 9950c02 - ci: Refactor CI workflow for Linux (Marko Justinek) +* a1713ae - fix: Resolve unsupported import for Linux (Marko Justinek) +* 2b28836 - tech: Updates to CI workflows (Marko Justinek) +* c8f7e40 - tech: Update gitignore (Marko Justinek) +* 2ed2c48 - tech: Removes toolbox package dependency (Marko Justinek) +* a7195b4 - chore: Bumping up minimum macOSX deployment target (Marko Justinek) +* dc616b9 - chore: Setting exact version for mock server dependency (Marko Justinek) +* a56db23 - docs: Updates EachLike example in-code docs (Marko Justinek) +# 0.13.1 - v0.13.1 +* a4ce230 - tech: Re-introduces CI workflow on macOS 10.15 (surpher) +* a376128 - fix: Adds compiler check for _implementationOnly imports (surpher) +* 89af3d7 - bugfix: Fixes Obj-C interface for merge feature (surpher) +* d4376cf - tech: Sets minimum version for dependency (surpher) +* f3703ed - chore: Fix dependency version (surpher) +* 795f409 - fix: Attempt to fix unit tests on CI (surpher) +* 977ecb4 - chore: Removing another print statement in unit test (surpher) +* 2cb7187 - fix: Tests with merge flag (surpher) +* c5d3d54 - fix: Disable logging in Xcode scheme (surpher) +* 8d415a9 - test: Increase timeout for test making multiple requests (surpher) +* 27fe3b7 - fix: Remove print statement from unit test (surpher) +* 0181b46 - chore: Adds project-level copyright template (surpher) +# 0.13.0 - v0.13.0 +* 2bbb90f - chore: Clean up in-code documentation (surpher) +* 80667bd - fix: Fixes type-os in inline documentation (Marko Justinek) +* 4856bd1 - refactor: Using typealias for code readability (Marko Justinek) +* ceb08db - docs: Update inline documentation for eachKeyLike (Marko Justinek) +* aac0236 - refactor: Makes eachKeyLike matcher API similar to existing matchers (Marko Justinek) +* 9565d9c - fix: Invalid processing of eachKeyLike matcher (Marko Justinek) +* 4d98eb5 - refactor: Making internal parameter a non-mutating property (Marko Justinek) +* 197e5b0 - feature: Adds EachKeyLike matcher (Marko Justinek) +# 0.12.1 - v0.12.1 +* f992c7c - tech: Updates to unit tests script (Marko Justinek) +* 28a3d76 - chore: Lock in mock service dependency version (Marko Justinek) +* 0501132 - feat: Init MockService with merge contract file Bool flag (Marko Justinek) +* eeffc8b - feat: Don’t finalize Pact contract when running on physical devices (Marko Justinek) +* a0c7ae6 - fix: Add homebrew bin folder to PATH (Marko Justinek) +* f90c382 - docs: Update README (Marko Justinek) +* a04d0dc - docs: Update README for Carthage setup (Marko Justinek) +* 9e778d9 - fix: Keep the reference alive (Marko Justinek) +* 9daecd4 - docs: Dropping support for Carthage (Marko Justinek) +* 88bf3f5 - chore: Remove print statement (Marko Justinek) +* c150786 - tech: Dropping CI workflow for macOS 10 and Xcode 12.x (Marko Justinek) +* 85e76b7 - chore: Ignoring Package.resolved (Marko Justinek) +* 0e7b7b1 - fix: Explicitly referencing self in closures for Swift 6 (Marko Justinek) +* 1c58927 - chore: Remove print statement (Marko Justinek) +* e33b0e8 - docs: Update README re Carthage version (Marko Justinek) +* 823e377 - fix: Import PactSwiftMockServer as implementation only (Marko Justinek) +* 6c9c3a1 - Update README.md (Marko Justinek) +* 959d987 - Update README.md (Marko Justinek) +* 4709275 - docs: Update README (Marko Justinek) +* e0c4d04 - chore: Update README (Marko Justinek) +# 0.12.0 - v0.12.0 +* 6611b13 - feat: Add support for datetime expressions (#84) (Marko Justinek) +* 02807e6 - fix: CI build environment for legacy platform (Marko Justinek) +* ba34db8 - tech: Remove pact_ffi commit pinning due cargo version (Marko Justinek) +* 2e745b2 - tech: Configures GitHub actions to use Xcode 13.2 (Marko Justinek) +* 33612da - docs: Update example in inline documentation (Marko Justinek) +# 0.11.2 - v0.11.2 +* 0d6d9b9 - test: Add basic tests for random UUID (Marko Justinek) +* 0b10fbf - docs: Wrap inline documentation comments (Marko Justinek) +* f41a291 - fix: Sets randomUUID value with format (Marko Justinek) +# 0.11.1 - v0.11.1 +* b134e29 - feat: Enhance random UUID example generator (Marko Justinek) +* 812cc4e - fix: Silencing SwiftLint error (Marko Justinek) +* a22a0e0 - doco: Update EachLike matcher code comments (Marko Justinek) +* 520ffd6 - chore: Update README.md (Marko Justinek) +* 8c816a1 - test: Add tests for ObjC FromProviderState matcher (Marko Justinek) +* 0d3f2a8 - test: Improves test coverage of AnyEncodables (Marko Justinek) +* 74207a7 - refactor: Change capure list in completion block (Marko Justinek) +* fabcc6d - chore: Code documentation cleanup (Marko Justinek) +* ff78112 - tech: Pin pact_ffi build for Linux to a specific commit sha (Marko Justinek) +* 47b9a64 - refactor: Checks for errors before setup and verify (Marko Justinek) +* a86ebc2 - feat: Bail mock server setup and verification early (Marko Justinek) +* b5b6120 - fix: RegexLike matcher validates value against pattern (Marko Justinek) +* 207812b - doco: Add high level explanations (Marko Justinek) +* 768883b - doco: Update CONTRIBUTING with link to Conventional Commits (Marko Justinek) +* 3c7c84c - tech: Adds a GitHub workflow for PRs (Marko Justinek) +* 368a51b - tech: Require titles and commit messages to follow semantic messages (Marko Justinek) +# 0.11.0 - v0.11.0 +* ee6c1ca - fix: Failing unit tests (Marko Justinek) +* 93286f5 - doco: Update readme for v0.11.0 (Marko Justinek) +* ad6a51e - feature: Verify a set of interactions in Obj-C project (Marko Justinek) +* 71e9a3c - refactor: Renames validate argument to verify (Marko Justinek) +* f670aa4 - feature: Validate multiple interactions (Marko Justinek) +* b9f16e0 - doco: Re-arrange README content (Marko Justinek) +# 0.10.0 - v0.10.0 +* 9b0231a - doco: Update README.md (Marko Justinek) +* a353ff1 - chore: Update dependency version (Marko Justinek) +* 78445ac - refactor: Rename argument in MockService initialser (Marko Justinek) +* c12e57e - fix: Failing tests on Linux (Marko Justinek) +* 44e054b - refactor: Upgrade PactSwiftMockServer version (Marko Justinek) +* 8ec91fc - feat: Instantiate MockService with Pact directory path (Marko Justinek) +* efd1d7f - chore: Update README.md (Marko Justinek) +* 7f69099 - tech: Exit release script when no version args provided (Marko Justinek) +# 0.9.0 - v0.9.0 +* d8514f9 - chore: Use depenecy version that supports provider verification (Marko Justinek) +* d9c3c62 - doco: Update README.md (Marko Justinek) +* e989267 - refactor: Update APIToken type initializer (Marko Justinek) +* b6addd6 - fix: Error message and copyrights (Marko Justinek) +* a640a09 - test: Add unit tests for provider verification interface (Marko Justinek) +* 124fc42 - doco: Update documentation for Provider Verification (Marko Justinek) +* c2b409e - refactor: Provider verification options (Marko Justinek) +* 6c3ab0a - refactor: Reduce namespacing for provider verification (Marko Justinek) +* 176d7eb - feature: Provider verification (Marko Justinek) +* 2de9622 - feat: Type safe ProviderVerifier options (Marko Justinek) +* 31edb21 - feature: Provider verification MVP (Marko Justinek) +* 4e55bda - doco: Improve example test in README.md (Marko Justinek) +* f2b6bec - tech: Run after_success CI step on ubuntu (Marko Justinek) +# 0.8.2 - v0.8.2 +* bfbbd78 - fix: Makes OneOf matchers init public (Marko Justinek) +* 98cefb7 - doco: Update examples in README.md (Marko Justinek) +# 0.8.1 - v0.8.1 +* e353841 - chore: Update PactSwiftMockServer dependency version (Marko Justinek) +* f023414 - doco: Update README and project structure ADR (Marko Justinek) +# 0.8.0 - v0.8.0 +* 934f7d1 - feat: Matching against Provider State injected value (Marko Justinek) +# 0.7.1 - v0.7.1 +* b663cdd - fix: Update Obj-C interface for PFMockService.run (Marko Justinek) +# 0.7.0 - v0.7.0 +* 18cee4f - fix: Each test runs on own mock server (Marko Justinek) +* f31b2f4 - feat: Hides baseURL property to return it in .run() function (Marko Justinek) +* 817b0b0 - ci: Remove pull request trigger (Marko Justinek) +* fa39340 - ci: Add config file for Semantic PR checks (Marko Justinek) +* 7b3fc1d - tech: Specifies on what branches to run CI (Marko Justinek) +# 0.6.2 - v0.6.2 +* 9796047 - fix: Dependency versions (Marko Justinek) +* 2276fb6 - tech: Ignoring a test on Linux (Marko Justinek) +* b77aa80 - fix: Bugfixes (Marko Justinek) +* 9f59e3a - fix: Drop Linux testing on CI (Marko Justinek) +# 0.6.1 - v0.6.1 +* a3f74ed - feat: Linux support (Marko Justinek) +* 6b63f31 - fix: Package.swift indentation (Marko Justinek) +* ad4ec02 - tech: Fix release script (again) (Marko Justinek) +* c5be211 - chore: Refactor DSL processing (Marko Justinek) +* 026d45c - chore: Improve code documentation (Marko Justinek) +* 34e5c41 - chore: Project files cleanup (Marko Justinek) +* f34f22d - chore: Remove Package.resolved from repo (Marko Justinek) +* 05742c0 - chore: Improve Matchers documentation in code (Marko Justinek) +* dcc23ed - chore: Commit Package.resolved to latest dependencies (Marko Justinek) +* 482f02f - tech: Improve test script (Marko Justinek) +* a0503f0 - chore: Update CONTRIBUTING.md (Marko Justinek) +* 46cbb17 - chore: Improve the release script (Marko Justinek) +# 0.5.0 - Release v0.5.0\n +* 3d0c4cc - chore: Release prep (Marko Justinek, Mon Jul 5 21:40:20 2021 +1000) +* bca1caf - fix: CI build workflows for swift package (Marko Justinek, Mon Jul 5 17:54:40 2021 +1000) +* 3edb4b2 - fix: Failing test in SPM (Marko Justinek, Mon Jul 5 17:43:24 2021 +1000) +* 215b5af - feat: Rename the waitFor paramter into timeout (Marko Justinek, Mon Jul 5 17:37:37 2021 +1000) +* 725d6c8 - chore: Skip a test if testing a swift package (Marko Justinek, Mon Jul 5 17:33:37 2021 +1000) +* 75ad882 - chore: Remove implementationOnly import for PactSwiftMockServer (Marko Justinek, Mon Jul 5 17:26:20 2021 +1000) +* 54aed81 - chore: Add Xcode version to Build workflows (Marko Justinek, Mon Jul 5 17:24:30 2021 +1000) +* 9d14dc0 - chore: Update metadata version (Marko Justinek, Mon Jul 5 17:22:25 2021 +1000) +* b0461e6 - chore: Update CI pipeline configuration (Marko Justinek, Mon Jul 5 16:23:52 2021 +1000) +* 5a59712 - chore: Remove unused resolved package (Marko Justinek, Mon Jul 5 16:23:40 2021 +1000) +* 258f606 - chore: Remove XCFramework in favour of sharing source (Marko Justinek, Mon Jul 5 16:23:20 2021 +1000) +* 9f0480d - chore: Expose as non-binary SPM package (Marko Justinek, Mon Jul 5 16:22:40 2021 +1000) +* 1bd93d5 - chore: Remove dead code (Marko Justinek, Mon Jul 5 16:21:33 2021 +1000) +* d22cf40 - chore: Slight improvements to release script (Marko Justinek, Mon Jun 14 16:00:27 2021 +1000) +* 6a266cc - fix: Update reference to source file in release script (Marko Justinek, Sun Jun 13 18:55:06 2021 +1000) +* 27f3956 - chore: Update script name (Marko Justinek, Sun Jun 13 18:54:48 2021 +1000) +* 30954ca - chore: Remove spec2 files (Marko Justinek, Sun Jun 13 17:04:25 2021 +1000) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..fd8e94f4 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,77 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at [@pact-up](https://twitter.com/pact_up?lang=en) +or direct messaging one of the `pact-swift` core collaborators at [Slack](https://pact-foundation.slack.com/archives/C9VBGNT4K). +All complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..b8129fd3 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,114 @@ +# Contributing to _PactSwift_ + +Read [CODE_OF_CONDUCT.md][code-of-conduct] first. + +## Bug Reports, Feature Requests and Questions + +Before submitting a new GitHub issue, please make sure to: + +- Read the `README` for [this repo][readme]. +- Read other documentation in this repo. +- Search the [existing issues][issues] for similar issues. + +If the above doesn't help, please [submit an issue][new-issue] via GitHub. + +**Note**: If you want to report a regression (something that has worked before, but broke with a new release), please label your issue with the `regression` label. This enables us to quickly detect and fix regressions. + +## Contributing Code + +### Finding things to do + +The [Core Contributors][core-contributor] usually tag issues that are ready to be worked on and easily accessible for new contributors with the [“good first issue”][good-first-issue] label. If you’ve never contributed to `PactSwift` before, these are a great place to start! + +If you want to work on something else, such as a new feature or fixing a bug, it would be helpful if you submit a new issue, so that we can have a chance to discuss it first. We might have some pointers for you on how to get started, or how to best integrate it with existing solutions. + +### Prepare the tools + +Use Homebrew to install [SwiftLint](https://github.com/realm/SwiftLint): + +```sh +brew install swiftlint +``` + +Install [Carthage](https://github.com/Carthage/Carthage) to test your changes and PactSwift builds successfully when distributing through Carthage: + +```sh +brew install carthage +``` + +Install [xcbeautify](https://github.com/thii/xcbeautify) + +```sh +brew tap thii/xcbeautify https://github.com/thii/xcbeautify.git +brew install xcbeautify +``` + +### Checking out the Code + +- Click the “Fork” button in the upper right corner of the [repo][repo]. +- Clone your fork (consult [GitHub documentation][fork-docs] about managing your forks): + +```sh +git clone git@github.com:/PactSwift.git` +``` + +#### Workflow + +- Create a new branch to work on with `git checkout -b `. + - Branch names should be descriptive of what you're working on, eg: `docs/updating-contributing-guide`, `fix/create-user-crash`. +- Use [good descriptive commit messages][commit-messages] when committing code. +- Write [semantic commit messages][semantic-commit-messages] following this [Conventional Commits][conventional-commits] spec. This is important because the change log is automated. Following these conventions allows us to avoid the tedious non-technical tasks that come with maintaining a project. + +## Testing + +- Please write unit tests for your code changes. +- Run the unit tests with `⌘U` in Xcode before submitting your Pull Request. +- Run tests in CLI `$PROJECT_DIR/Scripts/run_tests` + +## Submitting a Pull Request + +When you are ready to submit the PR, everything you need to know about submitting the PR itself is inside our [Pull Request Template][pr-template]. Some best practices are: + +- Use a descriptive title. +- Make sure you're not re-committing existing changes made on merged branches. +- Link the issues that are related to your PR in the body. + +## After the review + +Once a [Core Contributor][core-contributor] has reviewed your PR, you might need to make changes before it gets merged. To make it easier on us, please make sure to avoid amending commits or force pushing to your branch to make corrections. By avoiding rewriting the commit history, you will allow each round of edits to become its own visible commit. This helps the people who need to review your code easily understand exactly what has changed since the last time they looked. When you are done addressing your review, make sure you alert the reviewer in a comment or via GitHub's rerequest review command. See [GitHub's documentation for dealing with Pull Requests][pr-docs]. + +After your contribution is merged, it’s not immediately available to all users. Your change will be shipped as part of the next release. + +## Code of Conduct + +Help us keep this project diverse, open and inclusive. Please read and follow our [Code of Conduct][code-of-conduct]. + +## Thanks for Contributing! + +Thank you for taking the time to contribute to the project! + +## License + +This project is licensed under the terms of the MIT license. See the [LICENSE][license] file. + +All contributions to this project are also under this license as per [GitHub's Terms of Service][github-terms-contribution]. + +> This project is open source under the MIT license, which means you have full access to the source code and can modify it to fit your own needs. You are responsible for how you use _PactSwift_. + + +[code-of-conduct]: CODE_OF_CONDUCT.md +[conventional-commits]: https://www.conventionalcommits.org/en/v1.0.0/ +[commit-messages]: https://chris.beams.io/posts/git-commit/ +[core-contributor]: Documentation/CORE_CONTRIBUTOR.md +[fork-docs]: https://help.github.com/articles/working-with-forks/ +[github-terms-contribution]: https://help.github.com/en/github/site-policy/github-terms-of-service#6-contributions-under-repository-license +[gist-rust]: https://gist.github.com/surpher/bbf88e191e9d1f01ab2e2bbb85f9b528 +[good-first-issue]: https://github.com/surpher/PactSwift/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22 +[issues]: https://github.com/surpher/PactSwift/issues +[license]: LICENSE.md +[new-issue]: https://github.com/surpher/PactSwift/issues/new/choose +[pr-template]: .github/PULL_REQUEST_TEMPLATE.md +[pr-docs]: https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/requesting-a-pull-request-review +[readme]: https://github.com/surpher/PactSwift#readme +[repo]: https://github.com/surpher/PactSwift +[semantic-commit-messages]: https://gist.github.com/joshbuchea/6f47e86d2510bce28f8e7f42ae84c716 diff --git a/Configurations/Info-iOS-Tests.plist b/Configurations/Info-iOS-Tests.plist new file mode 100644 index 00000000..64d65ca4 --- /dev/null +++ b/Configurations/Info-iOS-Tests.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/Configurations/Info-iOS.plist b/Configurations/Info-iOS.plist new file mode 100644 index 00000000..fafe3a23 --- /dev/null +++ b/Configurations/Info-iOS.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSHumanReadableCopyright + Copyright © 2020-2021 Marko Justinek. All rights reserved. + + diff --git a/Configurations/Info-macOS-Tests.plist b/Configurations/Info-macOS-Tests.plist new file mode 100644 index 00000000..64d65ca4 --- /dev/null +++ b/Configurations/Info-macOS-Tests.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/Configurations/Info-macOS.plist b/Configurations/Info-macOS.plist new file mode 100644 index 00000000..fafe3a23 --- /dev/null +++ b/Configurations/Info-macOS.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSHumanReadableCopyright + Copyright © 2020-2021 Marko Justinek. All rights reserved. + + diff --git a/Configurations/Project-Debug.xcconfig b/Configurations/Project-Debug.xcconfig new file mode 100644 index 00000000..815b6eb1 --- /dev/null +++ b/Configurations/Project-Debug.xcconfig @@ -0,0 +1,14 @@ +#include "Configurations/Project-Shared.xcconfig" + +ONLY_ACTIVE_ARCH = YES +DEBUG_INFORMATION_FORMAT = dwarf +ENABLE_TESTABILITY = YES + +GCC_DYNAMIC_NO_PIC = NO +GCC_OPTIMIZATION_LEVEL = 0 +GCC_PREPROCESSOR_DEFINITIONS = DEBUG=1 $(inherited) + +SWIFT_OPTIMIZATION_LEVEL = -Onone +SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG + +MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE diff --git a/Configurations/Project-Release.xcconfig b/Configurations/Project-Release.xcconfig new file mode 100644 index 00000000..c7ae8803 --- /dev/null +++ b/Configurations/Project-Release.xcconfig @@ -0,0 +1,11 @@ +#include "Configurations/Project-Shared.xcconfig" + +DEBUG_INFORMATION_FORMAT = dwarf-with-dsym +VALIDATE_PRODUCT = YES + +ENABLE_NS_ASSERTIONS = NO + +MTL_ENABLE_DEBUG_INFO = NO + +SWIFT_OPTIMIZATION_LEVEL = -O +SWIFT_COMPILATION_MODE = wholemodule diff --git a/Configurations/Project-Shared.xcconfig b/Configurations/Project-Shared.xcconfig new file mode 100644 index 00000000..0a370485 --- /dev/null +++ b/Configurations/Project-Shared.xcconfig @@ -0,0 +1,61 @@ +// !!! WARNING: NEXT LINE IS AUTOMATED - DO NOT CHANGE OR MOVE !!! +MARKETING_VERSION = 1.1.0 + +CURRENT_PROJECT_VERSION = 1 +VERSIONING_SYSTEM = apple-generic + +SWIFT_VERSION = 5.0 + +COPY_PHASE_STRIP = NO + +ALWAYS_SEARCH_USER_PATHS = NO +ENABLE_STRICT_OBJC_MSGSEND = YES + +BUILD_LIBRARY_FOR_DISTRIBUTION = YES + +// GCC + +GCC_NO_COMMON_BLOCKS = YES +GCC_C_LANGUAGE_STANDARD = gnu11 +GCC_WARN_64_TO_32_BIT_CONVERSION = YES +GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR +GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE +GCC_WARN_UNUSED_FUNCTION = YES +GCC_WARN_UNUSED_VARIABLE = YES +GCC_WARN_UNDECLARED_SELECTOR = YES + +MTL_FAST_MATH = YES + +CLANG_CXX_LANGUAGE_STANDARD = gnu++14 +CLANG_CXX_LIBRARY = libc++ +CLANG_ENABLE_MODULES = YES +CLANG_ENABLE_OBJC_ARC = YES +CLANG_ENABLE_OBJC_WEAK = YES +CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES +CLANG_WARN_DOCUMENTATION_COMMENTS = YES +CLANG_WARN_EMPTY_BODY = YES +CLANG_WARN_BOOL_CONVERSION = YES +CLANG_WARN_CONSTANT_CONVERSION = YES +CLANG_WARN_ENUM_CONVERSION = YES +CLANG_WARN_INT_CONVERSION = YES +CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES +CLANG_WARN_INFINITE_RECURSION = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN_UNREACHABLE_CODE = YES +CLANG_WARN_RANGE_LOOP_ANALYSIS = YES +CLANG_WARN_SUSPICIOUS_MOVE = YES +CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_OBJC_LITERAL_CONVERSION = YES +CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES +CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +CLANG_ANALYZER_NONNULL = YES +CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES + +// Xcode 14 + +DEAD_CODE_STRIPPING = YES diff --git a/Configurations/Target-iOS-Debug.xcconfig b/Configurations/Target-iOS-Debug.xcconfig new file mode 100644 index 00000000..8d725784 --- /dev/null +++ b/Configurations/Target-iOS-Debug.xcconfig @@ -0,0 +1,3 @@ +#include "Configurations/Target-iOS-Shared.xcconfig" + +SWIFT_OPTIMIZATION_LEVEL = -Onone diff --git a/Configurations/Target-iOS-Release.xcconfig b/Configurations/Target-iOS-Release.xcconfig new file mode 100644 index 00000000..ff58b493 --- /dev/null +++ b/Configurations/Target-iOS-Release.xcconfig @@ -0,0 +1 @@ +#include "Configurations/Target-iOS-Shared.xcconfig" diff --git a/Configurations/Target-iOS-Shared.xcconfig b/Configurations/Target-iOS-Shared.xcconfig new file mode 100644 index 00000000..429aa8c8 --- /dev/null +++ b/Configurations/Target-iOS-Shared.xcconfig @@ -0,0 +1,33 @@ +PRODUCT_NAME = PactSwift + +IPHONEOS_DEPLOYMENT_TARGET = 12.0 +SDKROOT = iphoneos +INSTALL_PATH = $(LOCAL_LIBRARY_DIR)/Frameworks +SKIP_INSTALL = YES +TARGETED_DEVICE_FAMILY = 1,2 +SUPPORTS_MACCATALYST = NO +BUNDLE_VERSION_STRING_SHORT = $(MARKETING_VERSION) + +DYLIB_COMPATIBILITY_VERSION = 1 +DYLIB_CURRENT_VERSION = 1 +DYLIB_INSTALL_NAME_BASE = @rpath + +INFOPLIST_FILE = Configurations/Info-iOS.plist +PRODUCT_BUNDLE_IDENTIFIER = au.com.pact-foundation.iOS.PactSwift +LIBRARY_SEARCH_PATHS = $(inherited) $(PLATFORM_DIR)/Developer/usr/lib +LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks @loader_path/Frameworks +FRAMEWORK_SEARCH_PATHS = $(inherited) +HEADER_SEARCH_PATHS = $(PROJECT_DIR)/Sources/Headers + +OTHER_LDFLAGS = -weak_framework XCTEST -weak-lXCTestSwiftSupport + +DEFINES_MODULE = YES +CODE_SIGN_STYLE = Automatic +CLANG_ENABLE_MODULES = YES +ENABLE_BITCODE = NO +ENABLE_TESTING_SEARCH_PATHS = YES + +CODE_SIGN_IDENTITY = +ENABLE_MODULE_VERIFIER = YES +MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = gnu11 gnu++14 +MODULE_VERIFIER_SUPPORTED_LANGUAGES = objective-c objective-c++ diff --git a/Configurations/Target-iOS-Tests-Debug.xcconfig b/Configurations/Target-iOS-Tests-Debug.xcconfig new file mode 100644 index 00000000..603d15b7 --- /dev/null +++ b/Configurations/Target-iOS-Tests-Debug.xcconfig @@ -0,0 +1 @@ +#include "Configurations/Target-iOS-Tests-Shared.xcconfig" diff --git a/Configurations/Target-iOS-Tests-Release.xcconfig b/Configurations/Target-iOS-Tests-Release.xcconfig new file mode 100644 index 00000000..603d15b7 --- /dev/null +++ b/Configurations/Target-iOS-Tests-Release.xcconfig @@ -0,0 +1 @@ +#include "Configurations/Target-iOS-Tests-Shared.xcconfig" diff --git a/Configurations/Target-iOS-Tests-Shared.xcconfig b/Configurations/Target-iOS-Tests-Shared.xcconfig new file mode 100644 index 00000000..8cf87b57 --- /dev/null +++ b/Configurations/Target-iOS-Tests-Shared.xcconfig @@ -0,0 +1,10 @@ +SDKROOT = iphoneos +PRODUCT_NAME = $(TARGET_NAME) +TARGETED_DEVICE_FAMILY = 1,2 +IPHONEOS_DEPLOYMENT_TARGET = 12.0 + +INFOPLIST_FILE = Configurations/Info-iOS-Tests.plist +PRODUCT_BUNDLE_IDENTIFIER = au.com.pact-foundation.PactSwiftTests +CODE_SIGN_IDENTITY[sdk=macosx*] = - + +LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks @loader_path/Frameworks diff --git a/Configurations/Target-macOS-Debug.xcconfig b/Configurations/Target-macOS-Debug.xcconfig new file mode 100644 index 00000000..497d9b68 --- /dev/null +++ b/Configurations/Target-macOS-Debug.xcconfig @@ -0,0 +1,7 @@ +#include "Configurations/Target-macOS-Shared.xcconfig" +ONLY_ACTIVE_ARCH = YES + +DEBUG_INFORMATION_FORMAT = dwarf +ENABLE_TESTABILITY = YES + +MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE diff --git a/Configurations/Target-macOS-Release.xcconfig b/Configurations/Target-macOS-Release.xcconfig new file mode 100644 index 00000000..0cac745f --- /dev/null +++ b/Configurations/Target-macOS-Release.xcconfig @@ -0,0 +1,8 @@ +#include "Configurations/Target-macOS-Shared.xcconfig" + +DEBUG_INFORMATION_FORMAT = dwarf-with-dsym +ENABLE_NS_ASSERTIONS = NO + +SWIFT_COMPILATION_MODE = wholemodule +SWIFT_OPTIMIZATION_LEVEL = -O +MTL_ENABLE_DEBUG_INFO = NO diff --git a/Configurations/Target-macOS-Shared.xcconfig b/Configurations/Target-macOS-Shared.xcconfig new file mode 100644 index 00000000..927d850d --- /dev/null +++ b/Configurations/Target-macOS-Shared.xcconfig @@ -0,0 +1,42 @@ +PRODUCT_NAME = PactSwift + +ONLY_ACTIVE_ARCH = NO + +SDKROOT = macosx +MACOSX_DEPLOYMENT_TARGET = 10.15 +CODE_SIGN_STYLE = Automatic + +DEFINES_MODULE = YES +COMBINE_HIDPI_IMAGES = YES +INSTALL_PATH = $(LOCAL_LIBRARY_DIR)/Frameworks +SKIP_INSTALL = YES + +INFOPLIST_FILE = Configurations/Info-macOS.plist +PRODUCT_BUNDLE_IDENTIFIER = au.com.pact-foundation.macOS.PactSwift +DYLIB_INSTALL_NAME_BASE = @rpath +LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks @loader_path/Frameworks +LIBRARY_SEARCH_PATHS = $(inherited) $(PLATFORM_DIR)/Developer/usr/lib +FRAMEWORK_SEARCH_PATHS = $(inherited) + +OTHER_LDFLAGS = -weak_framework XCTEST -weak-lXCTestSwiftSupport + +DYLIB_COMPATIBILITY_VERSION = 1 +DYLIB_CURRENT_VERSION = 1 +ALWAYS_SEARCH_USER_PATHS = NO +VERSION_INFO_PREFIX = +COPY_PHASE_STRIP = NO + +GCC_DYNAMIC_NO_PIC = NO +GCC_OPTIMIZATION_LEVEL = 0 +GCC_PREPROCESSOR_DEFINITIONS = DEBUG=1 $(inherited) + +SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG +SWIFT_OPTIMIZATION_LEVEL = -Onone + +ENABLE_TESTING_SEARCH_PATHS = YES + +DEAD_CODE_STRIPPING = YES + +ENABLE_MODULE_VERIFIER = YES +MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = gnu11 gnu++14 +MODULE_VERIFIER_SUPPORTED_LANGUAGES = objective-c objective-c++ diff --git a/Configurations/Target-macOS-Tests-Debug.xcconfig b/Configurations/Target-macOS-Tests-Debug.xcconfig new file mode 100644 index 00000000..485f4e90 --- /dev/null +++ b/Configurations/Target-macOS-Tests-Debug.xcconfig @@ -0,0 +1,11 @@ +#include "Configurations/Target-macOS-Tests-Shared.xcconfig" + +ONLY_ACTIVE_ARCH = YES +ENABLE_TESTABILITY = YES +GCC_OPTIMIZATION_LEVEL = 0 +GCC_DYNAMIC_NO_PIC = NO +GCC_PREPROCESSOR_DEFINITIONS = DEBUG=1 $(inherited) +SWIFT_OPTIMIZATION_LEVEL = -Onone +SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG +MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE +DEBUG_INFORMATION_FORMAT = dwarf-with-dsym diff --git a/Configurations/Target-macOS-Tests-Release.xcconfig b/Configurations/Target-macOS-Tests-Release.xcconfig new file mode 100644 index 00000000..e20b5cb2 --- /dev/null +++ b/Configurations/Target-macOS-Tests-Release.xcconfig @@ -0,0 +1,5 @@ +#include "Configurations/Target-macOS-Tests-Shared.xcconfig" +SWIFT_OPTIMIZATION_LEVEL = -O +ENABLE_NS_ASSERTIONS = NO +MTL_ENABLE_DEBUG_INFO = NO +SWIFT_COMPILATION_MODE = wholemodule diff --git a/Configurations/Target-macOS-Tests-Shared.xcconfig b/Configurations/Target-macOS-Tests-Shared.xcconfig new file mode 100644 index 00000000..8efbbe40 --- /dev/null +++ b/Configurations/Target-macOS-Tests-Shared.xcconfig @@ -0,0 +1,18 @@ +//:configuration = Debug +SDKROOT = macosx +MACOSX_DEPLOYMENT_TARGET = 11.0 + +CODE_SIGN_STYLE = Automatic +COMBINE_HIDPI_IMAGES = YES + +INFOPLIST_FILE = Configurations/Info-macOS-Tests.plist +LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks @loader_path/Frameworks + +PRODUCT_BUNDLE_IDENTIFIER = au.com.pact-foundation.macOS.PactSwift-Tests +PRODUCT_NAME = $(TARGET_NAME) + +MTL_FAST_MATH = YES + +ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES + +DEAD_CODE_STRIPPING = YES diff --git a/Documentation/ADRs/ADR-001-Language_choice.md b/Documentation/ADRs/ADR-001-Language_choice.md new file mode 100644 index 00000000..77b19ada --- /dev/null +++ b/Documentation/ADRs/ADR-001-Language_choice.md @@ -0,0 +1,9 @@ +# Context + +iOS applications can be written in Objective-C or Swift. Objective-C offers greater interaction with C++ code but is considered a legacy language choice in the iOS developer community. The `pact-consumer-swift` framework was built to support Objective-C as well, but it's proven to become a bigger challenge supporting both with newer Xcode and Swift versions. + +# Decision + +The framework is written in Swift. + +# Consequences diff --git a/Documentation/ADRs/ADR-002-Coding_standard.md b/Documentation/ADRs/ADR-002-Coding_standard.md new file mode 100644 index 00000000..4c1b1236 --- /dev/null +++ b/Documentation/ADRs/ADR-002-Coding_standard.md @@ -0,0 +1,152 @@ +# Context + +We shouldn't feel bound by any pre-existing coding standards so this project and its code is written according to personal preferences based on practices that yielded good results acquired working in other projects with many collaborators. The code is relatively consistent but that might change once more developers contribute to the project. + +In general, Swift code has a fairly strong styling, relative to C or C++, due to opinionated aspects of the language itself and the styling used by the official language guides. Formatting around brace placement, `if` and `for` styling is fairly clearly set by the language. + +# Decision + +[Swiftlint configuration](./../../.swiftlint.yml) is used to enforce us adhering to _most_ of code style conventions. + +## Project file structure + +File structure follows Swift Package Manager boilerplate. Xcode's _Project Navigator_ displays all folders and files alphabetically for easier skimming and searching through the file structure. + +``` +. +|-- .config.yml +|-- .github +|-- CONTRIBUTING.md +|-- Documentation +| |-- ADRs +| | |-- ADR-001-Language_choice.md +| | |-- ... +|-- EmbeddedProject # follows the same file structure +| |-- Resources +| |-- Sources +| |-- Tests +| |-- ... +|-- Package.swift +|-- README.md +|-- Resources +| |-- Assets.xcassets +| |-- ... +|-- Sources +| |-- Extensions +| |-- PACTMockService.swift +| |-- ... +|-- Tests +| |-- PACTMockService +| | |-- TestCaseFile.swift +| | |-- ... +| |-- PACTXCTestCase.swift +| |-- Resources +| | |-- ErrorCapture.swift +| | |-- PACTMockServiceStub.swift +| | |-- ... +. . . +``` + +## Indentation +We are using **tabs** for indentation. The primary motivation behind using tabs for indentation is not around indentation itself but to deliberately discourage a separate practice: code formatting. + +Following the rule: + +> Code should be indented but never formatted. + +With this rule in place switching between 4-space indents, 2-space indents or tabs is a trivial matter of search and replace and can be changed on a whim. Without this rule, the codebase cannot be trivially searched to verify indenting and cannot be easily converted between indentation styles. Validating formatting requires full code semantic analysis and user-preferences end up overriding any clear, consistent rule. Parsing and validating indentation requires only parsing of braces and parentheses. + +Let's look at a coding practice to **AVOID**: + +```swift +func myFunc() { + someCall(paramOne: argOne, paramTwo: argTwo, paramThree: argThree, + paramFour: argFour, paramFive: argFive) +} +``` + +Xcode will automatically generate this style of formatting if you've selected "Syntax aware indenting" with "Automatically indent for ':'" on the Preferences -> Text Editing -> Indentation panel and you place a newline within a function call statement. We recommend *disabling* this feature in Xcode (and most of the other syntax aware indenting options). + +The following is a **PREFERRED** approach: + +```swift +func myFunc() { + someCall(paramOne: argOne, paramTwo: argTwo, paramThree: argThree, + paramFour: argFour, paramFive: argFive) +} +``` + +i.e. if you need to split a single statement across multiple lines, pick a clean point to insert a newline (preferrably after a comma) and merely increase the indentation width by 1 for the next line. + +OR, if a more structured, declarative representation is desired: + +```swift +func someCall( + paramOne: argOne, + paramTwo: argTwo, + paramThree: argThree, + paramFour: argFour, + paramFive: argFive +) -> Type {...} + +func myFunc() { + let myVar = someCall( + paramOne: argOne, + paramTwo: argTwo, + paramThree: argThree, + paramFour: argFour, + paramFive: argFive + ) +} +``` + +i.e. if you want to format the whole line to bring attention to the structure, start the indent at an open parenthesis and place the closing parenthesis on its own line to clearly show the end of the structure. + +How does the use of tabs as indentation support this? Different members of the team can set different tab widths in their editors (Imagine having two developers working on the project where one uses 4-spaces per tab and the other 3-spaces per tab). In this scenario, otherwise invisible indentation violations (mixing tabs and spaces, indentation by non-whole amounts or code formatting) are immediately apparent as merging/rebasing will not affect other develpers indentation preference. + +## Type padding + +This project follows a convention where single vertical space padding inside top-level only structures (immediately after the opening brace and immediately before the closing brace). As with all stylistic choices though, once you use it for a while, it gets inside your brain. + +Example to follow: + +``` +struct MyStruct { + + var myInt: Int + var myString: String + + struct MyNestedStruct { + var myNestedInt: Int + var myNestedString: String + } + +} + +class MyClass { + + // MARK: - Types + + enum MyEnum { + case one + case two + } + + // MARK: - Properties + + var myVar: SomeType + + // MARK: - Lifecycle + + init(myVar: SomeType) { + self.myVar = myVar + } + +} +``` + +# Consequences + +As this is an open-source project, it will be critical for anyone contributing to the codebase to follow these rules. Hopefully, setting up the project as best as possible for collaborative work will prove PRs will require less effort combing through differences that are not feature related (eg: we want to avoid PRs with changes due to code style/formatting). + +For Type Members organization see [ADR-003-Organization_of_type_members.md](ADR-003-Organization_of_type_members.md). diff --git a/Documentation/ADRs/ADR-003-Organization_of_type_members.md b/Documentation/ADRs/ADR-003-Organization_of_type_members.md new file mode 100644 index 00000000..a8ec3b1f --- /dev/null +++ b/Documentation/ADRs/ADR-003-Organization_of_type_members.md @@ -0,0 +1,92 @@ +# Context + +For legibility and discoverability, it is helpful to have a clear ordering of members within each type. Criteria which factor into this include: + +1. Member kind (property, method, subtype) +2. Access (public/internal/private) +3. Nature of member (stored or computed property, override or unique method) + +There are different approaches to how these should be prioritized in C++/Objective-C, whether you're focussing on the needs of the type's consumer or implementer and which slices of behavior you most want to separate. + +# Decision + +Where possible, members should be organized as follows: + +``` +class MyClass: BaseClass { + + // MARK: - Constants + + public static let valueA = 1 + private static let valueB = 2 + + // MARK: - Types + + public struct SubTypeA {} + private struct SubTypeB {} + + // MARK: - Stored Properties + + public var propertyA = 1 + private var propertyB = 2 + + // MARK: - Computed Properties + + public var propertyC: Int { return propertyA * 3 } + private var propertyD: Int { return propertyB * 4 } + + // MARK: - Constructors + + public init() {} + private init(param: Int) {} + + // MARK: - Methods + + public static func k() {} + + public func f() {} + private func g() {} + + private static func h() {} + + // MARK: - BaseClass overrides + + public override var propertyL: Int { return propertyA * 3 } + public override func base() {} + +} + +extension MyClass: SomeComformance { + + public var i: Int { return 0 } + + public func j() {} + +} +``` + +Important points to note: + +1. public before private +2. static lifetimes before properties before methods +3. stored properties before computed properties +4. constructors before other methods +5. overrides grouped based on the class they override +6. protocol conformances in separate extensions (unless auto-synthesis is involved) + +In most cases, these sections will not all be present... don't use a heading for a section not included + +# Consequences + +There are a couple points that aren't totally decided. + +They do not *need* to have "mark" headings and when they do, provided the contents themselves are organized, a simple "Properties" or "Methods" is sufficient to cover all methods or properties (e.g. doesn't need to be broken into "Stored" and "Computed"). + +However, overrides sections should have a heading indicating which class' methods they override, otherwise its purpose is difficult to understand. + +Static methods are all in one section with the other methods, with public static first and private static last (after all non-static methods). However: + +1. Most public static functions are constructors and should go in the constructor section (probably ahead of init functions) +2. Many private static functions are called from just one location, lifted out for purely syntactic reasons. Sometimes these might appear alongside the function they're lifted out-of, sometimes they might appear at the end of the file since they're mostly an implementation detail that can be ignored. + +There's a little flexibility here and when reviewing PR's suggestions and requests for improvement may be made prior to approving a PR. diff --git a/Documentation/ADRs/ADR-004-Dependencies-management.md b/Documentation/ADRs/ADR-004-Dependencies-management.md new file mode 100644 index 00000000..ec814fc1 --- /dev/null +++ b/Documentation/ADRs/ADR-004-Dependencies-management.md @@ -0,0 +1,25 @@ +# Context + +Almost all software we write depends on some other code, library or development tool which allows us to build what we want faster. Although this project attempts to avoid bringing in 3rd party dependencies, there are is functionality already written that is critical to this projects success. + +# Decision + +The main dependency is the programmable in-process mock server that can receive network requests and respond with the response we define. This dependency is written in rust and is available at [pact-foundation/pact-reference/rust](https://github.com/pact-foundation/pact-reference/tree/main/rust/pact_mock_server_ffi). + +The binary framework(s) that are built using `cargo lipo --release` command are added into the Xcode project. + +Unfortunately SPM doesn't handle the binary dependencies well at the time of this writing. Therefore a SPM package is required + +There will be a separation of responsibilities between PactSwift framework and PactSwiftServices in a separate (yet embedded) project which will provide extra functionality by reaching out to and/or interact with different services (interacting with Pact Mock Server, etc.). + +Matt's [CwlPreconditionTesting](https://github.com/mattgallagher/CwlPreconditionTesting) is a dependency this project can't really exist without. To support distributon of PactSwift using both Carthage and SPM, the dependency CwlPreconditionTesting is brougt into the PactSwiftServices project (files `./Carthage/Checkouts/CwlPreconditionTesting/*` added into the project itself). For SPM it is defined as a dependency in `./PactSwiftServices/Package.swift`. + +# Consequences + +Due to SPM not handling binary dependencies well. When linking and embedding a binary framework while building and running in Xcode everything works fine, `xcodebuild` command in command line builds the project and dependencies just fine. + +Yet, when running `swift build` in terminal, SPM doesn't know where to find it. That's why a separate SPM package to provide the binary framework as a dependency is required and unfortunately the binary framework is duplicated in the codebase - once in `PactSwiftServices` project and once in `PactMockServer` swift package. + +# Follow-up (September 30, 2020) + +All 3rd party dependencies have been successfully removed from this project/framework. diff --git a/Documentation/ADRs/ADR-005-Project-Structure.md b/Documentation/ADRs/ADR-005-Project-Structure.md new file mode 100644 index 00000000..7bfd41f2 --- /dev/null +++ b/Documentation/ADRs/ADR-005-Project-Structure.md @@ -0,0 +1,51 @@ +# Project Structure + +## Context + +`PactSwift` takes advantage of Mock Server FFI binaries built from shared Rust code. These are generally large binary files when it comes to iOS and macOS platforms and we are limited with hosting them in the GitHub repo. The FFI also follows it's own source and changes are available independently to changes to `PactSwift`'s functionality. Separating the responsibilities would be welcomed. + +Furthermore, the pain of managing multiple binaries with the same name but each with its specific architecture slice could be reduced by generating an `XCFramework` using an automated script and kept from the framework user. These can blow up to more than 100Mb each (the fat binary with all slices for iOS platform blew up to more than 300MB). Using `XCFramework` we can shed off a lot of the statically linked code. Mock Server FFI (`MockServer.swift`) is the only part of `PactSwift` package that depends on binaries being built for specific architectures and run platforms. With removal of binaries from the main `PactSwift` project, we should be able to avoid managing them, mixing them up (as they are all named the same), discarding them at `git add` and `commit` steps and rebuilding them at next `PactSwift` build/test cycle. + +## Decision + +- Mock Server FFI interface and implementation to be split into it's own Swift Package called `PactSwiftMockServer` and distributed as a binary (`XCFramework`) when on Apple platforms and as a source package when used on Linux platforms. +- Utilities used by both the main `PactSwift` and `PactSwiftMockServer` packages are split into one package called `PactSwiftToolbox`. +- Where it makes sense the dependencies' versions should be exact. If exact version is not set for a valid reason then `.upToMinor()` must be used to avoid breaking changes when releasing packages in isolation. +- Scripts to automate the release processes will be provided within the projects' scripts folders. + +## Consequences + +Instead of one repository there will be **4** repos to maintain. Most of the work and focus will be on main `PactSwift`. The number of repos to maintain in isolation will hopefully outweigh the complexity of maintaining one big one where developer process is hindered due to constant dance of adding and removing huge binaries. + +1. `PactSwift` - The main package handling the interactions and is responsible for preparing the Pact's structure. +2. `PactSwiftMockServer` - Wraps `libpact_ffi` and handles the mock server functionality. +3. `PactSwiftToolbox` - Is expected to not change much and should avoid implementing breaking changes. +4. `PactMockServer` - Only vends the header files for `PactSwiftMockServer` when it used from source (Linux platforms). + +Is expected to that `PactSwiftMockServer` will only require some attention when `XCFramework` needs to be updated and distributed as a binary only when `libpact_ffi` is updated (be it for bugfixes or updated functionality). + +⚠️ `PactSwiftMockServer` and `PactMockServer` must both expose the same `libpact_ffi.h` header file to achieve compatibility across all Apple and Linux platforms. + +`PactSwift` depends on packages: + +- `PactSwiftMockServer` for Apple platforms +- `PactSwiftMockServerLinux` for Linux platform +- `PactSwiftToolbox` + +`PactSwiftMockServer` depends on packages: + +- `PactSwiftToolbox` +- `PactMockServer` + +`PactSwiftMockServer` is vedning: + +- a binary `XCFramework` targed for Apple platforms +- source target for `Linux` platform + +## Follow up + +_July 5, 2021_ +Some pain and mistakes were made when deploying new versions and referencing the correct versions. Suggesting an automated release process that would update version numbers, make checks if things are aligned before creating a new release tag and related release. + +_August 17, 2021_ +Updated to reflect changes in project and package structure and dependencies. Update for Linux support. diff --git a/Documentation/CORE_CONTRIBUTOR.md b/Documentation/CORE_CONTRIBUTOR.md new file mode 100644 index 00000000..6e83e753 --- /dev/null +++ b/Documentation/CORE_CONTRIBUTOR.md @@ -0,0 +1,38 @@ +# _PactSwift_ Core Contributor Expectations + +We are always looking for active, enthusiastic members of the developer community to become core contributors. + +We hope to harness the diversity of the iOS/macOS Developer community to build the _PactSwift_ into an essential tool for developers. + +## How does one become a Core Contributor? + +Contributors who have displayed lasting commitment to the evolution and maintenance of this project will be invited to become Core Contributors. For instance, contributors who: + +- Love to help out other users with issues on GitHub +- Continue to make _PactSwift_ a stable product and encourage features aligned with our [vision][vision]. + +## Core Contributors + +- Review pull requests using the "Review Changes" feature in GitHub. +- Merge pull requests we review, except for PRs where the author has push access. +- Respond to issues and help others. +- Own regressions caused by our own contributions and PR approvals. +- Maintain consistent coding standards. +- Inform other Core Contributors when critical fixes are merged so a release can be prepared. +- Identify other community members who would be effective Core Contributors. +- Ensure that new contributions fit into the [Vision][vision]. +- Adhere to the [Code of Conduct][code-of-conduct]. +- Keep external dependencies to a minimum. +- Keep test coverage high and ensure up-to-date documentation. +- Are polite, friendly and having fun contributing. + +## Pull Request Ownership + +We work in a high-trust environment which implies that anyone and everyone is able to merge pull requests from the community. If the PR reviewer feels strongly about seeing a PR to completion, they should assign it to themselves and request necessary changes. + +## Adding Dependencies + +We want to keep _PactSwift_ focused and robust. Avoid adding new dependencies to the code base unless absolutely necessary. + +[vision]: VISION.md +[code-of-conduct]: ../CODE_OF_CONDUCT.md \ No newline at end of file diff --git a/Documentation/VISION.md b/Documentation/VISION.md new file mode 100644 index 00000000..159a030a --- /dev/null +++ b/Documentation/VISION.md @@ -0,0 +1,9 @@ +# Our Vision + +_PactSwift_ automates the process of generating Pact contracts. + +_PactSwift_ provides a simple and easy to use framework in one's Swift or Objective-C project that creates workflows where Pact contracts are generated. + +_PactSwift_ will continue to evolve, in a measured way that extends its functionality to make it an indespensable tool for developers writing [Consumer Driven contracts][consumer-driven-contracts]. + +[consumer-driven-contracts]: https://martinfowler.com/articles/consumerDrivenContracts.html diff --git a/Documentation/images/04_destination_dir.png b/Documentation/images/04_destination_dir.png new file mode 100644 index 00000000..b03edfdb Binary files /dev/null and b/Documentation/images/04_destination_dir.png differ diff --git a/Documentation/images/08_xcode_spm_search.png b/Documentation/images/08_xcode_spm_search.png new file mode 100644 index 00000000..a47dc57d Binary files /dev/null and b/Documentation/images/08_xcode_spm_search.png differ diff --git a/Documentation/images/09_xcode_spm_options.png b/Documentation/images/09_xcode_spm_options.png new file mode 100644 index 00000000..15473178 Binary files /dev/null and b/Documentation/images/09_xcode_spm_options.png differ diff --git a/Documentation/images/10_xcode_spm_add_package.png b/Documentation/images/10_xcode_spm_add_package.png new file mode 100644 index 00000000..d19b91de Binary files /dev/null and b/Documentation/images/10_xcode_spm_add_package.png differ diff --git a/Documentation/images/11_xcode_carthage_xcframework.png b/Documentation/images/11_xcode_carthage_xcframework.png new file mode 100644 index 00000000..dfdd240f Binary files /dev/null and b/Documentation/images/11_xcode_carthage_xcframework.png differ diff --git a/Documentation/images/12_xcode_scheme_env_setup.png b/Documentation/images/12_xcode_scheme_env_setup.png new file mode 100644 index 00000000..ee92af27 Binary files /dev/null and b/Documentation/images/12_xcode_scheme_env_setup.png differ diff --git a/Documentation/images/pact-swift.png b/Documentation/images/pact-swift.png new file mode 100644 index 00000000..20276c9d Binary files /dev/null and b/Documentation/images/pact-swift.png differ diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..b5f04557 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Marko Justinek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 00000000..5ec94528 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,16 @@ +{ + "object": { + "pins": [ + { + "package": "PactSwiftMockServer", + "repositoryURL": "https://github.com/surpher/PactSwiftServer.git", + "state": { + "branch": null, + "revision": "12ecc92092ecd3640dcc2dcb98ce6b3d1f2d76f7", + "version": "0.4.7" + } + } + ] + }, + "version": 1 +} diff --git a/Package.swift b/Package.swift new file mode 100644 index 00000000..77705d08 --- /dev/null +++ b/Package.swift @@ -0,0 +1,49 @@ +// swift-tools-version:5.3 + +import PackageDescription + +let package = Package( + name: "PactSwift", + + platforms: [ + .macOS(.v10_13), + .iOS(.v12), + .tvOS(.v12) + ], + + products: [ + .library( + name: "PactSwift", + targets: ["PactSwift"] + ) + ], + + dependencies: [ + .package(url: "https://github.com/surpher/PactSwiftServer.git", .exact("0.4.7")) + ], + + targets: [ + + // PactSwift + .target( + name: "PactSwift", + dependencies: [ + .product(name: "PactSwiftMockServer", package: "PactSwiftServer"), + ], + path: "./Sources" + ), + + // Tests + .testTarget( + name: "PactSwiftTests", + dependencies: [ + "PactSwift" + ], + path: "./Tests" + ), + + ], + + swiftLanguageVersions: [.v5] + +) diff --git a/PactSwift.xcfilelist b/PactSwift.xcfilelist new file mode 100644 index 00000000..9b5eb9a4 --- /dev/null +++ b/PactSwift.xcfilelist @@ -0,0 +1,68 @@ +$(SRCROOT)/Sources/MockService+Concurrency.swift +$(SRCROOT)/Sources/MockService+Extension.swift +$(SRCROOT)/Sources/Extensions/Bundle+PactSwift.swift +$(SRCROOT)/Sources/Extensions/Task+Timeout.swift +$(SRCROOT)/Sources/Extensions/Date+PactSwift.swift +$(SRCROOT)/Sources/Extensions/UUID+PactSwift.swift +$(SRCROOT)/Sources/Extensions/MockServer+Async.swift +$(SRCROOT)/Sources/Extensions/String+PactSwift.swift +$(SRCROOT)/Sources/Extensions/Dictionary+PactSwift.swift +$(SRCROOT)/Sources/Extensions/Sequence+PactSwift.swift +$(SRCROOT)/Sources/Toolbox/Logger.swift +$(SRCROOT)/Sources/Toolbox/PactFileManager.swift +$(SRCROOT)/Sources/Model/Constants.swift +$(SRCROOT)/Sources/Model/Metadata.swift +$(SRCROOT)/Sources/Model/ProviderVerifier+Error.swift +$(SRCROOT)/Sources/Model/TransferProtocol.swift +$(SRCROOT)/Sources/Model/PactPathParameter.swift +$(SRCROOT)/Sources/Model/PactInteractionNode.swift +$(SRCROOT)/Sources/Model/ProviderVerifier+Provider.swift +$(SRCROOT)/Sources/Model/PactHTTPMethod.swift +$(SRCROOT)/Sources/Model/PactSwiftVersion.swift +$(SRCROOT)/Sources/Model/Pact.swift +$(SRCROOT)/Sources/Model/ProviderVerifier+Options.swift +$(SRCROOT)/Sources/Model/ProviderState.swift +$(SRCROOT)/Sources/Model/Response.swift +$(SRCROOT)/Sources/Model/PactInteractionElement.swift +$(SRCROOT)/Sources/Model/Interaction.swift +$(SRCROOT)/Sources/Model/Request.swift +$(SRCROOT)/Sources/Model/EncodingError.swift +$(SRCROOT)/Sources/Model/ExampleGeneratorExpressible.swift +$(SRCROOT)/Sources/Model/PactBroker.swift +$(SRCROOT)/Sources/Model/ErrorReportable.swift +$(SRCROOT)/Sources/Model/WIPPacts.swift +$(SRCROOT)/Sources/Model/Pacticipant.swift +$(SRCROOT)/Sources/Model/MatchingRuleExpressible.swift +$(SRCROOT)/Sources/Model/ErrorReporter.swift +$(SRCROOT)/Sources/Model/Toolbox.swift +$(SRCROOT)/Sources/Model/AnyEncodable.swift +$(SRCROOT)/Sources/Model/VersionSelector.swift +$(SRCROOT)/Sources/PactBuilder.swift +$(SRCROOT)/Sources/Matchers/FromProviderState.swift +$(SRCROOT)/Sources/Matchers/OneOf.swift +$(SRCROOT)/Sources/Matchers/SomethingLike.swift +$(SRCROOT)/Sources/Matchers/EqualTo.swift +$(SRCROOT)/Sources/Matchers/EachLike.swift +$(SRCROOT)/Sources/Matchers/EachKeyLike.swift +$(SRCROOT)/Sources/Matchers/IntegerLike.swift +$(SRCROOT)/Sources/Matchers/Matcher.swift +$(SRCROOT)/Sources/Matchers/RegexLike.swift +$(SRCROOT)/Sources/Matchers/IncludesLike.swift +$(SRCROOT)/Sources/Matchers/DecimalLike.swift +$(SRCROOT)/Sources/Matchers/MatchNull.swift +$(SRCROOT)/Sources/PFMockService.swift +$(SRCROOT)/Sources/ProviderVerifier.swift +$(SRCROOT)/Sources/ExampleGenerators/RandomBool.swift +$(SRCROOT)/Sources/ExampleGenerators/RandomString.swift +$(SRCROOT)/Sources/ExampleGenerators/RandomDateTime.swift +$(SRCROOT)/Sources/ExampleGenerators/RandomDecimal.swift +$(SRCROOT)/Sources/ExampleGenerators/RandomHexadecimal.swift +$(SRCROOT)/Sources/ExampleGenerators/RandomInt.swift +$(SRCROOT)/Sources/ExampleGenerators/DateTimeExpression.swift +$(SRCROOT)/Sources/ExampleGenerators/RandomTime.swift +$(SRCROOT)/Sources/ExampleGenerators/RandomUUID.swift +$(SRCROOT)/Sources/ExampleGenerators/ExampleGenerator.swift +$(SRCROOT)/Sources/ExampleGenerators/RandomDate.swift +$(SRCROOT)/Sources/ExampleGenerators/ProviderStateGenerator.swift +$(SRCROOT)/Sources/ExampleGenerators/DateTime.swift +$(SRCROOT)/Sources/MockService.swift diff --git a/PactSwift.xcodeproj/project.pbxproj b/PactSwift.xcodeproj/project.pbxproj new file mode 100644 index 00000000..71ca3032 --- /dev/null +++ b/PactSwift.xcodeproj/project.pbxproj @@ -0,0 +1,1506 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 55E8E3522936F3AD003D57A6 /* AsyncMockServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E8E3512936F3AD003D57A6 /* AsyncMockServiceTests.swift */; }; + 55E8E3532936F3AD003D57A6 /* AsyncMockServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E8E3512936F3AD003D57A6 /* AsyncMockServiceTests.swift */; }; + A782763E29372E35003809F9 /* MockServer+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = A782763D29372E35003809F9 /* MockServer+Async.swift */; }; + A782763F29372E35003809F9 /* MockServer+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = A782763D29372E35003809F9 /* MockServer+Async.swift */; }; + A7AF63D029387BE900A7FD42 /* Task+Timeout.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7AF63CF29387BE900A7FD42 /* Task+Timeout.swift */; }; + A7AF63D129387BE900A7FD42 /* Task+Timeout.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7AF63CF29387BE900A7FD42 /* Task+Timeout.swift */; }; + AD07404027F93BC1000C498C /* EachKeyLikeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD07403F27F93BC1000C498C /* EachKeyLikeTests.swift */; }; + AD07404127F93BC1000C498C /* EachKeyLikeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD07403F27F93BC1000C498C /* EachKeyLikeTests.swift */; }; + AD07404327F93BD5000C498C /* EachKeyLike.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD07404227F93BD5000C498C /* EachKeyLike.swift */; }; + AD07404427F93BD5000C498C /* EachKeyLike.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD07404227F93BD5000C498C /* EachKeyLike.swift */; }; + AD08FA3E28A23DD40059884F /* PactFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD08FA3D28A23DD40059884F /* PactFileManager.swift */; }; + AD08FA3F28A23DD40059884F /* PactFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD08FA3D28A23DD40059884F /* PactFileManager.swift */; }; + AD0AF278272634A300848FB7 /* MatcherTestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD0AF277272634A300848FB7 /* MatcherTestHelpers.swift */; }; + AD0AF279272634A300848FB7 /* MatcherTestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD0AF277272634A300848FB7 /* MatcherTestHelpers.swift */; }; + AD0AF27B272644E800848FB7 /* ExampleGeneratorTestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD0AF27A272644E800848FB7 /* ExampleGeneratorTestHelpers.swift */; }; + AD0AF27C272644E800848FB7 /* ExampleGeneratorTestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD0AF27A272644E800848FB7 /* ExampleGeneratorTestHelpers.swift */; }; + AD10361424468AB3002C97CA /* MockService.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD10361324468AB3002C97CA /* MockService.swift */; }; + AD103618244693A6002C97CA /* MockServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD103617244693A6002C97CA /* MockServiceTests.swift */; }; + AD23310C26E9F08000D984A5 /* MockServiceWithDirectoryPathTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD23310B26E9F08000D984A5 /* MockServiceWithDirectoryPathTests.swift */; }; + AD23310D26E9F08000D984A5 /* MockServiceWithDirectoryPathTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD23310B26E9F08000D984A5 /* MockServiceWithDirectoryPathTests.swift */; }; + AD48EC5826CF90B40017E071 /* ProviderVerifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD48EC5726CF90B40017E071 /* ProviderVerifier.swift */; }; + AD48EC5926CF90B40017E071 /* ProviderVerifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD48EC5726CF90B40017E071 /* ProviderVerifier.swift */; }; + AD4B970725138FCE00C37560 /* RandomDecimalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD4B970425138F7600C37560 /* RandomDecimalTests.swift */; }; + AD4B970825138FCF00C37560 /* RandomDecimalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD4B970425138F7600C37560 /* RandomDecimalTests.swift */; }; + AD4B970A2513A04800C37560 /* RandomHexadecimal.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD4B97092513A04800C37560 /* RandomHexadecimal.swift */; }; + AD4B970B2513A04800C37560 /* RandomHexadecimal.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD4B97092513A04800C37560 /* RandomHexadecimal.swift */; }; + AD4B970D2513A0C300C37560 /* RandomHexadecimalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD4B970C2513A0C300C37560 /* RandomHexadecimalTests.swift */; }; + AD4B970E2513A0C300C37560 /* RandomHexadecimalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD4B970C2513A0C300C37560 /* RandomHexadecimalTests.swift */; }; + AD4B97102513A3DB00C37560 /* RandomString.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD4B970F2513A3DB00C37560 /* RandomString.swift */; }; + AD4B97112513A3DB00C37560 /* RandomString.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD4B970F2513A3DB00C37560 /* RandomString.swift */; }; + AD4B97132513A64700C37560 /* RandomStringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD4B97122513A64700C37560 /* RandomStringTests.swift */; }; + AD4B97142513A64700C37560 /* RandomStringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD4B97122513A64700C37560 /* RandomStringTests.swift */; }; + AD54435E27D32DCA00D4C464 /* DateTimeExpression.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD54435D27D32DCA00D4C464 /* DateTimeExpression.swift */; }; + AD54435F27D32DCA00D4C464 /* DateTimeExpression.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD54435D27D32DCA00D4C464 /* DateTimeExpression.swift */; }; + AD54436127D3316600D4C464 /* DateTimeExpressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD54436027D3316600D4C464 /* DateTimeExpressionTests.swift */; }; + AD54436227D3316600D4C464 /* DateTimeExpressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD54436027D3316600D4C464 /* DateTimeExpressionTests.swift */; }; + AD5E9F0226D375BE0002580D /* ProviderVerifier+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD5E9F0126D375BE0002580D /* ProviderVerifier+Error.swift */; }; + AD5E9F0326D375BE0002580D /* ProviderVerifier+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD5E9F0126D375BE0002580D /* ProviderVerifier+Error.swift */; }; + AD5E9F0526D4684B0002580D /* WIPPacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD5E9F0426D4684B0002580D /* WIPPacts.swift */; }; + AD5E9F0626D4684B0002580D /* WIPPacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD5E9F0426D4684B0002580D /* WIPPacts.swift */; }; + AD641A332434331300785CE1 /* Bundle+PactSwift.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD641A322434331300785CE1 /* Bundle+PactSwift.swift */; }; + AD641A352434345500785CE1 /* Metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD641A342434345500785CE1 /* Metadata.swift */; }; + AD641A382434355C00785CE1 /* MetadataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD641A372434355C00785CE1 /* MetadataTests.swift */; }; + AD641A3A24344D9500785CE1 /* Pact.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD641A3924344D9500785CE1 /* Pact.swift */; }; + AD641A3C24344ED400785CE1 /* Pacticipant.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD641A3B24344ED400785CE1 /* Pacticipant.swift */; }; + AD641A3E24344FA100785CE1 /* PacticipantTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD641A3D24344FA100785CE1 /* PacticipantTests.swift */; }; + AD641A402434518500785CE1 /* PactTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD641A3F2434518500785CE1 /* PactTests.swift */; }; + AD641A422434588200785CE1 /* Interaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD641A412434588200785CE1 /* Interaction.swift */; }; + AD72E54027B89CB900C7453F /* DateTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD72E53F27B89CB900C7453F /* DateTime.swift */; }; + AD72E54127B89CB900C7453F /* DateTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD72E53F27B89CB900C7453F /* DateTime.swift */; }; + AD72E54527B8A03000C7453F /* DateTimeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD72E54227B8A02200C7453F /* DateTimeTests.swift */; }; + AD72E54627B8A03100C7453F /* DateTimeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD72E54227B8A02200C7453F /* DateTimeTests.swift */; }; + AD7891B724414D500014BCC8 /* PactBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD7891B624414D500014BCC8 /* PactBuilderTests.swift */; }; + AD7891BA2441512E0014BCC8 /* SomethingLikeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD7891B92441512E0014BCC8 /* SomethingLikeTests.swift */; }; + AD7891BC244156FC0014BCC8 /* EachLikeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD7891BB244156FC0014BCC8 /* EachLikeTests.swift */; }; + AD7891BE24415C100014BCC8 /* IntegerLike.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD7891BD24415C100014BCC8 /* IntegerLike.swift */; }; + AD7891C024415C710014BCC8 /* IntegerLikeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD7891BF24415C710014BCC8 /* IntegerLikeTests.swift */; }; + AD7891C224415E3E0014BCC8 /* DecimalLike.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD7891C124415E3E0014BCC8 /* DecimalLike.swift */; }; + AD7891C424415E9F0014BCC8 /* DecimalLikeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD7891C324415E9F0014BCC8 /* DecimalLikeTests.swift */; }; + AD78FB48264FD21900765BD3 /* PactContractTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD78FB47264FD21900765BD3 /* PactContractTests.swift */; }; + AD78FB49264FD21900765BD3 /* PactContractTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD78FB47264FD21900765BD3 /* PactContractTests.swift */; }; + AD8546F42513601800211E28 /* RandomDateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD8546F32513601800211E28 /* RandomDateTests.swift */; }; + AD8546F52513601800211E28 /* RandomDateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD8546F32513601800211E28 /* RandomDateTests.swift */; }; + AD854D882A7E75080005C502 /* MockService+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD854D872A7E75080005C502 /* MockService+Extension.swift */; }; + AD854D892A7E75080005C502 /* MockService+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD854D872A7E75080005C502 /* MockService+Extension.swift */; }; + AD854D8B2A7E75E20005C502 /* MockService+Concurrency.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD854D8A2A7E75E20005C502 /* MockService+Concurrency.swift */; }; + AD854D8C2A7E75E20005C502 /* MockService+Concurrency.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD854D8A2A7E75E20005C502 /* MockService+Concurrency.swift */; }; + AD879B9D258242AC00F85B0B /* PactSwiftVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD879B9C258242AC00F85B0B /* PactSwiftVersion.swift */; }; + AD879B9E258242AC00F85B0B /* PactSwiftVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD879B9C258242AC00F85B0B /* PactSwiftVersion.swift */; }; + AD881808242C715B00BF510D /* PactSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AD8817FE242C715A00BF510D /* PactSwift.framework */; }; + AD88180F242C715B00BF510D /* PactSwift.h in Headers */ = {isa = PBXBuildFile; fileRef = AD881801242C715A00BF510D /* PactSwift.h */; settings = {ATTRIBUTES = (Public, ); }; }; + AD8DF42C243BF9430062CB1A /* AnyEncodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD8DF42B243BF9430062CB1A /* AnyEncodable.swift */; }; + AD8DF433243C437F0062CB1A /* AnyEncodableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD8DF432243C437F0062CB1A /* AnyEncodableTests.swift */; }; + AD8DF435243C53750062CB1A /* PactBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD8DF434243C53750062CB1A /* PactBuilder.swift */; }; + AD8DF437243EA4580062CB1A /* PactInteractionElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD8DF436243EA4580062CB1A /* PactInteractionElement.swift */; }; + AD8DF439243EEBDB0062CB1A /* SomethingLike.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD8DF438243EEBDB0062CB1A /* SomethingLike.swift */; }; + AD8DF43B243EEF390062CB1A /* MatchingRuleExpressible.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD8DF43A243EEF390062CB1A /* MatchingRuleExpressible.swift */; }; + AD8DF43F244055980062CB1A /* EachLike.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD8DF43E244055980062CB1A /* EachLike.swift */; }; + AD8FC7C02463A06F00361854 /* PactSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AD8FC7B72463A06F00361854 /* PactSwift.framework */; }; + AD8FC7E12463BB9F00361854 /* AnyEncodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD8DF42B243BF9430062CB1A /* AnyEncodable.swift */; }; + AD8FC7E22463BB9F00361854 /* ErrorReportable.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADDE4703244D101700E4F7EE /* ErrorReportable.swift */; }; + AD8FC7E32463BB9F00361854 /* ErrorReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADDE4705244D11DE00E4F7EE /* ErrorReporter.swift */; }; + AD8FC7E42463BB9F00361854 /* Interaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD641A412434588200785CE1 /* Interaction.swift */; }; + AD8FC7E52463BB9F00361854 /* Metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD641A342434345500785CE1 /* Metadata.swift */; }; + AD8FC7E62463BB9F00361854 /* Pact.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD641A3924344D9500785CE1 /* Pact.swift */; }; + AD8FC7E72463BB9F00361854 /* PactBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD8DF434243C53750062CB1A /* PactBuilder.swift */; }; + AD8FC7E82463BB9F00361854 /* PactHTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADB7C1682433280100A16CDE /* PactHTTPMethod.swift */; }; + AD8FC7E92463BB9F00361854 /* Pacticipant.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD641A3B24344ED400785CE1 /* Pacticipant.swift */; }; + AD8FC7EA2463BB9F00361854 /* PactInteractionElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD8DF436243EA4580062CB1A /* PactInteractionElement.swift */; }; + AD8FC7EB2463BB9F00361854 /* ProviderState.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADE2937E2435DB14008AFBC9 /* ProviderState.swift */; }; + AD8FC7EC2463BB9F00361854 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADB7C1602433269B00A16CDE /* Request.swift */; }; + AD8FC7ED2463BB9F00361854 /* Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADB7C16A2433283400A16CDE /* Response.swift */; }; + AD8FC7EE2463BBA400361854 /* Bundle+PactSwift.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD641A322434331300785CE1 /* Bundle+PactSwift.swift */; }; + AD8FC7EF2463BBA400361854 /* Dictionary+PactSwift.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADB7C11C2432D41E00A16CDE /* Dictionary+PactSwift.swift */; }; + AD8FC7F02463BBA900361854 /* PactSwift.h in Headers */ = {isa = PBXBuildFile; fileRef = AD881801242C715A00BF510D /* PactSwift.h */; settings = {ATTRIBUTES = (Public, ); }; }; + AD8FC7F22463BBB800361854 /* DecimalLike.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD7891C124415E3E0014BCC8 /* DecimalLike.swift */; }; + AD8FC7F32463BBB800361854 /* EachLike.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD8DF43E244055980062CB1A /* EachLike.swift */; }; + AD8FC7F42463BBB800361854 /* EqualTo.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD3C27324416CDF002E73B9 /* EqualTo.swift */; }; + AD8FC7F52463BBB800361854 /* IncludesLike.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD3C27524416DAE002E73B9 /* IncludesLike.swift */; }; + AD8FC7F62463BBB800361854 /* IntegerLike.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD7891BD24415C100014BCC8 /* IntegerLike.swift */; }; + AD8FC7F72463BBB800361854 /* MatchingRuleExpressible.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD8DF43A243EEF390062CB1A /* MatchingRuleExpressible.swift */; }; + AD8FC7F82463BBB800361854 /* RegexLike.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD3C26F24416891002E73B9 /* RegexLike.swift */; }; + AD8FC7F92463BBB800361854 /* SomethingLike.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD8DF438243EEBDB0062CB1A /* SomethingLike.swift */; }; + AD8FC7FA2463BBB800361854 /* MockService.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD10361324468AB3002C97CA /* MockService.swift */; }; + AD8FC7FE2463BBD000361854 /* AnyEncodableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD8DF432243C437F0062CB1A /* AnyEncodableTests.swift */; }; + AD8FC7FF2463BBD000361854 /* MetadataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD641A372434355C00785CE1 /* MetadataTests.swift */; }; + AD8FC8002463BBD000361854 /* PactBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD7891B624414D500014BCC8 /* PactBuilderTests.swift */; }; + AD8FC8012463BBD000361854 /* PacticipantTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD641A3D24344FA100785CE1 /* PacticipantTests.swift */; }; + AD8FC8022463BBD000361854 /* PactTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD641A3F2434518500785CE1 /* PactTests.swift */; }; + AD8FC8032463BBD000361854 /* DecimalLikeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD7891C324415E9F0014BCC8 /* DecimalLikeTests.swift */; }; + AD8FC8042463BBD000361854 /* EachLikeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD7891BB244156FC0014BCC8 /* EachLikeTests.swift */; }; + AD8FC8052463BBD000361854 /* IntegerLikeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD7891BF24415C710014BCC8 /* IntegerLikeTests.swift */; }; + AD8FC8062463BBD000361854 /* RegexLikeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD3C27124416A3E002E73B9 /* RegexLikeTests.swift */; }; + AD8FC8072463BBD000361854 /* SomethingLikeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD7891B92441512E0014BCC8 /* SomethingLikeTests.swift */; }; + AD8FC8082463BBD100361854 /* MockServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD103617244693A6002C97CA /* MockServiceTests.swift */; }; + AD8FC80A2463BBD100361854 /* ErrorCapture.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADDE4700244D0FD600E4F7EE /* ErrorCapture.swift */; }; + AD92805126BE1B60004FAA7E /* PFMockServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD92805026BE1B60004FAA7E /* PFMockServiceTests.swift */; }; + AD92805226BE1B60004FAA7E /* PFMockServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD92805026BE1B60004FAA7E /* PFMockServiceTests.swift */; }; + AD92805526BE3361004FAA7E /* String+PactSwiftTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD92805426BE3361004FAA7E /* String+PactSwiftTests.swift */; }; + AD92805626BE3361004FAA7E /* String+PactSwiftTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD92805426BE3361004FAA7E /* String+PactSwiftTests.swift */; }; + AD92805826BE3705004FAA7E /* TransferProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD92805726BE3705004FAA7E /* TransferProtocolTests.swift */; }; + AD92805926BE3705004FAA7E /* TransferProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD92805726BE3705004FAA7E /* TransferProtocolTests.swift */; }; + AD92E18F2A7DE6E7005C70E5 /* PactSwiftMockServer in Frameworks */ = {isa = PBXBuildFile; productRef = AD92E18E2A7DE6E7005C70E5 /* PactSwiftMockServer */; }; + AD957F3228A23A2300860AD1 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD957F3128A23A2300860AD1 /* Logger.swift */; }; + AD957F3328A23A2300860AD1 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD957F3128A23A2300860AD1 /* Logger.swift */; }; + AD9D7D8D26C8AD3400FE4137 /* ProviderStateGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD9D7D8C26C8AD3400FE4137 /* ProviderStateGenerator.swift */; }; + AD9D7D8E26C8AD3400FE4137 /* ProviderStateGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD9D7D8C26C8AD3400FE4137 /* ProviderStateGenerator.swift */; }; + AD9D7D9026C8B3DA00FE4137 /* FromProviderState.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD9D7D8F26C8B3DA00FE4137 /* FromProviderState.swift */; }; + AD9D7D9126C8B3DA00FE4137 /* FromProviderState.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD9D7D8F26C8B3DA00FE4137 /* FromProviderState.swift */; }; + ADA17E3B25137716004F1A82 /* RandomDateTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA17E3A25137716004F1A82 /* RandomDateTime.swift */; }; + ADA17E3C25137716004F1A82 /* RandomDateTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA17E3A25137716004F1A82 /* RandomDateTime.swift */; }; + ADA17E3E2513772B004F1A82 /* RandomDateTimeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA17E3D2513772B004F1A82 /* RandomDateTimeTests.swift */; }; + ADA17E3F2513772B004F1A82 /* RandomDateTimeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA17E3D2513772B004F1A82 /* RandomDateTimeTests.swift */; }; + ADA17E41251377A4004F1A82 /* Date+PactSwift.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA17E40251377A4004F1A82 /* Date+PactSwift.swift */; }; + ADA17E42251377A4004F1A82 /* Date+PactSwift.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA17E40251377A4004F1A82 /* Date+PactSwift.swift */; }; + ADA17E492513808B004F1A82 /* DateHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA17E482513808B004F1A82 /* DateHelper.swift */; }; + ADA17E4A2513808B004F1A82 /* DateHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA17E482513808B004F1A82 /* DateHelper.swift */; }; + ADA17E4C251383EB004F1A82 /* RandomTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA17E4B251383EB004F1A82 /* RandomTime.swift */; }; + ADA17E4D251383EB004F1A82 /* RandomTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA17E4B251383EB004F1A82 /* RandomTime.swift */; }; + ADA17E4F2513848A004F1A82 /* RandomTimeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA17E4E2513848A004F1A82 /* RandomTimeTests.swift */; }; + ADA17E502513848A004F1A82 /* RandomTimeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA17E4E2513848A004F1A82 /* RandomTimeTests.swift */; }; + ADA17E5225138908004F1A82 /* RandomDecimal.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA17E5125138908004F1A82 /* RandomDecimal.swift */; }; + ADA17E5325138908004F1A82 /* RandomDecimal.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA17E5125138908004F1A82 /* RandomDecimal.swift */; }; + ADA40177253028A100265DF3 /* MatchNull.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA40176253028A100265DF3 /* MatchNull.swift */; }; + ADA40178253028A100265DF3 /* MatchNull.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA40176253028A100265DF3 /* MatchNull.swift */; }; + ADA40182253033C400265DF3 /* MatchNullTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA40181253033C400265DF3 /* MatchNullTests.swift */; }; + ADA40183253033C400265DF3 /* MatchNullTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA40181253033C400265DF3 /* MatchNullTests.swift */; }; + ADA4B1DB26D31E5100A5AE88 /* ProviderVerifier+Options.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA4B1DA26D31E5100A5AE88 /* ProviderVerifier+Options.swift */; }; + ADA4B1DC26D31E5100A5AE88 /* ProviderVerifier+Options.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA4B1DA26D31E5100A5AE88 /* ProviderVerifier+Options.swift */; }; + ADA4B1DE26D31E9100A5AE88 /* ProviderVerifier+Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA4B1DD26D31E9100A5AE88 /* ProviderVerifier+Provider.swift */; }; + ADA4B1DF26D31E9100A5AE88 /* ProviderVerifier+Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA4B1DD26D31E9100A5AE88 /* ProviderVerifier+Provider.swift */; }; + ADA4B1E126D31EB100A5AE88 /* PactBroker.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA4B1E026D31EB100A5AE88 /* PactBroker.swift */; }; + ADA4B1E226D31EB100A5AE88 /* PactBroker.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA4B1E026D31EB100A5AE88 /* PactBroker.swift */; }; + ADA4B1E426D31EDA00A5AE88 /* VersionSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA4B1E326D31EDA00A5AE88 /* VersionSelector.swift */; }; + ADA4B1E526D31EDA00A5AE88 /* VersionSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA4B1E326D31EDA00A5AE88 /* VersionSelector.swift */; }; + ADB7C11D2432D41F00A16CDE /* Dictionary+PactSwift.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADB7C11C2432D41E00A16CDE /* Dictionary+PactSwift.swift */; }; + ADB7C1612433269B00A16CDE /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADB7C1602433269B00A16CDE /* Request.swift */; }; + ADB7C1692433280100A16CDE /* PactHTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADB7C1682433280100A16CDE /* PactHTTPMethod.swift */; }; + ADB7C16B2433283400A16CDE /* Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADB7C16A2433283400A16CDE /* Response.swift */; }; + ADBC3E5026DB2887006908E0 /* PactBrokerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADBC3E4F26DB2887006908E0 /* PactBrokerTests.swift */; }; + ADBC3E5126DB2887006908E0 /* PactBrokerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADBC3E4F26DB2887006908E0 /* PactBrokerTests.swift */; }; + ADBC3E5326DB322C006908E0 /* VersionSelectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADBC3E5226DB322C006908E0 /* VersionSelectorTests.swift */; }; + ADBC3E5426DB322C006908E0 /* VersionSelectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADBC3E5226DB322C006908E0 /* VersionSelectorTests.swift */; }; + ADBC3E5626DB36A8006908E0 /* WIPPactsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADBC3E5526DB36A8006908E0 /* WIPPactsTests.swift */; }; + ADBC3E5726DB36A8006908E0 /* WIPPactsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADBC3E5526DB36A8006908E0 /* WIPPactsTests.swift */; }; + ADBC3E5926DB386B006908E0 /* ProviderVerifier+OptionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADBC3E5826DB386B006908E0 /* ProviderVerifier+OptionsTests.swift */; }; + ADBC3E5A26DB386B006908E0 /* ProviderVerifier+OptionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADBC3E5826DB386B006908E0 /* ProviderVerifier+OptionsTests.swift */; }; + ADBC3E5C26DB4846006908E0 /* ProviderVerifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADBC3E5B26DB4846006908E0 /* ProviderVerifierTests.swift */; }; + ADBC3E5D26DB4846006908E0 /* ProviderVerifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADBC3E5B26DB4846006908E0 /* ProviderVerifierTests.swift */; }; + ADBC3E5F26DB521E006908E0 /* PactHTTPMethodTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADBC3E5E26DB521E006908E0 /* PactHTTPMethodTests.swift */; }; + ADBC3E6026DB521E006908E0 /* PactHTTPMethodTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADBC3E5E26DB521E006908E0 /* PactHTTPMethodTests.swift */; }; + ADBC60DC2513639700BBE666 /* RandomDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD8546F025135F4200211E28 /* RandomDate.swift */; }; + ADBC60DD2513639800BBE666 /* RandomDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD8546F025135F4200211E28 /* RandomDate.swift */; }; + ADC37269269825AA000DA90B /* OneOf.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADC37268269825AA000DA90B /* OneOf.swift */; }; + ADC3726A269825AA000DA90B /* OneOf.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADC37268269825AA000DA90B /* OneOf.swift */; }; + ADC3728226982E1E000DA90B /* OneOfTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADC3727326982D88000DA90B /* OneOfTests.swift */; }; + ADC3728726982E1F000DA90B /* OneOfTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADC3727326982D88000DA90B /* OneOfTests.swift */; }; + ADC372B4269877AF000DA90B /* Sequence+PactSwift.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADC372B3269877AF000DA90B /* Sequence+PactSwift.swift */; }; + ADC372B5269877AF000DA90B /* Sequence+PactSwift.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADC372B3269877AF000DA90B /* Sequence+PactSwift.swift */; }; + ADC3AA37247C8C4B0034446E /* InteractionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADC3AA36247C8C4B0034446E /* InteractionTests.swift */; }; + ADC3AA38247C8C4B0034446E /* InteractionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADC3AA36247C8C4B0034446E /* InteractionTests.swift */; }; + ADC3AA3D247CBB550034446E /* IncludesLikeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADC3AA3C247CBB550034446E /* IncludesLikeTests.swift */; }; + ADC3AA3E247CBB550034446E /* IncludesLikeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADC3AA3C247CBB550034446E /* IncludesLikeTests.swift */; }; + ADD0315F2512193500C6099B /* ExampleGeneratorExpressible.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD0315E2512193500C6099B /* ExampleGeneratorExpressible.swift */; }; + ADD031602512193500C6099B /* ExampleGeneratorExpressible.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD0315E2512193500C6099B /* ExampleGeneratorExpressible.swift */; }; + ADD03164251219B700C6099B /* ExampleGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD03163251219B700C6099B /* ExampleGenerator.swift */; }; + ADD03165251219B700C6099B /* ExampleGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD03163251219B700C6099B /* ExampleGenerator.swift */; }; + ADD03167251220FD00C6099B /* RandomBool.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD03166251220FD00C6099B /* RandomBool.swift */; }; + ADD03168251220FD00C6099B /* RandomBool.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD03166251220FD00C6099B /* RandomBool.swift */; }; + ADD0316C251221A300C6099B /* RandomBooleanTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD0316B251221A300C6099B /* RandomBooleanTests.swift */; }; + ADD0316D251221A300C6099B /* RandomBooleanTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD0316B251221A300C6099B /* RandomBooleanTests.swift */; }; + ADD0316F25122E4B00C6099B /* RandomUUID.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD0316E25122E4B00C6099B /* RandomUUID.swift */; }; + ADD0317025122E4B00C6099B /* RandomUUID.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD0316E25122E4B00C6099B /* RandomUUID.swift */; }; + ADD0317225122EA400C6099B /* RandomUUIDTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD0317125122EA400C6099B /* RandomUUIDTests.swift */; }; + ADD0317325122EA400C6099B /* RandomUUIDTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD0317125122EA400C6099B /* RandomUUIDTests.swift */; }; + ADD03175251235A800C6099B /* UUID+PactSwift.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD03174251235A800C6099B /* UUID+PactSwift.swift */; }; + ADD03176251235A800C6099B /* UUID+PactSwift.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD03174251235A800C6099B /* UUID+PactSwift.swift */; }; + ADD031782512425800C6099B /* RandomInt.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD031772512425800C6099B /* RandomInt.swift */; }; + ADD031792512425800C6099B /* RandomInt.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD031772512425800C6099B /* RandomInt.swift */; }; + ADD0317B2512439500C6099B /* RandomIntTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD0317A2512439500C6099B /* RandomIntTests.swift */; }; + ADD0317C2512439500C6099B /* RandomIntTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD0317A2512439500C6099B /* RandomIntTests.swift */; }; + ADD3C27024416891002E73B9 /* RegexLike.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD3C26F24416891002E73B9 /* RegexLike.swift */; }; + ADD3C27224416A3E002E73B9 /* RegexLikeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD3C27124416A3E002E73B9 /* RegexLikeTests.swift */; }; + ADD3C27424416CDF002E73B9 /* EqualTo.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD3C27324416CDF002E73B9 /* EqualTo.swift */; }; + ADD3C27624416DAE002E73B9 /* IncludesLike.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD3C27524416DAE002E73B9 /* IncludesLike.swift */; }; + ADDE4702244D0FDE00E4F7EE /* ErrorCapture.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADDE4700244D0FD600E4F7EE /* ErrorCapture.swift */; }; + ADDE4704244D101700E4F7EE /* ErrorReportable.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADDE4703244D101700E4F7EE /* ErrorReportable.swift */; }; + ADDE4706244D11DE00E4F7EE /* ErrorReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADDE4705244D11DE00E4F7EE /* ErrorReporter.swift */; }; + ADE2937F2435DB14008AFBC9 /* ProviderState.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADE2937E2435DB14008AFBC9 /* ProviderState.swift */; }; + ADE9B3C7250A3C4700274672 /* Matcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADE9B3C6250A3C4700274672 /* Matcher.swift */; }; + ADE9B3C8250A3C4700274672 /* Matcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADE9B3C6250A3C4700274672 /* Matcher.swift */; }; + ADE9B3CA250A435B00274672 /* EqualToTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADE9B3C9250A435B00274672 /* EqualToTests.swift */; }; + ADE9B3CB250A435B00274672 /* EqualToTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADE9B3C9250A435B00274672 /* EqualToTests.swift */; }; + ADEDDEF72547B6B700A45AD2 /* PactPathParameter.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADEDDEF62547B6B700A45AD2 /* PactPathParameter.swift */; }; + ADEDDEF82547B6B700A45AD2 /* PactPathParameter.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADEDDEF62547B6B700A45AD2 /* PactPathParameter.swift */; }; + ADEDDEFD2547B6FA00A45AD2 /* String+PactSwift.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADEDDEFC2547B6FA00A45AD2 /* String+PactSwift.swift */; }; + ADEDDEFE2547B6FA00A45AD2 /* String+PactSwift.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADEDDEFC2547B6FA00A45AD2 /* String+PactSwift.swift */; }; + ADEDDF092547CFC200A45AD2 /* ObjCMatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADEDDF082547CFC200A45AD2 /* ObjCMatcherTests.swift */; }; + ADEDDF0A2547CFC200A45AD2 /* ObjCMatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADEDDF082547CFC200A45AD2 /* ObjCMatcherTests.swift */; }; + ADEDDF122547D95700A45AD2 /* ObjCExampleGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADEDDF112547D95700A45AD2 /* ObjCExampleGeneratorTests.swift */; }; + ADEDDF132547D95700A45AD2 /* ObjCExampleGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADEDDF112547D95700A45AD2 /* ObjCExampleGeneratorTests.swift */; }; + ADEDDF1B2547DF3200A45AD2 /* ToolboxTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADEDDF1A2547DF3200A45AD2 /* ToolboxTests.swift */; }; + ADEDDF1C2547DF3200A45AD2 /* ToolboxTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADEDDF1A2547DF3200A45AD2 /* ToolboxTests.swift */; }; + ADEDDF252547E81300A45AD2 /* EncodingError.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADEDDF242547E81300A45AD2 /* EncodingError.swift */; }; + ADEDDF262547E81300A45AD2 /* EncodingError.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADEDDF242547E81300A45AD2 /* EncodingError.swift */; }; + ADEFD134253EEF230081A1B1 /* Toolbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADEFD133253EEF230081A1B1 /* Toolbox.swift */; }; + ADEFD135253EEF230081A1B1 /* Toolbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADEFD133253EEF230081A1B1 /* Toolbox.swift */; }; + ADF17CBA26B5019B008E7ECD /* PFMockService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADF17CB926B5019B008E7ECD /* PFMockService.swift */; }; + ADF17CBB26B5019B008E7ECD /* PFMockService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADF17CB926B5019B008E7ECD /* PFMockService.swift */; }; + ADF17CBD26B50796008E7ECD /* TransferProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADF17CBC26B50796008E7ECD /* TransferProtocol.swift */; }; + ADF17CBE26B50796008E7ECD /* TransferProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADF17CBC26B50796008E7ECD /* TransferProtocol.swift */; }; + ADF17CC026B50B66008E7ECD /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADF17CBF26B50B66008E7ECD /* Constants.swift */; }; + ADF17CC126B50B66008E7ECD /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADF17CBF26B50B66008E7ECD /* Constants.swift */; }; + ADF959CA26C8F6EB00C35536 /* FromProviderStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADF959C926C8F6EB00C35536 /* FromProviderStateTests.swift */; }; + ADF959CB26C8F6EB00C35536 /* FromProviderStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADF959C926C8F6EB00C35536 /* FromProviderStateTests.swift */; }; + ADF959CD26C8F98800C35536 /* ProviderStateGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADF959CC26C8F98800C35536 /* ProviderStateGeneratorTests.swift */; }; + ADF959CE26C8F98800C35536 /* ProviderStateGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADF959CC26C8F98800C35536 /* ProviderStateGeneratorTests.swift */; }; + ADF994CE2A7DE1720011D974 /* PactSwiftMockServer in Frameworks */ = {isa = PBXBuildFile; productRef = ADF994CD2A7DE1720011D974 /* PactSwiftMockServer */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + AD881809242C715B00BF510D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = AD8817F5242C715A00BF510D /* Project object */; + proxyType = 1; + remoteGlobalIDString = AD8817FD242C715A00BF510D; + remoteInfo = PACTSwift; + }; + AD8FC7C12463A06F00361854 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = AD8817F5242C715A00BF510D /* Project object */; + proxyType = 1; + remoteGlobalIDString = AD8FC7B62463A06F00361854; + remoteInfo = PactSwift_macOS; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 55E8E3512936F3AD003D57A6 /* AsyncMockServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncMockServiceTests.swift; sourceTree = ""; }; + A782763D29372E35003809F9 /* MockServer+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MockServer+Async.swift"; sourceTree = ""; }; + A7AF63CF29387BE900A7FD42 /* Task+Timeout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Task+Timeout.swift"; sourceTree = ""; }; + AD07403F27F93BC1000C498C /* EachKeyLikeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EachKeyLikeTests.swift; sourceTree = ""; }; + AD07404227F93BD5000C498C /* EachKeyLike.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EachKeyLike.swift; sourceTree = ""; }; + AD08FA3D28A23DD40059884F /* PactFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PactFileManager.swift; sourceTree = ""; }; + AD0AF277272634A300848FB7 /* MatcherTestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatcherTestHelpers.swift; sourceTree = ""; }; + AD0AF27A272644E800848FB7 /* ExampleGeneratorTestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleGeneratorTestHelpers.swift; sourceTree = ""; }; + AD10361324468AB3002C97CA /* MockService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockService.swift; sourceTree = ""; }; + AD103617244693A6002C97CA /* MockServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockServiceTests.swift; sourceTree = ""; }; + AD23310B26E9F08000D984A5 /* MockServiceWithDirectoryPathTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockServiceWithDirectoryPathTests.swift; sourceTree = ""; }; + AD2CB262242CC0A000D991F0 /* Project-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Project-Debug.xcconfig"; sourceTree = ""; }; + AD2CB263242CC0B900D991F0 /* Project-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Project-Release.xcconfig"; sourceTree = ""; }; + AD2CB264242CC10600D991F0 /* Target-iOS-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Target-iOS-Debug.xcconfig"; sourceTree = ""; }; + AD2CB265242CC10E00D991F0 /* Target-iOS-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Target-iOS-Release.xcconfig"; sourceTree = ""; }; + AD2CB266242CC11700D991F0 /* Target-iOS-Tests-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Target-iOS-Tests-Debug.xcconfig"; sourceTree = ""; }; + AD2CB267242CC12300D991F0 /* Target-iOS-Tests-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Target-iOS-Tests-Release.xcconfig"; sourceTree = ""; }; + AD48EC5726CF90B40017E071 /* ProviderVerifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProviderVerifier.swift; sourceTree = ""; }; + AD4B970425138F7600C37560 /* RandomDecimalTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandomDecimalTests.swift; sourceTree = ""; }; + AD4B97092513A04800C37560 /* RandomHexadecimal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandomHexadecimal.swift; sourceTree = ""; }; + AD4B970C2513A0C300C37560 /* RandomHexadecimalTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandomHexadecimalTests.swift; sourceTree = ""; }; + AD4B970F2513A3DB00C37560 /* RandomString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandomString.swift; sourceTree = ""; }; + AD4B97122513A64700C37560 /* RandomStringTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandomStringTests.swift; sourceTree = ""; }; + AD4FC5DD242CC2B20039342D /* Project-Shared.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Project-Shared.xcconfig"; sourceTree = ""; }; + AD4FC5DE242CC2C30039342D /* Target-iOS-Shared.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Target-iOS-Shared.xcconfig"; sourceTree = ""; }; + AD4FC5DF242CC2CB0039342D /* Target-iOS-Tests-Shared.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Target-iOS-Tests-Shared.xcconfig"; sourceTree = ""; }; + AD54435D27D32DCA00D4C464 /* DateTimeExpression.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTimeExpression.swift; sourceTree = ""; }; + AD54436027D3316600D4C464 /* DateTimeExpressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTimeExpressionTests.swift; sourceTree = ""; }; + AD5E9F0126D375BE0002580D /* ProviderVerifier+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProviderVerifier+Error.swift"; sourceTree = ""; }; + AD5E9F0426D4684B0002580D /* WIPPacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WIPPacts.swift; sourceTree = ""; }; + AD641A322434331300785CE1 /* Bundle+PactSwift.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+PactSwift.swift"; sourceTree = ""; }; + AD641A342434345500785CE1 /* Metadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Metadata.swift; sourceTree = ""; }; + AD641A372434355C00785CE1 /* MetadataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetadataTests.swift; sourceTree = ""; }; + AD641A3924344D9500785CE1 /* Pact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pact.swift; sourceTree = ""; }; + AD641A3B24344ED400785CE1 /* Pacticipant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pacticipant.swift; sourceTree = ""; }; + AD641A3D24344FA100785CE1 /* PacticipantTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacticipantTests.swift; sourceTree = ""; }; + AD641A3F2434518500785CE1 /* PactTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PactTests.swift; sourceTree = ""; }; + AD641A412434588200785CE1 /* Interaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Interaction.swift; sourceTree = ""; }; + AD72E53F27B89CB900C7453F /* DateTime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTime.swift; sourceTree = ""; }; + AD72E54227B8A02200C7453F /* DateTimeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTimeTests.swift; sourceTree = ""; }; + AD7891B624414D500014BCC8 /* PactBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PactBuilderTests.swift; sourceTree = ""; }; + AD7891B92441512E0014BCC8 /* SomethingLikeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SomethingLikeTests.swift; sourceTree = ""; }; + AD7891BB244156FC0014BCC8 /* EachLikeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EachLikeTests.swift; sourceTree = ""; }; + AD7891BD24415C100014BCC8 /* IntegerLike.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegerLike.swift; sourceTree = ""; }; + AD7891BF24415C710014BCC8 /* IntegerLikeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegerLikeTests.swift; sourceTree = ""; }; + AD7891C124415E3E0014BCC8 /* DecimalLike.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecimalLike.swift; sourceTree = ""; }; + AD7891C324415E9F0014BCC8 /* DecimalLikeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecimalLikeTests.swift; sourceTree = ""; }; + AD78FB47264FD21900765BD3 /* PactContractTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PactContractTests.swift; sourceTree = ""; }; + AD8546F025135F4200211E28 /* RandomDate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandomDate.swift; sourceTree = ""; }; + AD8546F32513601800211E28 /* RandomDateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandomDateTests.swift; sourceTree = ""; }; + AD854D872A7E75080005C502 /* MockService+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MockService+Extension.swift"; sourceTree = ""; }; + AD854D8A2A7E75E20005C502 /* MockService+Concurrency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MockService+Concurrency.swift"; sourceTree = ""; }; + AD879B9C258242AC00F85B0B /* PactSwiftVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PactSwiftVersion.swift; sourceTree = ""; }; + AD8817FE242C715A00BF510D /* PactSwift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = PactSwift.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + AD881801242C715A00BF510D /* PactSwift.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PactSwift.h; sourceTree = ""; }; + AD881802242C715A00BF510D /* Info-iOS.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Info-iOS.plist"; sourceTree = ""; }; + AD881807242C715B00BF510D /* PactSwiftTests_iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PactSwiftTests_iOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + AD88180E242C715B00BF510D /* Info-iOS-Tests.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Info-iOS-Tests.plist"; sourceTree = ""; }; + AD8A32D6242CCF5600283080 /* Scripts */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Scripts; sourceTree = ""; }; + AD8DF42B243BF9430062CB1A /* AnyEncodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyEncodable.swift; sourceTree = ""; }; + AD8DF432243C437F0062CB1A /* AnyEncodableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyEncodableTests.swift; sourceTree = ""; }; + AD8DF434243C53750062CB1A /* PactBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PactBuilder.swift; sourceTree = ""; }; + AD8DF436243EA4580062CB1A /* PactInteractionElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PactInteractionElement.swift; sourceTree = ""; }; + AD8DF438243EEBDB0062CB1A /* SomethingLike.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SomethingLike.swift; sourceTree = ""; }; + AD8DF43A243EEF390062CB1A /* MatchingRuleExpressible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchingRuleExpressible.swift; sourceTree = ""; }; + AD8DF43E244055980062CB1A /* EachLike.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EachLike.swift; sourceTree = ""; }; + AD8FC7B72463A06F00361854 /* PactSwift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = PactSwift.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + AD8FC7BA2463A06F00361854 /* Info-macOS.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Info-macOS.plist"; sourceTree = ""; }; + AD8FC7BF2463A06F00361854 /* PactSwiftTests_macOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PactSwiftTests_macOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + AD8FC7C62463A06F00361854 /* Info-macOS-Tests.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Info-macOS-Tests.plist"; sourceTree = ""; }; + AD8FC7D22463AD9800361854 /* Target-macOS-Shared.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Target-macOS-Shared.xcconfig"; sourceTree = ""; }; + AD8FC7D32463ADA700361854 /* Target-macOS-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Target-macOS-Debug.xcconfig"; sourceTree = ""; }; + AD8FC7D42463ADB000361854 /* Target-macOS-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Target-macOS-Release.xcconfig"; sourceTree = ""; }; + AD8FC7D52463ADC300361854 /* Target-macOS-Tests-Shared.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Target-macOS-Tests-Shared.xcconfig"; sourceTree = ""; }; + AD8FC7D62463ADCF00361854 /* Target-macOS-Tests-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Target-macOS-Tests-Debug.xcconfig"; sourceTree = ""; }; + AD8FC7D72463ADDB00361854 /* Target-macOS-Tests-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Target-macOS-Tests-Release.xcconfig"; sourceTree = ""; }; + AD92805026BE1B60004FAA7E /* PFMockServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PFMockServiceTests.swift; sourceTree = ""; }; + AD92805426BE3361004FAA7E /* String+PactSwiftTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+PactSwiftTests.swift"; sourceTree = ""; }; + AD92805726BE3705004FAA7E /* TransferProtocolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferProtocolTests.swift; sourceTree = ""; }; + AD957F3128A23A2300860AD1 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; + AD9D7D8C26C8AD3400FE4137 /* ProviderStateGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProviderStateGenerator.swift; sourceTree = ""; }; + AD9D7D8F26C8B3DA00FE4137 /* FromProviderState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FromProviderState.swift; sourceTree = ""; }; + ADA17E3A25137716004F1A82 /* RandomDateTime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandomDateTime.swift; sourceTree = ""; }; + ADA17E3D2513772B004F1A82 /* RandomDateTimeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandomDateTimeTests.swift; sourceTree = ""; }; + ADA17E40251377A4004F1A82 /* Date+PactSwift.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+PactSwift.swift"; sourceTree = ""; }; + ADA17E482513808B004F1A82 /* DateHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateHelper.swift; sourceTree = ""; }; + ADA17E4B251383EB004F1A82 /* RandomTime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandomTime.swift; sourceTree = ""; }; + ADA17E4E2513848A004F1A82 /* RandomTimeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandomTimeTests.swift; sourceTree = ""; }; + ADA17E5125138908004F1A82 /* RandomDecimal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandomDecimal.swift; sourceTree = ""; }; + ADA40176253028A100265DF3 /* MatchNull.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MatchNull.swift; sourceTree = ""; }; + ADA40181253033C400265DF3 /* MatchNullTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchNullTests.swift; sourceTree = ""; }; + ADA4B1DA26D31E5100A5AE88 /* ProviderVerifier+Options.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProviderVerifier+Options.swift"; sourceTree = ""; }; + ADA4B1DD26D31E9100A5AE88 /* ProviderVerifier+Provider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProviderVerifier+Provider.swift"; sourceTree = ""; }; + ADA4B1E026D31EB100A5AE88 /* PactBroker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PactBroker.swift; sourceTree = ""; }; + ADA4B1E326D31EDA00A5AE88 /* VersionSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionSelector.swift; sourceTree = ""; }; + ADB7C11C2432D41E00A16CDE /* Dictionary+PactSwift.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dictionary+PactSwift.swift"; sourceTree = ""; }; + ADB7C1602433269B00A16CDE /* Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; }; + ADB7C1682433280100A16CDE /* PactHTTPMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PactHTTPMethod.swift; sourceTree = ""; }; + ADB7C16A2433283400A16CDE /* Response.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Response.swift; sourceTree = ""; }; + ADBC3E4F26DB2887006908E0 /* PactBrokerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PactBrokerTests.swift; sourceTree = ""; }; + ADBC3E5226DB322C006908E0 /* VersionSelectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionSelectorTests.swift; sourceTree = ""; }; + ADBC3E5526DB36A8006908E0 /* WIPPactsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WIPPactsTests.swift; sourceTree = ""; }; + ADBC3E5826DB386B006908E0 /* ProviderVerifier+OptionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProviderVerifier+OptionsTests.swift"; sourceTree = ""; }; + ADBC3E5B26DB4846006908E0 /* ProviderVerifierTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProviderVerifierTests.swift; sourceTree = ""; }; + ADBC3E5E26DB521E006908E0 /* PactHTTPMethodTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PactHTTPMethodTests.swift; sourceTree = ""; }; + ADC37268269825AA000DA90B /* OneOf.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneOf.swift; sourceTree = ""; }; + ADC3727326982D88000DA90B /* OneOfTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneOfTests.swift; sourceTree = ""; }; + ADC372B3269877AF000DA90B /* Sequence+PactSwift.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sequence+PactSwift.swift"; sourceTree = ""; }; + ADC3AA36247C8C4B0034446E /* InteractionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionTests.swift; sourceTree = ""; }; + ADC3AA3C247CBB550034446E /* IncludesLikeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncludesLikeTests.swift; sourceTree = ""; }; + ADC6FAE024302BA800026714 /* Documentation */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Documentation; sourceTree = ""; }; + ADD0315E2512193500C6099B /* ExampleGeneratorExpressible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleGeneratorExpressible.swift; sourceTree = ""; }; + ADD03163251219B700C6099B /* ExampleGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleGenerator.swift; sourceTree = ""; }; + ADD03166251220FD00C6099B /* RandomBool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandomBool.swift; sourceTree = ""; }; + ADD0316B251221A300C6099B /* RandomBooleanTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandomBooleanTests.swift; sourceTree = ""; }; + ADD0316E25122E4B00C6099B /* RandomUUID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandomUUID.swift; sourceTree = ""; }; + ADD0317125122EA400C6099B /* RandomUUIDTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandomUUIDTests.swift; sourceTree = ""; }; + ADD03174251235A800C6099B /* UUID+PactSwift.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UUID+PactSwift.swift"; sourceTree = ""; }; + ADD031772512425800C6099B /* RandomInt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandomInt.swift; sourceTree = ""; }; + ADD0317A2512439500C6099B /* RandomIntTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandomIntTests.swift; sourceTree = ""; }; + ADD3C26F24416891002E73B9 /* RegexLike.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegexLike.swift; sourceTree = ""; }; + ADD3C27124416A3E002E73B9 /* RegexLikeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegexLikeTests.swift; sourceTree = ""; }; + ADD3C27324416CDF002E73B9 /* EqualTo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EqualTo.swift; sourceTree = ""; }; + ADD3C27524416DAE002E73B9 /* IncludesLike.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncludesLike.swift; sourceTree = ""; }; + ADD6993D242C861600C5C2C2 /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = ""; }; + ADDE4700244D0FD600E4F7EE /* ErrorCapture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorCapture.swift; sourceTree = ""; }; + ADDE4703244D101700E4F7EE /* ErrorReportable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorReportable.swift; sourceTree = ""; }; + ADDE4705244D11DE00E4F7EE /* ErrorReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorReporter.swift; sourceTree = ""; }; + ADE2937E2435DB14008AFBC9 /* ProviderState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProviderState.swift; sourceTree = ""; }; + ADE9B3C6250A3C4700274672 /* Matcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Matcher.swift; sourceTree = ""; }; + ADE9B3C9250A435B00274672 /* EqualToTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EqualToTests.swift; sourceTree = ""; }; + ADEDDEF62547B6B700A45AD2 /* PactPathParameter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PactPathParameter.swift; sourceTree = ""; }; + ADEDDEFC2547B6FA00A45AD2 /* String+PactSwift.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+PactSwift.swift"; sourceTree = ""; }; + ADEDDF082547CFC200A45AD2 /* ObjCMatcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjCMatcherTests.swift; sourceTree = ""; }; + ADEDDF112547D95700A45AD2 /* ObjCExampleGeneratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjCExampleGeneratorTests.swift; sourceTree = ""; }; + ADEDDF1A2547DF3200A45AD2 /* ToolboxTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolboxTests.swift; sourceTree = ""; }; + ADEDDF242547E81300A45AD2 /* EncodingError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncodingError.swift; sourceTree = ""; }; + ADEFD133253EEF230081A1B1 /* Toolbox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toolbox.swift; sourceTree = ""; }; + ADF17CB926B5019B008E7ECD /* PFMockService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PFMockService.swift; sourceTree = ""; }; + ADF17CBC26B50796008E7ECD /* TransferProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferProtocol.swift; sourceTree = ""; }; + ADF17CBF26B50B66008E7ECD /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; + ADF2E582267462090029507D /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; + ADF959C926C8F6EB00C35536 /* FromProviderStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FromProviderStateTests.swift; sourceTree = ""; }; + ADF959CC26C8F98800C35536 /* ProviderStateGeneratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProviderStateGeneratorTests.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + AD8817FB242C715A00BF510D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ADF994CE2A7DE1720011D974 /* PactSwiftMockServer in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AD881804242C715B00BF510D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + AD881808242C715B00BF510D /* PactSwift.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AD8FC7B42463A06F00361854 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + AD92E18F2A7DE6E7005C70E5 /* PactSwiftMockServer in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AD8FC7BC2463A06F00361854 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + AD8FC7C02463A06F00361854 /* PactSwift.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + AD01C9392432A31F00B75C9D /* Matchers */ = { + isa = PBXGroup; + children = ( + AD7891C124415E3E0014BCC8 /* DecimalLike.swift */, + AD07404227F93BD5000C498C /* EachKeyLike.swift */, + AD8DF43E244055980062CB1A /* EachLike.swift */, + ADD3C27324416CDF002E73B9 /* EqualTo.swift */, + AD9D7D8F26C8B3DA00FE4137 /* FromProviderState.swift */, + ADD3C27524416DAE002E73B9 /* IncludesLike.swift */, + AD7891BD24415C100014BCC8 /* IntegerLike.swift */, + ADE9B3C6250A3C4700274672 /* Matcher.swift */, + ADA40176253028A100265DF3 /* MatchNull.swift */, + ADC37268269825AA000DA90B /* OneOf.swift */, + ADD3C26F24416891002E73B9 /* RegexLike.swift */, + AD8DF438243EEBDB0062CB1A /* SomethingLike.swift */, + ); + path = Matchers; + sourceTree = ""; + }; + AD641A362434354C00785CE1 /* Model */ = { + isa = PBXGroup; + children = ( + AD8DF432243C437F0062CB1A /* AnyEncodableTests.swift */, + ADC3AA36247C8C4B0034446E /* InteractionTests.swift */, + AD641A372434355C00785CE1 /* MetadataTests.swift */, + ADBC3E4F26DB2887006908E0 /* PactBrokerTests.swift */, + AD7891B624414D500014BCC8 /* PactBuilderTests.swift */, + ADBC3E5E26DB521E006908E0 /* PactHTTPMethodTests.swift */, + AD641A3D24344FA100785CE1 /* PacticipantTests.swift */, + AD641A3F2434518500785CE1 /* PactTests.swift */, + ADBC3E5826DB386B006908E0 /* ProviderVerifier+OptionsTests.swift */, + ADEDDF1A2547DF3200A45AD2 /* ToolboxTests.swift */, + AD92805726BE3705004FAA7E /* TransferProtocolTests.swift */, + ADBC3E5226DB322C006908E0 /* VersionSelectorTests.swift */, + ADBC3E5526DB36A8006908E0 /* WIPPactsTests.swift */, + ); + path = Model; + sourceTree = ""; + }; + AD646FAB2460F75B00979AFC /* Pact */ = { + isa = PBXGroup; + children = ( + ADEDDF242547E81300A45AD2 /* EncodingError.swift */, + ADD0315E2512193500C6099B /* ExampleGeneratorExpressible.swift */, + AD641A412434588200785CE1 /* Interaction.swift */, + AD8DF43A243EEF390062CB1A /* MatchingRuleExpressible.swift */, + AD641A342434345500785CE1 /* Metadata.swift */, + AD641A3924344D9500785CE1 /* Pact.swift */, + ADB7C1682433280100A16CDE /* PactHTTPMethod.swift */, + AD641A3B24344ED400785CE1 /* Pacticipant.swift */, + AD8DF436243EA4580062CB1A /* PactInteractionElement.swift */, + ADEDDEF62547B6B700A45AD2 /* PactPathParameter.swift */, + ADA4B1D926D31E2C00A5AE88 /* Provider Verification */, + ADE2937E2435DB14008AFBC9 /* ProviderState.swift */, + ADB7C1602433269B00A16CDE /* Request.swift */, + ADB7C16A2433283400A16CDE /* Response.swift */, + ); + name = Pact; + sourceTree = ""; + }; + AD646FC72460F7ED00979AFC /* Services */ = { + isa = PBXGroup; + children = ( + AD103617244693A6002C97CA /* MockServiceTests.swift */, + AD23310B26E9F08000D984A5 /* MockServiceWithDirectoryPathTests.swift */, + AD78FB47264FD21900765BD3 /* PactContractTests.swift */, + AD92805026BE1B60004FAA7E /* PFMockServiceTests.swift */, + ADBC3E5B26DB4846006908E0 /* ProviderVerifierTests.swift */, + 55E8E3512936F3AD003D57A6 /* AsyncMockServiceTests.swift */, + ); + path = Services; + sourceTree = ""; + }; + AD7891B8244150FF0014BCC8 /* Matchers */ = { + isa = PBXGroup; + children = ( + AD7891C324415E9F0014BCC8 /* DecimalLikeTests.swift */, + AD07403F27F93BC1000C498C /* EachKeyLikeTests.swift */, + AD7891BB244156FC0014BCC8 /* EachLikeTests.swift */, + ADE9B3C9250A435B00274672 /* EqualToTests.swift */, + ADF959C926C8F6EB00C35536 /* FromProviderStateTests.swift */, + ADC3AA3C247CBB550034446E /* IncludesLikeTests.swift */, + AD7891BF24415C710014BCC8 /* IntegerLikeTests.swift */, + ADA40181253033C400265DF3 /* MatchNullTests.swift */, + ADEDDF082547CFC200A45AD2 /* ObjCMatcherTests.swift */, + ADC3727326982D88000DA90B /* OneOfTests.swift */, + ADD3C27124416A3E002E73B9 /* RegexLikeTests.swift */, + AD7891B92441512E0014BCC8 /* SomethingLikeTests.swift */, + ); + path = Matchers; + sourceTree = ""; + }; + AD8817F4242C715A00BF510D = { + isa = PBXGroup; + children = ( + ADD6993D242C861600C5C2C2 /* .swiftlint.yml */, + ADCB2562242C83E20048BD18 /* Configurations */, + ADC6FAE024302BA800026714 /* Documentation */, + ADF2E582267462090029507D /* Package.swift */, + AD8817FF242C715A00BF510D /* Products */, + AD8A32D6242CCF5600283080 /* Scripts */, + ADCB2560242C83670048BD18 /* Sources */, + ADCB2561242C83710048BD18 /* Tests */, + ); + sourceTree = ""; + }; + AD8817FF242C715A00BF510D /* Products */ = { + isa = PBXGroup; + children = ( + AD8817FE242C715A00BF510D /* PactSwift.framework */, + AD881807242C715B00BF510D /* PactSwiftTests_iOS.xctest */, + AD8FC7B72463A06F00361854 /* PactSwift.framework */, + AD8FC7BF2463A06F00361854 /* PactSwiftTests_macOS.xctest */, + ); + name = Products; + sourceTree = ""; + }; + AD92805326BE3343004FAA7E /* Extensions */ = { + isa = PBXGroup; + children = ( + AD92805426BE3361004FAA7E /* String+PactSwiftTests.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + AD957F3028A23A0800860AD1 /* Toolbox */ = { + isa = PBXGroup; + children = ( + AD957F3128A23A2300860AD1 /* Logger.swift */, + AD08FA3D28A23DD40059884F /* PactFileManager.swift */, + ); + path = Toolbox; + sourceTree = ""; + }; + ADA4B1D926D31E2C00A5AE88 /* Provider Verification */ = { + isa = PBXGroup; + children = ( + ADA4B1E026D31EB100A5AE88 /* PactBroker.swift */, + AD5E9F0126D375BE0002580D /* ProviderVerifier+Error.swift */, + ADA4B1DA26D31E5100A5AE88 /* ProviderVerifier+Options.swift */, + ADA4B1DD26D31E9100A5AE88 /* ProviderVerifier+Provider.swift */, + ADA4B1E326D31EDA00A5AE88 /* VersionSelector.swift */, + AD5E9F0426D4684B0002580D /* WIPPacts.swift */, + ); + name = "Provider Verification"; + sourceTree = ""; + }; + ADB7C11B2432D41100A16CDE /* Extensions */ = { + isa = PBXGroup; + children = ( + AD641A322434331300785CE1 /* Bundle+PactSwift.swift */, + ADA17E40251377A4004F1A82 /* Date+PactSwift.swift */, + ADB7C11C2432D41E00A16CDE /* Dictionary+PactSwift.swift */, + ADC372B3269877AF000DA90B /* Sequence+PactSwift.swift */, + ADEDDEFC2547B6FA00A45AD2 /* String+PactSwift.swift */, + ADD03174251235A800C6099B /* UUID+PactSwift.swift */, + A782763D29372E35003809F9 /* MockServer+Async.swift */, + A7AF63CF29387BE900A7FD42 /* Task+Timeout.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + ADB7C167243327E300A16CDE /* Model */ = { + isa = PBXGroup; + children = ( + AD8DF42B243BF9430062CB1A /* AnyEncodable.swift */, + ADEDDF232547E7B500A45AD2 /* Errors */, + AD646FAB2460F75B00979AFC /* Pact */, + AD879B9C258242AC00F85B0B /* PactSwiftVersion.swift */, + ADEFD133253EEF230081A1B1 /* Toolbox.swift */, + ADF17CBC26B50796008E7ECD /* TransferProtocol.swift */, + ADF17CBF26B50B66008E7ECD /* Constants.swift */, + ); + path = Model; + sourceTree = ""; + }; + ADCB2560242C83670048BD18 /* Sources */ = { + isa = PBXGroup; + children = ( + ADD031622512199500C6099B /* ExampleGenerators */, + ADB7C11B2432D41100A16CDE /* Extensions */, + ADCB2563242C844B0048BD18 /* Headers */, + AD01C9392432A31F00B75C9D /* Matchers */, + AD10361324468AB3002C97CA /* MockService.swift */, + AD854D8A2A7E75E20005C502 /* MockService+Concurrency.swift */, + AD854D872A7E75080005C502 /* MockService+Extension.swift */, + ADB7C167243327E300A16CDE /* Model */, + AD8DF434243C53750062CB1A /* PactBuilder.swift */, + ADF17CB926B5019B008E7ECD /* PFMockService.swift */, + AD48EC5726CF90B40017E071 /* ProviderVerifier.swift */, + AD957F3028A23A0800860AD1 /* Toolbox */, + ); + path = Sources; + sourceTree = ""; + usesTabs = 1; + }; + ADCB2561242C83710048BD18 /* Tests */ = { + isa = PBXGroup; + children = ( + ADD031692512218500C6099B /* ExampleGenerators */, + AD92805326BE3343004FAA7E /* Extensions */, + AD7891B8244150FF0014BCC8 /* Matchers */, + AD641A362434354C00785CE1 /* Model */, + AD646FC72460F7ED00979AFC /* Services */, + ADDE46FD244D0FBA00E4F7EE /* TestHelpers */, + ); + path = Tests; + sourceTree = ""; + usesTabs = 1; + }; + ADCB2562242C83E20048BD18 /* Configurations */ = { + isa = PBXGroup; + children = ( + AD88180E242C715B00BF510D /* Info-iOS-Tests.plist */, + AD881802242C715A00BF510D /* Info-iOS.plist */, + AD8FC7C62463A06F00361854 /* Info-macOS-Tests.plist */, + AD8FC7BA2463A06F00361854 /* Info-macOS.plist */, + AD2CB262242CC0A000D991F0 /* Project-Debug.xcconfig */, + AD2CB263242CC0B900D991F0 /* Project-Release.xcconfig */, + AD4FC5DD242CC2B20039342D /* Project-Shared.xcconfig */, + AD2CB264242CC10600D991F0 /* Target-iOS-Debug.xcconfig */, + AD2CB265242CC10E00D991F0 /* Target-iOS-Release.xcconfig */, + AD4FC5DE242CC2C30039342D /* Target-iOS-Shared.xcconfig */, + AD2CB266242CC11700D991F0 /* Target-iOS-Tests-Debug.xcconfig */, + AD2CB267242CC12300D991F0 /* Target-iOS-Tests-Release.xcconfig */, + AD4FC5DF242CC2CB0039342D /* Target-iOS-Tests-Shared.xcconfig */, + AD8FC7D32463ADA700361854 /* Target-macOS-Debug.xcconfig */, + AD8FC7D42463ADB000361854 /* Target-macOS-Release.xcconfig */, + AD8FC7D22463AD9800361854 /* Target-macOS-Shared.xcconfig */, + AD8FC7D62463ADCF00361854 /* Target-macOS-Tests-Debug.xcconfig */, + AD8FC7D72463ADDB00361854 /* Target-macOS-Tests-Release.xcconfig */, + AD8FC7D52463ADC300361854 /* Target-macOS-Tests-Shared.xcconfig */, + ); + path = Configurations; + sourceTree = ""; + }; + ADCB2563242C844B0048BD18 /* Headers */ = { + isa = PBXGroup; + children = ( + AD881801242C715A00BF510D /* PactSwift.h */, + ); + path = Headers; + sourceTree = ""; + }; + ADD031622512199500C6099B /* ExampleGenerators */ = { + isa = PBXGroup; + children = ( + AD72E53F27B89CB900C7453F /* DateTime.swift */, + AD54435D27D32DCA00D4C464 /* DateTimeExpression.swift */, + ADD03163251219B700C6099B /* ExampleGenerator.swift */, + AD9D7D8C26C8AD3400FE4137 /* ProviderStateGenerator.swift */, + ADD03166251220FD00C6099B /* RandomBool.swift */, + AD8546F025135F4200211E28 /* RandomDate.swift */, + ADA17E3A25137716004F1A82 /* RandomDateTime.swift */, + ADA17E5125138908004F1A82 /* RandomDecimal.swift */, + AD4B97092513A04800C37560 /* RandomHexadecimal.swift */, + ADD031772512425800C6099B /* RandomInt.swift */, + AD4B970F2513A3DB00C37560 /* RandomString.swift */, + ADA17E4B251383EB004F1A82 /* RandomTime.swift */, + ADD0316E25122E4B00C6099B /* RandomUUID.swift */, + ); + path = ExampleGenerators; + sourceTree = ""; + }; + ADD031692512218500C6099B /* ExampleGenerators */ = { + isa = PBXGroup; + children = ( + AD54436027D3316600D4C464 /* DateTimeExpressionTests.swift */, + AD72E54227B8A02200C7453F /* DateTimeTests.swift */, + ADEDDF112547D95700A45AD2 /* ObjCExampleGeneratorTests.swift */, + ADF959CC26C8F98800C35536 /* ProviderStateGeneratorTests.swift */, + ADD0316B251221A300C6099B /* RandomBooleanTests.swift */, + AD8546F32513601800211E28 /* RandomDateTests.swift */, + ADA17E3D2513772B004F1A82 /* RandomDateTimeTests.swift */, + AD4B970425138F7600C37560 /* RandomDecimalTests.swift */, + AD4B970C2513A0C300C37560 /* RandomHexadecimalTests.swift */, + ADD0317A2512439500C6099B /* RandomIntTests.swift */, + AD4B97122513A64700C37560 /* RandomStringTests.swift */, + ADA17E4E2513848A004F1A82 /* RandomTimeTests.swift */, + ADD0317125122EA400C6099B /* RandomUUIDTests.swift */, + ); + path = ExampleGenerators; + sourceTree = ""; + }; + ADDE46FD244D0FBA00E4F7EE /* TestHelpers */ = { + isa = PBXGroup; + children = ( + ADA17E482513808B004F1A82 /* DateHelper.swift */, + ADDE4700244D0FD600E4F7EE /* ErrorCapture.swift */, + AD0AF27A272644E800848FB7 /* ExampleGeneratorTestHelpers.swift */, + AD0AF277272634A300848FB7 /* MatcherTestHelpers.swift */, + ); + path = TestHelpers; + sourceTree = ""; + }; + ADEDDF232547E7B500A45AD2 /* Errors */ = { + isa = PBXGroup; + children = ( + ADDE4703244D101700E4F7EE /* ErrorReportable.swift */, + ADDE4705244D11DE00E4F7EE /* ErrorReporter.swift */, + ); + name = Errors; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + AD8817F9242C715A00BF510D /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + AD88180F242C715B00BF510D /* PactSwift.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AD8FC7B22463A06F00361854 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + AD8FC7F02463BBA900361854 /* PactSwift.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + AD8817FD242C715A00BF510D /* PactSwift_iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = AD881812242C715B00BF510D /* Build configuration list for PBXNativeTarget "PactSwift_iOS" */; + buildPhases = ( + AD8817F9242C715A00BF510D /* Headers */, + ADC3C4CC242CCCEB00D3FDCE /* Lint Project */, + ADC3C4CD242CCCF300D3FDCE /* Lint Target */, + ADD69940242C87E900C5C2C2 /* SwiftLint */, + AD8817FA242C715A00BF510D /* Sources */, + AD8817FB242C715A00BF510D /* Frameworks */, + AD8817FC242C715A00BF510D /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = PactSwift_iOS; + packageProductDependencies = ( + ADF994CD2A7DE1720011D974 /* PactSwiftMockServer */, + ); + productName = PACTSwift; + productReference = AD8817FE242C715A00BF510D /* PactSwift.framework */; + productType = "com.apple.product-type.framework"; + }; + AD881806242C715B00BF510D /* PactSwiftTests_iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = AD881815242C715B00BF510D /* Build configuration list for PBXNativeTarget "PactSwiftTests_iOS" */; + buildPhases = ( + ADC3C4CE242CCD0100D3FDCE /* Lint Target */, + AD881803242C715B00BF510D /* Sources */, + AD881804242C715B00BF510D /* Frameworks */, + AD881805242C715B00BF510D /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + AD88180A242C715B00BF510D /* PBXTargetDependency */, + ); + name = PactSwiftTests_iOS; + packageProductDependencies = ( + ); + productName = PACTSwiftTests; + productReference = AD881807242C715B00BF510D /* PactSwiftTests_iOS.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + AD8FC7B62463A06F00361854 /* PactSwift_macOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = AD8FC7CC2463A06F00361854 /* Build configuration list for PBXNativeTarget "PactSwift_macOS" */; + buildPhases = ( + AD8FC7B22463A06F00361854 /* Headers */, + AD8FC7D02463A09900361854 /* Lint Project */, + AD8FC7CF2463A08900361854 /* Lint Target */, + AD8FC7D12463A0A100361854 /* SwiftLint */, + AD8FC7B32463A06F00361854 /* Sources */, + AD8FC7B42463A06F00361854 /* Frameworks */, + AD8FC7B52463A06F00361854 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = PactSwift_macOS; + packageProductDependencies = ( + AD92E18E2A7DE6E7005C70E5 /* PactSwiftMockServer */, + ); + productName = PactSwift_macOS; + productReference = AD8FC7B72463A06F00361854 /* PactSwift.framework */; + productType = "com.apple.product-type.framework"; + }; + AD8FC7BE2463A06F00361854 /* PactSwiftTests_macOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = AD8FC7CD2463A06F00361854 /* Build configuration list for PBXNativeTarget "PactSwiftTests_macOS" */; + buildPhases = ( + AD8FC7BB2463A06F00361854 /* Sources */, + AD8FC7BC2463A06F00361854 /* Frameworks */, + AD8FC7BD2463A06F00361854 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + AD8FC7C22463A06F00361854 /* PBXTargetDependency */, + ); + name = PactSwiftTests_macOS; + packageProductDependencies = ( + ); + productName = PactSwift_macOSTests; + productReference = AD8FC7BF2463A06F00361854 /* PactSwiftTests_macOS.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + AD8817F5242C715A00BF510D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 1240; + LastUpgradeCheck = 1430; + ORGANIZATIONNAME = "PACT Foundation"; + TargetAttributes = { + AD8817FD242C715A00BF510D = { + CreatedOnToolsVersion = 11.4; + LastSwiftMigration = 1130; + }; + AD881806242C715B00BF510D = { + CreatedOnToolsVersion = 11.4; + }; + AD8FC7B62463A06F00361854 = { + CreatedOnToolsVersion = 11.5; + }; + AD8FC7BE2463A06F00361854 = { + CreatedOnToolsVersion = 11.5; + }; + }; + }; + buildConfigurationList = AD8817F8242C715A00BF510D /* Build configuration list for PBXProject "PactSwift" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = AD8817F4242C715A00BF510D; + packageReferences = ( + ADF994CC2A7DE1720011D974 /* XCRemoteSwiftPackageReference "PactSwiftServer" */, + ); + productRefGroup = AD8817FF242C715A00BF510D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + AD8817FD242C715A00BF510D /* PactSwift_iOS */, + AD881806242C715B00BF510D /* PactSwiftTests_iOS */, + AD8FC7B62463A06F00361854 /* PactSwift_macOS */, + AD8FC7BE2463A06F00361854 /* PactSwiftTests_macOS */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + AD8817FC242C715A00BF510D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AD881805242C715B00BF510D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AD8FC7B52463A06F00361854 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AD8FC7BD2463A06F00361854 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + AD8FC7CF2463A08900361854 /* Lint Target */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Lint Target"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PROJECT_DIR}\"/Scripts/BuildPhases/lint-target\n"; + }; + AD8FC7D02463A09900361854 /* Lint Project */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Lint Project"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PROJECT_DIR}\"/Scripts/BuildPhases/lint-project\n"; + }; + AD8FC7D12463A0A100361854 /* SwiftLint */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "$(SRCROOT)/PactSwift.xcfilelist", + ); + inputPaths = ( + ); + name = SwiftLint; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/swiftlint.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Run SwiftLint before even attempting to compile\n\"${PROJECT_DIR}\"/Scripts/build_file_list_and_swiftlint PactSwift ./.swiftlint.yml\n"; + }; + ADC3C4CC242CCCEB00D3FDCE /* Lint Project */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Lint Project"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PROJECT_DIR}\"/Scripts/BuildPhases/lint-project\n"; + }; + ADC3C4CD242CCCF300D3FDCE /* Lint Target */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Lint Target"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PROJECT_DIR}\"/Scripts/BuildPhases/lint-target\n"; + }; + ADC3C4CE242CCD0100D3FDCE /* Lint Target */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Lint Target"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PROJECT_DIR}\"/Scripts/BuildPhases/lint-target\n"; + }; + ADD69940242C87E900C5C2C2 /* SwiftLint */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "$(SRCROOT)/PactSwift.xcfilelist", + ); + inputPaths = ( + ); + name = SwiftLint; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/swiftlint.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Run SwiftLint before even attempting to compile\n\"${PROJECT_DIR}\"/Scripts/build_file_list_and_swiftlint PactSwift ./.swiftlint.yml\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + AD8817FA242C715A00BF510D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AD54435E27D32DCA00D4C464 /* DateTimeExpression.swift in Sources */, + AD854D8B2A7E75E20005C502 /* MockService+Concurrency.swift in Sources */, + AD641A3C24344ED400785CE1 /* Pacticipant.swift in Sources */, + ADA17E41251377A4004F1A82 /* Date+PactSwift.swift in Sources */, + AD879B9D258242AC00F85B0B /* PactSwiftVersion.swift in Sources */, + AD8DF43B243EEF390062CB1A /* MatchingRuleExpressible.swift in Sources */, + AD48EC5826CF90B40017E071 /* ProviderVerifier.swift in Sources */, + ADB7C16B2433283400A16CDE /* Response.swift in Sources */, + ADF17CBA26B5019B008E7ECD /* PFMockService.swift in Sources */, + AD8DF42C243BF9430062CB1A /* AnyEncodable.swift in Sources */, + AD641A352434345500785CE1 /* Metadata.swift in Sources */, + AD5E9F0226D375BE0002580D /* ProviderVerifier+Error.swift in Sources */, + ADEDDEFD2547B6FA00A45AD2 /* String+PactSwift.swift in Sources */, + ADEDDF252547E81300A45AD2 /* EncodingError.swift in Sources */, + ADD3C27024416891002E73B9 /* RegexLike.swift in Sources */, + ADC372B4269877AF000DA90B /* Sequence+PactSwift.swift in Sources */, + AD7891BE24415C100014BCC8 /* IntegerLike.swift in Sources */, + ADD03164251219B700C6099B /* ExampleGenerator.swift in Sources */, + ADA4B1DB26D31E5100A5AE88 /* ProviderVerifier+Options.swift in Sources */, + ADE2937F2435DB14008AFBC9 /* ProviderState.swift in Sources */, + AD641A332434331300785CE1 /* Bundle+PactSwift.swift in Sources */, + ADB7C1612433269B00A16CDE /* Request.swift in Sources */, + AD8DF43F244055980062CB1A /* EachLike.swift in Sources */, + ADA4B1E126D31EB100A5AE88 /* PactBroker.swift in Sources */, + ADDE4706244D11DE00E4F7EE /* ErrorReporter.swift in Sources */, + ADA17E3B25137716004F1A82 /* RandomDateTime.swift in Sources */, + ADE9B3C7250A3C4700274672 /* Matcher.swift in Sources */, + ADD031782512425800C6099B /* RandomInt.swift in Sources */, + A782763E29372E35003809F9 /* MockServer+Async.swift in Sources */, + AD72E54027B89CB900C7453F /* DateTime.swift in Sources */, + AD9D7D8D26C8AD3400FE4137 /* ProviderStateGenerator.swift in Sources */, + ADD3C27624416DAE002E73B9 /* IncludesLike.swift in Sources */, + ADD0316F25122E4B00C6099B /* RandomUUID.swift in Sources */, + AD957F3228A23A2300860AD1 /* Logger.swift in Sources */, + AD4B970A2513A04800C37560 /* RandomHexadecimal.swift in Sources */, + AD7891C224415E3E0014BCC8 /* DecimalLike.swift in Sources */, + AD641A422434588200785CE1 /* Interaction.swift in Sources */, + ADEFD134253EEF230081A1B1 /* Toolbox.swift in Sources */, + AD641A3A24344D9500785CE1 /* Pact.swift in Sources */, + ADA40177253028A100265DF3 /* MatchNull.swift in Sources */, + A7AF63D029387BE900A7FD42 /* Task+Timeout.swift in Sources */, + AD10361424468AB3002C97CA /* MockService.swift in Sources */, + ADA17E5225138908004F1A82 /* RandomDecimal.swift in Sources */, + ADD03167251220FD00C6099B /* RandomBool.swift in Sources */, + ADD0315F2512193500C6099B /* ExampleGeneratorExpressible.swift in Sources */, + AD4B97102513A3DB00C37560 /* RandomString.swift in Sources */, + AD8DF439243EEBDB0062CB1A /* SomethingLike.swift in Sources */, + AD9D7D9026C8B3DA00FE4137 /* FromProviderState.swift in Sources */, + AD854D882A7E75080005C502 /* MockService+Extension.swift in Sources */, + ADD3C27424416CDF002E73B9 /* EqualTo.swift in Sources */, + ADA4B1E426D31EDA00A5AE88 /* VersionSelector.swift in Sources */, + ADDE4704244D101700E4F7EE /* ErrorReportable.swift in Sources */, + ADB7C1692433280100A16CDE /* PactHTTPMethod.swift in Sources */, + AD07404327F93BD5000C498C /* EachKeyLike.swift in Sources */, + ADB7C11D2432D41F00A16CDE /* Dictionary+PactSwift.swift in Sources */, + ADF17CBD26B50796008E7ECD /* TransferProtocol.swift in Sources */, + ADEDDEF72547B6B700A45AD2 /* PactPathParameter.swift in Sources */, + AD8DF437243EA4580062CB1A /* PactInteractionElement.swift in Sources */, + ADBC60DC2513639700BBE666 /* RandomDate.swift in Sources */, + ADC37269269825AA000DA90B /* OneOf.swift in Sources */, + AD5E9F0526D4684B0002580D /* WIPPacts.swift in Sources */, + AD8DF435243C53750062CB1A /* PactBuilder.swift in Sources */, + ADF17CC026B50B66008E7ECD /* Constants.swift in Sources */, + ADA4B1DE26D31E9100A5AE88 /* ProviderVerifier+Provider.swift in Sources */, + ADD03175251235A800C6099B /* UUID+PactSwift.swift in Sources */, + ADA17E4C251383EB004F1A82 /* RandomTime.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AD881803242C715B00BF510D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AD0AF27B272644E800848FB7 /* ExampleGeneratorTestHelpers.swift in Sources */, + AD103618244693A6002C97CA /* MockServiceTests.swift in Sources */, + AD7891C024415C710014BCC8 /* IntegerLikeTests.swift in Sources */, + AD92805826BE3705004FAA7E /* TransferProtocolTests.swift in Sources */, + ADBC3E5326DB322C006908E0 /* VersionSelectorTests.swift in Sources */, + AD7891B724414D500014BCC8 /* PactBuilderTests.swift in Sources */, + ADC3AA37247C8C4B0034446E /* InteractionTests.swift in Sources */, + ADBC3E5C26DB4846006908E0 /* ProviderVerifierTests.swift in Sources */, + ADD3C27224416A3E002E73B9 /* RegexLikeTests.swift in Sources */, + ADBC3E5626DB36A8006908E0 /* WIPPactsTests.swift in Sources */, + ADD0317B2512439500C6099B /* RandomIntTests.swift in Sources */, + ADEDDF092547CFC200A45AD2 /* ObjCMatcherTests.swift in Sources */, + 55E8E3522936F3AD003D57A6 /* AsyncMockServiceTests.swift in Sources */, + ADEDDF1B2547DF3200A45AD2 /* ToolboxTests.swift in Sources */, + AD7891BA2441512E0014BCC8 /* SomethingLikeTests.swift in Sources */, + AD54436127D3316600D4C464 /* DateTimeExpressionTests.swift in Sources */, + ADA17E4F2513848A004F1A82 /* RandomTimeTests.swift in Sources */, + AD08FA3E28A23DD40059884F /* PactFileManager.swift in Sources */, + AD4B970825138FCF00C37560 /* RandomDecimalTests.swift in Sources */, + ADC3AA3D247CBB550034446E /* IncludesLikeTests.swift in Sources */, + ADEDDF122547D95700A45AD2 /* ObjCExampleGeneratorTests.swift in Sources */, + AD07404027F93BC1000C498C /* EachKeyLikeTests.swift in Sources */, + AD72E54527B8A03000C7453F /* DateTimeTests.swift in Sources */, + AD7891BC244156FC0014BCC8 /* EachLikeTests.swift in Sources */, + ADA17E492513808B004F1A82 /* DateHelper.swift in Sources */, + AD23310C26E9F08000D984A5 /* MockServiceWithDirectoryPathTests.swift in Sources */, + AD8546F42513601800211E28 /* RandomDateTests.swift in Sources */, + AD641A3E24344FA100785CE1 /* PacticipantTests.swift in Sources */, + ADBC3E5026DB2887006908E0 /* PactBrokerTests.swift in Sources */, + AD78FB48264FD21900765BD3 /* PactContractTests.swift in Sources */, + ADDE4702244D0FDE00E4F7EE /* ErrorCapture.swift in Sources */, + ADBC3E5926DB386B006908E0 /* ProviderVerifier+OptionsTests.swift in Sources */, + AD4B970D2513A0C300C37560 /* RandomHexadecimalTests.swift in Sources */, + ADD0316C251221A300C6099B /* RandomBooleanTests.swift in Sources */, + AD641A402434518500785CE1 /* PactTests.swift in Sources */, + ADBC3E5F26DB521E006908E0 /* PactHTTPMethodTests.swift in Sources */, + ADA40182253033C400265DF3 /* MatchNullTests.swift in Sources */, + AD92805126BE1B60004FAA7E /* PFMockServiceTests.swift in Sources */, + AD92805526BE3361004FAA7E /* String+PactSwiftTests.swift in Sources */, + AD641A382434355C00785CE1 /* MetadataTests.swift in Sources */, + ADD0317225122EA400C6099B /* RandomUUIDTests.swift in Sources */, + ADA17E3E2513772B004F1A82 /* RandomDateTimeTests.swift in Sources */, + ADC3728226982E1E000DA90B /* OneOfTests.swift in Sources */, + ADF959CD26C8F98800C35536 /* ProviderStateGeneratorTests.swift in Sources */, + AD0AF278272634A300848FB7 /* MatcherTestHelpers.swift in Sources */, + AD7891C424415E9F0014BCC8 /* DecimalLikeTests.swift in Sources */, + AD4B97132513A64700C37560 /* RandomStringTests.swift in Sources */, + AD8DF433243C437F0062CB1A /* AnyEncodableTests.swift in Sources */, + ADF959CA26C8F6EB00C35536 /* FromProviderStateTests.swift in Sources */, + ADE9B3CA250A435B00274672 /* EqualToTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AD8FC7B32463A06F00361854 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AD54435F27D32DCA00D4C464 /* DateTimeExpression.swift in Sources */, + AD854D8C2A7E75E20005C502 /* MockService+Concurrency.swift in Sources */, + AD8FC7E42463BB9F00361854 /* Interaction.swift in Sources */, + ADA17E42251377A4004F1A82 /* Date+PactSwift.swift in Sources */, + AD879B9E258242AC00F85B0B /* PactSwiftVersion.swift in Sources */, + AD8FC7E32463BB9F00361854 /* ErrorReporter.swift in Sources */, + AD48EC5926CF90B40017E071 /* ProviderVerifier.swift in Sources */, + AD8FC7E92463BB9F00361854 /* Pacticipant.swift in Sources */, + ADF17CBB26B5019B008E7ECD /* PFMockService.swift in Sources */, + AD8FC7E12463BB9F00361854 /* AnyEncodable.swift in Sources */, + AD8FC7F52463BBB800361854 /* IncludesLike.swift in Sources */, + AD5E9F0326D375BE0002580D /* ProviderVerifier+Error.swift in Sources */, + ADEDDEFE2547B6FA00A45AD2 /* String+PactSwift.swift in Sources */, + ADEDDF262547E81300A45AD2 /* EncodingError.swift in Sources */, + AD8FC7E22463BB9F00361854 /* ErrorReportable.swift in Sources */, + ADC372B5269877AF000DA90B /* Sequence+PactSwift.swift in Sources */, + AD8FC7FA2463BBB800361854 /* MockService.swift in Sources */, + AD8FC7F62463BBB800361854 /* IntegerLike.swift in Sources */, + ADA4B1DC26D31E5100A5AE88 /* ProviderVerifier+Options.swift in Sources */, + ADD03165251219B700C6099B /* ExampleGenerator.swift in Sources */, + AD8FC7E62463BB9F00361854 /* Pact.swift in Sources */, + AD8FC7F42463BBB800361854 /* EqualTo.swift in Sources */, + AD8FC7EF2463BBA400361854 /* Dictionary+PactSwift.swift in Sources */, + ADA4B1E226D31EB100A5AE88 /* PactBroker.swift in Sources */, + AD8FC7EA2463BB9F00361854 /* PactInteractionElement.swift in Sources */, + ADA17E3C25137716004F1A82 /* RandomDateTime.swift in Sources */, + ADE9B3C8250A3C4700274672 /* Matcher.swift in Sources */, + ADD031792512425800C6099B /* RandomInt.swift in Sources */, + A782763F29372E35003809F9 /* MockServer+Async.swift in Sources */, + AD72E54127B89CB900C7453F /* DateTime.swift in Sources */, + AD9D7D8E26C8AD3400FE4137 /* ProviderStateGenerator.swift in Sources */, + AD8FC7E82463BB9F00361854 /* PactHTTPMethod.swift in Sources */, + AD8FC7F72463BBB800361854 /* MatchingRuleExpressible.swift in Sources */, + AD957F3328A23A2300860AD1 /* Logger.swift in Sources */, + ADD0317025122E4B00C6099B /* RandomUUID.swift in Sources */, + AD4B970B2513A04800C37560 /* RandomHexadecimal.swift in Sources */, + AD8FC7E52463BB9F00361854 /* Metadata.swift in Sources */, + ADEFD135253EEF230081A1B1 /* Toolbox.swift in Sources */, + ADA40178253028A100265DF3 /* MatchNull.swift in Sources */, + AD8FC7F82463BBB800361854 /* RegexLike.swift in Sources */, + A7AF63D129387BE900A7FD42 /* Task+Timeout.swift in Sources */, + AD8FC7F32463BBB800361854 /* EachLike.swift in Sources */, + ADA17E5325138908004F1A82 /* RandomDecimal.swift in Sources */, + ADD03168251220FD00C6099B /* RandomBool.swift in Sources */, + ADD031602512193500C6099B /* ExampleGeneratorExpressible.swift in Sources */, + AD4B97112513A3DB00C37560 /* RandomString.swift in Sources */, + AD8FC7ED2463BB9F00361854 /* Response.swift in Sources */, + AD9D7D9126C8B3DA00FE4137 /* FromProviderState.swift in Sources */, + AD854D892A7E75080005C502 /* MockService+Extension.swift in Sources */, + AD8FC7F22463BBB800361854 /* DecimalLike.swift in Sources */, + ADA4B1E526D31EDA00A5AE88 /* VersionSelector.swift in Sources */, + AD8FC7EB2463BB9F00361854 /* ProviderState.swift in Sources */, + AD8FC7EC2463BB9F00361854 /* Request.swift in Sources */, + AD07404427F93BD5000C498C /* EachKeyLike.swift in Sources */, + AD8FC7EE2463BBA400361854 /* Bundle+PactSwift.swift in Sources */, + ADF17CBE26B50796008E7ECD /* TransferProtocol.swift in Sources */, + ADEDDEF82547B6B700A45AD2 /* PactPathParameter.swift in Sources */, + AD8FC7E72463BB9F00361854 /* PactBuilder.swift in Sources */, + AD8FC7F92463BBB800361854 /* SomethingLike.swift in Sources */, + ADC3726A269825AA000DA90B /* OneOf.swift in Sources */, + AD5E9F0626D4684B0002580D /* WIPPacts.swift in Sources */, + ADBC60DD2513639800BBE666 /* RandomDate.swift in Sources */, + ADF17CC126B50B66008E7ECD /* Constants.swift in Sources */, + ADA4B1DF26D31E9100A5AE88 /* ProviderVerifier+Provider.swift in Sources */, + ADD03176251235A800C6099B /* UUID+PactSwift.swift in Sources */, + ADA17E4D251383EB004F1A82 /* RandomTime.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AD8FC7BB2463A06F00361854 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AD0AF27C272644E800848FB7 /* ExampleGeneratorTestHelpers.swift in Sources */, + AD8FC8032463BBD000361854 /* DecimalLikeTests.swift in Sources */, + AD8FC7FE2463BBD000361854 /* AnyEncodableTests.swift in Sources */, + AD92805926BE3705004FAA7E /* TransferProtocolTests.swift in Sources */, + ADBC3E5426DB322C006908E0 /* VersionSelectorTests.swift in Sources */, + AD8FC8062463BBD000361854 /* RegexLikeTests.swift in Sources */, + ADC3AA38247C8C4B0034446E /* InteractionTests.swift in Sources */, + ADBC3E5D26DB4846006908E0 /* ProviderVerifierTests.swift in Sources */, + AD8FC8052463BBD000361854 /* IntegerLikeTests.swift in Sources */, + ADBC3E5726DB36A8006908E0 /* WIPPactsTests.swift in Sources */, + ADD0317C2512439500C6099B /* RandomIntTests.swift in Sources */, + ADEDDF0A2547CFC200A45AD2 /* ObjCMatcherTests.swift in Sources */, + 55E8E3532936F3AD003D57A6 /* AsyncMockServiceTests.swift in Sources */, + ADEDDF1C2547DF3200A45AD2 /* ToolboxTests.swift in Sources */, + AD8FC80A2463BBD100361854 /* ErrorCapture.swift in Sources */, + AD54436227D3316600D4C464 /* DateTimeExpressionTests.swift in Sources */, + ADA17E502513848A004F1A82 /* RandomTimeTests.swift in Sources */, + AD08FA3F28A23DD40059884F /* PactFileManager.swift in Sources */, + AD4B970725138FCE00C37560 /* RandomDecimalTests.swift in Sources */, + ADC3AA3E247CBB550034446E /* IncludesLikeTests.swift in Sources */, + ADEDDF132547D95700A45AD2 /* ObjCExampleGeneratorTests.swift in Sources */, + AD07404127F93BC1000C498C /* EachKeyLikeTests.swift in Sources */, + AD72E54627B8A03100C7453F /* DateTimeTests.swift in Sources */, + AD8FC7FF2463BBD000361854 /* MetadataTests.swift in Sources */, + ADA17E4A2513808B004F1A82 /* DateHelper.swift in Sources */, + AD23310D26E9F08000D984A5 /* MockServiceWithDirectoryPathTests.swift in Sources */, + AD8546F52513601800211E28 /* RandomDateTests.swift in Sources */, + AD8FC8022463BBD000361854 /* PactTests.swift in Sources */, + ADBC3E5126DB2887006908E0 /* PactBrokerTests.swift in Sources */, + AD78FB49264FD21900765BD3 /* PactContractTests.swift in Sources */, + AD8FC8082463BBD100361854 /* MockServiceTests.swift in Sources */, + ADBC3E5A26DB386B006908E0 /* ProviderVerifier+OptionsTests.swift in Sources */, + AD4B970E2513A0C300C37560 /* RandomHexadecimalTests.swift in Sources */, + ADD0316D251221A300C6099B /* RandomBooleanTests.swift in Sources */, + AD8FC8072463BBD000361854 /* SomethingLikeTests.swift in Sources */, + ADBC3E6026DB521E006908E0 /* PactHTTPMethodTests.swift in Sources */, + ADA40183253033C400265DF3 /* MatchNullTests.swift in Sources */, + AD92805226BE1B60004FAA7E /* PFMockServiceTests.swift in Sources */, + AD92805626BE3361004FAA7E /* String+PactSwiftTests.swift in Sources */, + AD8FC8002463BBD000361854 /* PactBuilderTests.swift in Sources */, + ADD0317325122EA400C6099B /* RandomUUIDTests.swift in Sources */, + ADA17E3F2513772B004F1A82 /* RandomDateTimeTests.swift in Sources */, + ADC3728726982E1F000DA90B /* OneOfTests.swift in Sources */, + ADF959CE26C8F98800C35536 /* ProviderStateGeneratorTests.swift in Sources */, + AD0AF279272634A300848FB7 /* MatcherTestHelpers.swift in Sources */, + AD8FC8012463BBD000361854 /* PacticipantTests.swift in Sources */, + AD4B97142513A64700C37560 /* RandomStringTests.swift in Sources */, + AD8FC8042463BBD000361854 /* EachLikeTests.swift in Sources */, + ADF959CB26C8F6EB00C35536 /* FromProviderStateTests.swift in Sources */, + ADE9B3CB250A435B00274672 /* EqualToTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + AD88180A242C715B00BF510D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = AD8817FD242C715A00BF510D /* PactSwift_iOS */; + targetProxy = AD881809242C715B00BF510D /* PBXContainerItemProxy */; + }; + AD8FC7C22463A06F00361854 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = AD8FC7B62463A06F00361854 /* PactSwift_macOS */; + targetProxy = AD8FC7C12463A06F00361854 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + AD881810242C715B00BF510D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AD2CB262242CC0A000D991F0 /* Project-Debug.xcconfig */; + buildSettings = { + }; + name = Debug; + }; + AD881811242C715B00BF510D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AD2CB263242CC0B900D991F0 /* Project-Release.xcconfig */; + buildSettings = { + }; + name = Release; + }; + AD881813242C715B00BF510D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AD2CB264242CC10600D991F0 /* Target-iOS-Debug.xcconfig */; + buildSettings = { + }; + name = Debug; + }; + AD881814242C715B00BF510D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AD2CB265242CC10E00D991F0 /* Target-iOS-Release.xcconfig */; + buildSettings = { + }; + name = Release; + }; + AD881816242C715B00BF510D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AD2CB266242CC11700D991F0 /* Target-iOS-Tests-Debug.xcconfig */; + buildSettings = { + }; + name = Debug; + }; + AD881817242C715B00BF510D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AD2CB267242CC12300D991F0 /* Target-iOS-Tests-Release.xcconfig */; + buildSettings = { + }; + name = Release; + }; + AD8FC7C82463A06F00361854 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AD8FC7D32463ADA700361854 /* Target-macOS-Debug.xcconfig */; + buildSettings = { + }; + name = Debug; + }; + AD8FC7C92463A06F00361854 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AD8FC7D42463ADB000361854 /* Target-macOS-Release.xcconfig */; + buildSettings = { + }; + name = Release; + }; + AD8FC7CA2463A06F00361854 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AD8FC7D62463ADCF00361854 /* Target-macOS-Tests-Debug.xcconfig */; + buildSettings = { + }; + name = Debug; + }; + AD8FC7CB2463A06F00361854 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AD8FC7D72463ADDB00361854 /* Target-macOS-Tests-Release.xcconfig */; + buildSettings = { + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + AD8817F8242C715A00BF510D /* Build configuration list for PBXProject "PactSwift" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AD881810242C715B00BF510D /* Debug */, + AD881811242C715B00BF510D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + AD881812242C715B00BF510D /* Build configuration list for PBXNativeTarget "PactSwift_iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AD881813242C715B00BF510D /* Debug */, + AD881814242C715B00BF510D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + AD881815242C715B00BF510D /* Build configuration list for PBXNativeTarget "PactSwiftTests_iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AD881816242C715B00BF510D /* Debug */, + AD881817242C715B00BF510D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + AD8FC7CC2463A06F00361854 /* Build configuration list for PBXNativeTarget "PactSwift_macOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AD8FC7C82463A06F00361854 /* Debug */, + AD8FC7C92463A06F00361854 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + AD8FC7CD2463A06F00361854 /* Build configuration list for PBXNativeTarget "PactSwiftTests_macOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AD8FC7CA2463A06F00361854 /* Debug */, + AD8FC7CB2463A06F00361854 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + ADF994CC2A7DE1720011D974 /* XCRemoteSwiftPackageReference "PactSwiftServer" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/surpher/PactSwiftServer.git"; + requirement = { + kind = exactVersion; + version = 0.4.7; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + AD92E18E2A7DE6E7005C70E5 /* PactSwiftMockServer */ = { + isa = XCSwiftPackageProductDependency; + package = ADF994CC2A7DE1720011D974 /* XCRemoteSwiftPackageReference "PactSwiftServer" */; + productName = PactSwiftMockServer; + }; + ADF994CD2A7DE1720011D974 /* PactSwiftMockServer */ = { + isa = XCSwiftPackageProductDependency; + package = ADF994CC2A7DE1720011D974 /* XCRemoteSwiftPackageReference "PactSwiftServer" */; + productName = PactSwiftMockServer; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = AD8817F5242C715A00BF510D /* Project object */; +} diff --git a/PactSwift.xcodeproj/xcshareddata/IDETemplateMacros.plist b/PactSwift.xcodeproj/xcshareddata/IDETemplateMacros.plist new file mode 100644 index 00000000..8745167e --- /dev/null +++ b/PactSwift.xcodeproj/xcshareddata/IDETemplateMacros.plist @@ -0,0 +1,23 @@ + + + + + FILEHEADER + +// Created by ___FULLUSERNAME___ on ___DATE___. +// Copyright © ___YEAR___ ___FULLUSERNAME___. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + + diff --git a/PactSwift.xcodeproj/xcshareddata/xcschemes/PactSwift-iOS.xcscheme b/PactSwift.xcodeproj/xcshareddata/xcschemes/PactSwift-iOS.xcscheme new file mode 100644 index 00000000..4083d3f1 --- /dev/null +++ b/PactSwift.xcodeproj/xcshareddata/xcschemes/PactSwift-iOS.xcscheme @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PactSwift.xcodeproj/xcshareddata/xcschemes/PactSwift-macOS.xcscheme b/PactSwift.xcodeproj/xcshareddata/xcschemes/PactSwift-macOS.xcscheme new file mode 100644 index 00000000..733d1126 --- /dev/null +++ b/PactSwift.xcodeproj/xcshareddata/xcschemes/PactSwift-macOS.xcscheme @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md new file mode 100644 index 00000000..c85668f6 --- /dev/null +++ b/README.md @@ -0,0 +1,401 @@ +# PactSwift + +[![Build](https://github.com/surpher/PactSwift/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/surpher/PactSwift/actions/workflows/build.yml) +[![codecov](https://codecov.io/gh/surpher/PactSwift/branch/main/graph/badge.svg)][codecov-io] +[![MIT License](https://img.shields.io/badge/license-MIT-green.svg?style=flat)][license] +[![PRs Welcome!](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)][contributing] +[![slack](http://slack.pact.io/badge.svg)][pact-slack] +[![Twitter](https://img.shields.io/badge/twitter-@pact__up-blue.svg?style=flat)][pact-twitter] + +

+ PactSwift logo +

+ +This framework provides a Swift DSL for generating and verifying [Pact][pact-docs] contracts. It provides the mechanism for [Consumer-Driven Contract Testing](https://dius.com.au/2016/02/03/pact-101-getting-started-with-pact-and-consumer-driven-contract-testing/) between dependent systems where the integration is based on HTTP. `PactSwift` allows you to test the communication boundaries between your app and services it integrates with. + +`PactSwift` implements [Pact Specification v3][pact-specification-v3] and runs the mock service "in-process". No need to set up any external mock services, stubs or extra tools 🎉. It supports contract creation along with client verification. It also supports provider verification and interaction with a Pact broker. + +## Installation + +Note: see [Upgrading][upgrading] for notes on upgrading and breaking changes. + +### Swift Package Manager + +#### Xcode + +1. Enter `https://github.com/surpher/PactSwift` in [Choose Package Repository](./Documentation/images/08_xcode_spm_search.png) search bar +2. Optionally set a minimum version when [Choosing Package Options](./Documentation/images/09_xcode_spm_options.png) +3. Add `PactSwift` to your [test](./Documentation/images/10_xcode_spm_add_package.png) target. Do not embed it in your application target. + +#### Package.swift + +```sh +dependencies: [ + .package(url: "https://github.com/surpher/PactSwift.git", .upToNextMinor(from: "0.11.0")) +] +``` + +#### Linux + +
Linux Installation Instructions + +When using `PactSwift` on a Linux platform you will need to compile your own `libpact_ffi.so` library for your Linux distribution from [pact-reference/rust/pact_ffi][pact-reference-rust] or fetch a `Pact FFI Library x.y.z` from [pact-reference releases](https://github.com/pact-foundation/pact-reference/releases). + +It is important that the version of `libpact_ffi.so` you build or fetch is compatible with the header files provided by `PactMockServer`. See [release notes](https://github.com/surpher/PactMockServer/releases) for details. + +See [`/Scripts/build_libpact_ffi`](https://github.com/surpher/PactSwiftMockServer/blob/main/Support/build_rust_dependencies) for some inspiration building libraries from Rust code. You can also go into [pact-swift-examples](https://github.com/surpher/pact-swift-examples) and look into the Linux example projects. There is one for consumer tests and one for provider verification. They contain the GitHub Workflows where building a pact_ffi `.so` binary and running Pact tests is automated with scripts. + +When testing your project you can either set `LD_LIBRARY_PATH` pointing to the folder containing your `libpact_ffi.so`: + +```sh +export LD_LIBRARY_PATH="/absolute/path/to/your/rust/target/release/:$LD_LIBRARY_PATH" +swift build +swift test -Xlinker -L/absolute/path/to/your/rust/target/release/ +``` + +or you can move your `libpact_ffi.so` into `/usr/local/lib`: + +```sh +mv /path/to/target/release/libpact_ffi.so /usr/local/lib/ +swift build +swift test -Xlinker -L/usr/local/lib/ +``` + +
+ +**NOTE:** + +- `PactSwift` is intended to be used in your [test target](./Documentation/images/11_xcode_carthage_xcframework.png). +- If running on `x86_64` (Intel machine) see [Scripts/carthage][carthage_script] ([#3019-1][carthage-issue-3019-1], [#3019-2][carthage-issue-3019-2], [#3201][carthage-issue-3201]) + +## Writing Pact tests + +- Instantiate a `MockService` object by defining [_pacticipants_][pacticipant], +- Define the state of the provider for an interaction (one Pact test), +- Define the expected `request` for the interaction, +- Define the expected `response` for the interaction, +- Run the test by making the API request using your API client and assert what you need asserted, +- When running on CI share the generated Pact contract file with your provider (eg: upload to a [Pact Broker][pact-broker]), +- When automating deployments in a CI step run [`can-i-deploy`][can-i-deploy] and if computer says OK, deploy with confidence! + +### Example Consumer Tests + +```swift +import XCTest +import PactSwift + +@testable import ExampleProject + +class PassingTestsExample: XCTestCase { + + static var mockService = MockService(consumer: "Example-iOS-app", provider: "some-api-service") + + // MARK: - Tests + + func testGetUsers() { + // #1 - Declare the interaction's expectations + PassingTestsExample.mockService + + // #2 - Define the interaction description and provider state for this specific interaction + .uponReceiving("A request for a list of users") + .given(ProviderState(description: "users exist", params: ["first_name": "John", "last_name": "Tester"]) + + // #3 - Declare what our client's request will look like + .withRequest( + method: .GET, + path: "/api/users", + ) + + // #4 - Declare what the provider should respond with + .willRespondWith( + status: 200, + headers: nil, // `nil` means we don't care what the headers returned from the API are. + body: [ + "page": Matcher.SomethingLike(1), // We expect an Int, 1 will be used in the unit test + "per_page": Matcher.SomethingLike(20), + "total": ExampleGenerator.RandomInt(min: 20, max: 500), // Expecting an Int between 20 and 500 + "total_pages": Matcher.SomethingLike(3), + "data": Matcher.EachLike( // We expect an array of objects + [ + "id": ExampleGenerator.RandomUUID(), // We can also use random example generators + "first_name": Matcher.SomethingLike("John"), + "last_name": Matcher.SomethingLike("Tester"), + "renumeration": Matcher.DecimalLike(125_000.00) + ] + ) + ] + ) + + // #5 - Fire up our API client + let apiClient = RestManager() + + // Run a Pact test and assert **our** API client makes the request exactly as we promised above + PassingTestsExample.mockService.run(timeout: 1) { [unowned self] mockServiceURL, done in + + // #6 - _Redirect_ your API calls to the address MockService runs on - replace base URL, but path should be the same + apiClient.baseUrl = mockServiceURL + + // #7 - Make the API request. + apiClient.getUsers() { users in + + // #8 - Test that **our** API client handles the response as expected. (eg: `getUsers() -> [User]`) + XCTAssertEqual(users.count, 20) + XCTAssertEqual(users.first?.firstName, "John") + XCTAssertEqual(users.first?.lastName, "Tester") + + // #9 - Always run the callback. Run it in your successful and failing assertions! + // Otherwise your test will time out. + done() + } + } + } + + // Another Pact test example... + func testCreateUser() { + PassingTestsExample.mockService + .uponReceiving("A request to create a user") + .given(ProviderState(description: "user does not exist", params: ["first_name": "John", "last_name": "Appleseed"]) + .withRequest( + method: .POST, + path: Matcher.RegexLike("/api/group/whoopeedeedoodah/users", term: #"^/\w+/group/([a-z])+/users$"#), + body: [ + // You can use matchers and generators here too, but are an anti-pattern. + // You should be able to have full control of your requests. + "first_name": "John", + "last_name": "Appleseed" + ] + ) + .willRespondWith( + status: 201, + body: [ + "identifier": Matcher.FromProviderState(parameter: "userId", value: .string("123e4567-e89b-12d3-a456-426614174000")), + "first_name": "John", + "last_name": "Appleseed" + ] + ) + + let apiClient = RestManager() + + PassingTestsExample.mockService.run { mockServiceURL, done in + // trigger your network request and assert the expectations + done() + } + } + // etc. +} +``` + +`MockService` holds all the interactions between your consumer and a provider. For each test method, a new instance of `XCTestCase` class is allocated and its instance setup is executed. +That means each test has it's own instance of `var mockService = MockService()`. Hence the reason we're using a `static var mockService` here to keep a reference to one instance of `MockService` for all the Pact tests. Alternatively you could wrap your `mockService` into a singleton. +Suggestions to improve this are welcome! See [contributing][contributing]. + +References: + +- [Issue #67](https://github.com/surpher/PactSwift/issues/67) +- [Writing Tests](https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/testing_with_xcode/chapters/04-writing_tests.html#//apple_ref/doc/uid/TP40014132-CH4-SW36) + +## Generated Pact contracts + +By default, generated Pact contracts are written to `/tmp/pacts`. If you want to specify a directory you want your Pact contracts to be written to, you can pass a `URL` object with absolute path to the desired directory when instantiating your `MockService` (Swift only): + +```swift +MockService( + consumer: "consumer", + provider: "provider", + writePactTo: URL(fileURLWithPath: "/absolute/path/pacts/folder", isDirectory: true) +) +```` + +Alternatively you can define a `PACT_OUTPUT_DIR` environment variable (in [`Run`](./Documentation/images/12_xcode_scheme_env_setup.png) section of your scheme) with the path to directory you want your Pact contracts to be written into. + +`PactSwift` first checks whether `URL` has been provided when initializing `MockService` object. If it is not provided it will check for `PACT_OUTPUT_DIR` environment variable. If env var is not set, it will attempt to write your Pact contract into `/tmp/pacts` directory. + +Note that sandboxed apps (macOS apps) are limited in where they can write Pact contract files to. The default location seems to be the `Documents` folder in the sandbox (eg: `~/Library/Containers/xyz.example.your-project-name/Data/Documents`). Setting the environment variable `PACT_OUTPUT_DIR` might not work without some extra leg work tweaking various settings. Look at the logs in debug area for the Pact file location. + +## Sharing Pact contracts + +If your setup is correct and your tests successfully finish, you should see the generated Pact files in your nominated folder as +`_consumer_name_-_provider_name_.json`. + +When running on CI use the [`pact-broker`][pact-broker-client] command line tool to publish your generated Pact file(s) to your [Pact Broker][pact-broker] or a hosted Pact broker service. That way your _API-provider_ team can always retrieve them from one location, set up web-hooks to trigger provider verification tasks when pacts change. Normally you do this regularly in you CI step/s. + +See how you can use a simple [Pact Broker Client][pact-broker-client] in your terminal (CI/CD) to upload and tag your Pact files. And most importantly check if you can [safely deploy][can-i-deploy] a new version of your app. + +## Provider verification + +In your unit tests suite, prepare a Pact Provider Verification unit test: + +1. Start your local Provider service +2. Optionally, instrument your API with ability to configure [provider states](https://github.com/pact-foundation/pact-provider-verifier/) +3. Run the Provider side verification step + +To dynamically retrieve pacts from a Pact Broker for a provider with token authentication, instantiate a `PactBroker` object with your configuration: + +```swift +// The provider being verified +let provider = ProviderVerifier.Provider(port: 8080) + +// The Pact broker configuration +let pactBroker = PactBroker( + url: URL(string: "https://broker.url/")!, + auth: auth: .token(PactBroker.APIToken("auth-token")), + providerName: "Your API Service Name" +) + +// Verification options +let options = ProviderVerifier.Options( + provider: provider, + pactsSource: .broker(pactBroker) +) + +// Run the provider verification task +ProviderVerifier().verify(options: options) { + // do something (eg: shutdown the provider) +} +``` + +To validate Pacts from local folders or specific Pact files use the desired case. + +
Examples + +```swift +// All Pact files from a directory +ProviderVerifier() + .verify(options: ProviderVerifier.Options( + provider: provider, + pactsSource: .directories(["/absolute/path/to/directory/containing/pact/files/"]) + ), + completionBlock: { + // do something + } +) +``` + +```swift +// Only the specific Pact files +pactsSource: .files(["/absolute/path/to/file/consumerName-providerName.json"]) +``` + +```swift +// Only the specific Pact files at URL +pactsSource: .urls([URL(string: "https://some.base.url/location/of/pact/consumerName-providerName.json")]) +``` + +
+ +### Submitting verification results + +To submit the verification results, provide `PactBroker.VerificationResults` object to `pactBroker`. + +
Example + +Set the provider version and optional provider version tags. See [version numbers](https://docs.pact.io/pact_broker/pacticipant_version_numbers) for best practices on Pact versioning. + +```swift +let pactBroker = PactBroker( + url: URL(string: "https://broker.url/")!, + auth: .token("auth-token"), + providerName: "Some API Service", + publishResults: PactBroker.VerificationResults( + providerVersion: "v1.0.0+\(ProcessInfo.processInfo.environment["GITHUB_SHA"])", + providerTags: ["\(ProcessInfo.processInfo.environment["GITHUB_REF"])"] + ) +) +``` + +
+ +For a full working example of Provider Verification see `Pact-Linux-Provider` project in [pact-swift-examples][demo-projects] repository. + +## Matching + +In addition to verbatim value matching, you can use a set of useful matching objects that can increase expressiveness and reduce brittle test cases. + +See [Wiki page about Matchers][matchers] for a list of matchers `PactSwift` implements and their basic usage. + +Or peek into [/Sources/Matchers/][pact-swift-matchers]. + +## Example Generators + +In addition to matching, you can use a set of example generators that generate random values each time you run your tests. + +In some cases, dates and times may need to be relative to the current date and time, and some things like tokens may have a very short life span. + +Example generators help you generate random values and define the rules around them. + +See [Wiki page about Example Generators][example-generators] for a list of example generators `PactSwift` implements and their basic usage. + +Or peek into [/Sources/ExampleGenerators/][pact-swift-example-generators]. + +## Objective-C support + +PactSwift can be used in your Objective-C project with a couple of limitations, (e.g. initializers with multiple optional arguments are limited to only one or two available initializers). See [Demo projects repository][demo-projects] for more examples. + +```swift +_mockService = [[PFMockService alloc] initWithConsumer: @"Consumer-app" + provider: @"Provider-server" + transferProtocol: TransferProtocolStandard]; +``` + +`PF` stands for Pact Foundation. + +Please feel free to raise any [issues](https://github.com/surpher/PactSwift/issues) as you encounter them, thanks. + +## Demo projects + +[![PactSwift - Consumer](https://github.com/surpher/pact-swift-examples/actions/workflows/test_projects.yml/badge.svg)](https://github.com/surpher/pact-swift-examples/actions/workflows/test_projects.yml) +[![PactSwift - Provider](https://github.com/surpher/pact-swift-examples/actions/workflows/verify_provider.yml/badge.svg)](https://github.com/surpher/pact-swift-examples/actions/workflows/verify_provider.yml) + +See [pact-swift-examples][demo-projects] for more examples of how to use `PactSwift`. + +## Contributing + +See: + +- [CODE_OF_CONDUCT.md][code-of-conduct] +- [CONTRIBUTING.md][contributing] + +## Acknowledgements + +This project takes inspiration from [pact-consumer-swift](https://github.com/DiUS/pact-consumer-swift) and pull request [Feature/native wrapper PR](https://github.com/DiUS/pact-consumer-swift/pull/50). + +Logo and branding images provided by [@cjmlgrto](https://github.com/cjmlgrto). + +[action-default]: https://github.com/surpher/PactSwift/actions?query=workflow%3A%22Test+-+Xcode+%28default%29%22 +[action-xcode11.5-beta]: https://github.com/surpher/PactSwift/actions?query=workflow%3A%22Test+-+Xcode+%2811.5-beta%29%22 +[can-i-deploy]: https://docs.pact.io/pact_broker/can_i_deploy +[carthage_script]: ./Scripts/carthage +[code-of-conduct]: ./CODE_OF_CONDUCT.md +[codecov-io]: https://codecov.io/gh/surpher/PactSwift +[contributing]: ./CONTRIBUTING.md +[demo-projects]: https://github.com/surpher/pact-swift-examples +[example-generators]: https://github.com/surpher/PactSwift/wiki/Example-generators + +[github-issues-52]: https://github.com/surpher/PactSwift/issues/52 +[issues]: https://github.com/surpher/PactSwift/issues +[license]: LICENSE.md +[matchers]: https://github.com/surpher/pact-swift/wiki/Matchers +[pacticipant]: https://docs.pact.io/pact_broker/advanced_topics/pacticipant/ +[pact-broker]: https://docs.pact.io/pact_broker +[pact-broker-client]: https://github.com/pact-foundation/pact_broker-client +[pact-consumer-swift]: https://github.com/dius/pact-consumer-swift +[pactswift-spec2]: https://github.com/surpher/PactSwift_spec2 +[pact-docs]: https://docs.pact.io +[pact-reference-rust]: https://github.com/pact-foundation/pact-reference +[pact-slack]: http://slack.pact.io +[pact-specification-v3]: https://github.com/pact-foundation/pact-specification/tree/version-3 +[pact-specification-v2]: https://github.com/pact-foundation/pact-specification/tree/version-2 +[pact-swift-example-generators]: https://github.com/surpher/PactSwift/tree/main/Sources/ExampleGenerators +[pact-swift-matchers]: https://github.com/surpher/PactSwift/tree/main/Sources/Matchers +[pact-twitter]: http://twitter.com/pact_up +[releases]: https://github.com/surpher/PactSwift/releases +[rust-lang-installation]: https://www.rust-lang.org/tools/install +[slack-channel]: https://pact-foundation.slack.com/archives/C9VBGNT4K + +[pact-swift-examples-workflow]: https://github.com/surpher/pact-swift-examples/actions/workflows/test_projects.yml + +[upgrading]: https://github.com/surpher/PactSwift/wiki/Upgrading + +[carthage-issue-3019-1]: https://github.com/Carthage/Carthage/issues/3019#issuecomment-665136323 +[carthage-issue-3019-2]: https://github.com/Carthage/Carthage/issues/3019#issuecomment-734415287 +[carthage-issue-3201]: https://github.com/Carthage/Carthage/issues/3201 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..034e8480 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,21 @@ +# Security Policy + +## Supported Versions + +Use this section to tell people about which versions of your project are +currently being supported with security updates. + +| Version | Supported | +| ------- | ------------------ | +| 5.1.x | :white_check_mark: | +| 5.0.x | :x: | +| 4.0.x | :white_check_mark: | +| < 4.0 | :x: | + +## Reporting a Vulnerability + +Use this section to tell people how to report a vulnerability. + +Tell them where to go, how often they can expect to get an update on a +reported vulnerability, what to expect if the vulnerability is accepted or +declined, etc. diff --git a/Scripts/BuildPhases/lint-project b/Scripts/BuildPhases/lint-project new file mode 100755 index 00000000..ce25acf5 --- /dev/null +++ b/Scripts/BuildPhases/lint-project @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +# PactSwift +# +# Created by Marko Justinek on 26/3/20. +# Copyright © 2020 Marko Justinek. All rights reserved. +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +# IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# + +set -eu + +echo "--- 🤖 Linting ${PROJECT_NAME} Project..." + +SCRIPT_DIR="${BASH_SOURCE[0]%/*}" + +echo "Setting the scripts dir to ${SCRIPT_DIR}" + +"${SCRIPT_DIR}"/validate-build-settings +"${SCRIPT_DIR}"/validate-project-config + +echo "--- 👍 ${PROJECT_NAME} project successfully validated and linted." diff --git a/Scripts/BuildPhases/lint-target b/Scripts/BuildPhases/lint-target new file mode 100755 index 00000000..6dbe98f3 --- /dev/null +++ b/Scripts/BuildPhases/lint-target @@ -0,0 +1,48 @@ +#!/usr/bin/env bash + +# PactSwift +# +# Created by Marko Justinek on 26/3/20. +# Copyright © 2020 Marko Justinek. All rights reserved. +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +# IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# + +set -eu + +echo "--- 🤖 Linting ${TARGET_NAME} Target Structure..." + +#### Variables + +#### Expecting for source files to be in ./Sources file (SPM file structure) +TARGET_SRCROOT="${SRCROOT}/Sources" + +errors=() + +#### Script steps + +if [[ ! -z "${INFOPLIST_FILE}" && ! -f "${INFOPLIST_FILE}" ]]; then + errors+=("error: Could not find Info.plist file "${INFOPLIST_FILE}" for target '${TARGET_NAME}'.") +fi + +if [[ ! -d "${TARGET_SRCROOT}" ]]; then + errors+=("error: Could not find root folder '${TARGET_SRCROOT}' for target '${TARGET_NAME}'.") +fi + +if [ ${#errors[@]} -ne 0 ]; then + for i in "${errors[@]}"; do + echo $i + done + exit 1 +fi + +echo "--- 👍 Linting ${TARGET_NAME} successful!" diff --git a/Scripts/BuildPhases/validate-build-settings b/Scripts/BuildPhases/validate-build-settings new file mode 100755 index 00000000..5a5819f6 --- /dev/null +++ b/Scripts/BuildPhases/validate-build-settings @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + +# PactSwift +# +# Created by Marko Justinek on 26/3/20. +# Copyright © 2020 Marko Justinek. All rights reserved. +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +# IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# + +set -eu + +echo "--- 🤖 Ensuring there are no build settings in Xcode project file." + +PBXPROJ_FILE_PATH="${PROJECT_FILE_PATH}/project.pbxproj" +NUMBER_OF_BUILD_SETTINGS=`grep "buildSettings" "$PBXPROJ_FILE_PATH" | wc -l` +NUMBER_OF_EMPTY_BUILD_SETTINGS=`grep -B 0 -A 1 "buildSettings" "$PBXPROJ_FILE_PATH" | grep "};" | wc -l` + +if [ $NUMBER_OF_BUILD_SETTINGS != $NUMBER_OF_EMPTY_BUILD_SETTINGS ]; then + NUMBER_WITH_SETTINGS=`expr $NUMBER_OF_BUILD_SETTINGS - $NUMBER_OF_EMPTY_BUILD_SETTINGS` + + echo "error: Found ${NUMBER_WITH_SETTINGS} build settings in Xcode project file! Build settings should only be defined in ./Configurations/*.xcconfig files." + exit 1 +fi + +echo "--- 👍 There are no build settings in Xcode project file." diff --git a/Scripts/BuildPhases/validate-project-config b/Scripts/BuildPhases/validate-project-config new file mode 100755 index 00000000..0eb5dcc6 --- /dev/null +++ b/Scripts/BuildPhases/validate-project-config @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +# PactSwift +# +# Created by Marko Justinek on 26/3/20. +# Copyright © 2020 Marko Justinek. All rights reserved. +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +# IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# + +set -eu + +echo "--- 🤖 Validating project config..." + +PROJECT_CONFIG_DIR="${SRCROOT}/Configurations" + +### Validate top level project configuration +if [[ ! -d "${PROJECT_CONFIG_DIR}" ]]; then + echo "error: Could not find 'Configurations' folder for project '${PROJECT_NAME}'." + exit 1 +fi + +echo "--- 👍 Project config validated." diff --git a/Scripts/build_file_list_and_swiftlint b/Scripts/build_file_list_and_swiftlint new file mode 100755 index 00000000..398c6da5 --- /dev/null +++ b/Scripts/build_file_list_and_swiftlint @@ -0,0 +1,54 @@ +#!/usr/bin/env bash + +# PactSwift +# +# Created by Marko Justinek on 26/3/20. +# Copyright © 2020 Marko Justinek. All rights reserved. +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +# IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# + +set -eu + +# Adds support for Apple Silicon brew directory +export PATH="$PATH:/opt/homebrew/bin" + +SRCROOT=${SRCROOT:-"."} +DERIVED_FILE_DIR=${DERIVED_FILE_DIR:-"."} + +if [ $# -ne 2 ]; then + echo "usage: build_file_list_and_swiftlint project_name swiftlint_yml" + exit 1 +fi + +echo "--- 🤖 Linting $SRCROOT/Sources/*.swift" + +if which swiftlint >/dev/null; then + # Build a list of Swift files in the Sources directory + find Sources -name \*.swift -exec echo "\$(SRCROOT)/"{} \; > $DERIVED_FILE_DIR/$1.xcfilelist + + # Update the xcfilelist if the list of Swift files has changed + cmp --silent $SRCROOT/$1.xcfilelist $DERIVED_FILE_DIR/$1.xcfilelist || cp -f $DERIVED_FILE_DIR/$1.xcfilelist $SRCROOT/$1.xcfilelist + + # Run swiftlint (TODO: - swiftlint by iterating through the $1.xcfilelist) + # swiftlint --config $2 -- #filename0 #filename1 #filename2 ... + swiftlint --config $2 + + # Output an empty derived file + touch $DERIVED_FILE_DIR/swiftlint.txt + + # All hunky dory + echo "--- 👍 .swift files linted" +else + echo "--- ⚠️ Swiftlint" + echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint" +fi diff --git a/Scripts/carthage b/Scripts/carthage new file mode 100755 index 00000000..f5ac5d14 --- /dev/null +++ b/Scripts/carthage @@ -0,0 +1,47 @@ +#!/usr/bin/env bash + +# carthage workaround +# Usage example: ./carthage build --platform iOS +# +# Source(s): +# - https://github.com/Carthage/Carthage/issues/3201 +# - https://github.com/Carthage/Carthage/issues/3019#issuecomment-665136323 +# - https://github.com/Carthage/Carthage/issues/3019#issuecomment-734415287 +# + +set -euo pipefail + +# Determine architecture of current machine +ACTIVE_ARCH=$(uname -m) + +if [[ $ACTIVE_ARCH == "x86_64" ]]; then + + # If running on Intel machine, do the excluded architectures dance 💃🕺 + + xcconfig=$(mktemp /tmp/static.xcconfig.XXXXXX) + trap 'rm -f "$xcconfig"' INT TERM HUP EXIT + + echo "⚠️ NOTE: Using Carthage workaround script..." + + # For Xcode 12 and 13 make sure EXCLUDED_ARCHS is set to arm architectures otherwise + # the build will fail on lipo due to duplicate architectures. + + CURRENT_XCODE_VERSION="$(xcodebuild -version | grep "Xcode" | cut -d' ' -f2 | cut -d'.' -f1)00" + CURRENT_XCODE_BUILD=$(xcodebuild -version | grep "Build version" | cut -d' ' -f3) + + echo "EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64__XCODE_${CURRENT_XCODE_VERSION}__BUILD_${CURRENT_XCODE_BUILD} = arm64 arm64e armv7 armv7s armv6 armv8" >> $xcconfig + + echo 'EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64__XCODE_'${CURRENT_XCODE_VERSION}' = $(EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64__XCODE_$(XCODE_VERSION_MAJOR)__BUILD_$(XCODE_PRODUCT_BUILD_VERSION))' >> $xcconfig + echo 'EXCLUDED_ARCHS = $(inherited) $(EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_$(EFFECTIVE_PLATFORM_SUFFIX)__NATIVE_ARCH_64_BIT_$(NATIVE_ARCH_64_BIT)__XCODE_$(XCODE_VERSION_MAJOR))' >> $xcconfig + + export XCODE_XCCONFIG_FILE="$xcconfig" + carthage "$@" + +else + + # Running on arm64 machine so just use the plain carthage + echo "⚠️ NOTE: Using plain vanilla Carthage..." + carthage "$@" + +fi + diff --git a/Scripts/check_build_tools b/Scripts/check_build_tools new file mode 100755 index 00000000..3e05dc37 --- /dev/null +++ b/Scripts/check_build_tools @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +set -eu +set -o pipefail + +# Checking for SwiftLint +if which swiftlint >/dev/null; then + echo "👍 swiftLint installed" +else + echo "⚠️ Swiftlint" + echo "warning: SwiftLint not installed, use 'brew install swiftlint' to install it." +fi + +# Checking for xcbeautify +if which xcbeautify >/dev/null; then + echo "👍 xcbeautify installed" +else + echo "⚠️ xcbeautify" + echo "warning: xcbeautify not installed, use 'brew install xcbeautify' to install it." +fi \ No newline at end of file diff --git a/Scripts/prepare_build_tools b/Scripts/prepare_build_tools new file mode 100755 index 00000000..38b8c310 --- /dev/null +++ b/Scripts/prepare_build_tools @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +brew tap thii/xcbeautify https://github.com/thii/xcbeautify.git +brew install xcbeautify + +if [[ "$CI" == false ]]; then + brew install swiftlint +fi \ No newline at end of file diff --git a/Scripts/release b/Scripts/release new file mode 100755 index 00000000..46aa2ab9 --- /dev/null +++ b/Scripts/release @@ -0,0 +1,221 @@ +#!/usr/bin/env bash + +# release +# +# Runs the required steps to prepare and tag new version of PactSwift. +# Usage: `./Scripts/release 1.0.0 'Bugfix Release' [-d]` +# +# Notes: +# Updates PactSwift version number in PactSwiftVersion.swift file +# Updates Marketing version number in Project-Shared.xcconfig file +# Updates CHANGELOG.md +# Commits the updates in the repo +# Creates a tag on the last commit +# Pushes the updates and tags to `remote` +# +# 🚨🚨🚨 WARNING 🚨🚨🚨 +# This is an extremely fragile script... because I just can't find decent time to DRY it up and set it up correctly. +# If you end up editing it, just be ready for a world of pain. Or not. Either way, you have been warned. +# + +# set -x +set -o pipefail + +#################### +# Utilities # +#################### + +function help { + echo "Usage: release VERSION RELEASE_NAME [DRY_RUN]" + echo + echo "VERSION should be the version to release and should not include the 'v' prefix" + echo "RELEASE_NAME should be the type of release 'Bugfix Release / Maintenance Release'" + echo + echo "FLAGS" + echo " -d Dry run, won't push anything" + echo + echo " Example: ./Scripts/release 1.0.0 'Bugfix Release' -d" + echo + exit 2 +} + +function die { + echo "🚨 [ERROR] $@" + echo + exit 1 +} + +function handleInput { + if [[ ! $1 =~ ^[Yy]$ ]]; then + echo "⚠️ Release aborted!" + exit 1 + fi +} + +##################### +# Preliminary checks # +##################### + +if [ $# -lt 2 ]; then + help +fi + +############################## +# Overridable Environment # +############################## + +if [[ "$CI" == true ]] ; then + die "🚨 Running on CI is not supported! Requires user input while running the release script." +else + echo "👮‍♀️ Running on local machine: 👍" + SCRIPTS_DIR="${BASH_SOURCE[0]%/*}" +fi + +############################## +# Pre-release checks # +############################## + +# Get the latest release tag +LATEST_TAG=`git describe --match "v[0-9].*" --abbrev=0 HEAD` + +echo "ℹ️ The last release tag number is: ${LATEST_TAG}" + +read -r -p "Did you provide the correctly incremented number for the new release tag? [Y/n] " -n 1 USER_INPUT +echo +handleInput $USER_INPUT +echo + +read -r -p "Have you updated all the documentation files? [Y/n] " -n 1 USER_INPUT +echo +handleInput $USER_INPUT +echo + +echo "ℹ️ The following commits will be recorded with the tag:" +git log --pretty='* %h - %s (%an)' ${LATEST_TAG}..HEAD +echo + +read -r -p "Have you prepared a draft release on GitHub.com? [Y/n] " -n 1 USER_INPUT +echo +handleInput $USER_INPUT + +read -r -p "You named the release name with something meaningful like \"Bugfix\" or \"Feature \". Right? [Y/n] " -n 1 USER_INPUT +echo +handleInput $USER_INPUT + +#################### +# Variables # +#################### + +CONFIGURATION_FILE="${SCRIPTS_DIR}/../Configurations/Project-Shared.xcconfig" +VERSION_FILE="${SCRIPTS_DIR}/../Sources/Model/PactSwiftVersion.swift" +RELEASE_NOTES="${SCRIPTS_DIR}/../CHANGELOG.md" +REMOTE_BRANCH=main +VERSION=$1 +RELEASE_NAME=$2 +DRY_RUN=$3 +VERSION_TAG="v$VERSION" + +#################### +# Setup # +#################### + +function updateVersionFile { + sed -i '' "2s/.*/let pactSwiftVersion = \"$@\"/" $VERSION_FILE + + MARKETING_VERSION="MARKETING_VERSION = $@" + sed -i '' "2s/.*/$MARKETING_VERSION/" $CONFIGURATION_FILE +} + +function pushNewVersion { + updateVersionFile "$VERSION" + + echo ${RELEASE_NAME} > TAG_MESSAGE_FILE.md + git log --pretty='* %h - %s (%an)' ${LATEST_TAG}..HEAD >> TAG_MESSAGE_FILE.md + + git add $VERSION_FILE + git add $CONFIGURATION_FILE + git add $RELEASE_NOTES + + git commit -m "${RELEASE_NAME}" + + echo "🏷 Tagging the current commit" + git tag "$VERSION_TAG" -F TAG_MESSAGE_FILE.md || die "Failed to tag version" + + echo "🚀 Tagging the current commit" + git push --atomic origin main $VERSION_TAG || die "Failed to push the release commit with tag '$VERSION_TAG' to origin" + +} + +#################### +# Release flow # +#################### + +echo "👮‍♀️ Verifying if version tag is reasonable..." + +echo $VERSION_TAG | grep -q "^vv" +if [ $? -eq 0 ]; then + die "This tag ($VERSION) is in an incorrect format. You should remove the 'v' prefix." +fi + +echo $VERSION_TAG | grep -q -E "^v[0-9]+\.[0-9]+\.[0-9]+(-\w+(\.\d)?)?$" +if [ $? -ne 0 ]; then + die "This tag $VERSION is in an incorrect format. It should be in 'v{MAJOR}.{MINOR}.{PATCH}(-{PRERELEASE_NAME}.{PRERELEASE_VERSION})' form." +fi + +echo "👮‍♀️ Verifying version ($VERSION) is unique..." +git describe --exact-match "$VERSION_TAG" > /dev/null 2>&1 +if [ $? -eq 0 ]; then + die "Tag ($VERSION) already exists! Aborting." +else + echo "👍 Tag is unique" +fi + +echo "🏗 Generating release notes into $RELEASE_NOTES" +# backup the existing CHANGELOG.md to CHANGELOG.backup +cp $RELEASE_NOTES ${RELEASE_NOTES}.backup + +# Prepare the title for this release +echo "# ${VERSION} - ${RELEASE_NAME}" > ${RELEASE_NOTES}.next + +# Get the commits from last change +git log --pretty='* %h - %s (%an)' ${LATEST_TAG}..HEAD >> ${RELEASE_NOTES}.next + +# Stage the updated CHANGELOG.md +git add $RELEASE_NOTES || { die "Failed to add ${RELEASE_NOTES} to INDEX"; } + +# Read the notes for this release and append them to the old CHANGELOG.md +cat $RELEASE_NOTES.next | cat - ${RELEASE_NOTES}.backup > ${RELEASE_NOTES} + +echo "🚢 Releasing version $VERSION (tag: $VERSION_TAG)..." + +echo "⛓ Ensuring no differences to origin/$REMOTE_BRANCH" +git fetch origin || die "Failed to fetch origin" +git diff --quiet HEAD "origin/$REMOTE_BRANCH" || die "HEAD is not aligned to origin/$REMOTE_BRANCH. Cannot update version safely." + +echo "🏷 Tagging release version..." + +if [ -z "$DRY_RUN" ]; then + echo "🚅 Pushing release version tag to origin..." + + pushNewVersion + + echo + echo "-------- 🎉 Created a new PactSwift version tag 🎉 --------------------------------" + echo "-------- 🏷 Version: $VERSION_TAG" + echo "-------- ✏️ Name: $RELEASE_NAME" + echo "--------" + echo "-------- 🚀 Go and link you draft release for '$VERSION_TAG' with this tagged commit." + echo "-------- 🔗 https://github.com/surpher/PactSwift/releases" + echo + +else + echo "-> Dry run completed." +fi + +#################### +# Cleanup # +#################### + +rm ${RELEASE_NOTES}.next +rm ${RELEASE_NOTES}.backup +rm TAG_MESSAGE_FILE.md diff --git a/Scripts/run_tests b/Scripts/run_tests new file mode 100755 index 00000000..ee84865c --- /dev/null +++ b/Scripts/run_tests @@ -0,0 +1,46 @@ +#!/usr/bin/env bash + +set -eu +set -o pipefail + +# Overridable Environment +SIMULATOR_NAME=${SIMULATOR_NAME:-'iPhone 14 Pro'} +SCRIPTS_DIR="${BASH_SOURCE[0]%/*}" + +# Determine architecture of current machine +ACTIVE_ARCH=$(uname -m) + +# Check for dependencies +$SCRIPTS_DIR/check_build_tools + +# Carthage build +echo "📦 Building as a Carthage dependency" +if [[ $ACTIVE_ARCH == "x86_64" ]]; then + ${SCRIPTS_DIR}/carthage build --no-skip-current --use-xcframeworks +else + carthage build --no-skip-current --use-xcframeworks +fi + +# Build and test for SPM +echo "📦 Verifying it works using Swift Package Manager" + +echo "ℹ️ Resolving package dependencies" +xcodebuild -resolvePackageDependencies + +echo "🏗 Building" +swift build | xcbeautify + +echo "🤖 Running tests" +swift test | xcbeautify + +# Run iOS tests +echo "📱 Running iOS tests" +set -o pipefail && xcodebuild clean test -project PactSwift.xcodeproj -scheme PactSwift-iOS -destination "platform=iOS Simulator,name=${SIMULATOR_NAME}" GCC_INSTRUMENT_PROGRAM_FLOW_ARCS=YES GCC_GENERATE_TEST_COVERAGE_FILES=YES | xcbeautify + +# Run macOS tests +echo "🖥 Running macOS tests" +set -o pipefail && xcodebuild clean test -project PactSwift.xcodeproj -scheme PactSwift-macOS -destination "platform=macOS,arch=${ACTIVE_ARCH}" GCC_INSTRUMENT_PROGRAM_FLOW_ARCS=YES GCC_GENERATE_TEST_COVERAGE_FILES=YES| xcbeautify + +# A-OK? + +echo "👍 All hunky dory!" diff --git a/Sources/ExampleGenerators/DateTime.swift b/Sources/ExampleGenerators/DateTime.swift new file mode 100644 index 00000000..2be3304b --- /dev/null +++ b/Sources/ExampleGenerators/DateTime.swift @@ -0,0 +1,53 @@ +// +// DateTime.swift +// PactSwift +// +// Created by Marko Justinek on 13/2/22. +// Copyright © 2022 Marko Justinek. All rights reserved. +// + +import Foundation + +public extension ExampleGenerator { + + /// Generates an example for DateTime using a specific `Date` + struct DateTime: ExampleGeneratorExpressible { + internal let value: Any + internal let generator: ExampleGenerator.Generator = .dateTime + internal var rules: [String: AnyEncodable]? + + /// Generates an example value for DateTime using a specific `Date` for consumer tests + /// + /// - Parameters: + /// - format: The format used for datetime + /// - use: The `Date` object used in consumer tests + /// + public init(format: String, use date: Date) { + self.value = date.formatted(format) + self.rules = [ + "format": AnyEncodable(format), + ] + } + } + +} + +// MARK: - Objective-C + +@objc(PFGeneratorDateTime) +public class ObjcDateTime: NSObject, ObjcGenerator { + + let type: ExampleGeneratorExpressible + + /// Generates an example value for DateTime using a specific `Date` for consumer tests + /// + /// - Parameters: + /// - format: The format used for datetime + /// - use: The `Date` object used in consumer tests + /// + @objc(date: format:) + public init(format: String, use date: Date) { + type = ExampleGenerator.DateTime(format: format, use: date) + } + +} diff --git a/Sources/ExampleGenerators/DateTimeExpression.swift b/Sources/ExampleGenerators/DateTimeExpression.swift new file mode 100644 index 00000000..af15c7a3 --- /dev/null +++ b/Sources/ExampleGenerators/DateTimeExpression.swift @@ -0,0 +1,62 @@ +// +// DateTimeExpression.swift +// PactSwift +// +// Created by Marko Justinek on 5/3/22. +// Copyright © 2022 Marko Justinek. All rights reserved. +// + +import Foundation + +public extension ExampleGenerator { + + /// Generates a generator for DateTime using an expression + /// + /// Warning: + /// Not all Pact impelmentations support this type of example generator! + /// + struct DateTimeExpression: ExampleGeneratorExpressible { + internal let value: Any + internal let generator: ExampleGenerator.Generator = .dateTime + internal var rules: [String: AnyEncodable]? + + /// Generates an example generator for DateTime using an expression + /// + /// - Parameters: + /// - expression: The expression provider should use when verifying + /// - format: The date time format + /// + /// - Warning: Not all Pact implementations support this type of example generator! + /// + public init(expression: String, format: String) { + self.value = Date().formatted(format) + self.rules = [ + "format": AnyEncodable(format), + "expression": AnyEncodable(expression), + ] + } + } + +} + +// MARK: - Objective-C + +@objc(PFGeneratorDateTimeExpression) +public class OjbcDateTimeExpression: NSObject, ObjcGenerator { + + let type: ExampleGeneratorExpressible + + /// Generates an example generator for DateTime using an expression + /// + /// - Parameters: + /// - expression: The expression provider should use when verifying + /// - format: The date time format + /// + /// - Warning: Not all Pact implementations support this type of example generator! + /// + @objc(expression: format:) + public init(expression: String, format: String) { + type = ExampleGenerator.DateTimeExpression(expression: expression, format: format) + } + +} diff --git a/Sources/ExampleGenerators/ExampleGenerator.swift b/Sources/ExampleGenerators/ExampleGenerator.swift new file mode 100644 index 00000000..4e4617f9 --- /dev/null +++ b/Sources/ExampleGenerators/ExampleGenerator.swift @@ -0,0 +1,109 @@ +// +// Created by Marko Justinek on 11/9/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +public struct ExampleGenerator { + // This is a namespace placeholder + // Implement any Example Generators as `Struct`s in an extension. + + // ⚠️ IMPORTANT ⚠️ + // Make sure PactBuilder.swift handles the example generator + // There is a bug in Swift where protocols are not handled properly when + // used as generics. + + // Example Generators are Encodable objects that conform to the defined structure. + // When DSL is being processed the Example Generator object is created and added to Pact. + // Every Example Generator conforms to ExampleGeneratorExpressible protocol which + // defines that at least `type` key is provided. Generally an Example Generator would + // set its type from the `generator` property. That then matches the agreed generator name + // and attributes defined in Pact Specification version 3 (see link below). The example values + // generated in PactSwift are re-generated with each test run and are re-generated at + // mock server point (when running `PactSwiftMockServer.verify()`). + // + // We use `AnyEncodable` type eraser because we can not predict what type the user will provide. + // + // As an example, the `RandomInt` example generator would use `ExampleGenerator.Generator` + // and dictionary `rules` defines the attributes to of the generated example value. + // + // Imagine the following properties of an Example Generator: + // + // let value: Any + // let generator: Example.Generator = .int + // let rules: [String: AnyEncodable] = ["min": AnyEncodable(3), "max": AnyEncodable(9)] + // + // would generate an object for the jsonPath where the example generator was used: + // + // { + // "type": "RandomInt", + // "min": 3, + // "max": 9 + // } + // + // This JSON object is applied to the specific jsonPath whilst the DSL structure is being processed. + // + // Example: + // + // // DSL + // let body = [ + // "randomInt": ExampleGenerator.RandomInt(min: 3, max: 9) + // ] + // + // // Extract from Pact contract (JSON file) + // "generators": { + // "body": { + // "$.randomInt": { + // "type": "RandomInt", + // "min": 3, + // "max": 9 + // } + // } + // } + // + // See: https://github.com/pact-foundation/pact-specification/tree/version-3#introduce-example-generators + // + +} + +extension ExampleGenerator { + + // A list of implemented Example Generators that map to a generator in Pact Specification + // See: https://github.com/pact-foundation/pact-specification/tree/version-3#introduce-example-generators + enum Generator: String { + case bool = "RandomBoolean" + case date = "Date" + case dateTime = "DateTime" + case decimal = "RandomDecimal" + case hexadecimal = "RandomHexadecimal" + case int = "RandomInt" + case providerState = "ProviderState" + case regex = "Regex" + case string = "RandomString" + case time = "Time" + case uuid = "Uuid" + } + +} + +// MARK: - Objective-C + +/// Acts as a bridge defining the Swift specific Generator type +protocol ObjcGenerator { + + var type: ExampleGeneratorExpressible { get } + +} diff --git a/Sources/ExampleGenerators/ProviderStateGenerator.swift b/Sources/ExampleGenerators/ProviderStateGenerator.swift new file mode 100644 index 00000000..6e59d8f7 --- /dev/null +++ b/Sources/ExampleGenerators/ProviderStateGenerator.swift @@ -0,0 +1,37 @@ +// +// Created by Marko Justinek on 15/8/21. +// Copyright © 2021 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +extension ExampleGenerator { + + // Works with Matcher.FromProviderState(references: value:) + struct ProviderStateGenerator: ExampleGeneratorExpressible { + let value: Any + let generator: ExampleGenerator.Generator = .providerState + var rules: [String: AnyEncodable]? + + init(parameter: String, value: Any) { + self.value = value + self.rules = [ + "expression": AnyEncodable(parameter), + "type": AnyEncodable(generator.rawValue), + ] + } + } + +} diff --git a/Sources/ExampleGenerators/RandomBool.swift b/Sources/ExampleGenerators/RandomBool.swift new file mode 100644 index 00000000..0ea5d94c --- /dev/null +++ b/Sources/ExampleGenerators/RandomBool.swift @@ -0,0 +1,46 @@ +// +// Created by Marko Justinek on 11/9/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +public extension ExampleGenerator { + + /// Generates a random boolean value + struct RandomBool: ExampleGeneratorExpressible { + internal let value: Any = Bool.random() + internal let generator: ExampleGenerator.Generator = .bool + internal let rules: [String: AnyEncodable]? = nil + + /// Generates a random boolean value + public init() { } + } + +} + +// MARK: - Objective-C + +@objc(PFGeneratorRandomBool) +public class ObjcRandomBool: NSObject, ObjcGenerator { + + let type: ExampleGeneratorExpressible = ExampleGenerator.RandomBool() + + /// Generates a random boolean value + @objc public override init() { + super.init() + } + +} diff --git a/Sources/ExampleGenerators/RandomDate.swift b/Sources/ExampleGenerators/RandomDate.swift new file mode 100644 index 00000000..a83094de --- /dev/null +++ b/Sources/ExampleGenerators/RandomDate.swift @@ -0,0 +1,63 @@ +// +// Created by Marko Justinek on 17/9/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +public extension ExampleGenerator { + + /// Generates a Date value from the current date either in ISO format or using the provided format string + struct RandomDate: ExampleGeneratorExpressible { + internal let value: Any + internal let generator: ExampleGenerator.Generator = .date + internal var rules: [String: AnyEncodable]? + + /// Generates a `Date` value from the current date either in ISO format or using the provided format string + /// + /// - Parameters: + /// - format: The format of generated date + /// + public init(format: String? = nil) { + self.value = Date.formattedDate(format: format, isoFormat: .date) + + if let format = format { + self.rules = [ + "format": AnyEncodable(format), + ] + } + } + } + +} + +// MARK: - Objective-C + +@objc(PFGeneratorRandomDate) +public class ObjcRandomDate: NSObject, ObjcGenerator { + + let type: ExampleGeneratorExpressible + + /// Generates a `Date` value from the current date either in ISO format or using the provided format string + /// + /// - Parameters: + /// - format: The format of generated date + /// + @objc(format:) + public init(format: String? = nil) { + type = ExampleGenerator.RandomDate(format: format) + } + +} diff --git a/Sources/ExampleGenerators/RandomDateTime.swift b/Sources/ExampleGenerators/RandomDateTime.swift new file mode 100644 index 00000000..cba4a849 --- /dev/null +++ b/Sources/ExampleGenerators/RandomDateTime.swift @@ -0,0 +1,63 @@ +// +// Created by Marko Justinek on 17/9/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +public extension ExampleGenerator { + + /// Generates a Date and Time (timestamp) value from the current date and time either in ISO format or using the provided format string + struct RandomDateTime: ExampleGeneratorExpressible { + internal let value: Any + internal let generator: ExampleGenerator.Generator = .dateTime + internal var rules: [String: AnyEncodable]? + + /// Generates a Date and Time (timestamp) value from the current date and time either in ISO format or using the provided format string + /// + /// - Parameters: + /// - format: The format of generated timestamp + /// + public init(format: String? = nil) { + self.value = Date.formattedDate(format: format, isoFormat: .dateTime) + + if let format = format { + self.rules = [ + "format": AnyEncodable(format), + ] + } + } + } + +} + +// MARK: - Objective-C + +@objc(PFGeneratorRandomDateTime) +public class ObjcRandomDateTime: NSObject, ObjcGenerator { + + let type: ExampleGeneratorExpressible + + /// Generates a Date and Time (timestamp) value from the current date and time either in ISO format or using the provided format string + /// + /// - Parameters: + /// - format: The format of generated timestamp + /// + @objc(format:) + public init(format: String? = nil) { + type = ExampleGenerator.RandomDateTime(format: format) + } + +} diff --git a/Sources/ExampleGenerators/RandomDecimal.swift b/Sources/ExampleGenerators/RandomDecimal.swift new file mode 100644 index 00000000..a591def7 --- /dev/null +++ b/Sources/ExampleGenerators/RandomDecimal.swift @@ -0,0 +1,78 @@ +// +// Created by Marko Justinek on 17/9/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +public extension ExampleGenerator { + + /// Generates a random decimal value (BigDecimal) with the provided number of digits + struct RandomDecimal: ExampleGeneratorExpressible { + internal let value: Any + internal let generator: ExampleGenerator.Generator = .decimal + internal var rules: [String: AnyEncodable]? + + /// Generates a random decimal value (BigDecimal) with the provided number of digits + /// + /// - Parameters: + /// - digits: Number of digits of the generated `Decimal` value (max 9) + /// + /// - Precondition: `digits` is a positive value + /// + public init(digits: Int = 6) { + let digits = digits < 9 ? digits : 9 + self.value = NumberHelper.randomDecimal(digits: digits) + self.rules = [ + "digits": AnyEncodable(digits < 9 ? digits : 9), + ] + } + } + +} + +private enum NumberHelper { + + static func randomDecimal(digits: Int) -> Decimal { + var randomDecimal: String = "" + (0..=5.5) +@_implementationOnly import PactSwiftMockServer +#else +import PactSwiftMockServer +#endif + +public extension ExampleGenerator { + + /// Generates a random string value of the provided size characters + struct RandomString: ExampleGeneratorExpressible { + internal let value: Any + internal let generator: ExampleGenerator.Generator + internal var rules: [String: AnyEncodable]? + + /// Generates a random string value of the provided size characters + /// + /// - Parameters: + /// - size: The size of generated `String` + /// + /// - Precondition: `size` is a positive value + /// + public init(size: Int = 20) { + self.generator = .string + self.value = String((0.. String { + let formatter = DateFormatter() + formatter.dateFormat = format + return formatter.string(from: self) + } + + // MARK: - Static interface + + static func formattedDate(format: String?, isoFormat: ISOFormat) -> String { + if let format = format { + return Date().formatted(format) + } + + let formatter = ISO8601DateFormatter() + formatter.formatOptions = isoFormat.formatOptions + return formatter.string(from: Date()) + } + +} diff --git a/Sources/Extensions/Dictionary+PactSwift.swift b/Sources/Extensions/Dictionary+PactSwift.swift new file mode 100644 index 00000000..156eb8bb --- /dev/null +++ b/Sources/Extensions/Dictionary+PactSwift.swift @@ -0,0 +1,23 @@ +// +// Created by Marko Justinek on 31/3/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +/// Merges two `Dictionary` objects and returns a `Dictionary` +func merge(_ lhs: [Key: Value], with rhs: [Key: Value]) -> [Key: Value] { + var result = lhs + rhs.forEach { result[$0] = $1 } + return result +} diff --git a/Sources/Extensions/MockServer+Async.swift b/Sources/Extensions/MockServer+Async.swift new file mode 100644 index 00000000..336e8a2a --- /dev/null +++ b/Sources/Extensions/MockServer+Async.swift @@ -0,0 +1,72 @@ +// +// Created by Oliver Jones on 30/11/2022. +// Copyright © 2022 Oliver Jones. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +#if canImport(_Concurrency) && compiler(>=5.7) + +import Foundation +@_implementationOnly import PactSwiftMockServer + +extension MockServer { + + /// Spins up a mock server with expected interactions defined in the provided Pact + /// + /// - Parameters: + /// - pact: The pact contract + /// - protocol: HTTP protocol + /// + @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) + @discardableResult + func setup(pact: Data, protocol: PactSwiftMockServer.TransferProtocol = .standard) async throws -> Int { + try await withCheckedThrowingContinuation { continuation in + self.setup(pact: pact, protocol: `protocol`) { result in + continuation.resume(with: result) + } + } + } + + /// Verifies all interactions passed to mock server + /// + /// By default pact files are written to `/tmp/pacts`. + /// Use `PACT_OUTPUT_DIR` environment variable with absolute path to your custom path in schema `run` configuration. + /// + @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) + func verify() async throws -> Bool { + try await withCheckedThrowingContinuation { continuation in + self.verify { result in + continuation.resume(with: result) + } + } + } + + /// Finalises Pact tests by writing the pact contract file to disk + /// + /// - Parameters: + /// - pact: The Pact contract to write + /// - completion: A completion block called when setup completes + /// + @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) + func finalize(pact: Data) async throws -> String { + try await withCheckedThrowingContinuation { continuation in + self.finalize(pact: pact) { result in + continuation.resume(with: result) + } + } + } + +} + +#endif diff --git a/Sources/Extensions/Sequence+PactSwift.swift b/Sources/Extensions/Sequence+PactSwift.swift new file mode 100644 index 00000000..1bad7801 --- /dev/null +++ b/Sources/Extensions/Sequence+PactSwift.swift @@ -0,0 +1,27 @@ +// +// Created by Marko Justinek on 9/7/21. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +extension Sequence where Iterator.Element: Hashable { + + var unique: [Iterator.Element] { + var seen: Set = [] + return filter { seen.insert($0).inserted } + } + +} diff --git a/Sources/Extensions/String+PactSwift.swift b/Sources/Extensions/String+PactSwift.swift new file mode 100644 index 00000000..5c838b51 --- /dev/null +++ b/Sources/Extensions/String+PactSwift.swift @@ -0,0 +1,29 @@ +// +// Created by Marko Justinek on 27/10/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +extension String: PactPathParameter { } + +extension String { + + /// Returns the `UUID` given the `String` itself represents a valid UUID + var uuid: UUID? { + UUID(uuidString: self) + } + +} diff --git a/Sources/Extensions/Task+Timeout.swift b/Sources/Extensions/Task+Timeout.swift new file mode 100644 index 00000000..492fc99b --- /dev/null +++ b/Sources/Extensions/Task+Timeout.swift @@ -0,0 +1,51 @@ +// +// Created by Oliver Jones on 1/12/2022. +// Copyright © 2022 Oliver Jones. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +#if canImport(_Concurrency) && compiler(>=5.7) + +import Foundation + +@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) +extension Task where Failure == Error { + + // Start a new Task with a timeout. If the timeout expires before the operation is + // completed then the task is cancelled and an error is thrown. + @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) + init(priority: TaskPriority? = nil, timeout: TimeInterval, operation: @escaping @Sendable () async throws -> Success) { + self = Task(priority: priority) { + try await withThrowingTaskGroup(of: Success.self) { group -> Success in + group.addTask(operation: operation) + group.addTask { + try await _Concurrency.Task.sleep(nanoseconds: UInt64(timeout * Double(NSEC_PER_SEC))) + throw TimeoutError(interval: timeout) + } + guard let success = try await group.next() else { + throw _Concurrency.CancellationError() + } + group.cancelAll() + return success + } + } + } +} + +struct TimeoutError: LocalizedError { + var interval: TimeInterval + var errorDescription: String? { "Task timed out after \(interval) seconds" } +} + +#endif diff --git a/Sources/Extensions/UUID+PactSwift.swift b/Sources/Extensions/UUID+PactSwift.swift new file mode 100644 index 00000000..bbe36ef0 --- /dev/null +++ b/Sources/Extensions/UUID+PactSwift.swift @@ -0,0 +1,32 @@ +// +// Created by Marko Justinek on 16/9/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +extension UUID { + + /// Returns `UUID` as String with no hyphens, lowercased + var uuidStringSimple: String { + self.uuidString.replacingOccurrences(of: "-", with: "").lowercased() + } + + /// Returns an RFC4122-compliant string created from the UUID, such as "385336e1-d647-44a8-8b1b-3dbf4a073416" + var rfc4122String: String { + uuidString.lowercased() + } + +} diff --git a/Sources/Headers/PactSwift.h b/Sources/Headers/PactSwift.h new file mode 100644 index 00000000..2e2649ca --- /dev/null +++ b/Sources/Headers/PactSwift.h @@ -0,0 +1,26 @@ +// +// Created by Marko Justinek on 26/3/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +#import + +//! Project version number for PactSwift. +FOUNDATION_EXPORT double PactSwiftVersionNumber; + +//! Project version string for PactSwift. +FOUNDATION_EXPORT const unsigned char PactSwiftVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import diff --git a/Sources/Matchers/DecimalLike.swift b/Sources/Matchers/DecimalLike.swift new file mode 100644 index 00000000..e4f5c728 --- /dev/null +++ b/Sources/Matchers/DecimalLike.swift @@ -0,0 +1,80 @@ +// +// Created by Marko Justinek on 11/4/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +public extension Matcher { + + /// Mathes a `Decimal` value. + /// + /// - Parameters: + /// - value: The value MockService should expect or respond with in your tests + /// + /// Use this matcher when you expect the type being returned by the API provider is a `Decimal`. + /// + /// ``` + /// [ + /// "foo": Matcher.DecimalLike(1234) + /// ] + /// ``` + /// + struct DecimalLike: MatchingRuleExpressible { + internal let value: Any + internal let rules: [[String: AnyEncodable]] = [["match": AnyEncodable("decimal")]] + + // MARK: - Initializer + + /// Mathes a `Decimal` value. + /// + /// - parameter value: The value MockService should expect or respond with in your tests + /// + public init(_ value: Decimal) { + self.value = value + } + } + +} + +// MARK: - Objective-C + +/// Mathes a `Decimal` value. +/// +/// Use this matcher when you expect the type being returned by the API provider is a `Decimal`. +/// +/// ``` +/// @{@"foo": [Matcher DecimalLike(1234)] } +/// ``` +/// +/// - Parameters: +/// - value: The value MockService should expect or respond with +/// +@objc(PFMatcherDecimalLike) +public class ObjcDecimalLike: NSObject, ObjcMatcher { + + let type: MatchingRuleExpressible + + /// Mathes a `Decimal` value. + /// + /// - Parameters: + /// - value: The value MockService should expect or respond with + /// + @objc(value:) + public init(value: Decimal) { + self.type = Matcher.DecimalLike(value) + } + +} diff --git a/Sources/Matchers/EachKeyLike.swift b/Sources/Matchers/EachKeyLike.swift new file mode 100644 index 00000000..00b8c40e --- /dev/null +++ b/Sources/Matchers/EachKeyLike.swift @@ -0,0 +1,179 @@ +// +// Created by Marko Justinek on 3/4/2022. +// Copyright © 2022 PACT Foundation. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +public extension Matcher { + + /// Object where the key itself is ignored, but the value template must match + /// + /// - Parameters: + /// - value: The value template to use in consumer test + /// + /// Use this matcher in situations when the `key` is not known in advance but the actual value structure and type + /// must match. You may use other `Matcher`s and `ExampleGenerator`s for its value. + /// + /// An example pact test set up: + /// + /// ``` + /// .willRespondWith( + /// status: 200, + /// body: [ + /// "abc": Matcher.EachKeyLike([ + /// "field1": Matcher.SomethingLike("value1"), + /// "field2": Matcher.IntegerLike(123) + /// ]), + /// "xyz": Matcher.EachKeyLike([ + /// "field1": Matcher.SomethingLike("value2"), + /// "field2": Matcher.IntegerLike(456) + /// ]) + /// ] + /// ) + /// ``` + /// + /// Will generate the response JSON: + /// + /// ``` + /// { + /// "abc": { "field1": "value1", "field2": 123 }, + /// "xyz": { "field1": "value2", "field2": 456 }, + /// } + /// ``` + /// + /// And the matching rules will be set to match any key: + /// + /// ``` + /// "matchingRules": { + /// "body": { + /// "$.*": { + /// "combine": "AND", + /// "matchers": [{"match": "type"}] + /// }, + /// "$.*.field1": { + /// "combine": "AND", + /// "matchers": [{"match": "type"}] + /// }, + /// "$.*.field2": { + /// "combine": "AND", + /// "matchers": [{"match": "integer"}] + /// } + /// } + /// }, + /// ... + /// ``` + /// + /// - Warning: Note that using a different matcher at the same level might + /// yield conflicting matching rules when validating the provider. + /// + struct EachKeyLike: MatchingRuleExpressible { + + let value: Any + let rules: [[String: AnyEncodable]] = [["match": AnyEncodable("type")]] + + // MARK: - Initializers + + /// Object where the key itself is ignored, but the value template must match + /// + /// - Parameters: + /// - value: The value template to use in consumer test + /// + /// Use this matcher in situations when the `key` is not known in advance but the actual value structure and type + /// must match. You may use other `Matcher`s and `ExampleGenerator`s for its value. + /// + /// An example pact test set up: + /// + /// ``` + /// .willRespondWith( + /// status: 200, + /// body: [ + /// "abc": Matcher.EachKeyLike([ + /// "field1": Matcher.SomethingLike("value1"), + /// "field2": Matcher.IntegerLike(123) + /// ]), + /// "xyz": Matcher.EachKeyLike([ + /// "field1": Matcher.SomethingLike("value2"), + /// "field2": Matcher.IntegerLike(456) + /// ]) + /// ] + /// ) + /// ``` + /// + /// Will generate the response JSON: + /// + /// ``` + /// { + /// "abc": { "field1": "value1", "field2": 123 }, + /// "xyz": { "field1": "value2", "field2": 456 }, + /// } + /// ``` + /// + /// And the matching rules will be set to match any key: + /// + /// ``` + /// "matchingRules": { + /// "body": { + /// "$.*": { + /// "combine": "AND", + /// "matchers": [{"match": "type"}] + /// }, + /// "$.*.field1": { + /// "combine": "AND", + /// "matchers": [{"match": "type"}] + /// }, + /// "$.*.field2": { + /// "combine": "AND", + /// "matchers": [{"match": "integer"}] + /// } + /// } + /// }, + /// ... + /// ``` + /// + /// - Warning: Note that using a different matcher at the same level might + /// yield conflicting matching rules when validating the provider. + /// + public init(_ value: Any) { + self.value = value + } + } + +} + +// MARK: - Objective-C + +@objc(PFMatcherEachKeyLike) +public class ObjcEachKeyLike: NSObject, ObjcMatcher { + + let type: MatchingRuleExpressible + + /// Object where the key itself is ignored, but the value template must match + /// + /// - Parameters: + /// - value: The value template to use in consumer test + /// + /// Use this matcher in situations when the `key` is not known in advance but the actual value structure and type + /// must match. You may use other `Matcher`s and `ExampleGenerator`s for its value. + /// + /// - Warning: Note that using a different matcher at the same level might + /// yield conflicting matching rules when validating the provider. + /// + @objc(value:) + public init(value: Any) { + type = Matcher.EachKeyLike(value) + } + +} diff --git a/Sources/Matchers/EachLike.swift b/Sources/Matchers/EachLike.swift new file mode 100644 index 00000000..7bf49d3e --- /dev/null +++ b/Sources/Matchers/EachLike.swift @@ -0,0 +1,165 @@ +// +// Created by Marko Justinek on 10/4/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +public extension Matcher { + + /// Matches a `Set` of enclosed elements. + /// + /// Use this matcher when you expect your API provider to return a `Set` of values. + /// You can specify the expected `min` and `max` occurrances of elements, + /// but the actual values are not important. + /// Elements inside this matcher can be other matchers or example generators. + /// + /// ``` + /// [ + /// "related_ids": Matcher.EachLike(1, min: 2), + /// "names": Matcher.EachLike("John", min: 1, max: 3), + /// "elements": Matcher.EachLike( + /// [ + /// "foo": "bar", + /// "bar": Matcher.SomethingLike(5) + /// ], + /// max: 10 + /// ) + /// ``` + /// + struct EachLike: MatchingRuleExpressible { + + internal let value: Any + internal let min: Int? + internal let max: Int? + + internal var rules: [[String: AnyEncodable]] { + var ruleValue = ["match": AnyEncodable("type")] + if let min = min { ruleValue["min"] = AnyEncodable(min) } + if let max = max { ruleValue["max"] = AnyEncodable(max) } + return [ruleValue] + } + + // MARK: - Initializers + + /// Matches a `Set` of provided types or objects. + /// + /// Expects the API provider to return a `Set` with minimum of `1` occurance of the provided element. + /// + /// - Parameters: + /// - value: Expected type or object + /// - count: Number of elements to generate for consumer tests + /// + public init(_ value: Any, count: Int = 1) { + self.value = Array(repeating: value, count: (count > 1) ? count : 1) + self.min = 1 + self.max = nil + } + + /// Matches a `Set` of provided types or objects. + /// + /// Expects the API provider to return a `Set` with at least `min` occurance of the provided element. + /// + /// - Parameters: + /// - value: Expected element + /// - min: Minimum expected number of occurances of the provided `value` + /// - count: Number of elements to generate for consumer tests + /// + /// - Precondition: `min` must either be a zero or a positive value and less than or equal to `count` + /// + public init(_ value: Any, min: Int, count: Int = 1) { + self.value = Array(repeating: value, count: (count > min) ? count : min) + self.min = min + self.max = nil + } + + /// Matches a `Set` of provided types or objects. + /// + /// Expects the API provider to return a `Set` with minimum of `1` and `max` occurances of the provided element. + /// + /// - Parameters: + /// - value: Expected type or object + /// - max: Maximum expected number of occurances of provided `value` + /// - count: Number of elements to generate for consumer tests + /// + /// - Precondition: `max` must be a positive value and not greater than `count`. + /// + public init(_ value: Any, max: Int, count: Int = 1) { + self.value = Array(repeating: value, count: (count > max) ? max : count) + self.min = nil + self.max = max + } + + /// Matches a `Set` of provided types or objects. + /// + /// Expects the API provider to return a `Set` with minimum of `min` and `max` occurances of the provided element. + /// + /// - Parameters: + /// - value: Expected type or object + /// - min: Minimum expected number of occurances of provided `value` + /// - max: Maximum expected number of occurances of provided `value` + /// - count: Number of elements to generate for consumer tests + /// + /// - Precondition: `min` must either be 0 or a positive value. `max` must be a positive value. + /// Lesser of the two values will be used as `min` and greater of the two will be used as `max`. + /// + /// - Precondition: `count` must be a value between `min` and `max`, else either `min` or `max` is used to generate the number of examples. + /// + public init(_ value: Any, min: Int, max: Int, count: Int = 1) { + self.value = Array(repeating: value, count: count < min ? min : (count > max) ? max : count) + self.min = Swift.min(min, max) + self.max = Swift.max(min, max) + } + } + +} + +// MARK: - Objective-C + +@objc(PFMatcherEachLike) +public class ObjcEachLike: NSObject, ObjcMatcher { + + let type: MatchingRuleExpressible + + /// Matches a `Set` of provided types or objects. + /// + /// - Parameters + /// - value: Expected type or object + /// - count: Number of elements to generate for consumer tests + /// + @objc(value: count:) + public init(value: Any, count: Int = 1) { + type = Matcher.EachLike(value, count: count) + } + + /// Matches a `Set` of provided types or objects. + /// + /// - Parameters: + /// - value: Expected type or object + /// - min: Minimum expected number of occurances of provided `value` + /// - max: Maximum expected number of occurances of provided `value` + /// - count: Number of elements to generate for consumer tests + /// + /// - Precondition: `min` must either be 0 or a positive value. `max` must be a positive value. + /// Lesser of the two values will be used as `min` and greater of the two will be used as `max`. + /// + /// - Precondition: `count` must be a value between `min` and `max`, else either `min` or `max` is used to generate the number of examples. + /// + @objc(value: min: max: count:) + public init(value: Any, min: Int, max: Int, count: Int = 1) { + type = Matcher.EachLike(value, min: min, max: max, count: count) + } + +} diff --git a/Sources/Matchers/EqualTo.swift b/Sources/Matchers/EqualTo.swift new file mode 100644 index 00000000..92ba0ed1 --- /dev/null +++ b/Sources/Matchers/EqualTo.swift @@ -0,0 +1,69 @@ +// +// Created by Marko Justinek on 11/4/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +public extension Matcher { + + /// Matches the API provided value varbatim. + /// + /// Use this matcher where you expect the exact type and value + /// in the interaction between your consumer and your provider. + /// + /// ``` + /// // DSL + /// [ + /// "foo": Matcher.EqualTo("bar"), + /// "bar": Matcher.EqualTo(847) + /// ] + /// ``` + /// + struct EqualTo: MatchingRuleExpressible { + + internal let value: Any + internal let rules: [[String: AnyEncodable]] = [["match": AnyEncodable("equality")]] + + // MARK: - Initializers + + /// Matches the API provided value varbatim. + /// + /// - parameter value: The exact value that is expected + /// + public init(_ value: Any) { + self.value = value + } + } + +} + +// MARK: - Objective-C + +@objc(PFMatcherEqualTo) +public class ObjcEqualTo: NSObject, ObjcMatcher { + + let type: MatchingRuleExpressible + + /// Matches the API provided value varbatim. + /// + /// - parameter value: The exact value that is expected + /// + @objc(value:) + public init(value: Any) { + type = Matcher.EqualTo(value) + } + +} diff --git a/Sources/Matchers/FromProviderState.swift b/Sources/Matchers/FromProviderState.swift new file mode 100644 index 00000000..589028ad --- /dev/null +++ b/Sources/Matchers/FromProviderState.swift @@ -0,0 +1,134 @@ +// +// Created by Marko Justinek on 15/8/21. +// Copyright © 2021 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +public extension Matcher { + + /// Matches the value provided by the provider state + struct FromProviderState: MatchingRuleExpressible, PactPathParameter { + + /// The type of Provider State provided parameter + public enum ParameterType { + case bool(Bool) + case double(Double) + case float(Float) + case int(Int) + case string(String) + case decimal(Decimal) + } + + internal var value: Any + internal let parameter: String + internal let rules: [[String: AnyEncodable]] = [["match": AnyEncodable("type")]] + + /// Matches the value provided by the provider state + /// + /// - Parameters: + /// - parameter: The provider state parameter name + /// - value: The value to use in consumer test + /// + public init(parameter: String, value: ParameterType) { + self.parameter = parameter + + switch value { + case .bool(let bool): self.value = bool + case .decimal(let decimal): self.value = decimal + case .double(let double): self.value = double + case .float(let float): self.value = float + case .int(let int): self.value = int + case .string(let string): self.value = string + } + } + } + +} + +// MARK: - Objective-C + +@objc(PFMatcherFromProviderState) +public class ObjcFromProviderState: NSObject, ObjcMatcher { + + let type: MatchingRuleExpressible + + /// Matches the value provided by the provider state + /// + /// - Parameters: + /// - parameter: The provider state parameter name + /// - value: The `Bool` value to use in consumer test + /// + @objc(parameter: withBoolValue:) + public init(parameter: String, value: Bool) { + type = Matcher.FromProviderState(parameter: parameter, value: .bool(value)) + } + + /// Matches the value provided by the provider state + /// + /// - Parameters: + /// - parameter: The provider state parameter name + /// - value: The `Double` value to use in consumer test + /// + @objc(parameter: withDoubleValue:) + public init(parameter: String, value: Double) { + type = Matcher.FromProviderState(parameter: parameter, value: .double(value)) + } + + /// Matches the value provided by the provider state + /// + /// - Parameters: + /// - parameter: The provider state parameter name + /// - value: The `Float` value to use in consumer test + /// + @objc(parameter: withFloatValue:) + public init(parameter: String, value: Float) { + type = Matcher.FromProviderState(parameter: parameter, value: .float(value)) + } + + /// Matches the value provided by the provider state + /// + /// - Parameters: + /// - parameter: The provider state parameter name + /// - value: The `Int` value to use in consumer test + /// + @objc(parameter: withIntValue:) + public init(parameter: String, value: Int) { + type = Matcher.FromProviderState(parameter: parameter, value: .int(value)) + } + + /// Matches the value provided by the provider state + /// + /// - Parameters: + /// - parameter: The provider state parameter name + /// - value: The `String` value to use in consumer test + /// + @objc(parameter: withStringValue:) + public init(parameter: String, value: String) { + type = Matcher.FromProviderState(parameter: parameter, value: .string(value)) + } + + /// Matches the value provided by the provider state + /// + /// - Parameters: + /// - parameter: The provider state parameter name + /// - value: The `Decimal` value to use in consumer test + /// + @objc(parameter: withDecimalValue:) + public init(parameter: String, value: Decimal) { + type = Matcher.FromProviderState(parameter: parameter, value: .decimal(value)) + } + +} diff --git a/Sources/Matchers/IncludesLike.swift b/Sources/Matchers/IncludesLike.swift new file mode 100644 index 00000000..09fcdd6a --- /dev/null +++ b/Sources/Matchers/IncludesLike.swift @@ -0,0 +1,116 @@ +// +// Created by Marko Justinek on 11/4/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +public extension Matcher { + + /// Matches response that contains any or all expected values. + /// + /// Use this matcher where you expect either all or + /// at least one of the provided `String` values to be present + /// in the response. + /// + /// ``` + /// [ + /// "foo": Matcher.IncludesLike("1", "Jane", "John", combine: .OR, generate: "Jane has 1 pony, John has none"), + /// "bar": Matcher.IncludesLike(["1", "Jane", "John"], combine: .AND) + /// ] + /// ``` + /// + struct IncludesLike: MatchingRuleExpressible { + public enum IncludeCombine: String { + case AND + case OR + } + + internal let value: Any + internal let combine: IncludeCombine + internal var rules: [[String: AnyEncodable]] { + includeStringValues.map { + [ + "match": AnyEncodable("include"), + "value": AnyEncodable($0), + ] + } + } + + private var includeStringValues: [String] + + // MARK: - Initializers + + /// Matches response that contains a `Set` of `String` values. + /// + /// - Parameters: + /// - values: The expected values to be returned + /// - combine: Defines whether matches are combined with logical `AND` or `OR` + /// - generate: The value to generate during tests + /// + public init(_ values: String..., combine: IncludeCombine = .AND, generate: String? = nil) { + self.value = generate ?? values.joined(separator: " ") + self.includeStringValues = values + self.combine = combine + } + + /// Matches response that contains a `Set` of `String` values. + /// + /// - Parameters: + /// - values: The expected values to be returned + /// - combine: Defines whether matches are combined with logical `AND` or `OR` + /// - generate: The value to generate during tests + /// + public init(_ values: [String], combine: IncludeCombine = .AND, generate: String? = nil) { + self.value = generate ?? values.joined(separator: " ") + self.includeStringValues = values + self.combine = combine + } + } + +} + +// MARK: - Objective-C + +@objc(PFMatcherIncludesLike) +public class ObjcIncludesLike: NSObject, ObjcMatcher { + + let type: MatchingRuleExpressible + + /// Matches response that contains all of the expected values. + /// + /// - Parameters: + /// - values: The expected values to be returned + /// - combine: Defines whether matches are combined with logical AND or OR + /// - generate: The value to generate during tests + /// + @objc(includesAll: generate:) + public init(includesAll: [String], generate: String?) { + type = Matcher.IncludesLike(includesAll, combine: .AND, generate: generate) + } + + /// Matches response that contains a any of the expected values. + /// + /// - Parameters: + /// - values: The expected values to be returned + /// - combine: Defines whether matches are combined with logical AND or OR + /// - generate: The value to generate during tests + /// + @objc(includesAny: generate:) + public init(includesAny: [String], generate: String?) { + type = Matcher.IncludesLike(includesAny, combine: .OR, generate: generate) + } + +} diff --git a/Sources/Matchers/IntegerLike.swift b/Sources/Matchers/IntegerLike.swift new file mode 100644 index 00000000..64334d23 --- /dev/null +++ b/Sources/Matchers/IntegerLike.swift @@ -0,0 +1,63 @@ +// +// Created by Marko Justinek on 11/4/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +public extension Matcher { + + /// Matches an `Int` type. + /// + /// Use this matcher where you expect an `Int` to be returned + /// by your API provider. + /// + struct IntegerLike: MatchingRuleExpressible { + + internal let value: Any + internal let rules: [[String: AnyEncodable]] = [["match": AnyEncodable("integer")]] + + // MARK: - Initializer + + /// Matches an `Int` type. + /// + /// - Parameters: + /// - value: Value to use in tests + /// + public init(_ value: Int) { + self.value = value + } + } + +} + +// MARK: - Objective-C + +@objc(PFMatcherIntegerLike) +public class ObjcIntegerLike: NSObject, ObjcMatcher { + + let type: MatchingRuleExpressible + + /// Matches an `Int` type. + /// + /// - Parameters: + /// - value: Value to use in tests + /// + @objc(value:) + public init(value: Int) { + type = Matcher.IntegerLike(value) + } + +} diff --git a/Sources/Matchers/MatchNull.swift b/Sources/Matchers/MatchNull.swift new file mode 100644 index 00000000..0dedac6e --- /dev/null +++ b/Sources/Matchers/MatchNull.swift @@ -0,0 +1,50 @@ +// +// Created by Marko Justinek on 9/10/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +public extension Matcher { + + /// Matches a `null` value. + struct MatchNull: MatchingRuleExpressible { + + internal let value: Any + internal let rules: [[String: AnyEncodable]] = [["match": AnyEncodable("null")]] + + // MARK: - Initializer + + /// The value returned by API provider matches `null`. + public init() { + value = "pact_matcher_null" + } + } + +} + +// MARK: - Objective-C + +@objc(PFMatcherNull) +public class ObjcMatchNull: NSObject, ObjcMatcher { + + let type: MatchingRuleExpressible = Matcher.MatchNull() + + /// The value returned by API provider matches `null`. + @objc public override init() { + super.init() + } + +} diff --git a/Sources/Matchers/Matcher.swift b/Sources/Matchers/Matcher.swift new file mode 100644 index 00000000..0cfef8cf --- /dev/null +++ b/Sources/Matchers/Matcher.swift @@ -0,0 +1,100 @@ +// +// Created by Marko Justinek on 10/9/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +public struct Matcher { + // This is a namespace placeholder. + // Implement any matchers as `Struct`s in a Matcher extension. + + // ⚠️ IMPORTANT ⚠️ + // Make sure PactBuilder.swift handles the the matcher + // There is a bug in Swift where protocols are not handled properly when + // used as generics. + + // Matchers are Encodable objects that conform to the defined structure. + // When DSL is being processed the Matcher object is created and added to Pact. + // Every Matcher conforms to MatchingRuleExpressible protocol which defines that + // at least `value` and `rules` keys are defined. Generally a Matcher would + // set its type as the `match` value in the rules key. That then matches the agreed matcher + // configuration and attributes defined in Pact Specification version 3 (see link below). + // + // We use `AnyEncodable` type eraser because we can not predict what type the user will provide. + // + // As an example, the `RegexLike` matcher would use the `value` user provides, sets + // the `match` value as `regex`, following the specification, and `regex` key's value + // is set as the regex `pattern` the user provides. + // + // Imagine the following properties of a `RegexMatcher` example: + // + // let value: Any + // let pattern: String = #"\d{8}"# + // let rules: [[String: AnyEncodable]] = ["match": AnyEncodable("regex"), "regex": AnyEncodable(pattern)] + // + // would generate a matchers object for the jsonPath where the matcher was used: + // + // "matchers": [ + // { + // "match": "regex", + // "regex": "\\d{8}" + // } + // ] + // + // This JSON object is applied to the specific jsonPath whilst the DSL structure is being processed. + // + // Example: + // + // // DSL + // .willRespondWith( + // body = [ + // "eightDigits": Matcher.RegexLike(value: "12345678", pattern: #"\d{8}"#) + // ] + // ) + // + // // Extract from Pact contract (JSON file) + // "response": { + // "body": { + // "eightDigits": "12345678" + // } + // } + // ... + // "matchingRules": { + // "body": { + // "$.eightDigits": { + // "matchers": [ + // { + // "match": "regex", + // "regex": "\\d{8}" + // } + // ] + // } + // } + // } + // + // See: https://github.com/pact-foundation/pact-specification/tree/version-3#matchers + // + +} + +// MARK: - Objective-C + +/// Acts as a bridge defining the Swift specific Matcher type +protocol ObjcMatcher { + + var type: MatchingRuleExpressible { get } + +} diff --git a/Sources/Matchers/OneOf.swift b/Sources/Matchers/OneOf.swift new file mode 100644 index 00000000..7f54d3cc --- /dev/null +++ b/Sources/Matchers/OneOf.swift @@ -0,0 +1,93 @@ +// +// Created by Marko Justinek on 9/7/21. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +public extension Matcher { + + /// Defines a Pact matcher that validates against one of the provided values + /// + /// Case sensitive. Uses the first provided value in consumer tests. Removes duplicate values. + /// Use this matcher when you're expecting API response values to fit an `enum` type. + /// + struct OneOf: MatchingRuleExpressible { + internal let value: Any + internal let pattern: String + + internal var rules: [[String: AnyEncodable]] { + [ + [ + "match": AnyEncodable("regex"), + "regex": AnyEncodable(pattern), + ], + ] + } + + // MARK: - Initializer + + /// Matches one of the provided values + /// + /// - Parameters: + /// - values: List of possible values + /// + /// Case sensitive. Uses the first provided value in the consumer test. Removes duplicated values. + /// + init(_ values: AnyHashable...) { + self.init(values: values) + } + + /// Matches one of the provided values + /// + /// - Parameters: + /// - values: The array of possible values + /// + /// Case sensitive. Uses the first provided value in the consumer test. Removes duplicated values. + /// + public init(values: [AnyHashable]) { + self.value = values.first as Any + self.pattern = Self.regexed(values) + } + + // MARK: - Private + + private static func regexed(_ values: [AnyHashable]) -> String { + "^(\(values.unique.map { "\($0)" }.joined(separator: "|")))$" + } + } + +} + +// MARK: - Objective-C + +@objc(PFMatcherOneOf) +public class ObjcOneOf: NSObject, ObjcMatcher { + + let type: MatchingRuleExpressible + + /// Matches one of the provided values + /// + /// - Parameters: + /// - values: The array of possible values + /// + /// Case sensitive. Uses the first provided value in the consumer test. Removes duplicated values. + /// + @objc(oneOfFloat:) + public init(values: [AnyHashable]) { + type = Matcher.OneOf(values: values) + } + +} diff --git a/Sources/Matchers/RegexLike.swift b/Sources/Matchers/RegexLike.swift new file mode 100644 index 00000000..ef2e357a --- /dev/null +++ b/Sources/Matchers/RegexLike.swift @@ -0,0 +1,112 @@ +// +// Created by Marko Justinek on 11/4/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation +import XCTest + +public extension Matcher { + + /// Matches a value that fits the provided `regex` pattern + /// + /// This matcher can be used in request `path`. + /// + /// ``` + /// [ + /// "foo": Matcher.RegexLike(value: "2020-04-27", pattern: "\d{4}-\d{2}-\d{2}") + /// ] + /// ``` + /// + struct RegexLike: MatchingRuleExpressible, PactPathParameter { + internal let value: Any + internal let pattern: String + + internal var rules: [[String: AnyEncodable]] { + [ + [ + "match": AnyEncodable("regex"), + "regex": AnyEncodable(pattern), + ], + ] + } + + // MARK: - Iitializer + + /// Matches a value that fits the provided `regex` term + /// + /// - Parameters: + /// - value: The value to be used in tests + /// - term: The regex term that describes the `value` + /// + /// This matcher can be used in request `path`. + /// + @available(*, deprecated, message: "Use `.init(value:pattern:) instead") + public init(_ value: String, term: String) { + self.value = value + self.pattern = term + } + + /// Matches a value that fits the provided `regex` pattern + /// + /// - Parameters: + /// - value: The value to be used in tests + /// - pattern: The regex term that describes the `value` + /// + /// This matcher can be used in request `path`. + /// + public init(value: String, pattern: String) { + self.value = value + self.pattern = pattern + } + } + +} + +// MARK: - Objective-C + +@objc(PFMatcherRegexLike) +public class ObjcRegexLike: NSObject, ObjcMatcher { + + let type: MatchingRuleExpressible + + /// Matches a value that fits the provided `regex` term. + /// + /// - Parameters: + /// - value: The value to be used in tests + /// - term: The regex term that describes the `value` + /// + /// This matcher can be used in request `path`. + /// + @available(*, deprecated, message: "Use `.init(value:pattern:) instead") + @objc(value: term:) + public init(value: String, term: String) { + type = Matcher.RegexLike(value: value, pattern: term) + } + + /// Matches a value that fits the provided `regex` pattern + /// + /// - Parameters: + /// - value: The value to be used in tests + /// - pattern: The regex term that describes the `value` + /// + /// This matcher can be used in request `path`. + /// + @objc(value: pattern:) + public init(value: String, pattern: String) { + type = Matcher.RegexLike(value: value, pattern: pattern) + } + +} diff --git a/Sources/Matchers/SomethingLike.swift b/Sources/Matchers/SomethingLike.swift new file mode 100644 index 00000000..1a5fc8f9 --- /dev/null +++ b/Sources/Matchers/SomethingLike.swift @@ -0,0 +1,69 @@ +// +// Created by Marko Justinek on 9/4/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +public extension Matcher { + + /// Matches a `Type`. + /// + /// Use this matcher where you expect a specific `Type` to be + /// returned by the API provider. + /// + /// ``` + /// [ + /// "foo": Matcher.SomethingLike("bar"), // Matches a `String` + /// "bar": Matcher.SomethingLike(1) // Matches an `Int` + /// ] + /// ``` + /// + struct SomethingLike: MatchingRuleExpressible { + internal let value: Any + internal let rules: [[String: AnyEncodable]] = [["match": AnyEncodable("type")]] + + // MARK: - Initializers + + /// Matches the provided `Type`. + /// + /// - Parameters: + /// - value: Value to use in tests + /// + public init(_ value: Any) { + self.value = value + } + } + +} + +// MARK: - Objective-C + +@objc(PFMatcherSomethingLike) +public class ObjcSomethingLike: NSObject, ObjcMatcher { + + let type: MatchingRuleExpressible + + /// Matches the provided `Type`. + /// + /// - Parameters: + /// - value: Value to use in tests + /// + @objc(value:) + public init(value: Any) { + type = Matcher.SomethingLike(value) + } + +} diff --git a/Sources/MockService+Concurrency.swift b/Sources/MockService+Concurrency.swift new file mode 100644 index 00000000..29407402 --- /dev/null +++ b/Sources/MockService+Concurrency.swift @@ -0,0 +1,141 @@ +// +// Created by Marko Justinek on 5/8/2023. +// Copyright © 2023 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +#if canImport(_Concurrency) && compiler(>=5.7) +@_implementationOnly import PactSwiftMockServer + +public extension MockService { + + /// Runs the Pact test against the code making the API request + /// + /// - Parameters: + /// - file: The file to report the failing test in + /// - line: The line on which to report the failing test + /// - verify: An array of specific `Interaction`s to verify. If none provided, the latest defined interaction is used + /// - timeout: Time before the test times out. Default is 10 seconds + /// - testFunction: Your async code making the API request + /// + /// The `testFunction` closure is passed a `String` representing the url of the active Mock Server. + /// + /// ``` + /// try await mockService.run { baseURL in + /// // async code making the request with provided `baseURL` + /// // assert response can be processed + /// } + /// ``` + /// + @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) + func run(_ file: FileString? = #file, line: UInt? = #line, verify interactions: [Interaction]? = nil, timeout: TimeInterval? = nil, testFunction: @escaping @Sendable (_ baseURL: String) async throws -> Void) async throws { + // Use the provided set or if not provided only the current interaction + pact.interactions = interactions ?? [currentInteraction] + + if checkForInvalidInteractions(pact.interactions, file: file, line: line) { + // Remove interactions with errors + pact.interactions.removeAll { $0.encodingErrors.isEmpty == false } + self.interactions.removeAll() + } else { + // Prepare a brand spanking new MockServer (Mock Provider) on its own port + let mockServer = MockServer() + + // Set the expectations so we don't wait for this async magic indefinitely + try await setupPactInteraction(timeout: timeout ?? Constants.kTimeout, file: file, line: line, mockServer: mockServer, testFunction: testFunction) + + // At the same time start listening to verification that Mock Server received the expected request + try await verifyPactInteraction(timeout: timeout ?? Constants.kTimeout, file: file, line: line, mockServer: mockServer) + } + } +} + +// MARK: - Internal + +extension MockService { + + @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) + func setupPactInteraction(timeout: TimeInterval, file: FileString?, line: UInt?, mockServer: MockServer, testFunction: @escaping @Sendable (String) async throws -> Void) async throws { + Logger.log(message: "Setting up pact test", data: pact.data) + do { + // Set up a Mock Server with Pact data and on desired http protocol + try await mockServer.setup(pact: pact.data!, protocol: transferProtocolScheme) + + // If Mock Server spun up, run the test function + let task = Task(timeout: timeout) { + try await testFunction(mockServer.baseUrl) + } + // await task completion (value is Void) + try await task.value + } catch { + // Failed to spin up a Mock Server. This could be due to bad Pact data. Most likely to Pact data. + failWith((error as? MockServerError)?.description ?? error.localizedDescription, file: file, line: line) + throw error + } + } + + @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) + func verifyPactInteraction(timeout: TimeInterval, file: FileString?, line: UInt?, mockServer: MockServer) async throws { + do { + let task = Task(timeout: timeout) { + try await mockServer.verify() + } + // await task completion (value is discarded) + _ = try await task.value + + // If the comsumer (in testFunction:) made the promised request to Mock Server, go and finalize the test. + // Only finalize when running in simulator or macOS. Running on a physical iOS device makes little sense due to + // writing a pact file to device's disk. `libpact_ffi` does the actual file writing it writes it onto the + // disk of the device it is being run on. + #if targetEnvironment(simulator) || os(macOS) + let message = try await finalize(file: file, line: line) + Logger.log(message: message, data: self.pact.data) + #else + print("[INFO]: Running on an iOS device. Writing Pact interaction into a contract skipped.") + #endif + } catch { + failWith((error as? MockServerError)?.description ?? error.localizedDescription, file: file, line: line) + throw error + } + } + + /// Writes a Pact contract file in JSON format + /// + /// By default Pact contracts are written to `/tmp/pacts` folder. + /// Set `PACT_OUTPUT_DIR` to `$(PATH)/to/desired/dir/` in `Build` phase of your `Scheme` to change the location. + /// + @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) + func finalize(file: FileString? = nil, line: UInt? = nil) async throws -> String { + // Spin up a fresh Mock Server with a directory to write to + let mockServer = MockServer(directory: pactsDirectory, merge: self.merge) + + // Gather all the interactions this MockService has received to set up and prepare Pact data with them all + pact.interactions = interactions.filter { $0.encodingErrors.isEmpty } + + // Validate the Pact `Data` is hunky dory + guard let pactData = pact.data else { + throw MockServerError.nullPointer + } + + // Ask Mock Server to do the actual Pact file writing to disk + do { + return try await mockServer.finalize(pact: pactData) + } catch { + failWith((error as? MockServerError)?.description ?? error.localizedDescription, file: file, line: line) + throw error + } + } +} +#endif diff --git a/Sources/MockService+Extension.swift b/Sources/MockService+Extension.swift new file mode 100644 index 00000000..88b95cc7 --- /dev/null +++ b/Sources/MockService+Extension.swift @@ -0,0 +1,96 @@ +// +// Created by Marko Justinek on 5/8/2023. +// Copyright © 2023 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation +import XCTest + +#if compiler(>=5.5) +@_implementationOnly import PactSwiftMockServer +#else +import PactSwiftMockServer +#endif + +extension MockService { + + /// Check there are no invalid interactions + func checkForInvalidInteractions(_ interactions: [Interaction], file: FileString? = nil, line: UInt? = nil) -> Bool { + let errors = interactions.flatMap(\.encodingErrors) + for error in errors { + failWith(error.localizedDescription, file: file, line: line) + } + return errors.isEmpty == false + } + + /// Writes a Pact contract file in JSON format + /// + /// - parameter completion: Result of the writing the Pact contract to JSON + /// + /// By default Pact contracts are written to `/tmp/pacts` folder. + /// Set `PACT_OUTPUT_DIR` to `$(PATH)/to/desired/dir/` in `Build` phase of your `Scheme` to change the location. + /// + func finalize(file: FileString? = nil, line: UInt? = nil, completion: ((Result) -> Void)? = nil) { + // Spin up a fresh Mock Server with a directory to write to + let mockServer = MockServer(directory: pactsDirectory, merge: self.merge) + + // Gather all the interactions this MockService has received to set up and prepare Pact data with them all + pact.interactions = interactions.filter { $0.encodingErrors.isEmpty } + + // Validate the Pact `Data` is hunky dory + guard let pactData = pact.data else { + completion?(.failure(.nullPointer)) + return + } + + // Ask Mock Server to do the actual Pact file writing to disk + mockServer.finalize(pact: pactData) { [unowned self] in + switch $0 { + case .success(let message): + completion?(.success(message)) + case .failure(let error): + failWith(error.description) + completion?(.failure(error)) + } + } + } + + /// Waits for test to be completed and fails if timed out + func waitForPactTestWith(timeout: TimeInterval, file: FileString?, line: UInt?, action: (@escaping () -> Void) -> Void) { + let expectation = XCTestExpectation(description: "waitForPactTest") + action { + expectation.fulfill() + } + + let result = XCTWaiter().wait(for: [expectation], timeout: timeout) + if result != .completed { + let message = "Test did not complete within \(timeout) second timeout! Did you run testCompleted() block?" + if let file = file, let line = line { + errorReporter.reportFailure(message, file: file, line: line) + } else { + errorReporter.reportFailure(message) + } + } + } + + /// Fail the test and raise the failure in `file` at `line` + func failWith(_ message: String, file: FileString? = nil, line: UInt? = nil) { + if let file = file, let line = line { + errorReporter.reportFailure(message, file: file, line: line) + } else { + errorReporter.reportFailure(message) + } + } +} diff --git a/Sources/MockService.swift b/Sources/MockService.swift new file mode 100644 index 00000000..f7f83cff --- /dev/null +++ b/Sources/MockService.swift @@ -0,0 +1,218 @@ +// +// Created by Marko Justinek on 15/4/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +#if compiler(>=5.5) +@_implementationOnly import PactSwiftMockServer +#else +import PactSwiftMockServer +#endif + +/// Initializes a `MockService` object that handles Pact interaction testing. +/// +/// When initializing with `.secure` scheme, the SSL certificate on Mock Server +/// is a self-signed certificate. +/// +open class MockService { + + // MARK: - Private properties + + var pact: Pact + var interactions: [Interaction] = [] + var currentInteraction: Interaction! + let errorReporter: ErrorReportable + let pactsDirectory: URL? + let merge: Bool + + var transferProtocolScheme: PactSwiftMockServer.TransferProtocol + + // MARK: - Initializers + + /// Initializes a `MockService` object that handles Pact interaction testing + /// + /// When initializing with `.secure` scheme, the SSL certificate on Mock Server + /// is a self-signed certificate + /// + /// - Parameters: + /// - consumer: Name of the API consumer (eg: "mobile-app") + /// - provider: Name of the API provider (eg: "auth-service") + /// - scheme: HTTP scheme + /// - writePactTo: The directory where to write the contract + /// - merge: Whether to merge interactions with an existing Pact contract + /// + public convenience init( + consumer: String, + provider: String, + scheme: TransferProtocol = .standard, + writePactTo directory: URL? = nil, + merge: Bool = true + ) { + self.init(consumer: consumer, provider: provider, scheme: scheme, directory: directory, merge: merge, errorReporter: ErrorReporter()) + } + + /// Initializes a `MockService` object that handles Pact interaction testing + /// + /// When initializing with `.secure` scheme, the SSL certificate on Mock Server + /// is a self-signed certificate. + /// + /// - Parameters: + /// - consumer: Name of the API consumer (eg: "mobile-app") + /// - provider: Name of the API provider (eg: "auth-service") + /// - scheme: HTTP scheme + /// - directory: The directory where to write the contract + /// - merge: Whether to merge interactions with an existing Pact contract + /// - errorReporter: Injectable object to intercept errors + /// + internal init( + consumer: String, + provider: String, + scheme: TransferProtocol = .standard, + directory: URL? = nil, + merge: Bool = true, + errorReporter: ErrorReportable? = nil + ) { + pact = Pact(consumer: Pacticipant.consumer(consumer), provider: Pacticipant.provider(provider)) + self.errorReporter = errorReporter ?? ErrorReporter() + self.transferProtocolScheme = scheme.bridge + self.pactsDirectory = directory + self.merge = merge + } + + // MARK: - Interface + + /// Describes the `Interaction` between the consumer and provider + /// + /// It is important that the `description` and provider state + /// combination is unique per consumer-provider contract. + /// + /// - parameter description: A description of the API interaction + /// + @discardableResult + public func uponReceiving(_ description: String) -> Interaction { + currentInteraction = Interaction().uponReceiving(description) + interactions.append(currentInteraction) + return currentInteraction + } + + /// Runs the Pact test against the code making the API request + /// + /// - Parameters: + /// - file: The file to report the failing test in + /// - line: The line on which to report the failing test + /// - verify: An array of specific `Interaction`s to verify. If none provided, the latest defined interaction is used + /// - timeout: Time before the test times out. Default is 10 seconds + /// - testFunction: Your code making the API request + /// + /// The `testFunction` block passes two values to your unit test. A `String` representing + /// the url of the active Mock Server and a `Void` function that you call when you are done with your unit test. + /// You must call this function within your `testFunction:` completion block when your test completes. It signals PactSwift + /// that your test finished. If you do not call it then your test will time out. + /// + /// ``` + /// mockService.run { baseURL, done in + /// // code making the request with provided `baseURL` + /// // assert response can be processed + /// done() + /// } + /// ``` + /// + public func run(_ file: FileString? = #file, line: UInt? = #line, verify interactions: [Interaction]? = nil, timeout: TimeInterval? = nil, testFunction: @escaping (_ baseURL: String, _ done: (@escaping @Sendable () -> Void)) throws -> Void) { + // Use the provided set or if not provided only the current interaction + pact.interactions = interactions ?? [currentInteraction] + + if checkForInvalidInteractions(pact.interactions, file: file, line: line) { + // Remove interactions with errors + pact.interactions.removeAll { $0.encodingErrors.isEmpty == false } + self.interactions.removeAll() + } else { + // Prepare a brand spanking new MockServer (Mock Provider) on its own port + let mockServer = MockServer() + + // Set the expectations so we don't wait for this async magic indefinitely + setupPactInteraction(timeout: timeout ?? Constants.kTimeout, file: file, line: line, mockServer: mockServer, testFunction: testFunction) + + // At the same time start listening to verification that Mock Server received the expected request + verifyPactInteraction(timeout: timeout ?? Constants.kTimeout, file: file, line: line, mockServer: mockServer) + } + } +} + +// MARK: - Private + +private extension MockService { + + func setupPactInteraction(timeout: TimeInterval, file: FileString?, line: UInt?, mockServer: MockServer, testFunction: (String, @escaping (@Sendable () -> Void)) throws -> Void) { + waitForPactTestWith(timeout: timeout, file: file, line: line) { [unowned self] completion in + Logger.log(message: "Setting up pact test", data: pact.data) + + // Set up a Mock Server with Pact data and on desired http protocol + mockServer.setup(pact: pact.data!, protocol: transferProtocolScheme) { + switch $0 { + case .success: + do { + // If Mock Server spun up, run the test function + try testFunction(mockServer.baseUrl) { + completion() + } + } catch { + failWith("🚨 Error thrown in test function: \(error.localizedDescription)", file: file, line: line) + completion() + } + case .failure(let error): + // Failed to spin up a Mock Server. This could be due to bad Pact data. Most likely to Pact data. + failWith(error.description) + completion() + } + } + } + } + + func verifyPactInteraction(timeout: TimeInterval, file: FileString?, line: UInt?, mockServer: MockServer) { + waitForPactTestWith(timeout: timeout, file: file, line: line) { [unowned self] completion in + // Ask Mock Server to verify the promised request (testFunction:) has been made + mockServer.verify { + switch $0 { + case .success: + // If the comsumer (in testFunction:) made the promised request to Mock Server, go and finalize the test. + // Only finalize when running in simulator or macOS. Running on a physical iOS device makes little sense due to + // writing a pact file to device's disk. `libpact_ffi` does the actual file writing it writes it onto the + // disk of the device it is being run on. + #if targetEnvironment(simulator) || os(macOS) + finalize(file: file, line: line) { + switch $0 { + case .success(let message): + Logger.log(message: message, data: self.pact.data) + completion() + case .failure(let error): + self.failWith(error.description, file: file, line: line) + completion() + } + } + #else + print("[INFO]: Running on an iOS device. Writing Pact interaction into a contract skipped.") + completion() + #endif + + case .failure(let error): + failWith(error.description, file: file, line: line) + completion() + } + } + } + } +} diff --git a/Sources/Model/AnyEncodable.swift b/Sources/Model/AnyEncodable.swift new file mode 100644 index 00000000..e7d2ba98 --- /dev/null +++ b/Sources/Model/AnyEncodable.swift @@ -0,0 +1,43 @@ +// +// Created by Marko Justinek on 6/4/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +struct AnyEncodable: Encodable { + + private let _encode: (Encoder) throws -> Void + + init(_ value: T) { + self._encode = { encoder in + var container = encoder.singleValueContainer() + try container.encode(value) + } + } + + // Passing a `nil` as a generic type is not allowed so we are piggy-backing off of String type. + init(_ value: String?) { + self._encode = { encoder in + var container = encoder.singleValueContainer() + (value != nil) ? try container.encode(value) : try container.encodeNil() + } + } + + func encode(to encoder: Encoder) throws { + try _encode(encoder) + } + +} diff --git a/Sources/Model/Constants.swift b/Sources/Model/Constants.swift new file mode 100644 index 00000000..7e4f9e4d --- /dev/null +++ b/Sources/Model/Constants.swift @@ -0,0 +1,26 @@ +// +// Created by Marko Justinek on 31/7/21. +// Copyright © 2021 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +/// Contains constant values used across `PactSwift` +enum Constants { + + /// The default value for timeout when running Pact tests + static let kTimeout: TimeInterval = 10 + +} diff --git a/Sources/Model/EncodingError.swift b/Sources/Model/EncodingError.swift new file mode 100644 index 00000000..427ce568 --- /dev/null +++ b/Sources/Model/EncodingError.swift @@ -0,0 +1,35 @@ +// +// Created by Marko Justinek on 27/10/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +enum EncodingError: Error { + case encodingFailure(Any?) + case unknown + + var localizedDescription: String { + switch self { + case .encodingFailure(let message): + var errorMessage = ["Error preparing pact!"] + message.map { errorMessage.append(String(describing: $0)) } + return errorMessage.joined(separator: " ") + + default: + return "Error casting unknown type into an Encodable type!" + } + } +} diff --git a/Sources/Model/ErrorReportable.swift b/Sources/Model/ErrorReportable.swift new file mode 100644 index 00000000..b33ace5b --- /dev/null +++ b/Sources/Model/ErrorReportable.swift @@ -0,0 +1,27 @@ +// +// Created by Marko Justinek on 20/4/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +public typealias FileString = StaticString + +public protocol ErrorReportable { + + func reportFailure(_ message: String) + func reportFailure(_ message: String, file: FileString, line: UInt) + +} diff --git a/Sources/Model/ErrorReporter.swift b/Sources/Model/ErrorReporter.swift new file mode 100644 index 00000000..a9712c46 --- /dev/null +++ b/Sources/Model/ErrorReporter.swift @@ -0,0 +1,33 @@ +// +// Created by Marko Justinek on 20/4/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation +import XCTest + +class ErrorReporter: ErrorReportable { + + /// Reports test failure in file and on line where this method is called + func reportFailure(_ message: String) { + XCTFail(message, file: #file, line: #line) + } + + /// Reports test failure in provided file and on provided line + func reportFailure(_ message: String, file: FileString, line: UInt) { + XCTFail(message, file: file, line: line) + } + +} diff --git a/Sources/Model/ExampleGeneratorExpressible.swift b/Sources/Model/ExampleGeneratorExpressible.swift new file mode 100644 index 00000000..7b860c30 --- /dev/null +++ b/Sources/Model/ExampleGeneratorExpressible.swift @@ -0,0 +1,37 @@ +// +// Created by Marko Justinek on 11/9/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +protocol ExampleGeneratorExpressible { + + var value: Any { get } + var generator: ExampleGenerator.Generator { get } + var rules: [String: AnyEncodable]? { get } + +} + +extension ExampleGeneratorExpressible { + + var attributes: [String: AnyEncodable] { + var generatorAttributes: [String: AnyEncodable] = rules ?? [:] + generatorAttributes["type"] = AnyEncodable(generator.rawValue) + + return generatorAttributes + } + +} diff --git a/Sources/Model/Interaction.swift b/Sources/Model/Interaction.swift new file mode 100644 index 00000000..7bfaf6a3 --- /dev/null +++ b/Sources/Model/Interaction.swift @@ -0,0 +1,306 @@ +// +// Created by Marko Justinek on 1/4/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +/// Defines the interaction between a consumer and a provider +@objc public class Interaction: NSObject { + var interactionDescription: String? + var providerState: String? + var providerStates: [ProviderState]? + var request: Request? + var response: Response? + + internal var encodingErrors = [EncodingError]() +} + +extension Interaction: Encodable { + + enum CodingKeys: String, CodingKey { + case interactionDescription = "description" + case providerState + case providerStates + case request + case response + } + +} + +// MARK: - Initialization + +extension Interaction { + + @objc convenience init(description: String) { + self.init() + self.interactionDescription = description + } + + convenience init(description: String, providerState: String, request: Request? = nil, response: Response? = nil) { + self.init() + self.interactionDescription = description + self.providerState = providerState + self.request = request + self.response = response + } + + convenience init(description: String, providerStates: [ProviderState], request: Request? = nil, response: Response? = nil) { + self.init() + self.interactionDescription = description + self.providerStates = providerStates + self.request = request + self.response = response + } + +} + +// MARK: - Interface + +extension Interaction { + + /// Defines the description of the interaction + /// + /// It is important to provide a meaningful description so that + /// developers on the provider side can understand what the + /// intent of the interaction is. This description is also listed in + /// the list of interactions on Pact Broker. + /// + /// Example: + /// ``` + /// .uponReceiving("A request for a list of users") + /// ``` + /// + /// - Parameters: + /// - interactionDescription: A `String` describing the interaction + /// + @discardableResult + func uponReceiving(_ interactionDescription: String) -> Interaction { + self.interactionDescription = interactionDescription + return self + } + + /// Defines the provider state for the given interaction + /// + /// It is important to provide a meaningful description with + /// values that help prepare provider Pact tests. + /// + /// Example: + /// ```users exist``` + /// + /// - Parameters: + /// - providerState: Description of the state. + /// + @discardableResult + public func given(_ providerState: String) -> Interaction { + self.providerState = providerState + return self + } + + /// Defines the provider states with parameters for the given interaction + /// + /// It is important to provide a meaningful description with + /// values that help prepare provider side Pact tests. + /// + /// Example: + /// ``` + /// .given([ + /// ProviderState( + /// description: "user exists", + /// params: ["id": "1"] + /// ) + /// ]) + /// ``` + /// + /// - Parameters: + /// - providerStates: A list of provider states + /// + @discardableResult + public func given(_ providerStates: [ProviderState]) -> Interaction { + self.providerStates = providerStates + return self + } + + /// Defines the provider state for the given interaction + /// + /// It is important to provide a meaningful description with + /// values that help prepare provider side Pact tests. + /// + /// Example: + /// ``` + /// .given( + /// ProviderState( + /// description: "user exists", + /// params: ["id": "1", "name": "Mary"] + /// ), + /// ProviderState( + /// description: "user has children", + /// params: ["count": "2"] + /// ) + /// ) + /// ``` + /// + /// - Parameters: + /// - providerStates: A list of provider states + /// + public func given(_ providerStates: ProviderState...) -> Interaction { + given(providerStates) + } + + /// Defines the expected request for the interaction + /// + /// At a minimum the `method` and `path` are required to define an interaction. + /// + /// `query` expects a dictionary where the value is an array conforming + /// to `String` or a string `Matcher`. + /// + /// ``` + /// // Verbatim matching for '?states=VIC,NSW,ACT' + /// query: ["states": ["ACT", "NSW", "VIC"]] + /// + /// // Matching using a string Matcher for + /// query: ["states": [Matcher.SomethingLike("VIC")]] // or + /// query: ["states": [Matcher.IncludesLike("VIC")] + /// ``` + /// + /// - Parameters: + /// - method: The HTTP method of the request + /// - path: The URL path of the request + /// - query: The query parameters of the request + /// - headers: The header parameters of the request + /// - body: The body of the request + /// + @discardableResult + public func withRequest(method: PactHTTPMethod, path: PactPathParameter, query: [String: [Any]]? = nil, headers: [String: Any]? = nil, body: Any? = nil) -> Interaction { + do { + self.request = try Request(method: method, path: path, query: query, headers: headers, body: body) + } catch let requestError { + encodingErrors.append(requestError as! EncodingError) + Logger.log(message: "Can not prepare a Request with non-encodable data! \(requestError)") + } + + return self + } + + /// Defines the expected response for the interaction. It defines the + /// values `MockService` will respond with when it receives the expected + /// request as defined in this interaction. + /// + /// At a minimum the `status` is required to test an API response. + /// By not providing a value for `headers` or `body` it is understood + /// that the presence of those values in the response is _not required_ + /// but they can be present. + /// + /// - Parameters: + /// - status: The response status code + /// - headers: The response headers + /// - body: The response body + /// + @discardableResult + public func willRespondWith(status: Int, headers: [String: Any]? = nil, body: Any? = nil) -> Interaction { + do { + self.response = try Response(statusCode: status, headers: headers, body: body) + } catch let responseError { + encodingErrors.append(responseError as! EncodingError) + Logger.log(message: "Can not prepare a Response with non-encodable data! \(responseError)") + } + return self + } + + // MARK: - Objective-C - + + /// Defines the description of the interaction + /// + /// It is important to provide a meaningful description so that + /// developers on the provider side can understand what the + /// intent of the interaction is. This description is also listed in + /// the list of interactions on Pact Broker. + /// + /// - Parameters: + /// - interactionDescription: A `String` describing the interaction + /// + @discardableResult + @objc(uponReceivingARequestWithDescription:) + func objCUponReceiving(_ interactionDescription: String) -> Interaction { + uponReceiving(interactionDescription) + } + + /// Defines the provider state for the given interaction + /// + /// It is important to provide a meaningful description with + /// values that help prepare provider Pact tests. + /// + /// - Parameters: + /// - providerState: Description of the state. + /// + @discardableResult + @objc(givenProviderState:) + public func objCGiven(_ providerState: String) -> Interaction { + given(providerState) + } + + /// Defines the provider states with parameters for the given interaction + /// + /// It is important to provide a meaningful description with + /// values that help prepare provider side Pact tests. + /// + /// - Parameters: + /// - providerStates: A list of provider states + /// + @discardableResult + @objc(givenProviderStates:) + public func objCGiven(_ providerStates: [ObjCProviderState]) -> Interaction { + self.providerStates = providerStates.map { $0.state } + return self + } + + /// Defines the expected request for the interaction + /// + /// - Parameters: + /// - method: The HTTP method of the request + /// - path: The URL path of the request + /// - query: The query parameters of the request + /// - headers: The header parameters of the request + /// - body: The body of the request + /// + @discardableResult + @objc(withRequestHTTPMethod: path: query: headers: body:) + public func objCWithRequest(method: PactHTTPMethod, path: String, query: [String: [Any]]? = nil, headers: [String: Any]? = nil, body: Any? = nil) -> Interaction { + withRequest(method: method, path: path, query: query, headers: headers, body: body) + return self + } + + /// Defines the expected response for the interaction. It defines the + /// values `MockService` will respond with when it receives the expected + /// request as defined in this interaction. + /// + /// At a minimum the `status` is required to test an API response. + /// By not providing a value for `headers` or `body` it is understood + /// that the presence of those values in the response is _not required_ + /// but they can be present. + /// + /// - Parameters: + /// - status: The response status code + /// - headers: The response headers + /// - body: The response body + /// + @discardableResult + @objc(willRespondWithStatus: headers: body:) + public func objCWillRespondWith(status: Int, headers: [String: Any]? = nil, body: Any? = nil) -> Interaction { + willRespondWith(status: status, headers: headers, body: body) + return self + } + +} diff --git a/Sources/Model/MatchingRuleExpressible.swift b/Sources/Model/MatchingRuleExpressible.swift new file mode 100644 index 00000000..849e400d --- /dev/null +++ b/Sources/Model/MatchingRuleExpressible.swift @@ -0,0 +1,25 @@ +// +// Created by Marko Justinek on 9/4/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +protocol MatchingRuleExpressible { + + var value: Any { get } + var rules: [[String: AnyEncodable]] { get } + +} diff --git a/Sources/Model/Metadata.swift b/Sources/Model/Metadata.swift new file mode 100644 index 00000000..2689e3c1 --- /dev/null +++ b/Sources/Model/Metadata.swift @@ -0,0 +1,48 @@ +// +// Created by Marko Justinek on 1/4/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +struct Metadata { + + let pactSpec = PactVersion("3.0.0") + let pactSwift = PactVersion(pactSwiftVersion) + + struct PactVersion: Encodable { + let version: String + + init(_ version: String) { + self.version = version + } + } + +} + +extension Metadata: Encodable { + + enum CodingKeys: String, CodingKey { + case pactSpec = "pactSpecification" + case pactSwift + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(pactSpec, forKey: .pactSpec) + try container.encode(pactSwift, forKey: .pactSwift) + } + +} diff --git a/Sources/Model/Pact.swift b/Sources/Model/Pact.swift new file mode 100644 index 00000000..4506741b --- /dev/null +++ b/Sources/Model/Pact.swift @@ -0,0 +1,51 @@ +// +// Created by Marko Justinek on 1/4/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +struct Pact: Encodable { + + private let metadata = Metadata() + + // MARK: - Properties + + let consumer: Pacticipant + let provider: Pacticipant + + var interactions: [Interaction] = [] + + // These are the top level required nodes of a Pact contract file + var payload: [String: Any] { + [ + "consumer": consumer.name, + "provider": provider.name, + "interactions": interactions, + "metadata": metadata, + ] + } + + var data: Data? { + do { + let encoder = JSONEncoder() + return try encoder.encode(self) + } catch { + Logger.log(message: error.localizedDescription) + } + return nil + } + +} diff --git a/Sources/Model/PactBroker.swift b/Sources/Model/PactBroker.swift new file mode 100644 index 00000000..7c71f3a2 --- /dev/null +++ b/Sources/Model/PactBroker.swift @@ -0,0 +1,143 @@ +// +// Created by Marko Justinek on 19/8/21. +// Copyright © 2021 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +/// Pact broker configuration used when verifying a provider +public struct PactBroker { + + public enum Either { + case auth(A) + case token(T) + } + + /// Simple authentication with Pact Broker + public struct SimpleAuth { + let username: String + let password: String + + /// Initializes authentication configuration store + /// + /// - Parameters: + /// - username: The username to use when authenticating with Pact Broker + /// - password: The password to use when authenticating with Pact Broker + /// + public init(username: String, password: String) { + self.username = username + self.password = password + } + } + + /// API token store for a Pact Broker (Pactflow) + public struct APIToken { + let token: String + + /// Initializes the Broker API token store + /// + /// - Parameters: + /// - token: The authorization token + /// + public init(_ token: String) { + self.token = token + } + } + + /// Verification results handling + public struct VerificationResults { + let publishResults = true + let providerVersion: String + let providerTags: [String]? + + /// Initializes the VerificationResults object + /// + /// - Parameters: + /// - providerVersion: The semantical version of the Provider API + /// - providerTags: Provider tags to use when publishing results + /// + public init(providerVersion version: String, providerTags tags: [String]? = nil) { + self.providerVersion = version + self.providerTags = tags + } + } + + // MARK: - Properties + + /// The URL of Pact Broker for broker-based verification + let url: String + + /// Pact Broker authentication + let authentication: Either + + /// Whether to publish the verification results to the Pact Broker + let publishVerificationResult: Bool + + /// The name of provider being verified + let providerName: String + + /// Version of the provider being verified. + /// + /// Required when publishing results + let providerVersion: String? + + /// Consumer tags to verify + let consumerTags: [VersionSelector]? + + /// Provider tags to use when publishing results + let providerTags: [String]? + + /// Includes pending pacts in verification + let includePending: Bool? + + /// Include WIP pacts in verification + let includeWIP: WIPPacts? + + // MARK: - Initialization + + /// Pact broker configuration + /// + /// - Parameters: + /// - url: The URL of Pact Broker + /// - auth: The authentication option + /// - providerName: The name of provider being verified + /// - consumerTags: List of consumer pacts to verify + /// - includePending: Include pending pacts in verification + /// - includeWIP: Allow pacts that don't match given consumer selectors (or tags) to be verified, without causing the overall task to fail + /// - verificationResults: When provided it submits the verification results with given provider version + /// + /// You should only submit verification results during CI builds. See [Pact broker](https://docs.pact.io/pact_broker) for more. + /// + /// Pending pacts and WIP pacts features are available on [Pactflow](https://pactflow.io/) by default, + /// and requires [configuration](https://docs.pact.io/pact_broker/advanced_topics/wip_pacts/) if using a self-hosted broker. + /// + /// - Warning: When including WIP pacts, `includePending` will be set to `true`. + /// + public init(url: URL, auth: Either, providerName: String, consumerTags: [VersionSelector]? = nil, includePending: Bool? = nil, includeWIP: WIPPacts? = nil, publishResults verificationResults: VerificationResults? = nil) { + self.url = url.absoluteString + self.authentication = auth + self.providerName = providerName + + self.consumerTags = consumerTags + self.includePending = includePending + self.includeWIP = includeWIP + + self.publishVerificationResult = verificationResults?.publishResults ?? false + // Provider version is only required when publishing verification results + self.providerVersion = verificationResults?.providerVersion + self.providerTags = verificationResults?.providerTags + } + +} diff --git a/Sources/Model/PactHTTPMethod.swift b/Sources/Model/PactHTTPMethod.swift new file mode 100644 index 00000000..52f50e4d --- /dev/null +++ b/Sources/Model/PactHTTPMethod.swift @@ -0,0 +1,62 @@ +// +// Created by Marko Justinek on 31/3/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +/// The HTTP method expected in the interaction +@objc public enum PactHTTPMethod: Int { + case GET + case HEAD + case POST + case PUT + case PATCH + case DELETE + case TRACE + case CONNECT + case OPTIONS +} + +extension PactHTTPMethod { + + var method: String { + switch self { + case .GET: return "get" + case .HEAD: return "head" + case .POST: return "post" + case .PUT: return "put" + case .PATCH: return "patch" + case .DELETE: return "delete" + case .TRACE: return "trace" + case .CONNECT: return "connect" + case .OPTIONS: return "options" + } + } + +} + +extension PactHTTPMethod: Encodable { + + enum CodingKeys: CodingKey { + case method + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(method) + } + +} diff --git a/Sources/Model/PactInteractionElement.swift b/Sources/Model/PactInteractionElement.swift new file mode 100644 index 00000000..ce4e4066 --- /dev/null +++ b/Sources/Model/PactInteractionElement.swift @@ -0,0 +1,27 @@ +// +// Created by Marko Justinek on 9/4/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +enum PactInteractionNode: String { + + case body + case header + case path + case query + +} diff --git a/Sources/Model/PactInteractionNode.swift b/Sources/Model/PactInteractionNode.swift new file mode 100644 index 00000000..30f4dafb --- /dev/null +++ b/Sources/Model/PactInteractionNode.swift @@ -0,0 +1,27 @@ +// +// Created by Marko Justinek on 9/4/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +enum PactInteractionElement: String { + + case body + case headers + case path + case query + +} diff --git a/Sources/Model/PactPathParameter.swift b/Sources/Model/PactPathParameter.swift new file mode 100644 index 00000000..c130b0eb --- /dev/null +++ b/Sources/Model/PactPathParameter.swift @@ -0,0 +1,21 @@ +// +// Created by Marko Justinek on 27/10/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +/// A protocol defining a Type that can be used as a request's path parameter (eg: a RegexLike matcher) +public protocol PactPathParameter { } diff --git a/Sources/Model/PactSwiftVersion.swift b/Sources/Model/PactSwiftVersion.swift new file mode 100644 index 00000000..a20e20e1 --- /dev/null +++ b/Sources/Model/PactSwiftVersion.swift @@ -0,0 +1,2 @@ +// !!! WARNING: THIS FILE IS AUTOMATED - DO NOT CHANGE OR MOVE !!! +let pactSwiftVersion = "1.1.0" diff --git a/Sources/Model/Pacticipant.swift b/Sources/Model/Pacticipant.swift new file mode 100644 index 00000000..fe82c584 --- /dev/null +++ b/Sources/Model/Pacticipant.swift @@ -0,0 +1,47 @@ +// +// Created by Marko Justinek on 1/4/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +/// An object representing a participant in the Pact contract. +enum Pacticipant { + + case consumer(String) + case provider(String) + + var name: String { + switch self { + case .consumer(let name), + .provider(let name): + return name + } + } + +} + +extension Pacticipant: Encodable { + + enum CodingKeys: CodingKey { + case name + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(name, forKey: .name) + } + +} diff --git a/Sources/Model/ProviderState.swift b/Sources/Model/ProviderState.swift new file mode 100644 index 00000000..fe5c156a --- /dev/null +++ b/Sources/Model/ProviderState.swift @@ -0,0 +1,68 @@ +// +// Created by Marko Justinek on 2/4/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +/// Object describing expected provider state for an interaction +public struct ProviderState: Encodable { + + let name: String + let params: [String: String] + + /// Object describing expected provider state for an interaction + /// + /// - parameter description: The description of the state + /// - parameter params: The `Key` `Value` pair of the expected state (eg: `"user_id": "1"`) + /// + public init(description: String, params: [String: String]) { + self.name = description + self.params = params + } + +} + +extension ProviderState: Equatable { + + static public func ==(lhs: ProviderState, rhs: ProviderState) -> Bool { + lhs.name == rhs.name && lhs.params == rhs.params + } + +} + +// MARK: - Objective-C +/*! + @brief Object describing expected provider state for an interaction + + @discussion This object holds information about the provider state and specific parameters that are required for the specific provider state. + + To use it, simply call @c[[ProviderState alloc] description:@"user exists" params:@{@"id": @"abc123"}]; + + @param description Description of the provider state + + @param params A dictionary of parameters describing data for the given state + */ +@objc(ProviderState) +public final class ObjCProviderState: NSObject { + + let state: ProviderState + + @objc(initWithDescription:params:) + public init(description: String, params: [String: String]) { + state = ProviderState(description: description, params: params) + } + +} diff --git a/Sources/Model/ProviderVerifier+Error.swift b/Sources/Model/ProviderVerifier+Error.swift new file mode 100644 index 00000000..180ff524 --- /dev/null +++ b/Sources/Model/ProviderVerifier+Error.swift @@ -0,0 +1,27 @@ +// +// Created by Marko Justinek on 23/8/21. +// Copyright © 2021 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +public extension ProviderVerifier { + + // A bridge to PactSwiftMockServer provider verification errors + enum VerificationError: Error { + case error(String) + } + +} diff --git a/Sources/Model/ProviderVerifier+Options.swift b/Sources/Model/ProviderVerifier+Options.swift new file mode 100644 index 00000000..3966e95f --- /dev/null +++ b/Sources/Model/ProviderVerifier+Options.swift @@ -0,0 +1,261 @@ +// +// Created by Marko Justinek on 19/8/21. +// Copyright © 2021 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +public extension ProviderVerifier { + + /// Defines the options to use when verifying a provider + struct Options { + + // MARK: - Types + + /// Logging level for provider verification + public enum LogLevel: String { + case error + case warn + case info + case debug + case trace + case none + } + + /// The source of Pact files + public enum PactsSource { + + /// Verify pacts on a Pact Broker + case broker(PactBroker) + + /// Verify pacts in directories + case directories([String]) + + /// Verify specific pact files + case files([String]) + + /// Verify specific pacts at URLs + case urls([URL]) + } + + /// Filter pacts + public enum FilterPacts { + + /// Only validate interactions that have no defined provider state + case noState + + /// Only validate interactions whose provider states match this filter + case states([String]) + + /// Only validate interactions whose descriptions match this filter + case descriptions([String]) + + /// Consumer name to filter the pacts to be verified + case consumers([String]) + } + + // MARK: - Properties + + /// The port of provider being verified + let port: Int + + /// URL of the provider being verified + let providerURL: URL? + + /// Only validated interactions that match this filter + let filterPacts: FilterPacts? + + /// Pacts source + let pactsSource: PactsSource + + /// URL to post state change requests to + let stateChangeURL: URL? + + /// Sets the log level + let logLevel: LogLevel + + // MARK: - Initialization + + /// Defines the options to use when verifying a provider + /// + /// - Parameters: + /// - provider: The provider information + /// - pactsSource: The locations of pacts + /// - filter: Only validates the interactions that match the filter + /// - stateChangeURL: URL to post state change requests to + /// - logLevel: Logging level + /// + public init( + provider: Provider, + pactsSource: PactsSource, + filter: FilterPacts? = nil, + stateChangeURL: URL? = nil, + logLevel: LogLevel = .warn + ) { + self.providerURL = provider.url + self.port = provider.port + self.pactsSource = pactsSource + self.filterPacts = filter + self.stateChangeURL = stateChangeURL + self.logLevel = logLevel + } + } + +} + +extension ProviderVerifier.Options { + + /// Newline delimited provider verification arguments + var args: String { + // Verification arguments to pass to pactffi_verify() + var newLineDelimitedArgs = [String]() + + // Set verified provider port + newLineDelimitedArgs.append("--port\n\(self.port)") + + // Set verified provider url + if let providerURL = providerURL { + newLineDelimitedArgs.append("--hostname\n\(providerURL.absoluteString)") + } + + // Pacts source + switch pactsSource { + + case .broker(let broker): + // Set broker url + newLineDelimitedArgs.append("--broker-url\n\(broker.url)") + + // Broker authentication type + // Authenticate with username and password + switch broker.authentication { + case .auth(let auth): + newLineDelimitedArgs.append("--user\n\(auth.username)") + newLineDelimitedArgs.append("--password\n\(auth.password)") + + // Authenticate with a Token (Pactflow) + case .token(let auth): + newLineDelimitedArgs.append("--token\n\(auth.token)") + } + + // Use the pact for provider with name + newLineDelimitedArgs.append("--provider-name\n\(broker.providerName)") + + // Publishing verification results back to Broker + if broker.publishVerificationResult, let providerVersion = broker.providerVersion, providerVersion.isEmpty == false { + newLineDelimitedArgs.append("--publish") + newLineDelimitedArgs.append("--provider-version\n\(providerVersion)") + + if let providerTags = broker.providerTags, providerTags.isEmpty == false { + newLineDelimitedArgs.append("--provider-tags\n\(providerTags.joined(separator: ","))") + } + } + + // Consumer tags + broker.consumerTags?.forEach { + do { + newLineDelimitedArgs.append("--consumer-version-selectors\n\(try $0.toJSONString())") + } catch { + Logger.log(message: "Failed to convert provider version to JSON representaion: \(String(describing: broker.consumerTags))") + } + } + + // Pending pacts + if broker.includePending == true { + newLineDelimitedArgs.append("--enable-pending\ntrue") + } + + // WIP pacts + if let includeWIP = broker.includeWIP { + // Enable pending pacts only if it wasn not set already! + let enablePendingArgs = "--enable-pending\ntrue" + if newLineDelimitedArgs.contains(where: { $0 == enablePendingArgs }) == false { + newLineDelimitedArgs.append(enablePendingArgs) + } + + // Set the date from which to include WIP pacts + newLineDelimitedArgs.append("--include-wip-pacts-since\n\(includeWIP.sinceDate.iso8601short)") + + // Explicitly set provider version argument but only when not publishing verification result (otherwise it would be duplicated) + // See [Work In Progress - Technical details](https://docs.pact.io/pact_broker/advanced_topics/wip_pacts/#technical-details) for more. + if broker.publishVerificationResult == false { + newLineDelimitedArgs.append("--provider-version\n\(includeWIP.providerVersion)") + } + } + + // Verify pacts from directories + case .directories(let pactDirs) where pactDirs.isEmpty == false: + pactDirs.forEach { newLineDelimitedArgs.append("--dir\n\($0)") } + + // Verify specific pact files + case .files(let files) where files.isEmpty == false: + files.forEach { newLineDelimitedArgs.append("--file\n\($0)") } + + // Verify pacts from specific URLs + case .urls(let pactURLs) where pactURLs.isEmpty == false: + pactURLs.forEach { newLineDelimitedArgs.append("--url\n\($0)") } + + default: + break + } + + // Set state filters + if let filterProviderStates = filterPacts { + switch filterProviderStates { + + // Only test interactions with no specific state defined + case .noState: + newLineDelimitedArgs.append("--filter-no-state\ntrue") + + // Only test interactions with specific states + case .states(let states) where states.isEmpty == false: + states.forEach { newLineDelimitedArgs.append("--filter-state\n\($0)") } + + // Only test interactions with specific descriptions + case .descriptions(let descriptions) where descriptions.isEmpty == false: + descriptions.forEach { newLineDelimitedArgs.append("--filter-description\n\($0)") } + + // Only test pact contracts with specific consumers + case .consumers(let consumers) where consumers.isEmpty == false: + consumers.forEach { newLineDelimitedArgs.append("--filter-consumer\n\($0)") } + + default: + break + } + } + + // State change URL + if let stateChangeURL = stateChangeURL { + newLineDelimitedArgs.append("--state-change-url\n\(stateChangeURL.absoluteString)") + } + + // Set logging level + newLineDelimitedArgs.append("--loglevel\n\(self.logLevel.rawValue)") + + // Convert all verification arguments to a `String` and return it + return newLineDelimitedArgs.joined(separator: "\n") + } + +} + +private extension Date { + + /// Date represented as string in short ISO8601 format (eg: "2021-08-24") + var iso8601short: String { + let formatter = DateFormatter() + formatter.dateFormat = "YYYY-MM-dd" + return formatter.string(from: self) + } + +} diff --git a/Sources/Model/ProviderVerifier+Provider.swift b/Sources/Model/ProviderVerifier+Provider.swift new file mode 100644 index 00000000..b737c55d --- /dev/null +++ b/Sources/Model/ProviderVerifier+Provider.swift @@ -0,0 +1,38 @@ +// +// Created by Marko Justinek on 21/8/21. +// Copyright © 2021 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +public extension ProviderVerifier { + + /// The provider being verified + struct Provider { + + /// The port of provider being verified + let port: Int + + /// URL of the provider being verified + let url: URL? + + /// The provider being verified + public init(url: URL? = nil, port: Int) { + self.url = url + self.port = port + } + } + +} diff --git a/Sources/Model/Request.swift b/Sources/Model/Request.swift new file mode 100644 index 00000000..d55ba3b0 --- /dev/null +++ b/Sources/Model/Request.swift @@ -0,0 +1,92 @@ +// +// Created by Marko Justinek on 31/3/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +/// An object representing an API request in Pact Specification Version 3.0 +public struct Request { + + let httpMethod: PactHTTPMethod + let path: PactPathParameter + let query: [String: [Any]]? + let headers: [String: Any]? + + var description: String { + let request = "\(httpMethod.method.uppercased()) \(path)" + let queryString = query?.compactMap { "\($0)=\($1.map { "\($0)," })" }.dropLast().joined(separator: "&") + let headersString = headers?.compactMap { "\"\($0)\": \"\($1)\"" }.joined(separator: "\n\t") + + return "\(request) \(queryString ?? "")\(headersString != nil ? "\n\n\tHeaders:\n\t" + headersString! : "")" + } + + private let bodyEncoder: (Encoder) throws -> Void + +} + +extension Request: Encodable { + + enum CodingKeys: String, CodingKey { + case httpMethod = "method" + case path + case query + case headers + case body + case matchingRules + case generators + } + + /// Creates an object representing a network `Request` + /// + /// - Parameters: + /// - method: The http method of the http request + /// - path: A url path of the http reuquest (without the base url) + /// - query: A url query + /// - headers: Headers of the http reqeust + /// - body: Optional body of the http request + /// + init(method: PactHTTPMethod, path: PactPathParameter, query: [String: [Any]]? = nil, headers: [String: Any]? = nil, body: Any? = nil) throws { + self.httpMethod = method + self.path = path + self.query = query + self.headers = headers + + let queryValues = try Toolbox.process(element: query, for: .query) + let headersValues = try Toolbox.process(element: headers, for: .header) + let bodyValues = try Toolbox.process(element: body, for: .body) + let pathValues = try Toolbox.process(element: path, for: .path) + + self.bodyEncoder = { + var container = $0.container(keyedBy: CodingKeys.self) + try container.encode(method, forKey: .httpMethod) + if let path = pathValues?.node { try container.encode(path, forKey: .path) } + if let query = queryValues?.node { try container.encode(query, forKey: .query) } + if let headers = headersValues?.node { try container.encode(headers, forKey: .headers) } + if let encodableBody = bodyValues?.node { try container.encode(encodableBody, forKey: .body) } + + if let matchingRules = Toolbox.merge(body: bodyValues?.rules, query: queryValues?.rules, header: headersValues?.rules, path: pathValues?.rules) { + try container.encode(matchingRules, forKey: .matchingRules) + } + + if let generators = Toolbox.merge(body: bodyValues?.generators, query: queryValues?.generators, header: headersValues?.generators, path: pathValues?.generators) { + try container.encode(generators, forKey: .generators) + } + } + } + + public func encode(to encoder: Encoder) throws { + try bodyEncoder(encoder) + } + +} diff --git a/Sources/Model/Response.swift b/Sources/Model/Response.swift new file mode 100644 index 00000000..22404975 --- /dev/null +++ b/Sources/Model/Response.swift @@ -0,0 +1,69 @@ +// +// Created by Marko Justinek on 31/3/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +/// An object representing an API response in Pact Specification Version 3.0 +public struct Response { + + var statusCode: Int + var headers: [String: Any]? + + private let bodyEncoder: (Encoder) throws -> Void + +} + +extension Response: Encodable { + + enum CodingKeys: String, CodingKey { + case statusCode = "status" + case headers + case body + case matchingRules + case generators + } + + /// Creates an object representing a network `Response` + /// - Parameters: + /// - statusCode: The status code of the API response + /// - headers: Headers of the API response + /// - body: Optional body in the API response + /// + init(statusCode: Int, headers: [String: Any]? = nil, body: Any? = nil) throws { + self.statusCode = statusCode + self.headers = headers + + let bodyValues = try Toolbox.process(element: body, for: .body) + let headerValues = try Toolbox.process(element: headers, for: .header) + + self.bodyEncoder = { + var container = $0.container(keyedBy: CodingKeys.self) + try container.encode(statusCode, forKey: .statusCode) + if let header = headerValues?.node { try container.encode(header, forKey: .headers) } + if let encodableBody = bodyValues?.node { try container.encode(encodableBody, forKey: .body) } + if let matchingRules = Toolbox.merge(body: bodyValues?.rules, header: headerValues?.rules) { + try container.encode(matchingRules, forKey: .matchingRules) + } + if let generators = Toolbox.merge(body: bodyValues?.generators, header: headerValues?.generators) { + try container.encode(generators, forKey: .generators) + } + } + } + + public func encode(to encoder: Encoder) throws { + try bodyEncoder(encoder) + } + +} diff --git a/Sources/Model/Toolbox.swift b/Sources/Model/Toolbox.swift new file mode 100644 index 00000000..7c348863 --- /dev/null +++ b/Sources/Model/Toolbox.swift @@ -0,0 +1,68 @@ +// Created by Marko Justinek on 20/10/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +enum Toolbox { + + /// Merges the Pact top level elements into one dictionary thac can be Encoded + /// - Parameters: + /// - body: The PactBuilder processed object representing interaction's body + /// - query: The PactBuilder processed object representing interaction's query + /// - header: The PactBuilder processed object representing interaction's header + /// - path: The PactBuilder processed object representing request path + /// + static func merge(body: AnyEncodable?, query: AnyEncodable? = nil, header: AnyEncodable? = nil, path: AnyEncodable? = nil) -> [String: AnyEncodable]? { + var merged: [String: AnyEncodable] = [:] + + if let header = header { + merged["header"] = header + } + + if let body = body { + merged["body"] = body + } + + if let query = query { + merged["query"] = query + } + + if let path = path { + merged["path"] = path + } + + return merged.isEmpty ? nil : merged + } + + /// Runs the `Any` type through PactBuilder and returns a Pact tuple + /// - Parameters: + /// - element: The object to process through PactBuilder + /// - interactionElement: The network interaction element the object relates to + /// + static func process(element: Any?, for interactionElement: PactInteractionNode) throws -> (node: AnyEncodable?, rules: AnyEncodable?, generators: AnyEncodable?)? { + if let element = element { + do { + let encodedElement = try PactBuilder(with: element, for: interactionElement).encoded() + return (node: encodedElement.node, rules: encodedElement.rules, generators: encodedElement.generators) + } catch { + throw error + } + } + + return nil + } + +} diff --git a/Sources/Model/TransferProtocol.swift b/Sources/Model/TransferProtocol.swift new file mode 100644 index 00000000..5eab8372 --- /dev/null +++ b/Sources/Model/TransferProtocol.swift @@ -0,0 +1,50 @@ +// +// Created by Marko Justinek on 31/7/21. +// Copyright © 2021 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +#if compiler(>=5.5) +// This is ridiculous! This works when building on macOS 11+. +@_implementationOnly import PactSwiftMockServer +#else +import PactSwiftMockServer +#endif + +/// Defines the transfer protocol on which `MockService` runs. +@objc public enum TransferProtocol: Int { + case standard + case secure + + var bridge: PactSwiftMockServer.TransferProtocol { + switch self { + case .standard: return .standard + case .secure: return .secure + } + } +} + +extension TransferProtocol { + + /// HTTP Transfer protocol + var `protocol`: String { + switch self { + case .standard: return "http" + case .secure: return "https" + } + } + +} diff --git a/Sources/Model/VersionSelector.swift b/Sources/Model/VersionSelector.swift new file mode 100644 index 00000000..765e1695 --- /dev/null +++ b/Sources/Model/VersionSelector.swift @@ -0,0 +1,78 @@ +// +// Created by Marko Justinek on 19/8/21. +// Copyright © 2021 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +/// Provides a way to configure which pacts the provider verifies. +public struct VersionSelector: Codable, Equatable { + + // MARK: - Properties + + /// The name of the tag which applies to the pacticipant versions of the pacts to verify + let tag: String + + /// Whether or not to verify only the pact that belongs to the latest application version + let latest: Bool + + /// A fallback tag if a pact for the specified `tag` does not exist + let fallbackTag: String? + + /// Filter pacts by the specified consumer + /// + /// When omitted, all consumers are included. + let consumer: String? + + // MARK: - Initialization + + /// Defines a version configuration for which pacts the provider verifies + /// + /// - Parameters: + /// - tag: The version `tag` name of the consumer to verify + /// - fallbackTag: The version `tag` to use if the initial `tag` does not exist + /// - latest: Whether to verify only the pact belonging to the latest application version + /// - consumer: Filter pacts by the specified consumer + /// + /// See [https://docs.pact.io/selectors](https://docs.pact.io/selectors) for more context. + /// + public init(tag: String, fallbackTag: String? = nil, latest: Bool = true, consumer: String? = nil) { + self.tag = tag + self.fallbackTag = fallbackTag + self.latest = latest + self.consumer = consumer + } + +} + +// MARK: - Internal + +extension VersionSelector { + + /// Converts to JSON string + /// + /// - Returns: A `String` representing `ProviderVerifier` in JSON format + /// + func toJSONString() throws -> String { + let jsonEncoder = JSONEncoder() + let jsonData = try jsonEncoder.encode(self) + guard let jsonString = String(data: jsonData, encoding: .utf8) else { + throw ProviderVerifier.VerificationError.error("Invalid consumer version selector specified: \(self)") + } + + return jsonString + } + +} diff --git a/Sources/Model/WIPPacts.swift b/Sources/Model/WIPPacts.swift new file mode 100644 index 00000000..3a512e2e --- /dev/null +++ b/Sources/Model/WIPPacts.swift @@ -0,0 +1,44 @@ +// +// Created by Marko Justinek on 24/8/21. +// Copyright © 2021 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +/// The configuration used when verifying WIP pacts +public struct WIPPacts { + + /// The date from which the changed pacts are to be included + let sinceDate: Date + + /// The provider + let providerVersion: String + + /// Configuration for verifying WIP pacts + /// + /// - Parameters: + /// - since: The date from which the WIP pacts are to be included in verification + /// - providerVersion: The provider version being verified + /// + /// See [Work in Progress pacts](https://docs.pact.io/pact_broker/advanced_topics/wip_pacts/) for more + + /// - Warning: The `providerVersion` value set in the `VerificationResult` object is used if provided + /// + public init(since date: Date, providerVersion: String) { + self.sinceDate = date + self.providerVersion = providerVersion + } + +} diff --git a/Sources/PFMockService.swift b/Sources/PFMockService.swift new file mode 100644 index 00000000..4835b3bc --- /dev/null +++ b/Sources/PFMockService.swift @@ -0,0 +1,113 @@ +// +// Created by Marko Justinek on 31/7/21. +// Copyright © 2021 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation +import XCTest + +#if compiler(>=5.5) +@_implementationOnly import PactSwiftMockServer +#else +import PactSwiftMockServer +#endif + +/// Initializes a `PFMockService` object that handles Pact interaction testing for projects written in Objective-C. For Swift projects use `MockService`. +/// +/// When initializing with `.secure` scheme, the SSL certificate on Mock Server +/// is a self-signed certificate. +/// +@objc open class PFMockService: NSObject { + + // MARK: - Properties + + private let mockService: MockService + + // MARK: - Initialization + + /// Initializes a `MockService` object that handles Pact interaction testing + /// + /// When initializing with `.secure` transferProtocol, + /// the SSL certificate on Mock Server is a self-signed certificate. + /// + /// - Parameters: + /// - consumer: Name of the API consumer (eg: "mobile-app") + /// - provider: Name of the API provider (eg: "auth-service") + /// - transferProtocol: HTTP scheme + /// - merge: Whether to merge interactions with an existing Pact contract + /// + @objc(initWithConsumer: provider: transferProtocol: merge:) + public convenience init( + consumer: String, + provider: String, + transferProtocol: TransferProtocol = .standard, + merge: Bool = true + ) { + self.init(consumer: consumer, provider: provider, scheme: transferProtocol, errorReporter: ErrorReporter(), merge: merge) + } + + internal init( + consumer: String, + provider: String, + scheme: TransferProtocol, + errorReporter: ErrorReportable? = nil, + merge: Bool + ) { + mockService = MockService(consumer: consumer, provider: provider, scheme: scheme, merge: merge, errorReporter: errorReporter ?? ErrorReporter()) + } + + // MARK: - Interface + + /// Describes the `Interaction` between the consumer and provider + /// + /// It is important that the `description` and provider state + /// combination is unique per consumer-provider contract. + /// + /// - parameter description: A description of the API interaction + /// + @discardableResult + @objc public func uponReceiving(_ description: String) -> Interaction { + mockService.uponReceiving(description) + } + + /// Runs the Pact test with default timeout + /// + /// Make sure you call the completion block at the end of your test. + /// + + @objc(run:) + public func objCRun(testFunction: @escaping (String, (@escaping () -> Void)) -> Void) { + mockService.run(timeout: Constants.kTimeout, testFunction: testFunction) + } + + /// Runs the Pact test with provided timeout + /// + /// Make sure you call the completion block at the end of your test. + /// + @objc(run: withTimeout:) + public func objCRun(testFunction: @escaping (String, (@escaping () -> Void)) -> Void, timeout: TimeInterval) { + mockService.run(timeout: timeout, testFunction: testFunction) + } + + /// Runs the Pact test with provided timeout verifying the provided set of interactions + /// + /// Make sure you call the completion block at the end of your test. + /// + @objc(run: verifyInteractions: withTimeout:) + public func objCRun(testFunction: @escaping (String, (@escaping () -> Void)) -> Void, verify interactions: [Interaction], timeout: TimeInterval) { + mockService.run(verify: interactions, timeout: timeout, testFunction: testFunction) + } + +} diff --git a/Sources/PactBuilder.swift b/Sources/PactBuilder.swift new file mode 100644 index 00000000..5134c38b --- /dev/null +++ b/Sources/PactBuilder.swift @@ -0,0 +1,361 @@ +// +// Created by Marko Justinek on 7/4/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +struct PactBuilder { + + typealias ProcessingResult = (node: AnyEncodable, rules: [String: AnyEncodable], generators: [String: AnyEncodable]) + + let typeDefinition: Any + let interactionNode: PactInteractionNode + + /// Creates a PactBuilder object which processes the DSL + /// + /// - Parameters: + /// - value: The DSL to process and extract matchers and example generators + /// - interactionNode: The part of interaction to process (eg: `body`, `header` or `query`) + /// + init(with value: Any, for interactionNode: PactInteractionNode) { + self.typeDefinition = value + self.interactionNode = interactionNode + } + + /// Returns a tuple of a Pact Contract interaction's node object (eg, request `body`) + /// and its corresponding matching rules and example generators. + /// It erases node object's type and casts the node and leaf values into an `Encodable`-safe type. + /// + /// Type erases the following `Type` into `AnyEncodable`: + /// `String`, `Int`, `Double`, `Decimal`, `Bool`, `Array`, `Dictionary`, `PactSwift.Matcher<>`, `PactSwift.ExampleGenerator<>` + /// + func encoded() throws -> (node: AnyEncodable?, rules: AnyEncodable?, generators: AnyEncodable?) { + let processedType = try process(element: typeDefinition, at: interactionNode == .body ? "$" : "") + let node = processedType.node + let rules = process(keyValues: processedType.rules) + let generators = process(keyValues: processedType.generators) + + return (node: node, rules: rules, generators: generators) + } + +} + +// MARK: - Private + +private extension PactBuilder { + + // swiftlint:disable:next cyclomatic_complexity function_body_length + func process(element: Any, at node: String, isEachLike: Bool = false) throws -> ProcessingResult { + let processedElement: (node: AnyEncodable, rules: [String: AnyEncodable], generators: [String: AnyEncodable]) + + let elementToProcess = mapPactObject(element) + + switch elementToProcess { + + // MARK: - Process Collections: + + case let array as [Any]: + let processedArray = try process(array, at: node, isEachLike: isEachLike) + processedElement = (node: AnyEncodable(processedArray.node), rules: processedArray.rules, generators: processedArray.generators) + + case let dict as [String: Any]: + let processedDict = try process(dict, at: node) + processedElement = (node: AnyEncodable(processedDict.node), rules: processedDict.rules, generators: processedDict.generators) + + // MARK: - Process Simple Types + + case let string as String: + processedElement = (node: AnyEncodable(string), rules: [:], generators: [:]) + + case let integer as Int: + processedElement = (node: AnyEncodable(integer), rules: [:], generators: [:]) + + case let double as Double: + processedElement = (node: AnyEncodable(double), rules: [:], generators: [:]) + + case let decimal as Decimal: + processedElement = (node: AnyEncodable(decimal), rules: [:], generators: [:]) + + case let bool as Bool: + processedElement = (node: AnyEncodable(bool), rules: [:], generators: [:]) + + // MARK: - Process Matchers + + // NOTE: There is a bug in Swift on macOS 10.x where type casting against a protocol does not work as expected. + // Works fine running on macOS 11.x! + // That is why each Matcher type is explicitly stated in its own case statement and is not DRY. + case let matcher as Matcher.DecimalLike: + processedElement = try processMatcher(matcher, at: node) + + case let matcher as Matcher.EachLike: + processedElement = try processEachLikeMatcher(matcher, at: node) + + case let matcher as Matcher.EachKeyLike: + processedElement = try processEachKeyLikeMatcher(matcher, at: node) + + case let matcher as Matcher.EqualTo: + processedElement = try processMatcher(matcher, at: node) + + case let matcher as Matcher.FromProviderState: + let stateParameterGenerator = ExampleGenerator.ProviderStateGenerator(parameter: matcher.parameter, value: matcher.value) + + // When processing for path, don't bother with setting the matching rule to "type", as it is always going to be a String + if interactionNode == .path { + processedElement = try processExampleGenerator(stateParameterGenerator, at: node) + } else { + // When processing for anything else then add the matching rule matching "type" along with provider state generated value + let processedStateParameter = try processExampleGenerator(stateParameterGenerator, at: node) + let processedMatcherValue = try processMatcher(matcher, at: node) + processedElement = ( + node: processedMatcherValue.node, + rules: processedMatcherValue.rules, + generators: processedStateParameter.generators + ) + } + + case let matcher as Matcher.IncludesLike: + let processedMatcherValue = try process(element: matcher.value, at: node, isEachLike: isEachLike) + processedElement = ( + node: processedMatcherValue.node, + rules: [node: AnyEncodable(["matchers": AnyEncodable(matcher.rules), "combine": AnyEncodable(matcher.combine.rawValue)])], + generators: processedMatcherValue.generators + ) + + case let matcher as Matcher.IntegerLike: + processedElement = try processMatcher(matcher, at: node) + + case let matcher as Matcher.MatchNull: + processedElement = ( + node: AnyEncodable(nil), + rules: [node: AnyEncodable(["matchers": AnyEncodable(matcher.rules)])], + generators: [:] + ) + + case let matcher as Matcher.OneOf: + processedElement = try processMatcher(matcher, at: node) + + case let matcher as Matcher.RegexLike: + guard + let value = matcher.value as? String, + value.range(of: matcher.pattern, options: .regularExpression) != nil + else { + throw EncodingError.encodingFailure("Value \"\(matcher.value)\" does not match the pattern \"\(matcher.pattern)\"") + } + + processedElement = try processMatcher(matcher, at: node) + + case let matcher as Matcher.SomethingLike: + // Process the root node + let processedNode = try processMatcher(matcher, at: node) + + // Merge any matchers and example generators found in the node's value + switch matcher.value { + case let dict as [String: Any]: + let processedDict = try process(dict, at: node) + processedElement = ( + node: AnyEncodable(processedDict.node), + rules: merge(processedNode.rules, with: processedDict.rules), + generators: merge(processedNode.generators, with: processedDict.generators) + ) + case let array as [Any]: + let processedArray = try process(array, at: node, isEachLike: false) + processedElement = ( + node: AnyEncodable(processedArray.node), + rules: merge(processedNode.rules, with: processedArray.rules), + generators: merge(processedNode.generators, with: processedArray.generators) + ) + default: + processedElement = processedNode + } + + // MARK: - Process Example generators + + // NOTE: There is a bug in Swift on macOS 10.x where type casting against a protocol does not work as expected. + // Works fine running on macOS 11.x! + // That is why each ExampleGenerator type is explicitly stated in its own case statement and is not DRY. + case let exampleGenerator as ExampleGenerator.DateTime: + processedElement = try processExampleGenerator(exampleGenerator, at: node) + + case let exampleGenerator as ExampleGenerator.RandomBool: + processedElement = try processExampleGenerator(exampleGenerator, at: node) + + case let exampleGenerator as ExampleGenerator.RandomDate: + processedElement = try processExampleGenerator(exampleGenerator, at: node) + + case let exampleGenerator as ExampleGenerator.RandomDateTime: + processedElement = try processExampleGenerator(exampleGenerator, at: node) + + case let exampleGenerator as ExampleGenerator.RandomDecimal: + processedElement = try processExampleGenerator(exampleGenerator, at: node) + + case let exampleGenerator as ExampleGenerator.RandomHexadecimal: + processedElement = try processExampleGenerator(exampleGenerator, at: node) + + case let exampleGenerator as ExampleGenerator.RandomInt: + processedElement = try processExampleGenerator(exampleGenerator, at: node) + + case let exampleGenerator as ExampleGenerator.RandomString: + processedElement = try processExampleGenerator(exampleGenerator, at: node) + + case let exampleGenerator as ExampleGenerator.RandomTime: + processedElement = try processExampleGenerator(exampleGenerator, at: node) + + case let exampleGenerator as ExampleGenerator.RandomUUID: + processedElement = try processExampleGenerator(exampleGenerator, at: node) + + case let exampleGenerator as ExampleGenerator.DateTimeExpression: + processedElement = try processExampleGenerator(exampleGenerator, at: node) + + // Anything else is not considered safe to encode in PactSwift + + case let threwError as EncodingError: + throw threwError + + default: + if let encodingError = (elementToProcess as? Interaction)?.encodingErrors.first { + throw encodingError + } + throw EncodingError.encodingFailure("A key or value in the structure does not conform to 'Encodable' protocol. The element attempted to encode: \(element)") + } + + return processedElement + } + + // MARK: - Processing + + // Processes a Matcher + func processMatcher(_ matcher: MatchingRuleExpressible, at node: String) throws -> ProcessingResult { + let processedMatcherValue = try process(element: matcher.value, at: node, isEachLike: false) + + return ( + node: processedMatcherValue.node, + rules: [node: AnyEncodable(["matchers": AnyEncodable(matcher.rules)])], + generators: processedMatcherValue.generators + ) + } + + // Processes an `EachLike` matcher + func processEachLikeMatcher(_ matcher: Matcher.EachLike, at node: String) throws -> ProcessingResult { + var newNode: String + let elementToProcess = mapPactObject(matcher.value) + + switch elementToProcess { + case _ as [Any]: + // Element is an `Array` + newNode = node + "[*]" + default: + // Element is a `Dictionary` + newNode = node + "[*].*" + } + + var processedMatcherValue = try process(element: matcher.value, at: newNode, isEachLike: true) + processedMatcherValue.rules[node] = AnyEncodable(["matchers": AnyEncodable(matcher.rules)]) + + return ( + node: processedMatcherValue.node, + rules: processedMatcherValue.rules, + generators: processedMatcherValue.generators + ) + } + + // Processes an `EachKeyLike` matcher + func processEachKeyLikeMatcher(_ matcher: Matcher.EachKeyLike, at node: String) throws -> ProcessingResult { + var processedMatcherValue = try process(element: matcher.value, at: node) + processedMatcherValue.rules[node] = AnyEncodable(["matchers": AnyEncodable(matcher.rules)]) + + return ( + node: processedMatcherValue.node, + rules: processedMatcherValue.rules, + generators: processedMatcherValue.generators + ) + } + + // Processes an Example Generator + func processExampleGenerator(_ exampleGenerator: ExampleGeneratorExpressible, at node: String) throws -> ProcessingResult { + let processedGeneratorValue = try process(element: exampleGenerator.value, at: node) + + return ( + node: processedGeneratorValue.node, + rules: processedGeneratorValue.rules, + generators: [node: AnyEncodable(exampleGenerator.attributes)] + ) + } + + // Processes the Matchers and Generators giving special consideration of processing `Request`. + // When processing for `path`, the key is "" as it does not need to conform to JSONPath the same way as body does. + func process(keyValues: [String: AnyEncodable]) -> AnyEncodable? { + if interactionNode == .path, let pathRulesKey = keyValues.keys.first, pathRulesKey.isEmpty == true { + return AnyEncodable(keyValues.values.first) + } else { + return keyValues.isEmpty ? nil : AnyEncodable(keyValues) + } + } + + // Processes an `Array` object and extracts any matchers or generators + func process(_ array: [Any], at node: String, isEachLike: Bool) throws -> (node: [AnyEncodable], rules: [String: AnyEncodable], generators: [String: AnyEncodable]) { + var encodableArray = [AnyEncodable]() + var matchingRules: [String: AnyEncodable] = [:] + var generators: [String: AnyEncodable] = [:] + + do { + try array + .enumerated() + .forEach { + let childElement = try process(element: $0.element, at: interactionNode == .body ? "\(node)\(isEachLike ? "" : "[\($0.offset)]")" : "\(node)", isEachLike: false) + encodableArray.append(childElement.node) + matchingRules = merge(matchingRules, with: childElement.rules) + generators = merge(generators, with: childElement.generators) + } + return (node: encodableArray, rules: matchingRules, generators: generators) + } catch { + throw EncodingError.encodingFailure("Failed to process array: \(array)") + } + } + + // Processes a `Dictionary` object and extracts any matchers or generators + func process(_ dictionary: [String: Any], at node: String) throws -> (node: [String: AnyEncodable], rules: [String: AnyEncodable], generators: [String: AnyEncodable]) { + var encodableDictionary: [String: AnyEncodable] = [:] + var matchingRules: [String: AnyEncodable] = [:] + var generators: [String: AnyEncodable] = [:] + + try dictionary + .enumerated() + .forEach { + let nodeKey = (($0.element.value as? Matcher.EachKeyLike) != nil) ? "*" : $0.element.key + let _node = node.isEmpty ? "\(nodeKey)" : "\(node).\(nodeKey)" + let childElement = try process(element: $0.element.value, at: _node) + encodableDictionary[$0.element.key] = childElement.node + matchingRules = merge(matchingRules, with: childElement.rules) + generators = merge(generators, with: childElement.generators) + } + return (node: encodableDictionary, rules: matchingRules, generators: generators) + } + + // MARK: - Type Mapping + + // Maps ObjC object to a Swift object + func mapPactObject(_ value: Any) -> Any { + switch value { + case let matcher as ObjcMatcher: + return matcher.type + case let generator as ObjcGenerator: + return generator.type + default: + return value + } + } + +} diff --git a/Sources/ProviderVerifier.swift b/Sources/ProviderVerifier.swift new file mode 100644 index 00000000..a388e773 --- /dev/null +++ b/Sources/ProviderVerifier.swift @@ -0,0 +1,89 @@ +// +// Created by Marko Justinek on 20/8/21. +// Copyright © 2021 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation +import XCTest + +#if compiler(>=5.5) +@_implementationOnly import PactSwiftMockServer +#else +import PactSwiftMockServer +#endif + +/// Entry point for provider verification +public final class ProviderVerifier { + + let verifier: ProviderVerifying + private let errorReporter: ErrorReportable + + /// Initializes a `Verifier` object for provider verification + public convenience init() { + self.init(verifier: Verifier(), errorReporter: ErrorReporter()) + } + + /// Initializes a `Verifier` object + /// + /// - Parameters: + /// - verifier: The verifier object handling provider verification + /// - errorReporter: Error reporting or intercepting object + /// + /// This initializer is marked `internal` for testing purposes! + /// + internal init(verifier: ProviderVerifying, errorReporter: ErrorReportable? = nil) { + self.verifier = verifier + self.errorReporter = errorReporter ?? ErrorReporter() + } + + /// Executes provider verification test + /// + /// - Parameters: + /// - options: Flags and args to use when verifying a provider + /// - file: The file in which to report the error in + /// - line: The line on which to report the error on + /// - completionBlock: Completion block executed at the end of verification + /// + /// - Returns: A `Result` where error describes the failure + /// + @discardableResult + public func verify(options: Options, file: FileString? = #file, line: UInt? = #line, completionBlock: (() -> Void)? = nil) -> Result { + switch verifier.verifyProvider(options: options.args) { + case .success(let value): + completionBlock?() + return .success(value) + case .failure(let error): + failWith(error.description, file: file, line: line) + completionBlock?() + return .failure(VerificationError.error(error.description)) + } + } + +} + +// MARK: - Private + +private extension ProviderVerifier { + + /// Fail the test and raise the failure in `file` at `line` + func failWith(_ message: String, file: FileString? = nil, line: UInt? = nil) { + if let file = file, let line = line { + errorReporter.reportFailure(message, file: file, line: line) + } else { + errorReporter.reportFailure(message) + } + } + +} diff --git a/Sources/Toolbox/Logger.swift b/Sources/Toolbox/Logger.swift new file mode 100644 index 00000000..4018ef32 --- /dev/null +++ b/Sources/Toolbox/Logger.swift @@ -0,0 +1,72 @@ +// +// Created by Marko Justinek on 9/8/2022. +// Copyright © 2022 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation +import os.log + +enum Logger { + + /// Logs Pact related messages. + /// + /// Looks for environment variable `PACT_ENABLE_LOGGING = "all"`. Can be set in project's scheme. Uses `os_log` on Apple platforms. + /// + /// - Parameters: + /// - message: The message to log + /// - data: Data to log + /// + static func log(message: String, data: Data? = nil) { + guard case .all = PactLoggingLevel(value: ProcessInfo.processInfo.environment["PACT_ENABLE_LOGGING"]) else { + return + } + + let stringData = data.flatMap { String(data: $0, encoding: .utf8) } ?? "" + + if #available(iOS 10, OSX 10.14, *) { + os_log( + "PactSwift: %{private}s", + log: .default, + type: .default, + "\(message): \(stringData)" + ) + } else { + print(message: "PactSwift: \(message)\n\(stringData)") + } + } + +} + +// MARK: - Private + +private extension Logger { + + static func print(message: String) { + debugPrint(message) + } + + enum PactLoggingLevel: String { + case all + case disabled + + init(value: String?) { + switch value { + case "all": self = .all + default: self = .disabled + } + } + } + +} diff --git a/Sources/Toolbox/PactFileManager.swift b/Sources/Toolbox/PactFileManager.swift new file mode 100644 index 00000000..3c663127 --- /dev/null +++ b/Sources/Toolbox/PactFileManager.swift @@ -0,0 +1,82 @@ +// +// Created by Marko Justinek on 9/8/2022. +// Copyright © 2022 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +enum PactFileManager { + + /// Where Pact contracts are written to. + /// + /// macOS: + /// + /// Running tests for macOS it will default to app's Documents folder: + /// + /// (eg: `~/Library/Containers/au.com.pact-foundation.Pact-macOS-Example/Data/Documents`) + /// + /// If testing a sandboxed macOS app, this is the default location and it can not be overwritten. + /// If testing a macOS app that is not sandboxed, define a `PACT_OUTPUT_DIR` Environment Variable (in the scheme) + /// with the path to where you want Pact contracts to be written to. + /// + /// iOS/tvOS or non-Xcode project: + /// + /// Default location where Pact contracts are written is `/tmp/pacts` and can be overwritten + /// with a `PACT_OUTPUT_DIR` environment variable set to an absolute path (eg: `$(PROJECT_DIR)/tmp/pacts`). + /// + static var pactDirectoryPath: String { + #if os(macOS) || os(OSX) + let defaultPath = NSHomeDirectory() + "/Documents" + if isSandboxed { + return defaultPath + } + return ProcessInfo.processInfo.environment["PACT_OUTPUT_DIR"] ?? defaultPath + #else + return ProcessInfo.processInfo.environment["PACT_OUTPUT_DIR"] ?? "/tmp/pacts" + #endif + } + + /// Returns true if the directory where Pact contracts are set to be written to exists. + /// If it does not exists, it attempts to create it and if successful, returns true. + /// + static func isPactDirectoryAvailable() -> Bool { + if FileManager.default.fileExists(atPath: pactDirectoryPath) == false { + do { + try FileManager.default.createDirectory(at: pactDirectoryURL, withIntermediateDirectories: true, attributes: nil) + } catch let error as NSError { + debugPrint("Directory \(pactDirectoryURL) could not be created! \(error.localizedDescription)") + return false + } + } + return true + } + +} + +// MARK: - Private + +private extension PactFileManager { + + static var pactDirectoryURL: URL { + URL(fileURLWithPath: pactDirectoryPath, isDirectory: true) + } + + /// Returns true if app is sandboxed + static var isSandboxed: Bool { + let environment = ProcessInfo.processInfo.environment + return environment["APP_SANDBOX_CONTAINER_ID"] != nil + } + +} diff --git a/Tests/ExampleGenerators/DateTimeExpressionTests.swift b/Tests/ExampleGenerators/DateTimeExpressionTests.swift new file mode 100644 index 00000000..b439e24d --- /dev/null +++ b/Tests/ExampleGenerators/DateTimeExpressionTests.swift @@ -0,0 +1,32 @@ +// +// DateTimeExpressionTests.swift +// PactSwift +// +// Created by Marko Justinek on 5/3/22. +// Copyright © 2022 Marko Justinek. All rights reserved. +// + +import XCTest + +@testable import PactSwift + +class DateTimeExpressionTests: XCTestCase { + + func testDateTimeExpressionExampleGenerator() throws { + let testFormat = "dd.MM.yyyy HH:mm:ss" + let testExpression = "tomorrow 5pm" + let sut = ExampleGenerator.DateTimeExpression(expression: testExpression, format: testFormat) + + XCTAssertEqual(sut.generator, .dateTime) + + let attributes = try XCTUnwrap(sut.rules) + XCTAssertTrue( + ["format", "expression"].allSatisfy { keyValue in + attributes.contains { key, _ in + key == keyValue + } + } + ) + } + +} diff --git a/Tests/ExampleGenerators/DateTimeTests.swift b/Tests/ExampleGenerators/DateTimeTests.swift new file mode 100644 index 00000000..bf3167fa --- /dev/null +++ b/Tests/ExampleGenerators/DateTimeTests.swift @@ -0,0 +1,36 @@ +// +// DateTimeTests.swift +// PactSwift +// +// Created by Marko Justinek on 13/2/22. +// Copyright © 2022 Marko Justinek. All rights reserved. +// + +import XCTest + +@testable import PactSwift + +class DateTimeTests: XCTestCase { + + func testDateTimeExampleGenerator() throws { + let testDate = Date() + let testFormat = "YYYY-MM-DD HH:mm" + let sut = ExampleGenerator.DateTime(format: testFormat, use: testDate) + + XCTAssertEqual(sut.generator, .dateTime) + + let resultValue = try XCTUnwrap(sut.value as? String) + let resultDate = try XCTUnwrap(DateHelper.dateFrom(string: resultValue, format: testFormat)) + // Assert using the same format due to loss of accuracy using a limited datetime format + XCTAssertEqual(testDate.formatted(testFormat), resultDate.formatted(testFormat)) + + let attributes = try XCTUnwrap(sut.rules) + XCTAssertTrue(attributes.contains { key, _ in + key == "format" + }) + + let resultFormat = try ExampleGeneratorTestHelpers.encodeDecode(sut.rules!) + XCTAssertEqual(resultFormat.format, testFormat) + } + +} diff --git a/Tests/ExampleGenerators/ObjCExampleGeneratorTests.swift b/Tests/ExampleGenerators/ObjCExampleGeneratorTests.swift new file mode 100644 index 00000000..63b5a84d --- /dev/null +++ b/Tests/ExampleGenerators/ObjCExampleGeneratorTests.swift @@ -0,0 +1,92 @@ +// +// Created by Marko Justinek on 27/10/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import XCTest + +@testable import PactSwift + +class ObjCExampleGeneratorTests: XCTestCase { + + func testObjCExampleGenerator_InitsWith_RandomBool() { + let testSubject = ObjcRandomBool() + + XCTAssertTrue((testSubject.type as Any) is ExampleGenerator.RandomBool) + } + + func testObjCExampleGenerator_InitsWith_RandomDate() throws { + let testSubject = ObjcRandomDate(format: "MM-YYYY") + + XCTAssertTrue((testSubject.type as Any) is ExampleGenerator.RandomDate) + XCTAssertEqual(testSubject.type.generator, .date) + } + + func testObjCExampleGenerator_InitsWith_DateTime() { + let testSubject = ObjcRandomDateTime() + + XCTAssertTrue((testSubject.type as Any) is ExampleGenerator.RandomDateTime) + XCTAssertEqual(testSubject.type.generator, .dateTime) + } + + func testObjCExampleGenerators_InitsWith_Decimal() { + let testSubject = ObjcRandomDecimal(digits: 4) + + XCTAssertTrue((testSubject.type as Any) is ExampleGenerator.RandomDecimal) + } + + func testObjCExampleGenerators_InitsWith_Hexadecimal() { + let testSubject = ObjcRandomHexadecimal(digits: 7) + + XCTAssertTrue((testSubject.type as Any) is ExampleGenerator.RandomHexadecimal) + } + + func testObjCExampleGenerators_InitsWith_Int() { + var testSubject = ObjcRandomInt(min: 1, max: 256) + testSubject = ObjcRandomInt() + XCTAssertTrue((testSubject.type as Any) is ExampleGenerator.RandomInt) + } + + func testObjCExampleGenerators_InitsWith_RandomString() { + let testSubject = ObjcRandomString(size: 10) + + XCTAssertTrue((testSubject.type as Any) is ExampleGenerator.RandomString) + } + + func testObjCExampleGenerators_InitsWith_RandomRegexString() { + let testSubject = ObjcRandomString(regex: #"\{d}2"#) + + XCTAssertTrue((testSubject.type as Any) is ExampleGenerator.RandomString) + } + + func testObjCExampleGenerators_InitsWith_RandomTime() { + let testSubject = ObjcRandomTime(format: #"MM-YYYY"#) + + XCTAssertTrue((testSubject.type as Any) is ExampleGenerator.RandomTime) + } + + func testObjCExampleGenerators_InitsWith_RandomUUID() { + let testSubject = ObjcRandomUUID() + + XCTAssertTrue((testSubject.type as Any) is ExampleGenerator.RandomUUID) + } + + func testObjCExampleGenerators_InitsWith_RandomUUIDFormat() { + let testSubject = ObjcRandomUUID(format: .uppercaseHyphenated) + + XCTAssertTrue((testSubject.type as Any) is ExampleGenerator.RandomUUID) + } + +} diff --git a/Tests/ExampleGenerators/ProviderStateGeneratorTests.swift b/Tests/ExampleGenerators/ProviderStateGeneratorTests.swift new file mode 100644 index 00000000..201d6e49 --- /dev/null +++ b/Tests/ExampleGenerators/ProviderStateGeneratorTests.swift @@ -0,0 +1,54 @@ +// +// Created by Marko Justinek on 15/8/21. +// Copyright © 2021 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import XCTest + +@testable import PactSwift + +class ProviderStateGeneratorTests: XCTestCase { + + let sut = ExampleGenerator.ProviderStateGenerator(parameter: "test-parameter", value: "test-value") + + func testProviderGenerator() throws { + XCTAssertEqual(sut.generator, .providerState) + } + + func testProviderGenerator_WithExpression() throws { + let valueResult = try XCTUnwrap((sut.value as Any) as? String) + XCTAssertEqual(valueResult, "test-value") + + let attributes = try XCTUnwrap(sut.rules) + XCTAssertTrue(attributes.contains { key, _ in + key == "expression" + }) + } + + func testProviderGenerator_WithType() throws { + let attributes = try XCTUnwrap(sut.rules) + XCTAssertTrue(attributes.contains { key, value in + key == "type" + }) + } + + func testProviderGenerator_SetsRules() throws { + let result = try ExampleGeneratorTestHelpers.encodeDecode(sut.rules!) + + XCTAssertEqual(result.type, "ProviderState") + XCTAssertEqual(result.expression, "test-parameter") + } + +} diff --git a/Tests/ExampleGenerators/RandomBooleanTests.swift b/Tests/ExampleGenerators/RandomBooleanTests.swift new file mode 100644 index 00000000..edaf076c --- /dev/null +++ b/Tests/ExampleGenerators/RandomBooleanTests.swift @@ -0,0 +1,31 @@ +// +// Created by Marko Justinek on 16/9/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import XCTest + +@testable import PactSwift + +class RandomBooleanTests: XCTestCase { + + func testBooleanExampleGenerator() { + let sut = ExampleGenerator.RandomBool() + XCTAssertTrue(sut.value is Bool) + XCTAssertEqual(sut.generator, .bool) + XCTAssertNil(sut.rules) + } + +} diff --git a/Tests/ExampleGenerators/RandomDateTests.swift b/Tests/ExampleGenerators/RandomDateTests.swift new file mode 100644 index 00000000..ad7deb9e --- /dev/null +++ b/Tests/ExampleGenerators/RandomDateTests.swift @@ -0,0 +1,52 @@ +// +// Created by Marko Justinek on 17/9/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import XCTest + +@testable import PactSwift + +class RandomDateTests: XCTestCase { + + func testRandomDate() { + let sut = ExampleGenerator.RandomDate() + + XCTAssertEqual(sut.generator, .date) + XCTAssertNil(sut.rules) + XCTAssertNotNil(DateHelper.dateFrom(isoString: try XCTUnwrap(sut.value as? String), isoFormat: [.withFullDate, .withDashSeparatorInDate])) + } + + func testRandomDate_WithFormat() throws { + let testFormat = "dd-MM-yyyy" + let sut = ExampleGenerator.RandomDate(format: testFormat) + + let attributes = try XCTUnwrap(sut.rules) + XCTAssertTrue(attributes.contains { key, _ in + key == "format" + }) + XCTAssertEqual(sut.generator, .date) + XCTAssertNotNil(DateHelper.dateFrom(string: try XCTUnwrap(sut.value as? String), format: testFormat)) + } + + func testRandomDate_SetsRules() throws { + let testFormat = "dd-MM-yyyy" + let sut = ExampleGenerator.RandomDate(format: testFormat) + let result = try ExampleGeneratorTestHelpers.encodeDecode(sut.rules!) + + XCTAssertEqual(result.format, testFormat) + } + +} diff --git a/Tests/ExampleGenerators/RandomDateTimeTests.swift b/Tests/ExampleGenerators/RandomDateTimeTests.swift new file mode 100644 index 00000000..575a5654 --- /dev/null +++ b/Tests/ExampleGenerators/RandomDateTimeTests.swift @@ -0,0 +1,52 @@ +// +// Created by Marko Justinek on 17/9/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import XCTest + +@testable import PactSwift + +class RandomDateTimeTests: XCTestCase { + + func testRandomDateTime() { + let sut = ExampleGenerator.RandomDateTime() + + XCTAssertEqual(sut.generator, .dateTime) + XCTAssertNil(sut.rules) + XCTAssertNotNil(DateHelper.dateFrom(isoString: try XCTUnwrap(sut.value as? String), isoFormat: [.withFullDate, .withFullTime])) + } + + func testRandomDateTime_WithFormat() throws { + let testFormat = "yyyy/MM/dd - HH:mm:ss.S" + let sut = ExampleGenerator.RandomDateTime(format: testFormat) + + let attributes = try XCTUnwrap(sut.rules) + XCTAssertTrue(attributes.contains { key, _ in + key == "format" + }) + XCTAssertEqual(sut.generator, .dateTime) + XCTAssertNotNil(DateHelper.dateFrom(string: try XCTUnwrap(sut.value as? String), format: testFormat)) + } + + func testRandomDateTime_SetsRules() throws { + let testFormat = "yyyy/MM/dd - HH:mm:ss.S" + let sut = ExampleGenerator.RandomDateTime(format: testFormat) + let result = try ExampleGeneratorTestHelpers.encodeDecode(sut.rules!) + + XCTAssertEqual(result.format, testFormat) + } + +} diff --git a/Tests/ExampleGenerators/RandomDecimalTests.swift b/Tests/ExampleGenerators/RandomDecimalTests.swift new file mode 100644 index 00000000..8b63ed8c --- /dev/null +++ b/Tests/ExampleGenerators/RandomDecimalTests.swift @@ -0,0 +1,59 @@ +// +// Created by Marko Justinek on 17/9/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import XCTest + +@testable import PactSwift + +class RandomDecimalTests: XCTestCase { + + func testRandomDecimal() throws { + let sut = ExampleGenerator.RandomDecimal() + + let attributes = try XCTUnwrap(sut.rules) + XCTAssertTrue(attributes.contains { key, _ in + key == "digits" + }) + XCTAssertEqual(sut.generator, .decimal) + let decimalValue = try XCTUnwrap(sut.value as? Decimal) + XCTAssertEqual(String(describing: decimalValue).count, 6) + } + + func testRandomDecimal_WithDigits() throws { + let sut = ExampleGenerator.RandomDecimal(digits: 12) + + let attributes = try XCTUnwrap(sut.rules) + XCTAssertTrue(attributes.contains { key, _ in + key == "digits" + }) + XCTAssertEqual(sut.generator, .decimal) + let decimalValue = try XCTUnwrap(sut.value as? Decimal) + + // Expecting 9 digits as it's the set as the cap in ExampleGenerator.Decimal + XCTAssertEqual(String(describing: decimalValue).count, 9) + } + + func testRandomDecimal_SetsRules() throws { + let sut = ExampleGenerator.RandomDecimal(digits: 12) + let result = try ExampleGeneratorTestHelpers.encodeDecode(sut.rules!) + + // Expecting a key `digits` with value 9 + XCTAssertEqual(result.digits, 9) + } + +} + diff --git a/Tests/ExampleGenerators/RandomHexadecimalTests.swift b/Tests/ExampleGenerators/RandomHexadecimalTests.swift new file mode 100644 index 00000000..288ac43c --- /dev/null +++ b/Tests/ExampleGenerators/RandomHexadecimalTests.swift @@ -0,0 +1,60 @@ +// +// Created by Marko Justinek on 17/9/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import XCTest + +@testable import PactSwift + +class RandomHexadecimalTests: XCTestCase { + + func testRandomHexadecimal() throws { + let sut = ExampleGenerator.RandomHexadecimal() + + XCTAssertEqual(sut.generator, .hexadecimal) + + let attributes = try XCTUnwrap(sut.rules) + XCTAssertTrue(attributes.contains { key, _ in + key == "digits" + }) + + let hexValue = try XCTUnwrap(sut.value as? String) + XCTAssertEqual(hexValue.count, 8) + } + + func testRandomHexadecimal_WithDigits() throws { + let sut = ExampleGenerator.RandomHexadecimal(digits: 16) + + XCTAssertEqual(sut.generator, .hexadecimal) + + let attributes = try XCTUnwrap(sut.rules) + XCTAssertTrue(attributes.contains { key, _ in + key == "digits" + }) + + let hexValue = try XCTUnwrap(sut.value as? String) + XCTAssertEqual(hexValue.count, 16) + } + + func testRandomHexadecimal_SetsRules() throws { + let sut = ExampleGenerator.RandomHexadecimal(digits: 16) + let result = try ExampleGeneratorTestHelpers.encodeDecode(sut.rules!) + + // Expecting a key `digits` with value 16 + XCTAssertEqual(result.digits, 16) + } + +} diff --git a/Tests/ExampleGenerators/RandomIntTests.swift b/Tests/ExampleGenerators/RandomIntTests.swift new file mode 100644 index 00000000..670bc802 --- /dev/null +++ b/Tests/ExampleGenerators/RandomIntTests.swift @@ -0,0 +1,43 @@ +// +// Created by Marko Justinek on 16/9/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import XCTest + +@testable import PactSwift + +class RandomIntTests: XCTestCase { + + func testRandomIntExampleGenerator() throws { + let expectedKeys = ["min", "max"] + let sut = ExampleGenerator.RandomInt(min: -4231, max: 64210) + + let attributes = try XCTUnwrap(sut.rules) + XCTAssertTrue((sut.value as Any) is Int) + XCTAssertEqual(sut.generator, .int) + XCTAssertTrue(attributes.allSatisfy { key, value in expectedKeys.contains(key) }) + XCTAssertTrue((-4231...64210).contains(sut.value as! Int)) + } + + func testRandomInt_SetsRules() throws { + let sut = ExampleGenerator.RandomInt(min: -4231, max: 2147483647) + let result = try ExampleGeneratorTestHelpers.encodeDecode(sut.rules!) + + XCTAssertEqual(result.min, -4231) + XCTAssertEqual(result.max, 2147483647) + } + +} diff --git a/Tests/ExampleGenerators/RandomStringTests.swift b/Tests/ExampleGenerators/RandomStringTests.swift new file mode 100644 index 00000000..befd71d7 --- /dev/null +++ b/Tests/ExampleGenerators/RandomStringTests.swift @@ -0,0 +1,74 @@ +// +// Created by Marko Justinek on 18/9/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import XCTest + +@testable import PactSwift + +class RandomStringTests: XCTestCase { + + func testRandomString() throws { + let sut = ExampleGenerator.RandomString() + + let attributes = try XCTUnwrap(sut.rules) + XCTAssertTrue(attributes.contains { key, _ in + key == "size" + }) + XCTAssertEqual(sut.generator, .string) + let stringValue = try XCTUnwrap(sut.value as? String) + XCTAssertEqual(stringValue.count, 20) + } + + func testRandomString_WithSize() throws { + let sut = ExampleGenerator.RandomString(size: 1145) + + let attributes = try XCTUnwrap(sut.rules) + XCTAssertTrue(attributes.contains { key, _ in + key == "size" + }) + XCTAssertEqual(sut.generator, .string) + let stringValue = try XCTUnwrap(sut.value as? String) + XCTAssertEqual(stringValue.count, 1145) + } + + func testRandomRegex() throws { + let sut = ExampleGenerator.RandomString(regex: #"\d{1,2}/\d{1,2}"#) + + let attributes = try XCTUnwrap(sut.rules) + XCTAssertTrue(attributes.contains { key, _ in + key == "regex" + }) + + XCTAssertEqual(sut.generator, .regex) + } + + func testRandomString_SetsSizeRules() throws { + let sut = ExampleGenerator.RandomString(size: 20) + let result = try ExampleGeneratorTestHelpers.encodeDecode(sut.rules!) + + XCTAssertEqual(result.size, 20) + } + + func testRandomString_SetsRegexRules() throws { + let regex = #"\d{1,2}/\d{1,2}"# + let sut = ExampleGenerator.RandomString(regex: regex) + let result = try ExampleGeneratorTestHelpers.encodeDecode(sut.rules!) + + XCTAssertEqual(result.regex, regex) + } + +} diff --git a/Tests/ExampleGenerators/RandomTimeTests.swift b/Tests/ExampleGenerators/RandomTimeTests.swift new file mode 100644 index 00000000..ad685800 --- /dev/null +++ b/Tests/ExampleGenerators/RandomTimeTests.swift @@ -0,0 +1,53 @@ +// +// Created by Marko Justinek on 17/9/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import XCTest + +@testable import PactSwift + +class RandomTimeTests: XCTestCase { + + func testRandomTime() throws { + let sut = ExampleGenerator.RandomTime() + + XCTAssertEqual(sut.generator, .time) + XCTAssertNil(sut.rules) + let isoTime = try XCTUnwrap(sut.value as? String) + XCTAssertNotNil(DateHelper.dateFrom(isoString: isoTime, isoFormat: [.withFullTime])) + } + + func testRandomTime_WithFormat() throws { + let testFormat = "HH:mm" + let sut = ExampleGenerator.RandomTime(format: testFormat) + + let attributes = try XCTUnwrap(sut.rules) + XCTAssertTrue(attributes.contains { key, _ in + key == "format" + }) + XCTAssertEqual(sut.generator, .time) + XCTAssertNotNil(DateHelper.dateFrom(string: try XCTUnwrap(sut.value as? String), format: testFormat)) + } + + func testRandomTime_SetsRules() throws { + let testFormat = "HH:mm" + let sut = ExampleGenerator.RandomTime(format: testFormat) + let result = try ExampleGeneratorTestHelpers.encodeDecode(sut.rules!) + + XCTAssertEqual(result.format, testFormat) + } + +} diff --git a/Tests/ExampleGenerators/RandomUuidTests.swift b/Tests/ExampleGenerators/RandomUuidTests.swift new file mode 100644 index 00000000..af20c722 --- /dev/null +++ b/Tests/ExampleGenerators/RandomUuidTests.swift @@ -0,0 +1,82 @@ +// +// Created by Marko Justinek on 16/9/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import XCTest + +@testable import PactSwift + +class RandomUUIDTests: XCTestCase { + + func testRandomUUIDExampleGenerator() throws { + let sut = ExampleGenerator.RandomUUID() + + XCTAssertNotNil(UUID(uuidString: try XCTUnwrap(sut.value as? String))) + XCTAssertEqual(sut.generator, .uuid) + XCTAssertNotNil(sut.rules) + } + + func testRandomUUIDDefaultFormat() throws { + let sut = ExampleGenerator.RandomUUID() + + let uuid = try XCTUnwrap(sut.value as? String) + XCTAssertEqual(uuid.count, 36) + + let result = try ExampleGeneratorTestHelpers.encodeDecode(sut.rules!) + XCTAssertEqual(result.format, "upper-case-hyphenated") + } + + func testRandomUUIDUpperCaseHyphenatedFormat() throws { + let sut = ExampleGenerator.RandomUUID(format: .uppercaseHyphenated) + + let uuid = try XCTUnwrap(sut.value as? String) + XCTAssertEqual(uuid.count, 36) + + let result = try ExampleGeneratorTestHelpers.encodeDecode(sut.rules!) + XCTAssertEqual(result.format, "upper-case-hyphenated") + } + + func testRandomUUIDSimpleFormat() throws { + let sut = ExampleGenerator.RandomUUID(format: .simple) + + let uuid = try XCTUnwrap(sut.value as? String) + XCTAssertEqual(uuid.count, 32) + + let result = try ExampleGeneratorTestHelpers.encodeDecode(sut.rules!) + XCTAssertEqual(result.format, "simple") + } + + func testRandomUUIDLowerCaseHyphenatedFormat() throws { + let sut = ExampleGenerator.RandomUUID(format: .lowercaseHyphenated) + + let uuid = try XCTUnwrap(sut.value as? String) + XCTAssertEqual(uuid.count, 36) + + let result = try ExampleGeneratorTestHelpers.encodeDecode(sut.rules!) + XCTAssertEqual(result.format, "lower-case-hyphenated") + } + + func testRandomUUIDURNFormat() throws { + let sut = ExampleGenerator.RandomUUID(format: .urn) + + let uuid = try XCTUnwrap(sut.value as? String) + XCTAssertEqual(uuid.prefix(9), "urn:uuid:") + + let result = try ExampleGeneratorTestHelpers.encodeDecode(sut.rules!) + XCTAssertEqual(result.format, "URN") + } + +} diff --git a/Tests/Extensions/String+PactSwiftTests.swift b/Tests/Extensions/String+PactSwiftTests.swift new file mode 100644 index 00000000..7ed4bc68 --- /dev/null +++ b/Tests/Extensions/String+PactSwiftTests.swift @@ -0,0 +1,32 @@ +// +// Created by Marko Justinek on 7/8/21. +// Copyright © 2021 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import XCTest + +@testable import PactSwift + +class StringExtensionTests: XCTestCase { + + func testConvertsSimpleUUID() { + XCTAssertEqual("1234abcd1234abcf12ababcdef1234567".uuid, UUID(uuidString: "1234abcd-1234-abcf-12ab-abcdef1234567")) + } + + func testInvalidStringUUIDIsNil() { + XCTAssertNil("a".uuid) + } + +} diff --git a/Tests/Matchers/DecimalLikeTests.swift b/Tests/Matchers/DecimalLikeTests.swift new file mode 100644 index 00000000..027dadad --- /dev/null +++ b/Tests/Matchers/DecimalLikeTests.swift @@ -0,0 +1,39 @@ +// +// Created by Marko Justinek on 11/4/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import XCTest + +@testable import PactSwift + +class DecimalLikeTests: XCTestCase { + + func testMatcher_DecimalLike_InitsWithValue() throws { + let testResult = try XCTUnwrap((Matcher.DecimalLike(1234).value as Any) as? Decimal) + XCTAssertEqual(testResult, 1234) + + let testDecimalResult = try XCTUnwrap((Matcher.DecimalLike(1234.56).value as Any) as? Decimal) + XCTAssertEqual(testDecimalResult, 1234.56) + } + + func testMatcher_DecimalLike_SetsRules() throws { + let sut = Matcher.DecimalLike(Decimal(1234)) + let result = try MatcherTestHelpers.encodeDecode(sut.rules) + + XCTAssertEqual(result.first?.match, "decimal") + } + +} diff --git a/Tests/Matchers/EachKeyLikeTests.swift b/Tests/Matchers/EachKeyLikeTests.swift new file mode 100644 index 00000000..47bc53e9 --- /dev/null +++ b/Tests/Matchers/EachKeyLikeTests.swift @@ -0,0 +1,34 @@ +// +// Created by Marko Justinek on 3/4/2022. +// Copyright © 2022 PACT Foundation. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import XCTest + +@testable import PactSwift + +class EachKeyLikeTests: XCTestCase { + + func testMatcher_EachKeyLike_InitsWithStringValue() throws { + let sut = try XCTUnwrap(Matcher.EachKeyLike("bar").value as? String) + XCTAssertEqual(sut, "bar") + } + + func testMatcher_EachKeyLike_InitsWithIntValue() throws { + let sut = try XCTUnwrap(Matcher.EachKeyLike(123).value as? Int) + XCTAssertEqual(sut, 123) + } + +} diff --git a/Tests/Matchers/EachLikeTests.swift b/Tests/Matchers/EachLikeTests.swift new file mode 100644 index 00000000..0fef2958 --- /dev/null +++ b/Tests/Matchers/EachLikeTests.swift @@ -0,0 +1,113 @@ +// +// Created by Marko Justinek on 11/4/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import XCTest + +@testable import PactSwift + +class EachLikeTests: XCTestCase { + + func testMatcher_EachLike_InitsWithValue() throws { + // Array of Strings + let testStringResult = try XCTUnwrap(Matcher.EachLike("foo").value as? [String]) + XCTAssertEqual(testStringResult, ["foo"]) + + // Array of Ints + let testIntResult = try XCTUnwrap(Matcher.EachLike(12345).value as? [Int]) + XCTAssertEqual(testIntResult, [12345]) + + // Array of Dictionaries + let testDictResult = try XCTUnwrap((Matcher.EachLike(["foo": 123.45]).value as? [[String: Double]])?.first) + XCTAssertEqual(testDictResult["foo"], 123.45) + } + + func testMatcher_EachLike_InitsWithDefault_MinValue() throws { + // Array of Strings + let testResult = try XCTUnwrap(Matcher.EachLike("foo").min) + XCTAssertEqual(testResult, 1) + } + + func testMatcher_EachLike_InitsWithProvided_MinValue() throws { + // Array of Strings + let testResult = try XCTUnwrap(Matcher.EachLike("foo", min: 99).min) + XCTAssertEqual(testResult, 99) + } + + func testMatcher_EachLike_InitsWithout_MaxValue() { + XCTAssertNil(Matcher.EachLike("foo").max) + } + + func testMatcher_EachLike_InitsWithProvided_MaxValue() throws { + // Array of Strings + let testResult = try XCTUnwrap(Matcher.EachLike("foo", max: 5).max) + XCTAssertEqual(testResult, 5) + } + + func testMatcher_EachLike_InitsWithMinAndMaxValue() throws { + // Array of Strings + let testResult = try XCTUnwrap(Matcher.EachLike("foo", min: 1, max: 5)) + XCTAssertEqual(testResult.min, 1) + XCTAssertEqual(testResult.max, 5) + } + + func testMatcher_EachLike_InitsWithCount() throws { + // Array of count + let testResult = try XCTUnwrap(Matcher.EachLike("foo", count: 3).value as? [String]) + XCTAssertEqual(testResult.count, 3) + } + + func testMatcher_EachLike_SetsMinMaxRules() throws { + let sut = Matcher.EachLike("test", min: 0, max: 666, count: 123) + let result = try MatcherTestHelpers.encodeDecode(sut.rules) + + XCTAssertTrue(result.contains { + $0.match == "type" && $0.min == 0 && $0.max == 666 + }) + } + + func testMatcher_EachLike_SetsDefaultMinParametersRules() throws { + let sut = Matcher.EachLike("test") + let result = try MatcherTestHelpers.encodeDecode(sut.rules) + + XCTAssertTrue(result.contains { $0.match == "type" && $0.min == 1}) + XCTAssertFalse(result.first(where: { $0.max != nil }) != nil) + } + + func testMatcher_EachLike_OmitsMaxParametersRules() throws { + let sut = Matcher.EachLike("test", min: 5) + let result = try MatcherTestHelpers.encodeDecode(sut.rules) + + XCTAssertTrue(result.contains { $0.match == "type" && $0.min == 5}) + XCTAssertFalse(result.first(where: { $0.max != nil }) != nil) + } + + func testMatcher_EachLike_OmitsDefaultMinParametersRules() throws { + let sut = Matcher.EachLike("test", max: 10) + let result = try MatcherTestHelpers.encodeDecode(sut.rules) + + XCTAssertTrue(result.contains { $0.match == "type" && $0.max == 10}) + XCTAssertFalse(result.first(where: { $0.min != nil }) != nil) + } + + func testMatcher_EachLike_HandlesBogusMinMax() throws { + let sut = Matcher.EachLike("test", min: 5, max: 3) + let result = try MatcherTestHelpers.encodeDecode(sut.rules) + + XCTAssertTrue(result.contains { $0.match == "type" && $0.min == 3 && $0.max == 5 }) + } + +} diff --git a/Tests/Matchers/EqualToTests.swift b/Tests/Matchers/EqualToTests.swift new file mode 100644 index 00000000..6e1b5beb --- /dev/null +++ b/Tests/Matchers/EqualToTests.swift @@ -0,0 +1,51 @@ +// +// Created by Marko Justinek on 10/9/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import XCTest + +@testable import PactSwift + +class EqualToTests: XCTestCase { + + func testMatcher_Equal_InitsWithToString() throws { + XCTAssertEqual(try XCTUnwrap(Matcher.EqualTo("TestValue").value as? String), "TestValue") + } + + func testMatcher_Equal_InitsWithToInt() throws { + XCTAssertEqual(try XCTUnwrap(Matcher.EqualTo(32).value as? Int), Int(32)) + } + + func testMatcher_EqualTo_InitsWithDouble() throws { + XCTAssertEqual(try XCTUnwrap(Matcher.EqualTo(128.32).value as? Double), Double(128.32)) + } + + func testMatcher_EqualTo_InitsWithArrayOfStrings() throws { + XCTAssertEqual(try XCTUnwrap(Matcher.EqualTo(["TestValue", "Teapot"]).value as? [String]), ["TestValue", "Teapot"]) + } + + func testMatcher_EqualTo_InitsWithDecimal() throws { + XCTAssertEqual(try XCTUnwrap(Matcher.EqualTo(Decimal(123.45)).value as? Decimal), Decimal(123.45)) + } + + func testMatcher_EqualTo_SetsRules() throws { + let sut = Matcher.EqualTo("test") + let result = try MatcherTestHelpers.encodeDecode(sut.rules) + + XCTAssertEqual(result.first?.match, "equality") + } + +} diff --git a/Tests/Matchers/FromProviderStateTests.swift b/Tests/Matchers/FromProviderStateTests.swift new file mode 100644 index 00000000..48146555 --- /dev/null +++ b/Tests/Matchers/FromProviderStateTests.swift @@ -0,0 +1,50 @@ +// +// Created by Marko Justinek on 15/8/21. +// Copyright © 2021 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import XCTest + +@testable import PactSwift + +class FromProviderStateTests: XCTestCase { + + func testMatcher_FromProviderState_InitsWithValue() throws { + let testStringResult = try XCTUnwrap((Matcher.FromProviderState(parameter: "testParameter", value: .string("some-string")).value as Any) as? String) + XCTAssertEqual(testStringResult, "some-string") + + let testIntResult = try XCTUnwrap((Matcher.FromProviderState(parameter: "testParameter", value: .int(100)).value as Any) as? Int) + XCTAssertEqual(testIntResult, 100) + + let testDecimalResult = try XCTUnwrap((Matcher.FromProviderState(parameter: "testParameter", value: .decimal(Decimal(123.45))).value as Any) as? Decimal) + XCTAssertEqual(testDecimalResult, Decimal(123.45)) + + let testBoolResult = try XCTUnwrap((Matcher.FromProviderState(parameter: "testParameter", value: .bool(true)).value as Any) as? Bool) + XCTAssertEqual(testBoolResult, true) + } + + func testMatcher_FromProviderState_InitsWithparameter() { + let testParameterResult = Matcher.FromProviderState(parameter: "testParameter", value: .int(100)).parameter + XCTAssertEqual(testParameterResult, "testParameter") + } + + func testMatcher_FromProviderState_SetsRules() throws { + let sut = Matcher.FromProviderState(parameter: "test", value: .string("value")) + let result = try MatcherTestHelpers.encodeDecode(sut.rules) + + XCTAssertEqual(result.first?.match, "type") + } + +} diff --git a/Tests/Matchers/IncludesLikeTests.swift b/Tests/Matchers/IncludesLikeTests.swift new file mode 100644 index 00000000..6a712249 --- /dev/null +++ b/Tests/Matchers/IncludesLikeTests.swift @@ -0,0 +1,65 @@ +// +// Created by Marko Justinek on 26/5/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import XCTest + +@testable import PactSwift + +class IncludesLikeTests: XCTestCase { + + func testInitsWith_ArrayArgument() throws { + let testResult = Matcher.IncludesLike(["Foo", "Bar"], combine: .AND) + + XCTAssertEqual(testResult.rules.count, 2) + XCTAssertEqual(try XCTUnwrap((testResult.value as Any) as? String), "Foo Bar") + } + + func testInitsWith_VariadicArgument() throws { + let testResult = Matcher.IncludesLike("Foo", "Bar", "Baz", combine: .OR) + + XCTAssertEqual(testResult.rules.count, 3) + XCTAssertEqual(testResult.combine, .OR) + XCTAssertEqual(try XCTUnwrap((testResult.value as Any) as? String), "Foo Bar Baz") + } + + func testInitsWith_ArrayArgument_AndGeneratedValue() throws { + let testResult = Matcher.IncludesLike(["I'm", "Teapot"], combine: .AND, generate: "I'm a little Teapot") + + XCTAssertEqual(testResult.rules.count, 2) + XCTAssertEqual(testResult.combine, .AND) + XCTAssertEqual(try XCTUnwrap((testResult.value as Any) as? String), "I'm a little Teapot") + } + + func testInitsWith_VariadicArgument_AndGeneratedValue() throws { + let testResult = Matcher.IncludesLike("Teapot", "I'm", combine: .AND, generate: "I'm a big Teapot") + + XCTAssertEqual(testResult.rules.count, 2) + XCTAssertEqual(testResult.combine, .AND) + XCTAssertEqual(try XCTUnwrap((testResult.value as Any) as? String), "I'm a big Teapot") + } + + func testMatcher_IncludesLike_SetsRules() throws { + let sut = Matcher.IncludesLike("Teapot", "I'm", combine: .AND, generate: "I'm a big Teapot") + let result = try MatcherTestHelpers.encodeDecode(sut.rules) + + XCTAssertTrue(result.allSatisfy { $0.match == "include" }) + XCTAssertTrue(result.allSatisfy { encodedModel in + ["Teapot", "I'm"].contains(encodedModel.value) + }) + } + +} diff --git a/Tests/Matchers/IntegerLikeTests.swift b/Tests/Matchers/IntegerLikeTests.swift new file mode 100644 index 00000000..6197a3a4 --- /dev/null +++ b/Tests/Matchers/IntegerLikeTests.swift @@ -0,0 +1,36 @@ +// +// Created by Marko Justinek on 11/4/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import XCTest + +@testable import PactSwift + +class IntegerLikeTests: XCTestCase { + + func testMatcher_IntegerLike_InitsWithValue() throws { + let testResult = try XCTUnwrap((Matcher.IntegerLike(1234).value as Any) as? Int) + XCTAssertEqual(testResult, 1234) + } + + func testMatcher_IntegerLike_SetsRules() throws { + let sut = Matcher.IntegerLike(123) + let result = try MatcherTestHelpers.encodeDecode(sut.rules) + + XCTAssertEqual(result.first?.match, "integer") + } + +} diff --git a/Tests/Matchers/MatchNullTests.swift b/Tests/Matchers/MatchNullTests.swift new file mode 100644 index 00000000..9fbfc025 --- /dev/null +++ b/Tests/Matchers/MatchNullTests.swift @@ -0,0 +1,32 @@ +// +// MatchNullTests.swift +// PactSwift +// +// Created by Marko Justinek on 9/10/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// + +import XCTest + +@testable import PactSwift + +class MatchNullTests: XCTestCase { + + func testMatcher_MatchNull() throws { + let matcherValue = try XCTUnwrap((Matcher.MatchNull().value as Any) as? String) + XCTAssertEqual(matcherValue, "pact_matcher_null") + + let matcherRules = try XCTUnwrap(Matcher.MatchNull().rules.first) + XCTAssertTrue(matcherRules.contains { key, value in + key == "match" + }) + } + + func testMatcher_MatchNull_SetsRules() throws { + let sut = Matcher.MatchNull() + let result = try MatcherTestHelpers.encodeDecode(sut.rules) + + XCTAssertEqual(result.first?.match, "null") + } + +} diff --git a/Tests/Matchers/ObjCMatcherTests.swift b/Tests/Matchers/ObjCMatcherTests.swift new file mode 100644 index 00000000..9bc9e6fa --- /dev/null +++ b/Tests/Matchers/ObjCMatcherTests.swift @@ -0,0 +1,170 @@ +// +// Created by Marko Justinek on 27/10/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import XCTest + +@testable import PactSwift + +class ObjCMatcherTests: XCTestCase { + + func testObjCMatcher_InitsWith_DecimalLike() throws { + let testSubject = ObjcDecimalLike(value: Decimal(string: "42")!) + + XCTAssertTrue((testSubject.type as Any) is Matcher.DecimalLike) + XCTAssertEqual(try XCTUnwrap(testSubject.type.value as? Decimal), Decimal(string: "42")!) + } + + func testObjCMatcher_InitsWith_EachLike() throws { + var testSubject = ObjcEachLike(value: "foo") + + XCTAssertTrue((testSubject.type as Any) is Matcher.EachLike) + XCTAssertEqual(try XCTUnwrap(testSubject.type.value as? [String]), ["foo"]) + + testSubject = ObjcEachLike(value: Int(1)) + XCTAssertEqual(try XCTUnwrap(testSubject.type.value as? [Int]), [1]) + + testSubject = ObjcEachLike(value: ["foo": "bar"]) + XCTAssertEqual(try XCTUnwrap(testSubject.type.value as? [[String: String]]), [["foo": "bar"]]) + } + + func testObjCMatcher_InitsWith_EachLike_MinMax() throws { + let testSubject = ObjcEachLike(value: "foo", min: 2, max: 9) + + XCTAssertTrue((testSubject.type as Any) is Matcher.EachLike) + XCTAssertEqual(try XCTUnwrap(testSubject.type.value as? [String]), ["foo", "foo"]) + XCTAssertEqual(try XCTUnwrap(testSubject.type as? Matcher.EachLike).min, 2) + XCTAssertEqual(try XCTUnwrap(testSubject.type as? Matcher.EachLike).max, 9) + } + + func testObjcMatcher_EachKeyLike() throws { + var testSubject = ObjcEachKeyLike(value: "bar") + XCTAssertEqual(try XCTUnwrap((testSubject.type as? Matcher.EachKeyLike)?.value as? String), "bar") + + testSubject = ObjcEachKeyLike(value: ["bar": 123]) + XCTAssertEqual(try XCTUnwrap((testSubject.type as? Matcher.EachKeyLike)?.value as? [String: Int]), ["bar": 123]) + } + + func testObjCMatcher_InitsWith_EqualTo() throws { + var testSubject = ObjcEqualTo(value: "foo") + + XCTAssertTrue((testSubject.type as Any) is Matcher.EqualTo) + XCTAssertEqual(try XCTUnwrap(testSubject.type.value as? String), "foo") + + testSubject = ObjcEqualTo(value: 42) + XCTAssertTrue((testSubject.type as Any) is Matcher.EqualTo) + XCTAssertEqual(try XCTUnwrap(testSubject.type.value as? Int), 42) + } + + func testObjCMatcher_InitsWith_IncludesLikeAll() throws { + let testSubject = ObjcIncludesLike(includesAll: ["foo", "bar"], generate: "This bar is totally foo") + + XCTAssertTrue((testSubject.type as Any) is Matcher.IncludesLike) + XCTAssertEqual(try XCTUnwrap((testSubject.type as? Matcher.IncludesLike)?.combine), .AND) + XCTAssertEqual(try XCTUnwrap((testSubject.type as? Matcher.IncludesLike)?.value as? String), "This bar is totally foo") + } + + func testObjCMatcher_InitsWith_IncludesLikeAny() throws { + let testSubject = ObjcIncludesLike(includesAny: ["foo", "bar"], generate: "This bar is totally foo") + + XCTAssertTrue((testSubject.type as Any) is Matcher.IncludesLike) + XCTAssertEqual(try XCTUnwrap((testSubject.type as? Matcher.IncludesLike)?.combine), .OR) + XCTAssertEqual(try XCTUnwrap((testSubject.type as? Matcher.IncludesLike)?.value as? String), "This bar is totally foo") + } + + func testObjcMatcher_InitsWith_IntegerLike() throws { + let testSubject = ObjcIntegerLike(value: 42) + + XCTAssertTrue((testSubject.type as Any) is Matcher.IntegerLike) + XCTAssertEqual(try XCTUnwrap(testSubject.type.value as? Int), 42) + } + + func testObjcMatcher_InitsWith_MatchNull() { + let testSubject = ObjcMatchNull() + + XCTAssertTrue((testSubject.type as Any) is Matcher.MatchNull) + } + + func testObjcMatcher_InitsWith_RegexLike() { + let testSubject = ObjcRegexLike(value: "31-01-2016", pattern: #"\d{2}-\d{2}-\d{4}"#) + + XCTAssertTrue((testSubject.type as Any) is Matcher.RegexLike) + XCTAssertEqual(try XCTUnwrap(testSubject.type.value as? String), "31-01-2016") + } + + func testObjcMatcher_InitsWith_SomethingLike() { + var testSubject = ObjcSomethingLike(value: "foo") + + XCTAssertTrue((testSubject.type as Any) is Matcher.SomethingLike) + XCTAssertEqual(try XCTUnwrap(testSubject.type.value as? String), "foo") + + testSubject = ObjcSomethingLike(value: 42) + XCTAssertEqual(try XCTUnwrap(testSubject.type.value as? Int), 42) + } + + func testObjcMatcher_InitsWith_OneOf() { + var testSubject = ObjcOneOf(values: [5, 1, 2, 3, 4]) + + XCTAssertTrue((testSubject.type as Any) is Matcher.OneOf) + XCTAssertEqual(try XCTUnwrap(testSubject.type.value as? Int), 5) + + testSubject = ObjcOneOf(values: ["five", "one", "two", "three"]) + XCTAssertEqual(try XCTUnwrap(testSubject.type.value as? String), "five") + } + + func testObjcMatcher_FromProviderState_String() { + let testSubject = ObjcFromProviderState(parameter: "testString", value: "string") + + XCTAssertTrue((testSubject.type as Any) is Matcher.FromProviderState) + XCTAssertEqual(try XCTUnwrap(testSubject.type.value as? String), "string") + } + + func testObjcMatcher_FromProviderState_Int() { + let testSubject = ObjcFromProviderState(parameter: "testInt", value: 666) + + XCTAssertTrue((testSubject.type as Any) is Matcher.FromProviderState) + XCTAssertEqual(try XCTUnwrap(testSubject.type.value as? Int), 666) + } + + func testObjcMatcher_FromProviderState_Bool() { + let testSubject = ObjcFromProviderState(parameter: "testBool", value: false) + + XCTAssertTrue((testSubject.type as Any) is Matcher.FromProviderState) + XCTAssertFalse(try XCTUnwrap(testSubject.type.value as? Bool)) + } + + func testObjcMatcher_FromProviderState_Decimal() { + let testSubject = ObjcFromProviderState(parameter: "testDecimal", value: Decimal(1234.56)) + + XCTAssertTrue((testSubject.type as Any) is Matcher.FromProviderState) + XCTAssertEqual(try XCTUnwrap(testSubject.type.value as? Decimal), Decimal(1234.56)) + } + + func testObjcMatcher_FromProviderState_Float() { + let testSubject = ObjcFromProviderState(parameter: "testFloat", value: Float(123.45)) + + XCTAssertTrue((testSubject.type as Any) is Matcher.FromProviderState) + XCTAssertEqual(try XCTUnwrap(testSubject.type.value as? Float), 123.45, accuracy: 0.0001) + } + + func testObjcMatcher_FromProviderState_Double() { + let testSubject = ObjcFromProviderState(parameter: "testDouble", value: Double(123.45)) + + XCTAssertTrue((testSubject.type as Any) is Matcher.FromProviderState) + XCTAssertEqual(try XCTUnwrap(testSubject.type.value as? Double), 123.45) + } + +} diff --git a/Tests/Matchers/OneOfTests.swift b/Tests/Matchers/OneOfTests.swift new file mode 100644 index 00000000..a2aa0b2b --- /dev/null +++ b/Tests/Matchers/OneOfTests.swift @@ -0,0 +1,80 @@ +// +// Created by Marko Justinek on 9/7/21. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import XCTest + +@testable import PactSwift + +class OneOfTests: XCTestCase { + + func testMatcher_OneOf_InitsWithStringValue() throws { + let testResult = try XCTUnwrap(Matcher.OneOf("enabled", "disabled")) + XCTAssertEqual(testResult.pattern, "^(enabled|disabled)$") + XCTAssertEqual(testResult.value as? String, "enabled") + XCTAssertNotNil(testResult.rules[0]["match"]) + XCTAssertNotNil(testResult.rules[0]["regex"]) + } + + func testMatcher_OneOf_InitsWithIntValue() throws { + let testResult = try XCTUnwrap(Matcher.OneOf(3, 1, 2)) + XCTAssertEqual(testResult.pattern, "^(3|1|2)$") + XCTAssertEqual(testResult.value as? Int, 3) + XCTAssertNotNil(testResult.rules[0]["match"]) + XCTAssertNotNil(testResult.rules[0]["regex"]) + } + + func testMatcher_OneOf_InitsWithDecimal() throws { + let testResult = try XCTUnwrap(Matcher.OneOf(Decimal(100.15), 100.24)) + XCTAssertEqual(testResult.pattern, "^(100.15|100.24)$") + XCTAssertEqual(testResult.value as? Decimal, Decimal(100.15)) + XCTAssertNotNil(testResult.rules[0]["match"]) + XCTAssertNotNil(testResult.rules[0]["regex"]) + } + + func testMatcher_OneOf_InitsWithFloat() throws { + let testResult = try XCTUnwrap(Matcher.OneOf(Float(100.15), 100.24)) + XCTAssertEqual(testResult.pattern, "^(100.15|100.24)$") + XCTAssertEqual(testResult.value as? Float, 100.15) + XCTAssertNotNil(testResult.rules[0]["match"]) + XCTAssertNotNil(testResult.rules[0]["regex"]) + } + + func testMatcher_OneOf_UsingOutOfBoundsIndex() throws { + let testResult = try XCTUnwrap(Matcher.OneOf(1, 2, 3, 4)) + XCTAssertEqual(testResult.pattern, "^(1|2|3|4)$") + XCTAssertEqual(testResult.value as? Int, 1) + XCTAssertNotNil(testResult.rules[0]["match"]) + XCTAssertNotNil(testResult.rules[0]["regex"]) + } + + func testMatcher_OneOf_InitsWithArray() throws { + let testResult = try XCTUnwrap(Matcher.OneOf(values: ["enabled", "disabled"])) + XCTAssertEqual(testResult.pattern, "^(enabled|disabled)$") + XCTAssertEqual(testResult.value as? String, "enabled") + XCTAssertNotNil(testResult.rules[0]["match"]) + XCTAssertNotNil(testResult.rules[0]["regex"]) + } + + func testMatcher_OneOf_SetsRules() throws { + let sut = Matcher.OneOf("A", "B", "C") + let result = try MatcherTestHelpers.encodeDecode(sut.rules) + + XCTAssertEqual(result.first?.match, "regex") + XCTAssertEqual(result.first?.regex, #"^(A|B|C)$"#) + } + +} diff --git a/Tests/Matchers/RegexLikeTests.swift b/Tests/Matchers/RegexLikeTests.swift new file mode 100644 index 00000000..4a6ca8dd --- /dev/null +++ b/Tests/Matchers/RegexLikeTests.swift @@ -0,0 +1,37 @@ +// +// Created by Marko Justinek on 11/4/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import XCTest + +@testable import PactSwift + +class RegexLikeTests: XCTestCase { + + func testMatcher_RegexLike_InitsWithValue() throws { + let testResult = try XCTUnwrap(( Matcher.RegexLike(value: "2020-11-04", pattern: "\\d{4}-\\d{2}-\\d{2}").value as Any) as? String) + XCTAssertEqual(testResult, "2020-11-04") + } + + func testMatcher_RegexLike_SetsRules() throws { + let sut = Matcher.RegexLike(value: "2020-11-04", pattern: "\\d{4}-\\d{2}-\\d{2}") + let result = try MatcherTestHelpers.encodeDecode(sut.rules) + + XCTAssertEqual(result.first?.match, "regex") + XCTAssertEqual(result.first?.regex, #"\d{4}-\d{2}-\d{2}"#) + } + +} diff --git a/Tests/Matchers/SomethingLikeTests.swift b/Tests/Matchers/SomethingLikeTests.swift new file mode 100644 index 00000000..b941d0da --- /dev/null +++ b/Tests/Matchers/SomethingLikeTests.swift @@ -0,0 +1,45 @@ +// +// Created by Marko Justinek on 11/4/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import XCTest + +@testable import PactSwift + +class SomethingLikeTests: XCTestCase { + + func testMatcher_SomethingLike_InitsWithValue() throws { + XCTAssertEqual(try XCTUnwrap((Matcher.SomethingLike("TestString").value as Any) as? String), "TestString") + XCTAssertEqual(try XCTUnwrap((Matcher.SomethingLike(200).value as Any) as? Int), 200) + XCTAssertEqual(try XCTUnwrap((Matcher.SomethingLike(123.45).value as Any) as? Double), 123.45) + + let dictResult = try XCTUnwrap((Matcher.SomethingLike(["foo": "bar"]).value as Any) as? [String: String]) + XCTAssertEqual(dictResult["foo"], "bar") + + let testArray = [1, 3, 2] + let arrayResult = try XCTUnwrap((Matcher.SomethingLike([1, 2, 3]).value as Any) as? [Int]) + XCTAssertEqual(arrayResult.count, 3) + XCTAssertTrue(arrayResult.allSatisfy { testArray.contains($0) }) + } + + func testMatcher_SomethingLike_SetsRules() throws { + let sut = Matcher.SomethingLike("test") + let result = try MatcherTestHelpers.encodeDecode(sut.rules) + + XCTAssertEqual(result.first?.match, "type") + } + +} diff --git a/Tests/Model/AnyEncodableTests.swift b/Tests/Model/AnyEncodableTests.swift new file mode 100644 index 00000000..230b36bc --- /dev/null +++ b/Tests/Model/AnyEncodableTests.swift @@ -0,0 +1,185 @@ +// +// Created by Marko Justinek on 7/4/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import XCTest + +@testable import PactSwift + +class AnyEncodableTests: XCTestCase { + + func testEncodableWrapper_Handles_StringValue() throws { + let anyEncodedObject = try PactBuilder(with: ["Foo": "Bar"], for: .body).encoded().node + let testResult = try XCTUnwrap(String(data: try JSONEncoder().encode(try XCTUnwrap(anyEncodedObject, "Oh noez!")), encoding: .utf8)) + XCTAssertEqual(testResult, #"{"Foo":"Bar"}"#) + } + + func testEncodableWrapper_Handles_IntegerValue() throws { + let anyEncodedObject = try PactBuilder(with: ["Foo": 123], for: .body).encoded().node + let testResult = try XCTUnwrap(String(data: try JSONEncoder().encode(try XCTUnwrap(anyEncodedObject, "Oh noez!")), encoding: .utf8)) + XCTAssertEqual(testResult, #"{"Foo":123}"#) + } + + func testEncodableWrapper_Handles_DoubleValue() throws { + let anyEncodedObject = try PactBuilder(with: ["Foo": Double(123.45)], for: .body).encoded().node + let testResult = try XCTUnwrap(String(data: try JSONEncoder().encode(try XCTUnwrap(anyEncodedObject, "Oh noez!")), encoding: .utf8)) + XCTAssertEqual(testResult, #"{"Foo":123.45}"#) + } + + func testEncodableWrapper_Handles_DecimalValue() throws { + let anyEncodedObject = try PactBuilder(with: ["Foo": Decimal(string: "123.45")], for: .body).encoded().node + let testResult = try XCTUnwrap(String(data: try JSONEncoder().encode(try XCTUnwrap(anyEncodedObject, "Oh noez!")), encoding: .utf8)) + XCTAssertEqual(testResult, #"{"Foo":123.45}"#) + } + + func testEncodableWrapper_Handles_BoolValue() throws { + let anyEncodedObject = try PactBuilder(with: ["Foo": true], for: .body).encoded().node + let testResult = try XCTUnwrap(String(data: try JSONEncoder().encode(try XCTUnwrap(anyEncodedObject, "Oh noez!")), encoding: .utf8)) + XCTAssertEqual(testResult, #"{"Foo":true}"#) + } + + func testEncodableWrapper_Handles_ArrayOfStringsValue() throws { + let anyEncodedObject = try PactBuilder(with: ["Foo": ["Bar", "Baz"]], for: .body).encoded().node + let testResult = try XCTUnwrap(String(data: try JSONEncoder().encode(try XCTUnwrap(anyEncodedObject, "Oh noez!")), encoding: .utf8)) + XCTAssertEqual(testResult, #"{"Foo":["Bar","Baz"]}"#) + } + + func testEncodableWrapper_Handles_ArrayOfDoublesValue() throws { + let anyEncodedObject = try PactBuilder(with: ["Foo": [Double(123.45), Double(789.23)]], for: .body).encoded().node + let testResult = try XCTUnwrap(String(data: try JSONEncoder().encode(try XCTUnwrap(anyEncodedObject, "Oh noez!")), encoding: .utf8)) + XCTAssertTrue(testResult.contains("789.23")) // NOT THE RIGHT WAY TO TEST THIS! But it will do for now. + XCTAssertTrue(testResult.contains(#"{"Foo":[123."#)) + } + + func testEncodableWrapper_Handles_DictionaryValue() throws { + let anyEncodedObject = try PactBuilder(with: ["Foo": ["Bar": "Baz"]], for: .body).encoded().node + let testResult = try JSONEncoder().encode(try XCTUnwrap(anyEncodedObject, "Oh noez!")) + XCTAssertEqual(String(data: testResult, encoding: .utf8), #"{"Foo":{"Bar":"Baz"}}"#) + } + + func testEncodableWrapper_Handles_EmbeddedSafeJSONValues() throws { + let anyEncodedObject = try PactBuilder( + with: [ + "Foo": 1, + "Bar": 1.23, + "Baz": ["Hello", "World"], + "Goo": [ + "one": [1, 23.45], + "two": true + ] as [String : Any] + ] as [String : Any], + for: .body + ).encoded().node + + let testResult = try XCTUnwrap(String(data: try JSONEncoder().encode(try XCTUnwrap(anyEncodedObject, "Oh noez!")), encoding: .utf8)) + + // WARNING: - This is not the greatest way to test this! But it will do for now. + // AnyEncodable `Request.body` is tested in `PactTests.swift` and handles this test on another level + XCTAssertTrue(testResult.contains(#""Foo":1"#)) + XCTAssertTrue(testResult.contains(#""Bar":1.23"#)) + XCTAssertTrue(testResult.contains(#""Baz":["Hello","World"]"#)) + XCTAssertTrue(testResult.contains(#""Goo":{"#)) + XCTAssertTrue(testResult.contains(#""one":[1,23.4"#)) + XCTAssertTrue(testResult.contains(#""two":true"#)) + } + + // MARK: - Testing throws + + func testEncodableWrapper_Handles_InvalidInput() { + struct FailingTestModel { + let unsupportedDate = Date() + } + + do { + _ = try PactBuilder(with: FailingTestModel(), for: .body).encoded().node + XCTFail("Expected the EncodableWrapper to throw!") + } catch { + do { + let testResult = try XCTUnwrap(error as? EncodingError) + XCTAssertTrue(testResult.localizedDescription.contains("unsupportedDate")) + } catch { + XCTFail("Expected an EncodableWrapper.EncodingError to be thrown") + } + } + } + + func testEncodableWrapper_Handles_InvalidArrayInput() { + let testDate = Date() + struct FailingTestModel { + let failingArray: Array + + init(array: [Date]) { + self.failingArray = array + } + } + + let testableObject = FailingTestModel(array: [testDate]) + + do { + _ = try PactBuilder(with: testableObject.failingArray, for: .body).encoded().node + XCTFail("Expected the EncodableWrapper to throw!") + } catch { + do { + let testResult = try XCTUnwrap(error as? EncodingError) + XCTAssertTrue( + testResult + .localizedDescription + .contains("Error preparing pact! Failed to process array: [\(dateComponents(from: testDate))"), + "Value not found in \"\(testResult.localizedDescription)\"" + ) + } catch { + XCTFail("Expected an EncodableWrapper.encodingFailed to be thrown") + } + } + } + + func testEncodableWrapper_Handles_InvalidDictInput() { + let testDate = Date() + struct FailingTestModel { + let failingDict: [String: Date] + + init(testDate: Date) { + self.failingDict = ["foo": testDate] + } + } + + let testableObject = FailingTestModel(testDate: testDate) + + do { + _ = try PactBuilder(with: testableObject.failingDict, for: .body).encoded().node + XCTFail("Expected the EncodableWrapper to throw!") + } catch { + do { + let testResult = try XCTUnwrap(error as? EncodingError) + XCTAssertTrue(testResult.localizedDescription.contains("Error preparing pact! A key or value in the structure does not conform to 'Encodable' protocol. The element attempted to encode: \(dateComponents(from: testDate))"), "Unexpected localizedDescription: \(testResult.localizedDescription)") + } catch { + XCTFail("Expected an EncodableWrapper.encodingFailed to be thrown") + } + } + } + +} + +private extension AnyEncodableTests { + + func dateComponents(from date: Date = Date()) -> String { + let format = DateFormatter() + format.dateFormat = "yyyy-MM-dd" + format.timeZone = TimeZone(identifier: "GMT") + return format.string(from: date) + } + +} diff --git a/Tests/Model/InteractionTests.swift b/Tests/Model/InteractionTests.swift new file mode 100644 index 00000000..dec31327 --- /dev/null +++ b/Tests/Model/InteractionTests.swift @@ -0,0 +1,52 @@ +// +// Created by Marko Justinek on 26/5/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import XCTest + +@testable import PactSwift + +class InteractionTests: XCTestCase { + + var sut: Interaction! + + func testConvenienceInit_WithDescription() { + sut = Interaction(description: "A test request") + XCTAssertEqual(sut.interactionDescription, "A test request") + } + + func testGivenState_WithArray() { + sut = Interaction(description: "A test request with array of states") + let providerState = ProviderState(description: "array exists", params: ["foo": "bar"]) + let sutWithInteraction = sut.given([providerState]) + + XCTAssertEqual(sutWithInteraction.providerStates, [providerState]) + XCTAssertEqual(sutWithInteraction.providerStates?.count, 1) + } + + func testGivenState_WithVariadicParameter() throws { + sut = Interaction(description: "A test request with states as variadic param") + let oneProviderState = ProviderState(description: "array exists", params: ["foo": "bar"]) + let twoProviderState = ProviderState(description: "variadic exists", params: ["bar": "baz"]) + let sutWithInteraction = sut.given(oneProviderState, twoProviderState) + + let providerStates = try XCTUnwrap(sutWithInteraction.providerStates) + XCTAssertEqual(providerStates.count, 2) + XCTAssertTrue(providerStates.contains(oneProviderState)) + XCTAssertTrue(providerStates.contains(twoProviderState)) + } + +} diff --git a/Tests/Model/MetadataTests.swift b/Tests/Model/MetadataTests.swift new file mode 100644 index 00000000..d14ca7b6 --- /dev/null +++ b/Tests/Model/MetadataTests.swift @@ -0,0 +1,52 @@ +// +// Created by Marko Justinek on 1/4/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import XCTest + +@testable import PactSwift + +class MetadataTests: XCTestCase { + + func testMetadata_SetsPactSpecificationVersion() { + XCTAssertEqual(Metadata().pactSpec.version, "3.0.0") + } + + #if !SWIFT_PACKAGE + func testMetadata_SetsPactSwiftVersion() throws { + guard let expectedResult = bundleVersion() else { + XCTFail("Expected version number") + return + } + XCTAssertEqual(try XCTUnwrap(Metadata().pactSwift.version), expectedResult) + } + #endif + +} + +private extension MetadataTests { + + func bundleVersion() -> String? { + #if os(iOS) + return Bundle(identifier: "au.com.pact-foundation.iOS.PactSwift")?.infoDictionary?["CFBundleShortVersionString"] as? String + #elseif os(macOS) + return Bundle(identifier: "au.com.pact-foundation.macOS.PactSwift")?.infoDictionary?["CFBundleShortVersionString"] as? String ?? pactSwiftVersion + #else + return nil + #endif + } + +} diff --git a/Tests/Model/PactBrokerTests.swift b/Tests/Model/PactBrokerTests.swift new file mode 100644 index 00000000..70f3d0d8 --- /dev/null +++ b/Tests/Model/PactBrokerTests.swift @@ -0,0 +1,94 @@ +// +// Created by Marko Justinek on 29/8/21. +// Copyright © 2021 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import XCTest + +@testable import PactSwift + +class PactBrokerTests: XCTestCase { + + func testDoesNotSetPublishVerificationResults() { + let testSubject = PactBroker( + url: URL(string: "http://test.url")!, + auth: .token(.init("test-token")), + providerName: "test provider", + publishResults: nil + ) + + XCTAssertEqual(testSubject.url, "http://test.url") + XCTAssertEqual(testSubject.providerName, "test provider") + XCTAssertFalse(testSubject.publishVerificationResult) + XCTAssertNil(testSubject.providerTags) + XCTAssertNil(testSubject.includePending) + XCTAssertNil(testSubject.includeWIP) + + guard case .token(let testToken) = testSubject.authentication else { + XCTFail("Expected token authentication option") + return + } + XCTAssertEqual(testToken.token, "test-token") + } + + func testSetsPublishVerificationResultsDetails() throws { + let testTags = ["test-tags", "dev-tag"] + let testSubject = PactBroker( + url: URL(string: "http://test.url/")!, + auth: .token(.init("test-token")), + providerName: "test provider", + publishResults: .init(providerVersion: "unit-test-version", providerTags: testTags) + ) + + XCTAssertTrue(testSubject.publishVerificationResult) + XCTAssertTrue(try testTags.allSatisfy { try XCTUnwrap(testSubject.providerTags).contains($0) }) + XCTAssertNil(testSubject.includePending) + XCTAssertNil(testSubject.includeWIP) + } + + func testUsernamePasswordAuthOption() { + let testSubject = PactBroker( + url: URL(string: "http://some.pact.broker.url")!, + auth: .auth(.init(username: "test-user", password: "test-pass")), + providerName: "Test API Provider" + ) + + XCTAssertEqual(testSubject.url, "http://some.pact.broker.url") + XCTAssertEqual(testSubject.providerName, "Test API Provider") + guard case .auth(let testAuth) = testSubject.authentication else { + XCTFail("Expected basic authentication option") + return + } + XCTAssertEqual(testAuth.username, "test-user") + XCTAssertEqual(testAuth.password, "test-pass") + } + + func testPactBrokerSetsProviderTags() { + let testTags = [ + VersionSelector(tag: "SIT"), + VersionSelector(tag: "prod"), + ] + + let testSubject = PactBroker( + url: URL(string: "http://test.pact.broker")!, + auth: .token(.init("some-test-token")), + providerName: "Test API Provider", + consumerTags: testTags + ) + + XCTAssertEqual(testSubject.consumerTags, testTags) + } + +} diff --git a/Tests/Model/PactBuilderTests.swift b/Tests/Model/PactBuilderTests.swift new file mode 100644 index 00000000..f89dac80 --- /dev/null +++ b/Tests/Model/PactBuilderTests.swift @@ -0,0 +1,680 @@ +// +// Created by Marko Justinek on 11/4/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import XCTest + +@testable import PactSwift + +class PactBuilderTests: XCTestCase { + + // MARK: - EqualTo() + + func testPact_SetsMatcher_EqualTo() throws { + let testBody: Any = [ + "data": Matcher.EqualTo("2016-07-19") + ] + + let testPact = prepareTestPact(responseBody: testBody) + let testResult = try XCTUnwrap(try JSONDecoder().decode(GenericLikeTestModel.self, from: testPact.data!).interactions.first?.response.matchingRules.body.node.matchers.first) + + XCTAssertEqual(testResult.match, "equality") + } + + // MARK: - SomethingLike() + + func testPact_SetsMatcher_SomethingLike() throws { + let testBody: Any = [ + "data": Matcher.SomethingLike("2016-07-19") + ] + + let testPact = prepareTestPact(responseBody: testBody) + let testResult = try XCTUnwrap(try JSONDecoder().decode(GenericLikeTestModel.self, from: testPact.data!).interactions.first?.response.matchingRules.body.node.matchers.first) + + XCTAssertEqual(testResult.match, "type") + } + + func testPact_SetsNestedMatchers_in_SomethingLike() throws { + let testBody: [String : Any] = [ + "body": Matcher.SomethingLike( + [ + "refresh_token": Matcher.SomethingLike("xxxxxx"), + "user": Matcher.SomethingLike( + [ + "id": Matcher.SomethingLike("xxxxxx-xxxx-xxxx-xxxx-xxxxxxx"), + "email": Matcher.RegexLike(value: "admin@xxxxx.au", pattern: #"^([-\.\w]+@[-\w]+\.)+[\w-]{2,4}$"#) + ] as [String : Any] + ) + ] + ) + ] + + let testPact = prepareTestPact(responseBody: testBody) + let result = try XCTUnwrap( + try JSONDecoder() + .decode(NestedSomethingLikeTestModel.self, from: testPact.data!) + .interactions + .first? + .response + .matchingRules + .body + ) + + XCTAssertEqual(try XCTUnwrap(result.body.matchers.first).match, "type") + XCTAssertEqual(try XCTUnwrap(result.bodyUser.matchers.first).match, "type") + XCTAssertEqual(try XCTUnwrap(result.bodyUserID.matchers.first).match, "type") + XCTAssertEqual(try XCTUnwrap(result.bodyUserEmail.matchers.first).match, "regex") + XCTAssertEqual(try XCTUnwrap(result.bodyUserEmail.matchers.first).regex, #"^([-\.\w]+@[-\w]+\.)+[\w-]{2,4}$"#) + } + + // MARK: - EachLike() + + func testPact_SetsDefaultMin_EachLikeMatcher() throws { + let testBody: Any = [ + "data": [ + "array1": Matcher.EachLike( + [ + "dob": Matcher.SomethingLike("2016-07-19"), + "id": Matcher.SomethingLike("1600309982"), + "name": Matcher.SomethingLike("FVsWAGZTFGPLhWjLuBOd") + ] + ) + ] + ] + + let testPact = prepareTestPact(responseBody: testBody) + let testResult = try XCTUnwrap(try JSONDecoder().decode(SetLikeTestModel.self, from: testPact.data!).interactions.first?.response.matchingRules.body.node.matchers.first) + + XCTAssertEqual(testResult.min, 1) + XCTAssertEqual(testResult.match, "type") + } + + func testPact_SetsProvidedMin_ForEachLikeMatcher() throws { + let testBody: Any = [ + "data": [ + "array1": Matcher.EachLike( + [ + "dob": Matcher.SomethingLike("2016-07-19"), + "id": Matcher.SomethingLike("1600309982"), + "name": Matcher.SomethingLike("FVsWAGZTFGPLhWjLuBOd") + ] + , min: 3 + ) + ] + ] + + let testPact = prepareTestPact(responseBody: testBody) + let testResult = try XCTUnwrap(try JSONDecoder().decode(SetLikeTestModel.self, from: testPact.data!).interactions.first?.response.matchingRules.body.node.matchers.first) + + XCTAssertEqual(testResult.min, 3) + XCTAssertEqual(testResult.match, "type") + } + + func testPact_SetsProvidedMax_ForEachLikeMatcher() throws { + let testBody: Any = [ + "data": [ + "array1": Matcher.EachLike( + [ + "dob": Matcher.SomethingLike("2016-07-19"), + "id": Matcher.SomethingLike("1600309982"), + "name": Matcher.SomethingLike("FVsWAGZTFGPLhWjLuBOd") + ] + , max: 5 + ) + ] + ] + + let testPact = prepareTestPact(responseBody: testBody) + let testResult = try XCTUnwrap(try JSONDecoder().decode(SetLikeTestModel.self, from: testPact.data!).interactions.first?.response.matchingRules.body.node.matchers.first) + + XCTAssertEqual(testResult.max, 5) + XCTAssertEqual(testResult.match, "type") + } + + func testPact_SetsMinMax_ForEachLikeMatcher() throws { + let testBody: Any = [ + "data": [ + "array1": Matcher.EachLike( + [ + "dob": Matcher.SomethingLike("2016-07-19"), + "id": Matcher.SomethingLike("1600309982"), + "name": Matcher.SomethingLike("FVsWAGZTFGPLhWjLuBOd") + ], + min: 1, + max: 5 + ) + ] + ] + + let testPact = prepareTestPact(responseBody: testBody) + let testResult = try XCTUnwrap(try JSONDecoder().decode(SetLikeTestModel.self, from: testPact.data!).interactions.first?.response.matchingRules.body.node.matchers.first) + + XCTAssertEqual(testResult.min, 1) + XCTAssertEqual(testResult.max, 5) + XCTAssertEqual(testResult.match, "type") + } + + // MARK: - IntegerLike() + + func testPact_SetsMatcher_IntegerLike() throws { + let testBody: Any = [ + "data": Matcher.IntegerLike(1234) + ] + + let testPact = prepareTestPact(responseBody: testBody) + let testResult = try XCTUnwrap(try JSONDecoder().decode(GenericLikeTestModel.self, from: testPact.data!).interactions.first?.response.matchingRules.body.node.matchers.first) + + XCTAssertEqual(testResult.match, "integer") + } + + // MARK: - DecimalLike() + + func testPact_SetsMatcher_DecimalLike() throws { + let testBody: Any = [ + "data": Matcher.DecimalLike(1234) + ] + + let testPact = prepareTestPact(responseBody: testBody) + let testResult = try XCTUnwrap(try JSONDecoder().decode(GenericLikeTestModel.self, from: testPact.data!).interactions.first?.response.matchingRules.body.node.matchers.first) + + XCTAssertEqual(testResult.match, "decimal") + } + + // MARK: - RegexLike() + + func testPact_SetsMatcher_RegexLike() throws { + let testBody: Any = [ + "data": Matcher.RegexLike(value: "2020-12-31", pattern: "\\d{4}-\\d{2}-\\d{2}") + ] + + let testPact = prepareTestPact(responseBody: testBody) + let matchers = try XCTUnwrap(try JSONDecoder().decode(GenericLikeTestModel.self, from: testPact.data!).interactions.first?.response.matchingRules.body.node) + + XCTAssertEqual(matchers.matchers.first?.match, "regex") + XCTAssertEqual(matchers.matchers.first?.regex, "\\d{4}-\\d{2}-\\d{2}") + XCTAssertNil(matchers.combine) + } + + func testPact_FailsMatcher_InvalidRegex() { + let interaction = Interaction( + description: "test Encodable Pact", + providerStates: [ + ProviderState( + description: "an alligator with the given name exists", + params: ["name": "Mary"] + ) + ] + ) + .withRequest(method: .GET, path: "/") + .willRespondWith( + status: 200, + headers: ["Content-Type": "application/json; charset=UTF-8", "X-Value": "testCode"], + body: ["data": Matcher.RegexLike(value: "foo", pattern: #"\{3}-\w+$"#)] + ) + + do { + _ = try PactBuilder(with: interaction, for: .body).encoded() + XCTFail("Expecting to fail encoding when Regex matcher's value doesn't match the pattern") + } catch { + if case .encodingFailure(let message) = error as? EncodingError { + XCTAssertTrue(String(describing: message).contains(#"Value \"foo\" does not match the pattern \"\\{3}-\\w+$\""#), "Unexpected error message: \"\(String(describing: message))\"") + } else { + XCTFail("Expecting an Encoding error!") + } + } + } + + // MARK: - IncludesLike() + + func testPact_SetsMatcher_IncludesLike_DefaultsToAND() throws { + let expectedValues = ["2020-12-31", "2019-12-31"] + let testBody: Any = [ + "data": Matcher.IncludesLike("2020-12-31", "2019-12-31") + ] + + let testPact = prepareTestPact(responseBody: testBody) + let testResult = try XCTUnwrap(try JSONDecoder().decode(GenericLikeTestModel.self, from: testPact.data!).interactions.first?.response.matchingRules.body.node) + + XCTAssertEqual(testResult.combine, "AND") + XCTAssertEqual(testResult.matchers.count, 2) + XCTAssertTrue(testResult.matchers.allSatisfy { expectedValues.contains($0.value ?? "FAIL!") }) + XCTAssertTrue(testResult.matchers.allSatisfy { $0.match == "include" }) + } + + func testPact_SetsMatcher_IncludesLike_CombineMatchersWithOR() throws { + let expectedValues = ["2020-12-31", "2019-12-31"] + let testBody: Any = [ + "data": Matcher.IncludesLike("2020-12-31", "2019-12-31", combine: .OR) + ] + + let testPact = prepareTestPact(responseBody: testBody) + let testResult = try XCTUnwrap(try JSONDecoder().decode(GenericLikeTestModel.self, from: testPact.data!).interactions.first?.response.matchingRules.body.node) + + XCTAssertEqual(testResult.combine, "OR") + XCTAssertEqual(testResult.matchers.count, 2) + XCTAssertTrue(testResult.matchers.allSatisfy { expectedValues.contains($0.value ?? "FAIL!") }) + XCTAssertTrue(testResult.matchers.allSatisfy { $0.match == "include" }) + } + + // MARK: - Example generators + + func testPact_SetsExampleGenerator_RandomBool() throws { + let testBody: Any = [ + "data": ExampleGenerator.RandomBool() + ] + + let testPact = prepareTestPact(responseBody: testBody) + let testResult = try XCTUnwrap(try JSONDecoder().decode(GenericExampleGeneratorTestModel.self, from: testPact.data!).interactions.first?.response.generators.body.node) + + XCTAssertEqual(testResult.type, "RandomBoolean") + } + + func testPact_SetsExampleGenerator_RandomDate() throws { + let testBody: Any = [ + "data": ExampleGenerator.RandomDate(format: "dd-MM-yyyy") + ] + + let testPact = prepareTestPact(responseBody: testBody) + let testResult = try XCTUnwrap(try JSONDecoder().decode(GenericExampleGeneratorTestModel.self, from: testPact.data!).interactions.first?.response.generators.body.node) + + XCTAssertEqual(testResult.type, "Date") + XCTAssertEqual(testResult.format, "dd-MM-yyyy") + } + + func testPact_SetsExampleGenerator_RandomDateTime() throws { + let testBody: [String : Any] = [ + "data": ExampleGenerator.RandomDate(format: "dd-MM-yyyy"), + "foo": ExampleGenerator.RandomDateTime(format: "HH:mm (dd/MM)") + ] + + let testPact = prepareTestPact(responseBody: testBody) + let testResult = try XCTUnwrap(try JSONDecoder().decode(GenericExampleGeneratorTestModel.self, from: testPact.data!).interactions.first?.response.generators.body) + + XCTAssertEqual(testResult.node.type, "Date") + XCTAssertEqual(testResult.node.format, "dd-MM-yyyy") + + XCTAssertEqual(testResult.foo?.type, "DateTime") + XCTAssertEqual(testResult.foo?.format, "HH:mm (dd/MM)") + } + + func testPact_SetsExampleGenerator_RandomDecimal() throws { + let testBody: Any = [ + "data": ExampleGenerator.RandomDecimal(digits: 5) + ] + + let testPact = prepareTestPact(responseBody: testBody) + let testResult = try XCTUnwrap(try JSONDecoder().decode(GenericExampleGeneratorTestModel.self, from: testPact.data!).interactions.first?.response.generators.body.node) + + XCTAssertEqual(testResult.type, "RandomDecimal") + XCTAssertEqual(testResult.digits, 5) + } + + func testPact_SetsExampleGenerator_RandomHexadecimal() throws { + let testBody: Any = [ + "data": ExampleGenerator.RandomHexadecimal(digits: 16) + ] + + let testPact = prepareTestPact(responseBody: testBody) + let testResult = try XCTUnwrap(try JSONDecoder().decode(GenericExampleGeneratorTestModel.self, from: testPact.data!).interactions.first?.response.generators.body.node) + + XCTAssertEqual(testResult.type, "RandomHexadecimal") + XCTAssertEqual(testResult.digits, 16) + } + + func testPact_SetsExampleGenerator_RandomInt() throws { + let testBody: Any = [ + "data": ExampleGenerator.RandomInt(min: 2, max: 16) + ] + + let testPact = prepareTestPact(responseBody: testBody) + + let testResult = try XCTUnwrap(try JSONDecoder().decode(GenericExampleGeneratorTestModel.self, from: testPact.data!).interactions.first?.response.generators.body.node) + + XCTAssertEqual(testResult.type, "RandomInt") + XCTAssertEqual(testResult.min, 2) + XCTAssertEqual(testResult.max, 16) + } + + func testPact_SetsExampleGenerator_RandomString() throws { + let testBody: Any = [ + "data": ExampleGenerator.RandomString(size: 32), + "foo": ExampleGenerator.RandomString(regex: #"\d{3}"#) + ] + + let testPact = prepareTestPact(responseBody: testBody) + + let testResult = try XCTUnwrap(try JSONDecoder().decode(GenericExampleGeneratorTestModel.self, from: testPact.data!).interactions.first?.response.generators.body) + + XCTAssertEqual(testResult.node.type, "RandomString") + XCTAssertEqual(testResult.node.size, 32) + + XCTAssertEqual(testResult.foo?.type, "Regex") + XCTAssertEqual(testResult.foo?.regex, "\\d{3}") + } + + func testPact_SetsExampleGenerator_RandomTime() throws { + let testBody: Any = [ + "data": ExampleGenerator.RandomTime(format: "hh - mm") + ] + + let testPact = prepareTestPact(responseBody: testBody) + let testResult = try XCTUnwrap(try JSONDecoder().decode(GenericExampleGeneratorTestModel.self, from: testPact.data!).interactions.first?.response.generators.body.node) + + XCTAssertEqual(testResult.type, "Time") + XCTAssertEqual(testResult.format, "hh - mm") + } + + func testPact_SetsExampleGenerator_RandomUUID() throws { + let testBody: Any = [ + "data": ExampleGenerator.RandomUUID() + ] + + let testPact = prepareTestPact(responseBody: testBody) + let testResult = try XCTUnwrap(try JSONDecoder().decode(GenericExampleGeneratorTestModel.self, from: testPact.data!).interactions.first?.response.generators.body.node) + + XCTAssertEqual(testResult.type, "Uuid") + } + + // MARK: - Testing parsing for headers + + func testPact_ProcessesMatchers_InHeaders() throws { + let testHeaders: [String : Any] = [ + "foo": Matcher.SomethingLike("bar"), + "bar": ExampleGenerator.RandomBool(), + ] + let testBody: [String : Any] = [ + "foo": Matcher.SomethingLike("baz"), + ] + + let testPact = prepareTestPact(requestBody: testBody, requestHeaders: testHeaders) + let testResult = try XCTUnwrap(try JSONDecoder().decode(SomethingLikeTestModel.self, from: testPact.data!).interactions.first?.request) + + XCTAssertEqual(testResult.matchingRules.header?.foo.matchers.first?.match, "type") + XCTAssertEqual(testResult.generators?.header.bar.type, "RandomBoolean") + XCTAssertEqual(testResult.matchingRules.body?.foo?.matchers.first?.match, "type") + } + + // MARK: - Testing parsing for path + + func testPact_ProcessesMatcher_InRequestPath() throws { + let path = Matcher.RegexLike(value: "/some/1234", pattern: #"^/some/\d{4}+$"#) + + let testHeaders: [String : Any] = [ + "foo": Matcher.SomethingLike("bar"), + "bar": ExampleGenerator.RandomBool(), + ] + let testBody: [String : Any] = [ + "foo": Matcher.SomethingLike("baz"), + ] + + let testPact = prepareTestPact(path: path, requestBody: testBody, requestHeaders: testHeaders) + let testResult = try XCTUnwrap(try JSONDecoder().decode(SomethingLikeTestModel.self, from: testPact.data!).interactions.first?.request.matchingRules.path?.matchers.first) + + XCTAssertEqual(testResult.match, "regex") + XCTAssertEqual(testResult.regex, #"^/some/\d{4}+$"#) + } + +} + +// MARK: - Private Utils - + +private extension PactBuilderTests { + + struct NestedSomethingLikeTestModel: Decodable { + let interactions: [TestInteractionModel] + struct TestInteractionModel: Decodable { + let response: TestResponseModel + struct TestResponseModel: Decodable { + let matchingRules: TestMatchingRulesModel + struct TestMatchingRulesModel: Decodable { + let body: TestNodeModel + struct TestNodeModel: Decodable { + let body: TestMatchersModel + let bodyUser: TestMatchersModel + let bodyUserID: TestMatchersModel + let bodyUserEmail: TestMatchersModel + enum CodingKeys: String, CodingKey { + case body = "$.body" + case bodyUser = "$.body.user" + case bodyUserID = "$.body.user.id" + case bodyUserEmail = "$.body.user.email" + } + struct TestMatchersModel: Decodable { + let matchers: [TestTypeModel] + let combine: String? + struct TestTypeModel: Decodable { + let match: String + let regex: String? + } + } + } + } + } + } + } + + // This test model is tightly coupled with the SomethingLike Matcher for the purpouse of these tests + struct GenericLikeTestModel: Decodable { + let interactions: [TestInteractionModel] + struct TestInteractionModel: Decodable { + let response: TestResponseModel + struct TestResponseModel: Decodable { + let matchingRules: TestMatchingRulesModel + struct TestMatchingRulesModel: Decodable { + let body: TestNodeModel + struct TestNodeModel: Decodable { + let node: TestMatchersModel + let foo: TestMatchersModel? + let bar: TestMatchersModel? + enum CodingKeys: String, CodingKey { + case node = "$.data" + case foo = "$.foo" + case bar = "$.bar" + } + struct TestMatchersModel: Decodable { + let matchers: [TestTypeModel] + let combine: String? + struct TestTypeModel: Decodable { + let match: String + let regex: String? + let value: String? + let min: Int? + let max: Int? + } + } + } + } + } + } + } + + // This test model is tightly coupled with the ExampleGenerator for the purpose of these tests + struct GenericExampleGeneratorTestModel: Decodable { + let interactions: [TestInteractionModel] + struct TestInteractionModel: Decodable { + let response: TestResponseModel + struct TestResponseModel: Decodable { + let generators: TestGeneratorModel + struct TestGeneratorModel: Decodable { + let body: TestNodeModel + struct TestNodeModel: Decodable { + let node: TestAttributesModel + let foo: TestAttributesModel? + let bar: TestAttributesModel? + enum CodingKeys: String, CodingKey { + case node = "$.data" + case foo = "$.foo" + case bar = "$.bar" + } + struct TestAttributesModel: Decodable { + let type: String + let min: Int? + let max: Int? + let digits: Int? + let size: Int? + let regex: String? + let format: String? + } + } + } + } + } + } + + // This test model is tightly coupled with the EachLike Matcher for the purpouse of these tests + struct SetLikeTestModel: Decodable { + let interactions: [TestInteractionModel] + struct TestInteractionModel: Decodable { + let response: TestRequestModel + struct TestRequestModel: Decodable { + let matchingRules: TestMatchingRulesModel + struct TestMatchingRulesModel: Decodable { + let body: TestNodeModel + struct TestNodeModel: Decodable { + let node: TestMatchersModel + enum CodingKeys: String, CodingKey { + case node = "$.data.array1" + } + struct TestMatchersModel: Decodable { + let matchers: [TestMinModel] + struct TestMinModel: Decodable { + let min: Int? + let max: Int? + let match: String + } + } + } + } + } + } + } + + // This test model is tightly coupled with the Pact that includes matchers in request body + struct SomethingLikeTestModel: Decodable { + let interactions: [TestInteractionModel] + struct TestInteractionModel: Decodable { + let request: TestResponseModel + struct TestResponseModel: Decodable { + let matchingRules: TestMatchingRulesModel + let generators: TestGeneratorsModel? + struct TestMatchingRulesModel: Decodable { + let body: TestBodyModel? + let header: TestHeadersModel? + let path: TestPathModel? + struct TestBodyModel: Decodable { + let foo: TestMatchersModel? + let bar: TestMatchersModel? + enum CodingKeys: String, CodingKey { + case foo = "$.foo" + case bar = "$.bar" + } + struct TestMatchersModel: Decodable { + let matchers: [TestTypeModel] + let combine: String? + struct TestTypeModel: Decodable { + let match: String + let regex: String? + let value: String? + let min: Int? + let max: Int? + } + } + } + struct TestHeadersModel: Decodable { + let foo: TestMatchersModel + let bar: TestMatchersModel? + struct TestMatchersModel: Decodable { + let matchers: [TestTypeModel] + let combine: String? + struct TestTypeModel: Decodable { + let match: String + let regex: String? + let value: String? + let min: Int? + let max: Int? + } + } + } + struct TestPathModel: Decodable { + let matchers: [TestTypeModel] + let combine: String? + struct TestTypeModel: Decodable { + let match: String + let regex: String + } + } + } + struct TestGeneratorsModel: Decodable { + let header: TestHeaderModel + struct TestHeaderModel: Decodable { + let bar: TestAttributesModel + struct TestAttributesModel: Decodable { + let type: String + let min: Int? + let max: Int? + let digits: Int? + let size: Int? + let regex: String? + let format: String? + } + } + } + } + } + } + + func prepareTestPact(responseBody: Any) -> Pact { + let firstProviderState = ProviderState(description: "an alligator with the given name exists", params: ["name": "Mary"]) + + let interaction = Interaction(description: "test Encodable Pact", providerStates: [firstProviderState]) + .withRequest(method: .GET, path: "/") + .willRespondWith( + status: 200, + headers: ["Content-Type": "application/json; charset=UTF-8", "X-Value": "testCode"], + body: responseBody + ) + + return Pact( + consumer: Pacticipant.consumer("test-consumer"), + provider: Pacticipant.provider("test-provider"), + interactions: [interaction] + ) + } + + func prepareTestPact(path: PactPathParameter = "/", requestBody: Any, requestHeaders: Any?) -> Pact { + let firstProviderState = ProviderState(description: "an alligator with the given name exists", params: ["name": "Mary"]) + + let headers: [String: Any]? = requestHeaders != nil ? (requestHeaders as! [String : Any]) : nil + + let interaction = Interaction(description: "test Encodable Pact", providerStates: [firstProviderState]) + .withRequest( + method: .GET, + path: path, + headers: headers, + body: requestBody + ) + .willRespondWith( + status: 200 + ) + + return Pact( + consumer: Pacticipant.consumer("test-consumer"), + provider: Pacticipant.provider("test-provider"), + interactions: [interaction] + ) + } + +} diff --git a/Tests/Model/PactHTTPMethodTests.swift b/Tests/Model/PactHTTPMethodTests.swift new file mode 100644 index 00000000..ac77f2ac --- /dev/null +++ b/Tests/Model/PactHTTPMethodTests.swift @@ -0,0 +1,35 @@ +// +// Created by Marko Justinek on 29/8/21. +// Copyright © 2021 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import XCTest +@testable import PactSwift + +final class PactHTTPMehtodTests: XCTestCase { + + func testPactHTTPMethods() { + XCTAssertEqual(PactHTTPMethod.GET.method, "get") + XCTAssertEqual(PactHTTPMethod.HEAD.method, "head") + XCTAssertEqual(PactHTTPMethod.POST.method, "post") + XCTAssertEqual(PactHTTPMethod.PUT.method, "put") + XCTAssertEqual(PactHTTPMethod.PATCH.method, "patch") + XCTAssertEqual(PactHTTPMethod.DELETE.method, "delete") + XCTAssertEqual(PactHTTPMethod.TRACE.method, "trace") + XCTAssertEqual(PactHTTPMethod.CONNECT.method, "connect") + XCTAssertEqual(PactHTTPMethod.OPTIONS.method, "options") + } + +} diff --git a/Tests/Model/PactTests.swift b/Tests/Model/PactTests.swift new file mode 100644 index 00000000..11cc8522 --- /dev/null +++ b/Tests/Model/PactTests.swift @@ -0,0 +1,281 @@ +// +// Created by Marko Justinek on 1/4/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import XCTest + +@testable import PactSwift + +class PactTests: XCTestCase { + + let testConsumer = "test_consumer" + let testProvider = "test_provider" + + func testPact_SetsProvider() throws { + XCTAssertEqual(try XCTUnwrap(prepareTestPact().payload["provider"] as? String), testProvider) + } + + func testPact_SetsConsumer() throws { + XCTAssertEqual(try XCTUnwrap(prepareTestPact().payload["consumer"] as? String), testConsumer) + } + + func testPact_SetsMetadata() { + XCTAssertNotNil(prepareTestPact().payload["metadata"]) + } + + // MARK: - Interactions + + func testPact_SetsInteractionDescription() throws { + let expectedResult = "test interaction" + let interaction = try prepareInteraction(description: expectedResult) + + let testPact = prepareTestPact(interactions: interaction) + + let testResult = try XCTUnwrap((testPact.payload["interactions"] as? [Interaction])?.first).interactionDescription + XCTAssertEqual(testResult, expectedResult) + } + + // MARK: - Interaction request + + func testPact_SetsInteractionRequestMethod() throws { + let expectedResult: PactHTTPMethod = .POST + let interaction = try prepareInteraction(description: "test_post_interaction", method: .POST) + + let testPact = prepareTestPact(interactions: interaction) + + let testResult = try XCTUnwrap((testPact.payload["interactions"] as? [Interaction])?.first?.request?.httpMethod) + XCTAssertEqual(testResult, expectedResult) + } + + func testPact_SetsInteractionRequestPath() throws { + let expectedResult = "/interactions" + let interaction = try prepareInteraction(description: "test_path_interaction", path: expectedResult) + + let testPact = prepareTestPact(interactions: interaction) + + let testResult = try XCTUnwrap((testPact.payload["interactions"] as? [Interaction])?.first?.request?.path as? String) + XCTAssertEqual(testResult, expectedResult) + } + + func testPact_SetsInteractionRequestHeaders() throws { + + let expectedResult: [String: String] = ["Content-Type": "applicatoin/json; charset=UTF-8", "X-Value": "testCode"] + let interaction = Interaction( + description: "test_request_headers", + providerStates: [ + ProviderState( + description: "an alligator with the given name exists", + params: ["name": "Mary"] + ), + ProviderState( + description: "the user is logged in", + params: ["user": "Fred"] + ) + ], + request: try Request( + method: .GET, + path: "/", + query: nil, + headers: expectedResult, + body: nil + ), + response: try Response( + statusCode: 200, + headers: nil + ) + ) + + let testPact = prepareTestPact(interactions: interaction) + + let testResult = try XCTUnwrap(((testPact.payload["interactions"] as? [Interaction])?.first?.request?.headers) as? [String: String]) + XCTAssertEqual(testResult["Content-Type"], expectedResult["Content-Type"]) + XCTAssertEqual(testResult["X-Value"], expectedResult["X-Value"]) + } + + // MARK: - Interaction request query + + func testPact_SetsInteractionReqeustQuery() throws { + let expectedResult = [ + "max_results": ["100"], + "state": ["NSW"], + "term": ["80 CLARENCE ST, SYDNEY NSW 2000"] + ] + + let interaction = Interaction( + description: "test query dictionary", + providerState: "some testable provider state", + request: try Request( + method: .GET, + path: "/autoComplete/address", + query: expectedResult, + headers: nil + ), + response: try Response( + statusCode: 200, + headers: nil + ) + ) + + let testPact = prepareTestPact(interactions: interaction) + + let testResult = try XCTUnwrap(((testPact.payload["interactions"] as? [Interaction])?.first?.request?.query)) + XCTAssertTrue(try (XCTUnwrap(testResult["max_results"]).contains { $0 as! String == "100" })) + XCTAssertTrue(try (XCTUnwrap(testResult["state"]).contains { $0 as! String == "NSW" })) + XCTAssertTrue(try (XCTUnwrap(testResult["term"]).contains { $0 as! String == "80 CLARENCE ST, SYDNEY NSW 2000" })) + } + + func testPact_SetsProviderState() throws { + let expectedResult = "some testable provider state" + + let interaction = Interaction( + description: "test provider state", + providerState: expectedResult, + request: try Request(method: .GET, path: "/"), + response: try Response(statusCode: 200) + ) + + let testPact = prepareTestPact(interactions: interaction) + + let testResult = try XCTUnwrap(((testPact.payload["interactions"] as? [Interaction])?.first)?.providerState) + XCTAssertEqual(testResult, expectedResult) + } + + func testPact_SetsProviderStates() throws { + let firstProviderState = ProviderState(description: "an alligator with the given name exists", params: ["name": "Mary"]) + let secondProviderState = ProviderState(description: "the user is logged in", params: ["username": "Fred"]) + let expectedResult = [firstProviderState, secondProviderState] + + let interaction = Interaction( + description: "test provider states", + providerStates: [ + ProviderState(description: "an alligator with the given name exists", params: ["name": "Mary"]), + ProviderState(description: "the user is logged in", params: ["username": "Fred"]) + ], + request: try Request(method: .GET, path: "/"), + response: try Response(statusCode: 200) + ) + + let testPact = prepareTestPact(interactions: interaction) + let testResult = try XCTUnwrap(((testPact.payload["interactions"] as? [Interaction])?.first)?.providerStates) + + XCTAssertTrue(expectedResult.allSatisfy { expectedState in + testResult.contains { $0 == expectedState } + }) + } + + // MARK: - Interaction response + + func testPact_SetsInteractionResponseStatusCode() throws { + let expectedResult = 201 + let interaction = try prepareInteraction(description: "test_statusCode_interaction", statusCode: expectedResult) + + let testPact = prepareTestPact(interactions: interaction) + + let testResult = try XCTUnwrap((testPact.payload["interactions"] as? [Interaction])?.first?.response?.statusCode) + XCTAssertEqual(testResult, expectedResult) + } + + // MARK: Encodable + + func testPact_SetsRequestBody() throws { + let firstProviderState = ProviderState(description: "an alligator with the given name exists", params: ["name": "Mary"]) + let secondProviderState = ProviderState(description: "the user is logged in", params: ["username": "Fred"]) + + let testBody: Any = [ + "foo": "Bar", + "baz": 200.0, + "bar": [ + "goo": 123.45 + ], + "fuu": ["xyz", "abc"], + "num": [1, 2, 3] + ] as [String : Any] + + let interaction = Interaction( + description: "test Encodable Pact", + providerStates: [firstProviderState, secondProviderState], + request: try Request( + method: .GET, + path: "/", + query: ["max_results": ["100"]], + headers: ["Content-Type": "applicatoin/json; charset=UTF-8", "X-Value": "testCode"], + body: testBody + ), + response: try Response( + statusCode: 200 + ) + ) + + let testPact = Pact( + consumer: Pacticipant.consumer("test-consumer"), + provider: Pacticipant.provider("test-provider"), + interactions: [interaction] + ) + + let testResult = try XCTUnwrap(try JSONDecoder().decode(TestPactModel.self, from: testPact.data!).interactions.first).request.body + XCTAssertEqual(testResult.foo, "Bar") + XCTAssertEqual(testResult.baz, 200.0) + XCTAssertTrue(testResult.fuu.allSatisfy { ((testBody as! [String: Any])["fuu"] as! Array).contains($0) }) + XCTAssertTrue(testResult.num.allSatisfy { ((testBody as! [String: Any])["num"] as! Array).contains($0) }) + XCTAssertEqual(testResult.bar, ["goo": 123.45]) + } + +} + +private extension PactTests { + + // MARK: - Test resources and definitions + + struct TestPactModel: Decodable { + let interactions: [TestInteractionModel] + + struct TestInteractionModel: Decodable { + let request: TestRequestModel + + struct TestRequestModel: Decodable { + let body: TestBodyModel + + struct TestBodyModel: Decodable { + let foo: String + let baz: Double + let bar: [String: Double] + let fuu: [String] + let num: [Int] + } + } + } + } + + // MARK: - Test Helper functions + + func prepareTestPact() -> Pact { + Pact(consumer: Pacticipant.consumer(testConsumer), provider: Pacticipant.provider(testProvider)) + } + + func prepareTestPact(interactions: Interaction...) -> Pact { + Pact(consumer: Pacticipant.consumer(testConsumer), provider: Pacticipant.provider(testProvider), interactions: interactions) + } + + func prepareInteraction(description: String, method: PactHTTPMethod = .GET, path: String = "/", statusCode: Int = 200) throws -> Interaction { + Interaction( + description: description, + providerState: "some testable provider state", + request: try Request(method: method, path: path), + response: try Response(statusCode: statusCode) + ) + } + +} diff --git a/Tests/Model/PacticipantTests.swift b/Tests/Model/PacticipantTests.swift new file mode 100644 index 00000000..75c68652 --- /dev/null +++ b/Tests/Model/PacticipantTests.swift @@ -0,0 +1,32 @@ +// +// Created by Marko Justinek on 1/4/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import XCTest + +@testable import PactSwift + +class PacticipantTests: XCTestCase { + + func testPacticipant_ReturnsConsumerName() { + XCTAssertEqual(Pacticipant.consumer("test_consumer").name, "test_consumer") + } + + func testPacticipant_ReturnsProviderName() { + XCTAssertEqual(Pacticipant.provider("test_provider").name, "test_provider") + } + +} diff --git a/Tests/Model/ProviderVerifier+OptionsTests.swift b/Tests/Model/ProviderVerifier+OptionsTests.swift new file mode 100644 index 00000000..da4fcd6f --- /dev/null +++ b/Tests/Model/ProviderVerifier+OptionsTests.swift @@ -0,0 +1,302 @@ +// +// Created by Marko Justinek on 29/8/21. +// Copyright © 2021 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import XCTest + +@testable import PactSwift + +final class ProviderVerifierOptionsTests: XCTestCase { + + func testArgsWithConfiguredProvider() { + let testSubject = ProviderVerifier.Options( + provider: .init(url: URL(string: "https://localhost")!, port: 1234), + pactsSource: .directories(["/tmp/pacts"]) + ) + + XCTAssertTrue(testSubject.args.contains("--port\n1234")) + XCTAssertTrue(testSubject.args.contains("--hostname\nhttps://localhost")) + } + + func testArgsWhenPactSourceIsDirectories() { + let testSubject = ProviderVerifier.Options( + provider: ProviderVerifier.Provider(port: 8080), + pactsSource: .directories(["/tmp/pacts"]) + ) + + XCTAssertTrue(testSubject.args.contains("--port\n8080")) + XCTAssertTrue(testSubject.args.contains("--dir\n/tmp/pacts")) + } + + func testArgsWhenPactsSourceIsFiles() { + let testSubject = ProviderVerifier.Options( + provider: ProviderVerifier.Provider(port: 8080), + pactsSource: .files(["/tmp/pacts/one.json", "/tmp/pacts/two.json"]) + ) + + XCTAssertTrue(testSubject.args.contains("--port\n8080")) + XCTAssertTrue(testSubject.args.contains("--file\n/tmp/pacts/one.json")) + XCTAssertTrue(testSubject.args.contains("--file\n/tmp/pacts/two.json")) + } + + func testArgsWhenPactsSourceIsURLs() { + let testSubject = ProviderVerifier.Options( + provider: ProviderVerifier.Provider(port: 8080), + pactsSource: .urls([URL(string: "http://some.url/file.json")!]) + ) + + XCTAssertTrue(testSubject.args.contains("--port\n8080")) + XCTAssertTrue(testSubject.args.contains("--url\nhttp://some.url/file.json")) + } + + func testArgsWithStateChangeURL() { + let testSubject = ProviderVerifier.Options( + provider: .init(port: 8080), + pactsSource: .directories(["/tmp/pacts"]), + stateChangeURL: URL(string: "https://provider.url/stateChangeURL")! + ) + + XCTAssertTrue(testSubject.args.contains("--state-change-url\nhttps://provider.url/stateChangeURL")) + } + + func testArgsWithLogLevel() { + let testSubject = ProviderVerifier.Options( + provider: .init(port: 8080), + pactsSource: .directories(["/tmp/pacts"]), + logLevel: .trace + ) + + XCTAssertTrue(testSubject.args.contains("--loglevel\ntrace")) + } + + func testArgsWithFilterProviderStates() { + let testSubject = ProviderVerifier.Options( + provider: .init(port: 8080), + pactsSource: .directories(["/tmp/pacts"]), + filter: .noState + ) + + XCTAssertTrue(testSubject.args.contains("--filter-no-state\ntrue")) + } + + func testArgsWithFilterStates() { + let testSubject = ProviderVerifier.Options( + provider: .init(port: 8080), + pactsSource: .directories(["/tmp/pacts"]), + filter: .states(["state A", "state B"]) + ) + + XCTAssertTrue(testSubject.args.contains("--filter-state\nstate A")) + XCTAssertTrue(testSubject.args.contains("--filter-state\nstate B")) + } + + func testArgsWithFilterDescriptions() { + let testSubject = ProviderVerifier.Options( + provider: .init(port: 8080), + pactsSource: .directories(["/tmp/pacts"]), + filter: .descriptions(["A description", "B description"]) + ) + + XCTAssertTrue(testSubject.args.contains("--filter-description\nA description")) + XCTAssertTrue(testSubject.args.contains("--filter-description\nB description")) + } + + func testArgsWithFilterConsumers() { + let testSubject = ProviderVerifier.Options( + provider: .init(port: 8080), + pactsSource: .directories(["/tmp/pacts"]), + filter: .consumers(["Mobile Consumer", "Web Consumer"]) + ) + + XCTAssertTrue(testSubject.args.contains("--filter-consumer\nMobile Consumer")) + XCTAssertTrue(testSubject.args.contains("--filter-consumer\nWeb Consumer")) + } + + func testArgsWithPactBrokerUsingToken() { + let testBroker = PactBroker( + url: URL(string: "https://broker.url")!, + auth: .token(PactBroker.APIToken("test-token")), + providerName: "API Provider Name" + ) + + let testSubject = ProviderVerifier.Options( + provider: .init(port: 1234), + pactsSource: .broker(testBroker) + ) + + XCTAssertTrue(testSubject.args.contains("--broker-url\nhttps://broker.url")) + XCTAssertTrue(testSubject.args.contains("--token\ntest-token")) + XCTAssertTrue(testSubject.args.contains("--provider-name\nAPI Provider Name")) + } + + func testArgsWithPactBrokerBasicAuth() { + let testBroker = PactBroker( + url: URL(string: "https://broker.url")!, + auth: .auth(.init(username: "test-user", password: "test-pass")), + providerName: "API Provider Name" + ) + + let testSubject = ProviderVerifier.Options( + provider: .init(port: 1234), + pactsSource: .broker(testBroker) + ) + + XCTAssertTrue(testSubject.args.contains("--user\ntest-user")) + XCTAssertTrue(testSubject.args.contains("--password\ntest-pass")) + } + + func testArgsPublishingVerification() { + let testBroker = PactBroker( + url: URL(string: "https://broker.url")!, + auth: .auth(.init(username: "test-user", password: "test-pass")), + providerName: "API Provider Name", + publishResults: .init(providerVersion: "test-998877", providerTags: ["test", "unit"]) + ) + + let testSubject = ProviderVerifier.Options( + provider: .init(port: 1234), + pactsSource: .broker(testBroker) + ) + + XCTAssertTrue(testSubject.args.contains("--publish\n")) + XCTAssertTrue(testSubject.args.contains("--provider-version\ntest-998877")) + XCTAssertTrue(testSubject.args.contains("--provider-tags\ntest,unit")) + } + + func testArgsPublishingVerificationWithoutTags() { + let testBroker = PactBroker( + url: URL(string: "https://broker.url")!, + auth: .auth(.init(username: "test-user", password: "test-pass")), + providerName: "API Provider Name", + publishResults: .init(providerVersion: "test-123456") + ) + + let testSubject = ProviderVerifier.Options( + provider: .init(port: 1234), + pactsSource: .broker(testBroker) + ) + + XCTAssertTrue(testSubject.args.contains("--publish\n")) + XCTAssertTrue(testSubject.args.contains("--provider-version\ntest-123456")) + XCTAssertFalse(testSubject.args.contains("--provider-tags")) + } + + func testArgsBrokerWithConsumerTags() { + let testBroker = PactBroker( + url: URL(string: "https://broker.url")!, + auth: .auth(.init(username: "test-user", password: "test-pass")), + providerName: "API Provider Name", + consumerTags: [ + VersionSelector(tag: "prod", fallbackTag: "main", latest: true, consumer: "Test-app"), + VersionSelector(tag: "v2.3.5", fallbackTag: "prod", latest: false, consumer: "Web-app"), + ] + ) + + let testSubject = ProviderVerifier.Options( + provider: .init(port: 1234), + pactsSource: .broker(testBroker) + ) + + XCTAssertTrue(testSubject.args.contains("--consumer-version-selectors\n{")) + XCTAssertTrue(testSubject.args.contains("\"tag\":\"prod\"")) + XCTAssertTrue(testSubject.args.contains("\"tag\":\"v2.3.5\"")) + XCTAssertTrue(testSubject.args.contains("\"fallbackTag\":\"main\"")) + XCTAssertTrue(testSubject.args.contains("\"fallbackTag\":\"prod\"")) + XCTAssertTrue(testSubject.args.contains("\"latest\":true")) + XCTAssertTrue(testSubject.args.contains("\"latest\":false")) + XCTAssertTrue(testSubject.args.contains("\"consumer\":\"Test-app\"")) + XCTAssertTrue(testSubject.args.contains("\"consumer\":\"Web-app\"")) + } + + func testArgsBrokerWithPendingPacts() { + let testBroker = PactBroker( + url: URL(string: "https://broker.url")!, + auth: .auth(.init(username: "test-user", password: "test-pass")), + providerName: "API Provider Name", + includePending: true + ) + + let testSubject = ProviderVerifier.Options( + provider: .init(port: 1234), + pactsSource: .broker(testBroker) + ) + + XCTAssertTrue(testSubject.args.contains("--enable-pending\ntrue")) + } + + func testArgsBrokerDefaultsNotIncludePendingPacts() { + let testBroker = PactBroker( + url: URL(string: "https://broker.url")!, + auth: .auth(.init(username: "test-user", password: "test-pass")), + providerName: "API Provider Name" + ) + + let testSubject = ProviderVerifier.Options( + provider: .init(port: 1234), + pactsSource: .broker(testBroker) + ) + + XCTAssertFalse(testSubject.args.contains("--enable-pending")) + } + + func testArgsBrokerDefaltsNotIncludeWIPPacts() { + let testBroker = PactBroker( + url: URL(string: "https://broker.url")!, + auth: .auth(.init(username: "test-user", password: "test-pass")), + providerName: "API Provider Name" + ) + + let testSubject = ProviderVerifier.Options( + provider: .init(port: 1234), + pactsSource: .broker(testBroker) + ) + + XCTAssertFalse(testSubject.args.contains("--enable-pending")) + XCTAssertFalse(testSubject.args.contains("--include-wip")) + } + + func testArgsBrokerIncludeWIPPacts() { + let testDate = Date() + let todaysISODateString = isoDate(testDate) + + let testBroker = PactBroker( + url: URL(string: "https://broker.url")!, + auth: .auth(.init(username: "test-user", password: "test-pass")), + providerName: "API Provider Name", + includeWIP: WIPPacts(since: testDate, providerVersion: "v1.2.3") + ) + + let testSubject = ProviderVerifier.Options( + provider: .init(port: 1234), + pactsSource: .broker(testBroker) + ) + + XCTAssertTrue(testSubject.args.contains("--enable-pending\ntrue")) + XCTAssertTrue(testSubject.args.contains("--include-wip-pacts-since\n\(todaysISODateString)")) + XCTAssertTrue(testSubject.args.contains("--provider-version\nv1.2.3")) + } + +} + +private extension ProviderVerifierOptionsTests { + + func isoDate(_ date: Date) -> String { + let formatter = DateFormatter() + formatter.dateFormat = "YYYY-MM-dd" + return formatter.string(from: date) + } + +} diff --git a/Tests/Model/ToolboxTests.swift b/Tests/Model/ToolboxTests.swift new file mode 100644 index 00000000..53ea4a31 --- /dev/null +++ b/Tests/Model/ToolboxTests.swift @@ -0,0 +1,81 @@ +// +// ToolboxTests.swift +// PactSwift +// +// Created by Marko Justinek on 27/10/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import XCTest + +@testable import PactSwift + +class ToolboxTests: XCTestCase { + + func testMerge_ReturnsNil() { + let testSubject = Toolbox.merge(body: nil, query: nil, header: nil, path: nil) + + XCTAssertNil(testSubject) + } + + func testMerge_HandlesArguments() throws { + let testBody = AnyEncodable("foo:body") + let testQuery = AnyEncodable("foo:query") + let testHeader = AnyEncodable("foo:header") + + let testSubject = Toolbox.merge(body: testBody, query: testQuery, header: testHeader, path: nil) + let mergedObjectKeys = try XCTUnwrap(processAnyEncodable(object: testSubject as Any)) + + let expectedObjectKeys = ["body", "query", "header"] + XCTAssertTrue( + expectedObjectKeys.allSatisfy { value in + mergedObjectKeys.contains(value) + } + ) + } + + func testProcess_Throws_NonEncodable() { + struct NonSupportedType { + let some = "Uh oh!" + } + let nonSupportedType = NonSupportedType() + + do { + _ = try Toolbox.process(element: nonSupportedType, for: .body) + XCTFail("Should fail with EncodingError") + } catch { + XCTAssertTrue((error as Any) is EncodingError) + } + } + +} + +private extension ToolboxTests { + + func processAnyEncodable(object: Any) -> [String]? { + switch object { + case let dict as [String: Any]: + var mergedKeys: [String] = [] + for key in dict.keys { + mergedKeys.append(key) + } + return mergedKeys + default: + XCTFail("Should only expect a Dictionary here as Toolbox.merge() should return it") + return nil + } + } + +} diff --git a/Tests/Model/TransferProtocolTests.swift b/Tests/Model/TransferProtocolTests.swift new file mode 100644 index 00000000..f9399b2f --- /dev/null +++ b/Tests/Model/TransferProtocolTests.swift @@ -0,0 +1,40 @@ +// +// Created by Marko Justinek on 7/8/21. +// Copyright © 2021 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import XCTest + +@testable import PactSwift + +#if compiler(>=5.5) +@_implementationOnly import PactSwiftMockServer +#else +import PactSwiftMockServer +#endif + +class TransferProtocolTests: XCTestCase { + + func testTransferProtocol() { + XCTAssertEqual(TransferProtocol.standard.protocol, "http") + XCTAssertEqual(TransferProtocol.secure.protocol, "https") + } + + func testTransferProtocolBridge() { + XCTAssertEqual(TransferProtocol.standard.bridge, PactSwiftMockServer.TransferProtocol.standard) + XCTAssertEqual(TransferProtocol.secure.bridge, PactSwiftMockServer.TransferProtocol.secure) + } + +} diff --git a/Tests/Model/VersionSelectorTests.swift b/Tests/Model/VersionSelectorTests.swift new file mode 100644 index 00000000..8301c361 --- /dev/null +++ b/Tests/Model/VersionSelectorTests.swift @@ -0,0 +1,61 @@ +// +// Created by Marko Justinek on 28/8/21. +// Copyright © 2021 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import XCTest + +@testable import PactSwift + +final class VersionSelectorTests: XCTestCase { + + func testVersionSelectorInitializes() { + let testSubject = VersionSelector(tag: "test-tag") + + XCTAssertEqual(testSubject.tag, "test-tag") + XCTAssertNil(testSubject.fallbackTag) + XCTAssertTrue(testSubject.latest) + XCTAssertNil(testSubject.consumer) + } + + func testVersionSelectorSetsFallbackTag() { + let testSubject = VersionSelector(tag: "test-tag", fallbackTag: "fallback-tag") + + XCTAssertEqual(testSubject.tag, "test-tag") + XCTAssertEqual(testSubject.fallbackTag, "fallback-tag") + } + + func testVersionSelectorSetsLatest() { + let testSubject = VersionSelector(tag: "test-tag", latest: false) + + XCTAssertFalse(testSubject.latest) + } + + func testVersionSelectorSetsConsumer() { + let testSubject = VersionSelector(tag: "test-tag", consumer: "api-consumer") + + XCTAssertEqual(testSubject.consumer, "api-consumer") + } + + func testVersionSelectorJSONString() throws { + let testSubject = try VersionSelector(tag: "test", fallbackTag: "main", latest: true, consumer: "api-consumer").toJSONString() + + XCTAssertTrue(testSubject.contains("\"tag\":\"test\"")) + XCTAssertTrue(testSubject.contains("\"fallbackTag\":\"main\"")) + XCTAssertTrue(testSubject.contains("\"latest\":true")) + XCTAssertTrue(testSubject.contains("\"consumer\":\"api-consumer\"")) + } + +} diff --git a/Tests/Model/WIPPactsTests.swift b/Tests/Model/WIPPactsTests.swift new file mode 100644 index 00000000..5eaa547d --- /dev/null +++ b/Tests/Model/WIPPactsTests.swift @@ -0,0 +1,36 @@ +// +// Created by Marko Justinek on 28/8/21. +// Copyright © 2021 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// +import XCTest + +@testable import PactSwift + +final class WIPPactsTests: XCTestCase { + + func testWIPPactsInitializesWithDate() { + let testDate = Date() + let testSubject = WIPPacts(since: testDate, providerVersion: "test") + + XCTAssertEqual(testSubject.sinceDate, testDate) + } + + func testWIPPactsInitializesWithProviderVersion() { + let testSubject = WIPPacts(since: Date(), providerVersion: "test") + + XCTAssertEqual(testSubject.providerVersion, "test") + } + +} diff --git a/Tests/Services/AsyncMockServiceTests.swift b/Tests/Services/AsyncMockServiceTests.swift new file mode 100644 index 00000000..3f6ed3d4 --- /dev/null +++ b/Tests/Services/AsyncMockServiceTests.swift @@ -0,0 +1,146 @@ +// +// Created by Huw Rowlands on 30/11/2022. +// Copyright © 2022 Huw Rowlands. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// +import XCTest + +@testable import PactSwift + +import Foundation + +#if canImport(_Concurrency) && compiler(>=5.7) +final class MockServiceAsyncTests: XCTestCase { + + var mockService: MockService! + var errorCapture: ErrorCapture! + + private var secureProtocol: Bool = false + + // MARK: - Lifecycle + + override func setUpWithError() throws { + try super.setUpWithError() + + guard #available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) else { + throw XCTSkip("Unsupported OS") + } + + errorCapture = ErrorCapture() + mockService = MockService(consumer: "pactswift-unit-tests", provider: "unit-test-api-provider", errorReporter: errorCapture) + } + + @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) + func testMockService_SimpleGetRequest_InsideTask() { + mockService + .uponReceiving("Request for a list") + .given("elements exist") + .withRequest(method: .GET, path: "/elements") + .willRespondWith(status: 200) + + let testExpectation = expectation(description: #function) + + mockService.run(timeout: 1) { baseURL, completion in + let session = URLSession.shared + Task { + let (_, response) = try await session.data(from: URL(string: "\(baseURL)/elements")!) + + guard let response = response as? HTTPURLResponse else { + XCTFail("Expecting HTTPURLResponse") + return + } + + XCTAssertEqual(200, response.statusCode) + completion() + testExpectation.fulfill() + } + } + waitForExpectations(timeout: 1) + } + + @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) + func testMockService_SimpleGetRequest_RunAsync() async throws { + mockService + .uponReceiving("Request for a list") + .given("elements exist") + .withRequest(method: .GET, path: "/elements") + .willRespondWith(status: 200) + + try await mockService.run(timeout: 1) { baseURL in + let session = URLSession.shared + + let (_, response) = try await session.data(from: URL(string: "\(baseURL)/elements")!) + + guard let response = response as? HTTPURLResponse else { + XCTFail("Expecting HTTPURLResponse") + return + } + + XCTAssertEqual(200, response.statusCode) + } + } + + @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) + func testMockService_SimpleGetRequest_RunAsyncTimesOut() async { + mockService + .uponReceiving("Request for a list") + .given("elements exist") + .withRequest(method: .GET, path: "/elements") + .willRespondWith(status: 200) + + do { + try await self.mockService.run(timeout: 1) { baseURL in + let session = URLSession.shared + + _ = try await session.data(from: URL(string: "\(baseURL)/elements")!) + + try await Task.sleep(nanoseconds: 10 * NSEC_PER_SEC) + } + XCTFail("Expected timeout") + } catch { + XCTAssertEqual(errorCapture.error?.message, "Task timed out after 1.0 seconds") + XCTAssertTrue(error is TimeoutError) + } + } + + @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) + func testMockService_SimpleGetRequest_RunAsyncThrows() async { + + struct TestError: LocalizedError { + var failureReason: String? { "Test Failure" } + } + + mockService + .uponReceiving("Request for a list") + .given("elements exist") + .withRequest(method: .GET, path: "/elements") + .willRespondWith(status: 200) + + do { + try await self.mockService.run(timeout: 1) { baseURL in + let session = URLSession.shared + + _ = try await session.data(from: URL(string: "\(baseURL)/elements")!) + + throw TestError() + } + XCTFail("Should not be reached") + } catch { + XCTAssertTrue(error is TestError) + XCTAssertEqual(errorCapture.error?.message, TestError().localizedDescription) + } + } + +} +#endif diff --git a/Tests/Services/MockServiceTests.swift b/Tests/Services/MockServiceTests.swift new file mode 100644 index 00000000..3df9749c --- /dev/null +++ b/Tests/Services/MockServiceTests.swift @@ -0,0 +1,1030 @@ +// +// Created by Marko Justinek on 15/4/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import XCTest + +@testable import PactSwift + +class MockServiceTests: XCTestCase { + + var mockService: MockService! + var errorCapture: ErrorCapture! + + private var secureProtocol: Bool = false + + // MARK: - Lifecycle + + override func setUp() { + super.setUp() + + errorCapture = ErrorCapture() + mockService = MockService(consumer: "pactswift-unit-tests", provider: "unit-test-api-provider", errorReporter: errorCapture) + } + + override func tearDown() { + mockService = nil + errorCapture = nil + + super.tearDown() + } + + // MARK: - Tests + + func testMockService_SimpleGetRequest() { + mockService + .uponReceiving("Request for a list") + .given("elements exist") + .withRequest(method: .GET, path: "/elements") + .willRespondWith(status: 200) + + let testExpectation = expectation(description: #function) + + mockService.run(timeout: 1) { baseURL, completion in + let session = URLSession.shared + let task = session.dataTask(with: URL(string: "\(baseURL)/elements")!) { data, response, error in + if let response = response as? HTTPURLResponse { + XCTAssertEqual(200, response.statusCode) + completion() + testExpectation.fulfill() + } else { + XCTFail("Expecting response code 200 in \(#function)") + } + } + task.resume() + } + waitForExpectations(timeout: 1) + } + + func testMockService_SuccessfulGETRequest() { + mockService + .uponReceiving("Request for list of users") + .given("users exist") + .withRequest(method: .GET, path: "/user") + .willRespondWith( + status: 200, + body: [ + "foo": "bar" + ] + ) + + let testExpectation = expectation(description: #function) + + mockService.run(timeout: 1) { baseURL, completion in + let session = URLSession.shared + let task = session.dataTask(with: URL(string: "\(baseURL)/user")!) { data, response, error in + if let data = data { + do { + let testResult: TestModel = try XCTUnwrap(self.decodeResponse(data: data)) + XCTAssertEqual(testResult.foo, "bar") + completion() + testExpectation.fulfill() + } catch { + XCTFail("Expected TestModel in \(#function)") + } + } else { + XCTFail("Expecting a decodable response data in \(#function)") + } + } + task.resume() + } + + waitForExpectations(timeout: 1) + } + + // Linux does not seem to support this and URLSessionDelegate + func testMockService_SuccessfulGETRequest_OverSSL() { + mockService = MockService( + consumer: "pactswift-unit-tests", + provider: "unit-test-api-provider", + scheme: .secure, + errorReporter: errorCapture + ) + + mockService + .uponReceiving("Request for list of users over SSL connection") + .given("users exist") + .withRequest(method: .GET, path: "/user") + .willRespondWith( + status: 200, + body: [ + "foo": "bar" + ] + ) + + let testExpectation = expectation(description: #function) + + mockService.run { baseURL, completion in + self.secureProtocol = true + let session = URLSession(configuration: .ephemeral, delegate: self, delegateQueue: .main) + XCTAssertTrue(baseURL.contains("https://")) + + let task = session.dataTask(with: URL(string: "\(baseURL)/user")!) { data, response, error in + if let data = data { + do { + let testResult: TestModel = try XCTUnwrap(self.decodeResponse(data: data)) + XCTAssertEqual(testResult.foo, "bar") + completion() + testExpectation.fulfill() + } catch { + XCTFail("Expecting a TestModel in \(#function)") + } + } + if let error = error { + XCTFail(error.localizedDescription) + } + } + task.resume() + } + + waitForExpectations(timeout: 1) + } + + func testMockService_Fails_WhenRequestMissing() { + mockService + .uponReceiving("Request for alligators") + .given("alligators exist") + .withRequest(method: .GET, path: "/actors") + .willRespondWith( + status: 200 + ) + + mockService.run { _, completion in completion() } + + do { + let testResult = try XCTUnwrap(errorCapture.error?.message) + XCTAssertTrue(testResult.contains("Missing request")) + } catch { + XCTFail("Expecting an errorCapture object to intercept the failing tests message in \(#function)") + } + } + + func testMockService_Fails_WhenRequestPathInvalid() throws { + let expectedValues = [ + "Failed to verify Pact!", + "Actual request does not match expected interactions...", + "Missing request", + "Expected", + "GET /user", + "Actual", + "GET /invalidPath" + ] + + mockService + .uponReceiving("Request for alligators") + .given("alligators exist") + .withRequest(method: .GET, path: "/user") + .willRespondWith( + status: 200, + body: [ + "foo": "bar" + ] + ) + + let testExpectation = expectation(description: #function) + + mockService.run { baseURL, completion in + let session = URLSession.shared + let task = session.dataTask(with: URL(string: "\(baseURL)/invalidPath")!) { data, response, error in + completion() + testExpectation.fulfill() + } + task.resume() + } + + let testResult = try XCTUnwrap(errorCapture.error?.message) + XCTAssertTrue(expectedValues.allSatisfy { testResult.contains($0) }) + + waitForExpectations(timeout: 1) + } + + func testMockService_Fails_WhenUnexpectedQuery() throws { + let expectedValues = [ + "Failed to verify Pact!", + "Actual request does not match expected interactions...", + "Request does not match", + "Request", + "GET /user", + "state", "NSW", + "Actual", + "query param 'page'", + "query param 'state'" + ] + + mockService + .uponReceiving("Request for list of users") + .given("users exist") + .withRequest(method: .GET, path: "/user", query: ["state": [Matcher.EqualTo("NSW")], "page": ["2"]]) + .willRespondWith( + status: 200 + ) + + let testExpectation = expectation(description: #function) + + mockService.run { baseURL, completion in + let session = URLSession.shared + let task = session.dataTask(with: URL(string: "\(baseURL)/user?state=VIC")!) { data, response, error in + completion() + testExpectation.fulfill() + } + task.resume() + } + + let testResult = try XCTUnwrap(errorCapture.error?.message) + XCTAssertTrue(expectedValues.allSatisfy { testResult.contains($0) }) + + waitForExpectations(timeout: 1) + } + + func testMockService_Fails_WhenBodyMismatch() throws { + let expectedValues = [ + "Failed to verify Pact!", + "Actual request does not match expected interactions...", + "Request does not match", + "Body does not match the expected body definition" + ] + + mockService + .uponReceiving("Request for list of users") + .given("users exist") + .withRequest(method: .POST, path: "/user", body: ["foo": "bar"]) + .willRespondWith( + status: 201 + ) + + let testExpectation = expectation(description: #function) + + mockService.run { baseURL, completion in + let requestURL = URL(string: "\(baseURL)/user")! + let session = URLSession.shared + var request = URLRequest(url: requestURL) + + request.httpMethod = "POST" + request.httpBody = "{\"foo\":\"baz\"}".data(using: .utf8)! + + let task = session.dataTask(with: request) { data, response, error in + completion() + testExpectation.fulfill() + } + task.resume() + } + + let testResult = try XCTUnwrap(errorCapture.error?.message) + XCTAssertTrue(expectedValues.allSatisfy { testResult.contains($0) }) + + waitForExpectations(timeout: 1) + } + + func testMockService_Fails_WhenBodyIsEmptyObject() throws { + let expectedValues = [ + "Failed to verify Pact!", + "Actual request does not match expected interactions...", + "Request does not match", + "Body does not match the expected body definition" + ] + + mockService + .uponReceiving("Request for list of users") + .given("users exist") + .withRequest(method: .POST, path: "/user", body: ["foo": "bar"]) + .willRespondWith( + status: 201 + ) + + let testExpectation = expectation(description: #function) + + mockService.run { baseURL, completion in + let requestURL = URL(string: "\(baseURL)/user")! + let session = URLSession.shared + var request = URLRequest(url: requestURL) + + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpBody = "{\n\n}".data(using: .utf8)! + + let task = session.dataTask(with: request) { data, response, error in + completion() + testExpectation.fulfill() + } + task.resume() + } + + let testResult = try XCTUnwrap(errorCapture.error?.message) + XCTAssertTrue(expectedValues.allSatisfy { testResult.contains($0) }) + + waitForExpectations(timeout: 1) + } + + func testMockService_Fails_WhenRequestBodyMissing() throws { + let expectedValues = [ + "Failed to verify Pact!", + "Actual request does not match expected interactions...", + "Request does not match", + "Body does not match the expected body definition" + ] + + mockService + .uponReceiving("Request for list of users") + .given("users exist") + .withRequest(method: .POST, path: "/user", body: ["foo": "bar"]) + .willRespondWith( + status: 201 + ) + + let testExpectation = expectation(description: #function) + + mockService.run { baseURL, completion in + let requestURL = URL(string: "\(baseURL)/user")! + let session = URLSession.shared + var request = URLRequest(url: requestURL) + request.httpMethod = "POST" + let task = session.dataTask(with: request) { data, response, error in + completion() + testExpectation.fulfill() + } + task.resume() + } + + let testResult = try XCTUnwrap(errorCapture.error?.message) + XCTAssertTrue(expectedValues.allSatisfy { testResult.contains($0) }) + + waitForExpectations(timeout: 1) + } + + func testMockService_Fails_WithHeaderMismatch() throws { + let expectedValues = [ + "Failed to verify Pact!", + "Actual request does not match expected interactions...", + "Request does not match", + "header", + "'testKey'" + ] + + mockService + .uponReceiving("Request for list of users") + .given("users exist") + .withRequest(method: .GET, path: "/user", headers: ["testKey": "test/value"]) + .willRespondWith( + status: 200 + ) + + let testExpectation = expectation(description: #function) + + mockService.run { baseURL, completion in + let session = URLSession.shared + let task = session.dataTask(with: URL(string: "\(baseURL)/user")!) { data, response, error in + testExpectation.fulfill() + completion() + } + task.resume() + } + + let testResult = try XCTUnwrap(errorCapture.error?.message) + XCTAssertTrue(expectedValues.allSatisfy { testResult.contains($0) }) + waitForExpectations(timeout: 1) + } + + // MARK: - Using matchers + + func testMockService_Succeeds_ForGetWithMatcherInRequestPath() { + mockService + .uponReceiving("Request for a list of foo") + .given("foos exist") + .withRequest( + method: .GET, + path: Matcher.RegexLike(value: "/hello/dear/world", pattern: #"^/\w+/([a-z])+/world$"#) + ) + .willRespondWith(status: 200) + + let testExpectation = expectation(description: #function) + + mockService.run { baseURL, completion in + let session = URLSession.shared + let task = session.dataTask(with: URL(string: "\(baseURL)/hello/cruel/world")!) { data, response, error in + if let response = response as? HTTPURLResponse { + XCTAssertEqual(response.statusCode, 200) + } + testExpectation.fulfill() + completion() + } + task.resume() + } + + waitForExpectations(timeout: 1) + } + + func testMockService_Succeeds_ForPOSTRequestWithMatchersInRequestBody() throws { + let now = Date() + let modifiedDate = try XCTUnwrap(Calendar.current.date(byAdding: .hour, value: 1, to: now)) + let dateFormat = "yyyy-MM-dd HH:mm" + + mockService + .uponReceiving("Request to create a new user") + .given("user does not exist") + .withRequest( + method: .POST, + path: "/user/add", + query: nil, + headers: ["Content-Type": "application/json"], + body: [ + "name": Matcher.SomethingLike("Joe"), + "age": Matcher.IntegerLike(42) + ] as [String : Any] + ) + .willRespondWith( + status: 201, + body: [ + "start": ExampleGenerator.DateTimeExpression(expression: "@ next hour", format: dateFormat) + ] + ) + + let testExpectation = expectation(description: #function) + + mockService.run { baseURL, completion in + let requestURL = URL(string: "\(baseURL)/user/add")! + let session = URLSession.shared + var request = URLRequest(url: requestURL) + + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpBody = #"{"name":"Joseph","age":24}"#.data(using: .utf8) + + let task = session.dataTask(with: request) { data, response, error in + if let response = response as? HTTPURLResponse { + XCTAssertEqual(response.statusCode, 201) + } + guard let data = data, let resultBody = String(data: data, encoding: .utf8) else { + XCTFail("Failed to cast response data into String") + return + } + // This is an approximation and a fragile test - MockServer (pact-rust) generates the date based on expression and can be milisecond to seconds difference + XCTAssertEqual(resultBody, "{\"start\":\"\(DateHelper.stringFrom(date: modifiedDate, format: dateFormat))\"}") + testExpectation.fulfill() + completion() + } + task.resume() + } + + waitForExpectations(timeout: 1) + } + + func testMockService_Succeeds_WithMatchersInRequestBody() { + mockService + .uponReceiving("Request to update a user") + .given("user exists") + .withRequest( + method: .PUT, + path: "/user/update", + headers: ["Content-Type": Matcher.EqualTo("application/json")], + body: [ + "name": Matcher.SomethingLike("Joe"), + "age": Matcher.SomethingLike(42) + ] + ) + .willRespondWith( + status: 201 + ) + + let testExpectation = expectation(description: #function) + + mockService.run { baseURL, completion in + let requestURL = URL(string: "\(baseURL)/user/update")! + let session = URLSession.shared + var request = URLRequest(url: requestURL) + + request.httpMethod = "PUT" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpBody = #"{"name":"Joe","age":42}"#.data(using: .utf8) + + let task = session.dataTask(with: request) { data, response, error in + if let response = response as? HTTPURLResponse { + XCTAssertEqual(response.statusCode, 201) + } + testExpectation.fulfill() + completion() + } + task.resume() + } + + waitForExpectations(timeout: 1) + } + + func testMockService_Succeeds_WithMatchersInEachLike() { + mockService + .uponReceiving("Request to get a user with options") + .given("user with options exists") + .withRequest( + method: .GET, + path: "/user/options" + ) + .willRespondWith( + status: 200, + body: [ + "options": Matcher.EachLike( + Matcher.SomethingLike("option_one"), + min: 3 + ) + ] + ) + + let testExpectation = expectation(description: #function) + + mockService.run { baseURL, completion in + let requestURL = URL(string: "\(baseURL)/user/options")! + let session = URLSession.shared + + let task = session.dataTask(with: URLRequest(url: requestURL)) { data, response, error in + if let data = data { + do { + let testResult: EmbeddedMatcherTestModel = try XCTUnwrap(self.decodeResponse(data: data)) + + XCTAssertEqual(testResult.options.count, 3) + XCTAssertTrue(testResult.options.contains("option_one")) + } catch { + XCTFail("Expecting an EmbeddedMatcherTestModel object in \(#function)") + } + } + + testExpectation.fulfill() + completion() + } + task.resume() + } + + waitForExpectations(timeout: 1) + } + + func testMockService_Succeeds_WithMatchers() { + mockService + .uponReceiving("Request for list of users") + .given("users exist") + .withRequest(method: .GET, path: "/user") + .willRespondWith( + status: 200, + body: [ + "foo": Matcher.SomethingLike("bar"), + "baz": Matcher.EachLike(123, min: 1, max: 5), + "nullable_key": Matcher.MatchNull(), + "one_of_string": Matcher.OneOf("white", "gray", "blue", "yellow", "green", "black"), + "one_of_int": Matcher.OneOf(3, 1, 2, 4) + ] as [String : Any] + ) + + let testExpectation = expectation(description: #function) + + mockService.run { baseURL, completion in + let session = URLSession.shared + let task = session.dataTask(with: URL(string: "\(baseURL)/user")!) { data, response, error in + if let data = data { + do { + let testResult: TestModel = try XCTUnwrap(self.decodeResponse(data: data)) + XCTAssertEqual(testResult.foo, "bar") + XCTAssertEqual(testResult.baz?.first, 123) + XCTAssertNil(testResult.nullable_key) + XCTAssertEqual(testResult.one_of_string, "white") + XCTAssertEqual(testResult.one_of_int, 3) + + let responseData = try XCTUnwrap(String(data: data, encoding: .utf8)) + XCTAssertTrue(responseData.contains("nullable_key")) + } catch { + XCTFail("Expecting a nullable_key key with null value for Match.MatchNull() in \(#function)") + } + } + testExpectation.fulfill() + completion() + } + task.resume() + } + + waitForExpectations(timeout: 1) + } + + func testMockService_Succeeds_WithMatcherInHeaders() throws { + mockService + .uponReceiving("Request for list of movies") + .withRequest( + method: .GET, + path: "/movies", + query: nil, + headers: ["Authorization": Matcher.RegexLike(value: "Bearer abcd12345", pattern: #"^Bearer \w+$"#)], + body: nil + ) + .willRespondWith(status: 200, body: [ + "foo": "bar", + "baz": Matcher.EachLike(1) + ] as [String : Any]) + + let testExpectation = expectation(description: #function) + + mockService.run { baseURL, completion in + let requestURL = URL(string: "\(baseURL)/movies")! + var request = URLRequest(url: requestURL) + let session = URLSession.shared + + request.setValue("Bearer bdjksl1234352", forHTTPHeaderField: "Authorization") + + let task = session.dataTask(with: request) { data, response, error in + if let data = data { + do { + let testResult: TestModel = try XCTUnwrap(self.decodeResponse(data: data)) + XCTAssertEqual(testResult.foo, "bar") + } catch { + XCTFail("Expecting a valid response object when asking for a list of /movies in \(#function)") + } + } + + testExpectation.fulfill() + completion() + } + task.resume() + } + + waitForExpectations(timeout: 5) + } + + func testMockService_Succeeds_WithMatchersInRequestQuery() throws { + mockService + .uponReceiving("Request for list of songs") + .withRequest( + method: .GET, + path: "/songs", + query: [ + "page": [Matcher.SomethingLike("5")], + "size": [Matcher.SomethingLike("25")] + ] + ) + .willRespondWith(status: 200, body: [ + "foo": "bar", + "array_example": [ + Matcher.EachLike("one", min: 2, max: 10, count: 5), + Matcher.EachLike(1, min: 1, max: 25), + ] + ] as [String : Any]) + + let testExpectation = expectation(description: #function) + + mockService.run { baseURL, completion in + let requestURL = URL(string: "\(baseURL)/songs?page=&size=25")! + var request = URLRequest(url: requestURL) + let session = URLSession.shared + + request.setValue("Bearer bdjksl1234352", forHTTPHeaderField: "Authorization") + + let task = session.dataTask(with: request) { data, response, error in + if let data = data { + do { + let testResult: TestModel = try XCTUnwrap(self.decodeResponse(data: data)) + XCTAssertEqual(testResult.foo, "bar") + } catch { + XCTFail("Expecting a valid response object when asking for a list of /movies in \(#function)") + } + } + + testExpectation.fulfill() + completion() + } + task.resume() + } + + waitForExpectations(timeout: 5) + } + + func testMockService_Succeeds_WithMatcherFromProviderState() { + mockService + .uponReceiving("a request") + .given( + ProviderState( + description: "a provider state with injectable values", + params: [ + "valueA": "A", + "valueB": "100", + ] + ) + ) + .withRequest( + method: .POST, + path: "/values", + headers: ["Content-Type": "application/json"], + body: [ + "name": Matcher.SomethingLike("George") + ] + ) + .willRespondWith( + status: 201, + body: [ + "identifier": Matcher.FromProviderState(parameter: "userId", value: .int(100)), + "name": Matcher.SomethingLike("Mary") + ] as [String : Any] + ) + + let testExpectation = expectation(description: #function) + + mockService.run { baseURL, done in + let requestURL = URL(string: "\(baseURL)/values")! + let session = URLSession.shared + var request = URLRequest(url: requestURL) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpBody = Data(#"{"name":"John"}"#.utf8) + + let task = session.dataTask(with: request) { data, response, error in + if let response = response as? HTTPURLResponse { + XCTAssertEqual(response.statusCode, 201) + } + testExpectation.fulfill() + done() + } + task.resume() + } + + waitForExpectations(timeout: 1) + } + + // MARK: - Using Example Generators + + func testMockService_Succeeds_WithGenerators() { + let testRegex = #"\d{3}/\d{4,8}"# + let testDateTime = Date() + let testDateTimeFormat = #"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"# + let testResultFormat = #"YYYY-MM-DD HH:mm"# + + mockService + .uponReceiving("Request for list of pets") + .given("animals exist") + .withRequest(method: .GET, path: "/pet") + .willRespondWith( + status: 200, + body: [ + "randomBool": ExampleGenerator.RandomBool(), + "randomUUID": ExampleGenerator.RandomUUID(), + "randomInt": ExampleGenerator.RandomInt(min: -42, max: 4242), + "randomDecimal": ExampleGenerator.RandomDecimal(digits: 4), + "randomHex": ExampleGenerator.RandomHexadecimal(digits: 14), + "randomString": ExampleGenerator.RandomString(size: 38), + "randomRegex": ExampleGenerator.RandomString(regex: testRegex), + "randomDate": ExampleGenerator.RandomDate(format: "yyyy/MM"), + "randomTime": ExampleGenerator.RandomTime(format: "HH:mm - ss"), + "randomDateTime": ExampleGenerator.RandomDateTime(format: "HH:mm - dd.MM.yy"), + "specificDateTime": ExampleGenerator.DateTime(format: testDateTimeFormat, use: testDateTime), + ] as [String : Any] + ) + + let testExpectation = expectation(description: #function) + + mockService.run { baseURL, completion in + let session = URLSession.shared + let task = session.dataTask(with: URL(string: "\(baseURL)/pet")!) { data, response, error in + if let data = data { + do { + let testResult: GeneratorsTestModel = try XCTUnwrap(self.decodeResponse(data: data)) + + // Verify Bool example generator + XCTAssertTrue(((testResult.randomBool) as Any) is Bool) + + // Verify UUID example generator + let uuidResult = try XCTUnwrap(testResult.randomUUID) + if uuidResult.contains("-") { + XCTAssertNotNil(UUID(uuidString: uuidResult)) + } else { + XCTAssertNotNil(uuidResult.uuid) + } + + // Verify RandomInt example generator + let intResult = try XCTUnwrap(testResult.randomInt) + XCTAssertTrue((-42...4242).contains(intResult)) + + // Verify Decimal example generator + let decimalResult = try XCTUnwrap(testResult.randomDecimal) + XCTAssertTrue((decimalResult as Any) is Decimal) + + // TODO - Investigate why MockServer sometimes returns 1 digit less than defined in ExampleGenerator.Decimal(digits: X) + // XCTAssertEqual(String(describing: decimalResult).count, 4, accuracy: 1) + + // Verify Hexadecimal value + let hexValue = try XCTUnwrap(testResult.randomHex) + XCTAssertEqual(hexValue.count, 14) + + // Verify Random String value + let stringValue = try XCTUnwrap(testResult.randomString) + XCTAssertEqual(stringValue.count, 38) + + // Verify Regex value + let regexValue = try XCTUnwrap(testResult.randomRegex) + let regex = try! NSRegularExpression(pattern: testRegex) + let range = NSRange(location: 0, length: regexValue.utf16.count) + XCTAssertNotNil(regex.firstMatch(in: regexValue, options: [], range: range)) + + // Verify random date value + let dateValue = try XCTUnwrap(testResult.randomDate) + let dateRegex = try! NSRegularExpression(pattern: #"\d{4}/\d{2}"#) + let dateRange = NSRange(location: 0, length: dateValue.utf16.count) + XCTAssertNotNil(dateRegex.firstMatch(in: dateValue, options: [], range: dateRange)) + + // Verify random time value + let timeValue = try XCTUnwrap(testResult.randomTime) + let timeRegex = try! NSRegularExpression(pattern: #"\d{2}:\d{2} - \d{2}"#) + let timeRange = NSRange(location: 0, length: timeValue.utf16.count) + XCTAssertNotNil(timeRegex.firstMatch(in: timeValue, options: [], range: timeRange)) + + // Verify random date time value + let dateTimeValue = try XCTUnwrap(testResult.randomDateTime) + let dateTimeRegex = try! NSRegularExpression(pattern: #"\d{2}:\d{2} - \d{2}.\d{2}.\d{2}"#) + let dateTimeRange = NSRange(location: 0, length: dateTimeValue.utf16.count) + XCTAssertNotNil(dateTimeRegex.firstMatch(in: dateTimeValue, options: [], range: dateTimeRange)) + + // Verify specific datetime generator + let specificDateTimeValue = try XCTUnwrap(testResult.specificDateTime) + let specificDateTimeResult = DateHelper.dateFrom(string: specificDateTimeValue, format: testDateTimeFormat) + // Asserting with reduced accuracy of provided format + XCTAssertEqual(testDateTime.formatted(testResultFormat), specificDateTimeResult?.formatted(testResultFormat)) + } catch { + XCTFail("Expecting GeneratorsTestModel in \(#function)") + } + } + + testExpectation.fulfill() + completion() + } + task.resume() + } + + waitForExpectations(timeout: 1) + } + + // MARK: - Write pact contract + + func testMockService_Writes_PactContract() throws { + let expectedFileName = "pactswift_unit_tests_write_file-with_api_provider.json" + removeFile(expectedFileName) + + mockService = MockService(consumer: "pactswift_unit_tests_write_file", provider: "with_api_provider", errorReporter: errorCapture) + + mockService + .uponReceiving("Request for list of users") + .given("users exist") + .withRequest(method: .GET, path: "/user") + .willRespondWith( + status: 200, + body: [ + "foo": Matcher.SomethingLike("bar"), + "baz": Matcher.EachLike(123, min: 1, max: 5, count: 3), + "array": Matcher.EachLike( + [ + Matcher.SomethingLike("array_value"), + Matcher.RegexLike(value: "2021-05-15", pattern: #"\d{4}-\d{2}-\d{2}"#), + ExampleGenerator.RandomUUID(), + Matcher.EachLike( + [ + "3rd_level_nested": Matcher.EachLike(Matcher.IntegerLike(369)) + ] + ) + ] as [Any] + ), + "regex_array": Matcher.EachLike( + [ + "regex_key": Matcher.EachLike( + Matcher.RegexLike(value: "1234", pattern: #"\d{4}"#), + min: 2 + ), + "regex_nested_object": Matcher.EachLike( + [ + "regex_nested_key": Matcher.RegexLike(value: "12345678", pattern: #"\d{8}"#) + ] + ) + ] + ) + ] as [String : Any] + ) + + let testExpectation = expectation(description: #function) + + mockService.run { baseURL, completion in + let session = URLSession.shared + let task = session.dataTask(with: URL(string: "\(baseURL)/user")!) { data, response, error in + if let data = data { + do { + let testResult: TestModel = try XCTUnwrap(self.decodeResponse(data: data)) + XCTAssertEqual(testResult.foo, "bar") + XCTAssertEqual(testResult.baz?.first, 123) + XCTAssertEqual(testResult.baz?.count, 3) + XCTAssertEqual(testResult.regex_array?.first?.regex_key.first, "1234") + XCTAssertEqual(testResult.regex_array?.first?.regex_key.count, 2) + } catch { + XCTFail("Expecting a TestModel in \(#function)") + } + } + + completion() + } + task.resume() + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + XCTAssertTrue(self.fileExists(expectedFileName)) + testExpectation.fulfill() + } + + waitForExpectations(timeout: 1) + } + +} + +// MARK: - Fixtures + +private extension MockServiceTests { + + func decodeResponse(data: Data) -> D? { + try? JSONDecoder().decode(D.self, from: data) + } + + struct TestModel: Decodable { + struct TestRegex: Decodable { + let regex_key: [String] + } + let foo: String + let baz: [Int]? + let nullable_key: String? + let options: [String]? + let regex_array: [TestRegex]? + let one_of_string: String? + let one_of_int: Int? + } + + struct GeneratorsTestModel: Decodable { + let randomBool: Bool + let randomInt: Int + let randomHex: String + let randomDecimal: Decimal + let randomUUID: String + let randomString: String + let randomRegex: String + let randomDate: String + let randomTime: String + let randomDateTime: String + let specificDateTime: String + } + + struct EmbeddedMatcherTestModel: Decodable { + let options: [String] + } + +} + +// MARK: - URLSessionDelegate + +extension MockServiceTests: URLSessionDelegate { + + func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + guard + secureProtocol, + challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, + (challenge.protectionSpace.host.contains("0.0.0.0") || challenge.protectionSpace.host.contains("127.0.0.1") || challenge.protectionSpace.host.contains("localhost")), + let serverTrust = challenge.protectionSpace.serverTrust + else { + completionHandler(.performDefaultHandling, nil) + return + } + + let credential = URLCredential(trust: serverTrust) + completionHandler(.useCredential, credential) + } + +} + +// MARK: - FileManager + +extension MockServiceTests { + + @discardableResult + func fileExists(_ filename: String) -> Bool { + FileManager.default.fileExists(atPath: PactFileManager.pactDirectoryPath + "/\(filename)") + } + + @discardableResult + func removeFile(_ filename: String) -> Bool { + if fileExists(filename) { + do { + try FileManager.default.removeItem(at: URL(fileURLWithPath: PactFileManager.pactDirectoryPath + "/\(filename)")) + return true + } catch { + debugPrint("Could not remove file \(PactFileManager.pactDirectoryPath + "/\(filename)")") + return false + } + } + return false + } + +} diff --git a/Tests/Services/MockServiceWithDirectoryPathTests.swift b/Tests/Services/MockServiceWithDirectoryPathTests.swift new file mode 100644 index 00000000..a3e4e6f7 --- /dev/null +++ b/Tests/Services/MockServiceWithDirectoryPathTests.swift @@ -0,0 +1,96 @@ +// +// Created by Marko Justinek on 9/9/21. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import XCTest + +@testable import PactSwift + +final class MockServiceWithDirectoryPathTests: XCTestCase { + + static private let expectedTargetDirectory = URL(fileURLWithPath: "/tmp/pacts/custom/path", isDirectory: true) + static private var mockService: MockService! + + let session = URLSession(configuration: .ephemeral) + + override class func setUp() { + mockService = MockService(consumer: "custom-dir-consumer", provider: "provider", writePactTo: MockServiceWithDirectoryPathTests.expectedTargetDirectory) + } + + override class func tearDown() { + XCTAssertTrue(FileManager.default.fileExists(atPath: "/tmp/pacts/custom/path/custom-dir-consumer-provider.json"), "Failed to write Pact contract to a custom directory path!") + + super.tearDown() + } + + // MARK: - Tests + + func testRunAPactTest() { + MockServiceWithDirectoryPathTests.mockService + .uponReceiving("a request for animals with children") + .given("animals have children") + .withRequest(method: .GET, path: "/animals") + .willRespondWith( + status: 200, + body: [ + "animals": Matcher.EachLike( + [ + "children": Matcher.EachLike( + Matcher.SomethingLike("Mary"), + min: 0 + ), + ] + ) + ] + ) + + MockServiceWithDirectoryPathTests.mockService.run { [unowned self] baseURL, completed in + let url = URL(string: "\(baseURL)/animals")! + session + .dataTask(with: url) { [unowned self] data, response, error in + guard + error == nil, + (response as? HTTPURLResponse)?.statusCode == 200 + else { + self.fail(function: #function, request: url.absoluteString, response: response.debugDescription, error: error) + return + } + // We don't care about the network response here, so we tell PactSwift we're done with the Pact test + // This is tested in `MockServiceTests.swift` + completed() + } + .resume() + } + } + +} + +// MARK: - Utilities + +private extension MockServiceWithDirectoryPathTests { + + func fail(function: String, request: String? = nil, response: String? = nil, error: Error? = nil) { + XCTFail( + """ + Expected network request to succeed in \(function)! + Request URL: \t\(String(describing: request)) + Response:\t\(String(describing: response)) + Reason: \t\(String(describing: error?.localizedDescription)) + """ + ) + } + +} diff --git a/Tests/Services/PFMockServiceTests.swift b/Tests/Services/PFMockServiceTests.swift new file mode 100644 index 00000000..1448350e --- /dev/null +++ b/Tests/Services/PFMockServiceTests.swift @@ -0,0 +1,161 @@ +// +// Created by Marko Justinek on 31/7/21. +// Copyright © 2021 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import XCTest + +@testable import PactSwift + +class PFMockServiceTests: XCTestCase { + + var testSubject: PFMockService! + var errorCapture: ErrorCapture! + + override func setUpWithError() throws { + try super.setUpWithError() + + errorCapture = ErrorCapture() + testSubject = PFMockService( + consumer: "pfpactswift-consumer", + provider: "pfpactswift-provider", + scheme: .standard, + merge: false + ) + } + + override func tearDownWithError() throws { + errorCapture = nil + testSubject = nil + + try super.tearDownWithError() + } + + // MARK: - Tests + func testSimpleGETRequest() { + testSubject + .uponReceiving("Request for a list") + .given("elements exist") + .withRequest(method: .GET, path: "/elements") + .willRespondWith(status: 200) + + let testExpectation = expectation(description: #function) + + testSubject.objCRun { baseURL, done in + let session = URLSession.shared + + session.dataTask(with: URL(string: "\(baseURL)/elements")!) { data, response, error in + if let response = response as? HTTPURLResponse { + XCTAssertEqual(200, response.statusCode) + done() + testExpectation.fulfill() + } else { + XCTFail("Expecting response code 200 in \(#function)") + } + } + .resume() + } + + waitForExpectations(timeout: 10) + } + + func testSimplePOSTRequestWithTimeout() { + testSubject + .uponReceiving("Request for a list") + .given("elements exist") + .withRequest(method: .POST, path: "/elements", body: ["name": Matcher.SomethingLike("John")]) + .willRespondWith(status: 201) + + let testExpectation = expectation(description: #function) + + testSubject.objCRun( + testFunction: { baseURL, done in + let session = URLSession.shared + var request = URLRequest(url: URL(string: "\(baseURL)/elements")!) + request.httpBody = Data("{\"name\":\"George\"}".utf8) + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpMethod = "POST" + + session + .dataTask(with: request) { data, response, error in + if let response = response as? HTTPURLResponse { + XCTAssertEqual(201, response.statusCode) + done() + testExpectation.fulfill() + } else { + XCTFail("Expecting response code 200 in \(#function)") + } + } + .resume() + }, + timeout: 2 + ) + waitForExpectations(timeout: 5) + } + + func testTwoRequestsInOneTest() { + let firstExpectation = expectation(description: "first") + let firstInteraction = testSubject + .uponReceiving("Request for a list") + .given("elements exist") + .withRequest(method: .POST, path: "/first", body: ["name": Matcher.SomethingLike("John")]) + .willRespondWith(status: 201) + + let secondExpectation = expectation(description: "second") + let secondInteraction = testSubject + .uponReceiving("Request for a list") + .given("elements exist") + .withRequest(method: .GET, path: "/second") + .willRespondWith(status: 200) + + testSubject.objCRun( + testFunction: { [unowned self] url, done in + let session = URLSession.shared + var request = URLRequest(url: URL(string: "\(url)/first")!) + request.httpBody = Data("{\"name\":\"George\"}".utf8) + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpMethod = "POST" + + session + .dataTask(with: request) { data, response, error in + if let response = response as? HTTPURLResponse { + XCTAssertEqual(201, response.statusCode) + firstExpectation.fulfill() + } else { + XCTFail("Expecting response code 200 in \(#function)") + } + } + .resume() + + let secondRequest = URLRequest(url: URL(string: "\(url)/second")!) + session + .dataTask(with: secondRequest) { data, response, error in + if let response = response as? HTTPURLResponse { + XCTAssertEqual(200, response.statusCode) + secondExpectation.fulfill() + } else { + XCTFail("Expecting response code 200 in \(#function)") + } + } + .resume() + + self.waitForExpectations(timeout: 10) { _ in done() } + }, + verify: [firstInteraction, secondInteraction], + timeout: 10 + ) + } + +} diff --git a/Tests/Services/PactContractTests.swift b/Tests/Services/PactContractTests.swift new file mode 100644 index 00000000..de96abc4 --- /dev/null +++ b/Tests/Services/PactContractTests.swift @@ -0,0 +1,737 @@ +// +// Created by Marko Justinek on 15/5/21. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import XCTest + +@testable import PactSwift + +private class MockServiceWrapper { + static let shared = MockServiceWrapper() + + let errorCapture = ErrorCapture() + let consumer = "sanity-consumer" + let provider = "sanity-provider" + + var mockService: MockService + + init() { + mockService = MockService(consumer: consumer, provider: provider, merge: false, errorReporter: errorCapture) + } + +} + +class PactContractTests: XCTestCase { + + var mockService = MockServiceWrapper.shared.mockService + + let session = URLSession(configuration: .ephemeral) + + static var errorCapture = MockServiceWrapper.shared.errorCapture + static let pactContractFileName = "\(MockServiceWrapper.shared.consumer)-\(MockServiceWrapper.shared.provider).json" + + // MARK: - Validate Pact contract at the end + + override class func setUp() { + super.setUp() + + // Remove any previously generated Pact contracts for this test case + PactContractTests.removeFile(pactContractFileName) + } + + override class func tearDown() { + do { + let fileContents = try XCTUnwrap(FileManager.default.contents(atPath: PactFileManager.pactDirectoryPath + "/" + self.pactContractFileName)) + let jsonObject = try JSONSerialization.jsonObject(with: fileContents, options: []) as! [String: Any] + + // Validate the final Pact contract file contains values that were specified in tests' expectations + + // MARK: - Validate Interactions + + let interactions = try XCTUnwrap(jsonObject["interactions"] as? [Any]) + let numOfExpectedInteractions = 11 + + assert( + interactions.count == numOfExpectedInteractions, + "Expected \(numOfExpectedInteractions) interactions in Pact contract but got \(interactions.count)!" + ) + + // MARK: - Validate Matchers for Interactions + + // Validate interaction "bug example" + let bugExampleInteraction = try PactContractTests.extract(.matchingRules, in: .response, interactions: interactions, description: "bug example") + // print("\nMATCHERS:\n\(matchersOne)") + let expectedMatchersOne = [ + "$.array_of_objects", + "$.array_of_objects[*].key_for_explicit_array[0]", + "$.array_of_objects[*].key_for_explicit_array[1]", + "$.array_of_objects[*].key_for_explicit_array[2]", + "$.array_of_objects[*].key_for_explicit_array[3]", + "$.array_of_objects[*].key_for_explicit_array[4]", + "$.array_of_objects[*].key_for_matcher_array", + "$.array_of_objects[*].key_for_matcher_array[*]", + "$.array_of_objects[*].key_int", + "$.array_of_objects[*].key_string", + "$.array_of_strings", + "$.array_of_strings[*]", + "$.includes_like", + ] + assert( + expectedMatchersOne.allSatisfy { expectedKey -> Bool in + bugExampleInteraction.contains { generatedKey, _ -> Bool in + expectedKey == generatedKey + } + }, + "Not all expected generators found in Pact contract file" + ) + + // Validate interaction "a request for roles with sub-roles" + let arrayOnRootInteraction = try PactContractTests.extract(.matchingRules, in: .response, interactions: interactions, description: "a request for roles with sub-roles") + let expectedNodesForArrayOnRoot = [ + "$[*].id" + ] + assert( + expectedNodesForArrayOnRoot.allSatisfy { expectedKey -> Bool in + arrayOnRootInteraction.contains { generatedKey, _ -> Bool in + expectedKey == generatedKey + } + }, + "Not all expected generators found in Pact contract file" + ) + + // Validate interaction "Request for animals with children" + let animalsWithChildrenInteraction = try PactContractTests.extract(.matchingRules, in: .response, interactions: interactions, description: "a request for animals with children") + let expectedNodesForAnimalsWithChildren = [ + "$.animals", + "$.animals[*].children", + "$.animals[*].children[*]", + ] + assert( + expectedNodesForAnimalsWithChildren.allSatisfy { expectedKey -> Bool in + animalsWithChildrenInteraction.contains { generatedKey, _ -> Bool in + expectedKey == generatedKey + } + } + ) + + // Validate interaction "Request for list of users in state" + let requestMatchers = try PactContractTests.extract(.matchingRules, in: .request, interactions: interactions, description: "Request for list of users in state") + let expectedMatchersTwo = [ + "$.foo" + ] + assert( + expectedMatchersTwo.allSatisfy { expectedKey -> Bool in + requestMatchers.contains { generatedKey, _ -> Bool in + expectedKey == generatedKey + } + } + , "Not all expected generators found in Pact contract file" + ) + + // Validate eachKeyLike matcher from interaction + let eachKeyLikeInteraction = try PactContractTests.extract(.matchingRules, in: .response, interactions: interactions, description: "Request for an object with wildcard matchers") + // print("\nMATCHERS:\n\(matchersOne)") + let expectedEachKeyLikePaths = [ + "$.articles", + "$.articles[*].variants.*", + "$.articles[*].variants.*.bundles.*", + "$.articles[*].variants.*.bundles.*.description", + "$.articles[*].variants.*.bundles.*.referencedArticles", + "$.articles[*].variants.*.bundles.*.referencedArticles[*].bundleId", + ] + assert( + expectedEachKeyLikePaths.allSatisfy { expectedKey -> Bool in + eachKeyLikeInteraction.contains { generatedKey, _ -> Bool in + expectedKey == generatedKey + } + }, + "Not all expected generators found in Pact contract file for eachKeyLike matcher" + ) + + // Validate eachKeyLike matcher from interaction + let eachKeyLikeSimplerInteraction = try PactContractTests.extract(.matchingRules, in: .response, interactions: interactions, description: "Request for a simpler object with wildcard matchers") + // print("\nMATCHERS:\n\(matchersOne)") + let expectedSimplerEachKeyLikePaths = [ + "$.*", + "$.*.field1", + "$.*.field2", + ] + assert( + expectedSimplerEachKeyLikePaths.allSatisfy { expectedKey -> Bool in + eachKeyLikeSimplerInteraction.contains { generatedKey, _ -> Bool in + expectedKey == generatedKey + } + }, + "Not all expected generators found in Pact contract file for eachKeyLike matcher" + ) + + // MARK: - Validate Generators + + let responseGenerators = try extract(.generators, in: .response, interactions: interactions, description: "Request for list of users") + let expectedGeneratorsType = [ + "$.array_of_arrays[*][2]": [ + "type": "Uuid", + "format": "upper-case-hyphenated" + ] + ] + + assert( + expectedGeneratorsType.allSatisfy { expectedKey, expectedValue -> Bool in + responseGenerators.contains { generatedKey, generatedValue -> Bool in + expectedKey == generatedKey + && expectedValue["type"] == (generatedValue as? [String: String])?["type"] + && expectedValue["format"] == (generatedValue as? [String: String])?["format"] + } + }, + "Not all expected generators found in Pact contract file" + ) + + // MARK: - Test two of same matchers used + + let twoMatchersTest = try PactContractTests.extract(.matchingRules, in: .response, interactions: interactions, description: "Request for a simple object") + let expectedTwoMatchers = [ + "$.identifier", + "$.group_identifier", + ] + + assert(expectedTwoMatchers.allSatisfy { expectedKey -> Bool in + twoMatchersTest.contains { generatedKey, _ -> Bool in + expectedKey == generatedKey + } + }, + "Not all expected matchers are found in Pact interaction" + ) + + } catch { + XCTFail(error.localizedDescription) + } + + super.tearDown() + } + + // MARK: - Tests that write the Pact contract + + func testBugExample() { + mockService + .uponReceiving("bug example") + .given("some state") + .withRequest(method: .GET, path: "/bugfix") + .willRespondWith( + status: 200, + body: [ + "array_of_objects": Matcher.EachLike( + [ + "key_string": Matcher.SomethingLike("String value"), + "key_int": Matcher.IntegerLike(123), + "key_for_matcher_array": Matcher.EachLike( + Matcher.SomethingLike("matcher_array_value") + ), + "key_for_explicit_array": [ + Matcher.SomethingLike("explicit_array_value_one"), + Matcher.SomethingLike(1), + Matcher.IntegerLike(936), + Matcher.DecimalLike(123.23), + Matcher.RegexLike(value: "2021-05-17", pattern: #"\d{4}-\d{2}-\d{2}"#), + Matcher.IncludesLike("in", "array", generate: "Included in explicit array") + ] as [Any], + "key_for_datetime_expression": ExampleGenerator.DateTimeExpression(expression: "today +1 day", format: "yyyy-MM-dd") + ] as [String : Any] + ), + "array_of_strings": Matcher.EachLike( + Matcher.SomethingLike("A string") + ), + "includes_like": Matcher.IncludesLike("included", generate: "Value _included_ is included in this string") + ] as [String : Any] + ) + + mockService.run { [unowned self] baseURL, completed in + let url = URL(string: "\(baseURL)/bugfix")! + self.session + .dataTask(with: url) { data, response, error in + guard + error == nil, + (response as? HTTPURLResponse)?.statusCode == 200 + else { + self.fail(function: #function, request: url.absoluteString, response: response.debugDescription, error: error) + return + } + // We don't care about the network response here, so we tell PactSwift we're done with the Pact test + // This is tested in `MockServiceTests.swift` + completed() + } + .resume() + } + } + + func testExample_AnimalsWithChildren() { + mockService + .uponReceiving("a request for animals with children") + .given("animals have children") + .withRequest(method: .GET, path: "/animals") + .willRespondWith( + status: 200, + body: [ + "animals": Matcher.EachLike( + [ + "children": Matcher.EachLike( + Matcher.SomethingLike("Mary"), + min: 0 + ), + ] + ) + ] + ) + + mockService.run { [unowned self] baseURL, completed in + let url = URL(string: "\(baseURL)/animals")! + session + .dataTask(with: url) { data, response, error in + guard + error == nil, + (response as? HTTPURLResponse)?.statusCode == 200 + else { + self.fail(function: #function, request: url.absoluteString, response: response.debugDescription, error: error) + return + } + // We don't care about the network response here, so we tell PactSwift we're done with the Pact test + // This is tested in `MockServiceTests.swift` + completed() + } + .resume() + } + } + + func testExample_AnimalsWithChildrenMultipleInteractionsInOneTest() { + let firstInteraction = mockService + .uponReceiving("a request for animals with children") + .given("animals have children") + .withRequest(method: .GET, path: "/animals1") + .willRespondWith( + status: 200, + body: [ + "animals": Matcher.EachLike( + [ + "children": Matcher.EachLike( + Matcher.SomethingLike("Mary"), + min: 0 + ), + ] + ) + ] + ) + + let secondInteraction = mockService + .uponReceiving("a request for animals with Children") + .given("animals have children") + .withRequest(method: .GET, path: "/animals2") + .willRespondWith( + status: 200, + body: [ + "animals": Matcher.EachLike( + [ + "children": Matcher.EachLike( + Matcher.SomethingLike("Mary"), + min: 0 + ), + ] + ) + ] + ) + + mockService.run(verify: [firstInteraction, secondInteraction]) { [unowned self] baseURL, completed in + let urlOne = URL(string: "\(baseURL)/animals1")! + let urlTwo = URL(string: "\(baseURL)/animals2")! + + let expOne = expectation(description: "one") + let expTwo = expectation(description: "two") + + session + .dataTask(with: urlOne) { data, response, error in + guard + error == nil, + (response as? HTTPURLResponse)?.statusCode == 200 + else { + self.fail(function: #function, request: urlOne.absoluteString, response: response.debugDescription, error: error) + return + } + // We don't care about the network response here, so we tell PactSwift we're done with the Pact test + // This is tested in `MockServiceTests.swift` + expOne.fulfill() + } + .resume() + + session + .dataTask(with: urlTwo) { data, response, error in + guard + error == nil, + (response as? HTTPURLResponse)?.statusCode == 200 + else { + self.fail(function: #function, request: urlOne.absoluteString, response: response.debugDescription, error: error) + return + } + // We don't care about the network response here, so we tell PactSwift we're done with the Pact test + // This is tested in `MockServiceTests.swift` + expTwo.fulfill() + } + .resume() + + waitForExpectations(timeout: 5) { _ in + completed() + } + } + } + + func testExample_ArrayOnRoot() { + mockService + .uponReceiving("a request for roles with sub-roles") + .given("roles have sub-rules") + .withRequest(method: .GET, path: "/roles") + .willRespondWith( + status: 200, + body: + Matcher.EachLike( + [ + "id": Matcher.RegexLike( + value: "1234abcd-1234-abcf-12ab-abcdef1234567", + pattern: #"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"# + ) + ] + ) + ) + + mockService.run { [unowned self] baseURL, completed in + let url = URL(string: "\(baseURL)/roles")! + session + .dataTask(with: url) { data, response, error in + guard + error == nil, + (response as? HTTPURLResponse)?.statusCode == 200 + else { + self.fail(function: #function, request: url.absoluteString, response: response.debugDescription, error: error) + return + } + // We don't care about the network response here, so we tell PactSwift we're done with the Pact test + // This is tested in `MockServiceTests.swift` + completed() + } + .resume() + } + } + + func testPactContract_WritesMatchersAndGenerators() { + mockService + .uponReceiving("Request for list of users") + .given("users exist") + .withRequest(method: .GET, path: "/users") + .willRespondWith( + status: 200, + body: [ + "foo": Matcher.SomethingLike("bar"), + "baz": Matcher.EachLike(123, min: 1, max: 5, count: 3), + "array_of_arrays": Matcher.EachLike( + [ + Matcher.SomethingLike("array_value"), + Matcher.RegexLike(value: "2021-05-15", pattern: #"\d{4}-\d{2}-\d{2}"#), + ExampleGenerator.RandomUUID(format: .uppercaseHyphenated), + Matcher.EachLike( + [ + "3rd_level_nested": Matcher.EachLike(Matcher.IntegerLike(369), count: 2) + ] + ) + ] as [Any] + ), + "regex_array": Matcher.EachLike( + [ + "regex_key": Matcher.EachLike( + Matcher.RegexLike(value: "1235", pattern: #"\d{4}"#), + min: 2 + ), + "regex_nested_object": Matcher.EachLike( + [ + "regex_nested_key": Matcher.RegexLike(value: "12345678", pattern: #"\d{8}"#) + ] + ) + ] + ), + "enum_value": Matcher.OneOf("night", "morning", "mid-day", "afternoon", "evening") + ] as [String : Any] + ) + + mockService.run { [unowned self] baseURL, completed in + let url = URL(string: "\(baseURL)/users")! + session + .dataTask(with: url) { data, response, error in + guard + error == nil, + (response as? HTTPURLResponse)?.statusCode == 200 + else { + self.fail(function: #function, request: url.absoluteString, response: response.debugDescription, error: error) + return + } + // We don't care about the network response here, so we tell PactSwift we're done with the Pact test + // This is tested in `MockServiceTests.swift` + completed() + } + .resume() + } + } + + func testPactContract_ArrayAsRoot() { + mockService + .uponReceiving("Request for an explicit array") + .given("array exist") + .withRequest( + method: .GET, + path: Matcher.RegexLike(value: "/arrays/explicit", pattern: #"^/arrays/e\w+$"#) + ) + .willRespondWith( + status: 200, + body: + [ + [ + "id": Matcher.SomethingLike(19231421) + ], + [ + "id": Matcher.SomethingLike(49817231) + ] + ] + ) + + mockService.run { [unowned self] baseURL, completed in + let url = URL(string: "\(baseURL)/arrays/explicit")! + session + .dataTask(with: url) { data, response, error in + guard + error == nil, + (response as? HTTPURLResponse)?.statusCode == 200 + else { + self.fail(function: #function, request: url.absoluteString, response: response.debugDescription, error: error) + return + } + // We don't care about the network response here, so we tell PactSwift we're done with the Pact test + // This is tested in `MockServiceTests.swift` + completed() + } + .resume() + } + } + + func testPactContract_WithMatcherInRequestBody() { + mockService + .uponReceiving("Request for list of users in state") + .given("users in that state exist") + .withRequest( + method: .POST, + path: Matcher.FromProviderState(parameter: "/users/state/${stateIdentifier}", value: .string("/users/state/nsw")), + body: ["foo": Matcher.SomethingLike("bar")] + ) + .willRespondWith( + status: 200, + body: [ + "identifier": Matcher.FromProviderState(parameter: "userId", value: .int(100)), + "randomCode": Matcher.FromProviderState(parameter: "rndCode", value: .string("some-random-code")), + "foo": Matcher.SomethingLike("bar"), + "baz": Matcher.SomethingLike("qux") + ] as [String : Any] + ) + + mockService.run { [unowned self] baseURL, completed in + var request = URLRequest(url: URL(string: "\(baseURL)/users/state/nsw")!) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpBody = #"{"foo": "bar"}"#.data(using: .utf8) + + session + .dataTask(with: request) { _, response, error in + guard + error == nil, + (response as? HTTPURLResponse)?.statusCode == 200 + else { + self.fail(function: #function, request: request.debugDescription, response: response.debugDescription, error: error) + return + } + // We don't care about the network response here, so we tell PactSwift we're done with the Pact test + // This is tested in `MockServiceTests.swift` + completed() + } + .resume() + } + } + + func testPactContract_WithTwoMatchersOfSameType() { + mockService + .uponReceiving("Request for a simple object") + .given("data exists") + .withRequest(method: .GET, path: "/users/data") + .willRespondWith( + status: 200, + body: [ + "identifier": Matcher.SomethingLike(1), + "group_identifier": Matcher.SomethingLike(1) + ] + ) + + mockService.run { [unowned self] baseURL, completed in + let url = URL(string: "\(baseURL)/users/data")! + session + .dataTask(with: url) { data, response, error in + guard + error == nil, + (response as? HTTPURLResponse)?.statusCode == 200 + else { + self.fail(function: #function, request: url.absoluteString, response: response.debugDescription, error: error) + return + } + // We don't care about the network response here, so we tell PactSwift we're done with the Pact test + // This is tested in `MockServiceTests.swift` + completed() + } + .resume() + } + } + + func testPactContract_WithEachKeyLikeMatcher() { + mockService + .uponReceiving("Request for an object with wildcard matchers") + .given("keys in response itself are ignored") + .withRequest(method: .GET, path: "/articles/nested/keyLikeMatcher") + .willRespondWith( + status: 200, + body: [ + "articles": Matcher.EachLike( + [ + "variants": [ + "001": Matcher.EachKeyLike([ + "bundles": [ + "001-A": Matcher.EachKeyLike([ + "description": Matcher.SomethingLike("someDescription"), + "referencedArticles": Matcher.EachLike([ + "bundleId": Matcher.SomethingLike("someId") + ]) + ] as [String : Any]) + ] + ]) + ] + ] + ) + ] + ) + + mockService.run { [unowned self] baseURL, done in + let url = URL(string: "\(baseURL)/articles/nested/keyLikeMatcher")! + session + .dataTask(with: url) { data, response, error in + guard + error == nil, + (response as? HTTPURLResponse)?.statusCode == 200 + else { + self.fail(function: #function, request: url.absoluteString, response: response.debugDescription, error: error) + return + } + done() + } + .resume() + } + } + + func testPactContract_WithSimplerEachKeyLikeMatcher() { + mockService + .uponReceiving("Request for a simpler object with wildcard matchers") + .given("keys in response itself are ignored") + .withRequest(method: .GET, path: "/articles/simpler/keyLikeMatcher") + .willRespondWith( + status: 200, + body: [ + "abc": Matcher.EachKeyLike([ + "field1": Matcher.SomethingLike("value1"), + "field2": Matcher.IntegerLike(123) + ] as [String : Any]), + "xyz": Matcher.EachKeyLike([ + "field1": Matcher.SomethingLike("value2"), + "field2": Matcher.IntegerLike(456) + ] as [String : Any]) + ] + ) + + mockService.run { [unowned self] baseURL, done in + let url = URL(string: "\(baseURL)/articles/simpler/keyLikeMatcher")! + session + .dataTask(with: url) { data, response, error in + guard + error == nil, + (response as? HTTPURLResponse)?.statusCode == 200 + else { + self.fail(function: #function, request: url.absoluteString, response: response.debugDescription, error: error) + return + } + done() + } + .resume() + } + } + +} + +private extension PactContractTests { + + enum PactNode: String { + case matchingRules + case generators + } + + enum Direction: String { + case request + case response + } + + func fail(function: String, request: String? = nil, response: String? = nil, error: Error? = nil) { + XCTFail( + """ + Expected network request to succeed in \(function)! + Request URL: \t\(String(describing: request)) + Response:\t\(String(describing: response)) + Reason: \t\(String(describing: error?.localizedDescription)) + """ + ) + } + + static func extract(_ type: PactNode, in direction: Direction, interactions: [Any], description: String) throws -> [String: Any] { + let interaction = interactions.first { interaction -> Bool in + (interaction as! [String: Any])["description"] as! String == description + } + return try XCTUnwrap((((interaction as? [String: Any])?[direction.rawValue] as? [String: Any])?[type.rawValue] as? [String: Any])?["body"] as? [String: Any]) + } + + @discardableResult + static func fileExists(_ filename: String) -> Bool { + FileManager.default.fileExists(atPath: PactFileManager.pactDirectoryPath + "/\(filename)") + } + + @discardableResult + static func removeFile(_ filename: String) -> Bool { + if fileExists(filename) { + do { + try FileManager.default.removeItem(at: URL(fileURLWithPath: PactFileManager.pactDirectoryPath + "/\(filename)")) + return true + } catch { + debugPrint("Could not remove file \(PactFileManager.pactDirectoryPath + "/\(filename)")") + return false + } + } + return false + } + +} diff --git a/Tests/Services/ProviderVerifierTests.swift b/Tests/Services/ProviderVerifierTests.swift new file mode 100644 index 00000000..c361dfad --- /dev/null +++ b/Tests/Services/ProviderVerifierTests.swift @@ -0,0 +1,132 @@ +// +// Created by Marko Justinek on 19/8/21. +// Copyright © 2021 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import XCTest + +@testable import PactSwift + +#if compiler(>=5.5) +@_implementationOnly import PactSwiftMockServer +#else +import PactSwiftMockServer +#endif + +final class ProviderVerifierTests: XCTestCase { + + var errorReporter: ErrorCapture! + var mockVerifier: ProviderVerifying! + var testSubject: ProviderVerifier! + + override func setUpWithError() throws { + try super.setUpWithError() + + errorReporter = ErrorCapture() + mockVerifier = MockVerifier() + testSubject = ProviderVerifier(verifier: mockVerifier, errorReporter: errorReporter) + } + + override func tearDownWithError() throws { + errorReporter = nil + mockVerifier = nil + testSubject = nil + + try super.tearDownWithError() + } + + // MARK: - Tests + + func testVerifyProviderReturnsSuccess() { + let testOptions = ProviderVerifier.Options( + provider: .init(port: 1234), + pactsSource: .directories(["/tmp/pacts"]) + ) + + guard case .success = testSubject.verify(options: testOptions) else { + XCTFail("Expected verification to succeed!") + return + } + } + + func testVerifyProviderReturnsError() throws { + let testOptions = ProviderVerifier.Options( + provider: .init(port: 1234), + pactsSource: .directories(["/tmp/pacts"]) + ) + + let mockVerifier = MockVerifier { .failure(ProviderVerificationError.invalidArguments) } + let testSubject = ProviderVerifier(verifier: mockVerifier, errorReporter: errorReporter) + + guard case .failure = testSubject.verify(options: testOptions) else { + XCTFail("Expected verification to fail!") + return + } + + let expectedError = try XCTUnwrap(errorReporter.error?.message) + XCTAssertEqual(expectedError, "Provider Verification Error: Invalid arguments were provided to the verification process.") + } + + func testVerifyingProviderTriggersCompletionBlock() { + let testOptions = ProviderVerifier.Options( + provider: .init(port: 1234), + pactsSource: .directories(["/tmp/pacts"]) + ) + + let testExp = expectation(description: "Completion block on succcessful verification") + testSubject.verify(options: testOptions) { + testExp.fulfill() + } + + waitForExpectations(timeout: 0.1) + } + + func testVerifyingProviderFailureTriggersCompletionBlock() throws { + let testOptions = ProviderVerifier.Options( + provider: .init(port: 1234), + pactsSource: .directories(["/tmp/pacts"]) + ) + + let testExp = expectation(description: "Completion block on failed verification") + let mockVerifier = MockVerifier { .failure(ProviderVerificationError.verificationFailed) } + let testSubject = ProviderVerifier(verifier: mockVerifier, errorReporter: errorReporter) + testSubject.verify(options: testOptions, completionBlock: { + testExp.fulfill() + }) + + let expectedError = try XCTUnwrap(errorReporter.error?.message) + XCTAssertEqual(expectedError, "Provider Verification Error: The verification process failed, see output for errors.") + waitForExpectations(timeout: 0.1) + } + +} + +// MARK: - Mocks + +private class MockVerifier: ProviderVerifying { + + typealias VerifyProviderHandler = () -> Result + + let verifyProviderHandler: VerifyProviderHandler? + + init(verifyProviderHandler: VerifyProviderHandler? = nil) { + self.verifyProviderHandler = verifyProviderHandler + } + + func verifyProvider(options args: String) -> Result { + verifyProviderHandler?() ?? .success(true) + } + +} diff --git a/Tests/TestHelpers/DateHelper.swift b/Tests/TestHelpers/DateHelper.swift new file mode 100644 index 00000000..35ae7eb1 --- /dev/null +++ b/Tests/TestHelpers/DateHelper.swift @@ -0,0 +1,40 @@ +// +// Created by Marko Justinek on 17/9/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +enum DateHelper { + + static func dateFrom(isoString: String, isoFormat: ISO8601DateFormatter.Options) -> Date? { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = isoFormat + return formatter.date(from: isoString) + } + + static func dateFrom(string: String, format: String) -> Date? { + let formatter = DateFormatter() + formatter.dateFormat = format + return formatter.date(from: string) + } + + static func stringFrom(date: Date, format: String) -> String { + let formatter = DateFormatter() + formatter.dateFormat = format + return formatter.string(from: date) + } + +} diff --git a/Tests/TestHelpers/ErrorCapture.swift b/Tests/TestHelpers/ErrorCapture.swift new file mode 100644 index 00000000..c010f0ae --- /dev/null +++ b/Tests/TestHelpers/ErrorCapture.swift @@ -0,0 +1,42 @@ +// +// Created by Marko Justinek on 20/4/20. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +@testable import PactSwift + +struct ErrorReceived { + + var message: String + var file: FileString? + var line: UInt? + +} + +class ErrorCapture: ErrorReportable { + + public var error: ErrorReceived? + + func reportFailure(_ message: String) { + self.error = ErrorReceived(message: message, file: nil, line: nil) + } + + func reportFailure(_ message: String, file: FileString, line: UInt) { + self.error = ErrorReceived(message: message, file: file, line: line) + } + +} diff --git a/Tests/TestHelpers/ExampleGeneratorTestHelpers.swift b/Tests/TestHelpers/ExampleGeneratorTestHelpers.swift new file mode 100644 index 00000000..25c95a9f --- /dev/null +++ b/Tests/TestHelpers/ExampleGeneratorTestHelpers.swift @@ -0,0 +1,44 @@ +// +// Created by Marko Justinek on 25/10/21. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation +@testable import PactSwift + +enum ExampleGeneratorTestHelpers { + + /// Encodes a model containing `AnyEncodable` type and decodes the value into String? or Int? type + static func encodeDecode(_ model: [String: AnyEncodable]) throws -> DecodableTypeModel { + let data = try JSONEncoder().encode(EncodableModel(params: model).params) + return try JSONDecoder().decode(DecodableTypeModel.self, from: data) + } + + struct EncodableModel: Encodable { + let params: [String: AnyEncodable] + } + + struct DecodableTypeModel: Decodable { + let expression: String? + let digits: Int? + let format: String? + let max: Int? + let min: Int? + let regex: String? + let size: Int? + let type: String? + } + +} diff --git a/Tests/TestHelpers/MatcherTestHelpers.swift b/Tests/TestHelpers/MatcherTestHelpers.swift new file mode 100644 index 00000000..ac6f39dd --- /dev/null +++ b/Tests/TestHelpers/MatcherTestHelpers.swift @@ -0,0 +1,41 @@ +// +// Created by Marko Justinek on 25/10/21. +// Copyright © 2020 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation +@testable import PactSwift + +enum MatcherTestHelpers { + + /// Encodes a model containing `AnyEncodable` type and decodes the value into String type + static func encodeDecode(_ model: [[String: AnyEncodable]]) throws -> [DecodableModel] { + let data = try JSONEncoder().encode(EncodableModel(params: model).params) + return try JSONDecoder().decode([DecodableModel].self, from: data) + } + + struct EncodableModel: Encodable { + let params: [[String: AnyEncodable]] + } + + struct DecodableModel: Decodable { + let match: String + let min: Int? + let max: Int? + let regex: String? + let value: String? + } + +} diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000..1a5b9d53 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,30 @@ +codecov: + notify: + require_ci_to_pass: yes + +coverage: + precision: 2 + round: down + range: "75...100" + + status: + project: yes + patch: yes + changes: no + + ignore: + - "Tests/**/*" + - "Tests" + +parsers: + gcov: + branch_detection: + conditional: yes + loop: yes + method: no + macro: no + +comment: + layout: "header, diff" + behavior: default + require_changes: no