diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 759c17e1..1c21fe22 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,44 +23,21 @@ jobs: with: ruby-version: ${{ matrix.ruby_version }} bundler-cache: true - env: - BUNDLE_WITH: v2 - uses: pact-foundation/pact-cli@main - run: pact plugin install --yes https://github.com/mefellows/pact-matt-plugin/releases/tag/v0.1.1 - name: Test Pact-Ruby Specs run: "bundle exec rake" + - name: Test Pact-Ruby Consumer Tests + run: "bundle exec rake pact:spec" + - name: Test Pact-Ruby Provider Verification + run: "bundle exec rake pact:verify" - name: Test Pact-Ruby Zoo App Specs run: "bundle install && bundle exec rake spec" - if: matrix.ruby_version > '3.0' working-directory: example/zoo-app - - name: Test Pact-Ruby Animal Service Specs + - name: Test Pact-Ruby v2 Animal Service Specs run: "bundle install && bundle exec rake pact:verify" - if: matrix.os != 'windows-latest' && matrix.ruby_version > '3.0' + if: matrix.os != 'windows-latest' working-directory: example/animal-service - - name: Test Pact-Ruby v2 spec:v2 - run: "bundle exec rake spec:v2" - env: - BUNDLE_WITH: v2 - - name: Test Pact-Ruby v2 pact:v2:spec - run: "bundle exec rake pact:v2:spec" - env: - BUNDLE_WITH: v2 - - name: Test Pact-Ruby v2 pact:v2:verify - run: "bundle exec rake pact:v2:verify" - env: - BUNDLE_WITH: v2 - - name: Test Pact-Ruby v2 Zoo App Specs - run: "bundle install && bundle exec rake spec:v2" - if: matrix.ruby_version > '3.0' - working-directory: example/zoo-app-v2 - env: - BUNDLE_WITH: v2 - - name: Test Pact-Ruby v2 Animal Service Specs - run: "bundle install && bundle exec rake pact:v2:verify" - if: matrix.os != 'windows-latest' && matrix.ruby_version > '3.0' - working-directory: example/animal-service-v2 - env: - BUNDLE_WITH: v2 test-with-rack-2: runs-on: ${{ matrix.os }} @@ -75,52 +52,12 @@ jobs: with: ruby-version: ${{ matrix.ruby_version }} bundler-cache: true - env: - BUNDLE_WITH: v2 - uses: pact-foundation/pact-cli@main - run: pact plugin install --yes https://github.com/mefellows/pact-matt-plugin/releases/tag/v0.1.1 - run: "bundle exec appraisal install" - env: - BUNDLE_WITH: v2 - run: "bundle exec appraisal rack-2 rake" - - run: "bundle exec appraisal rack-2 rake spec:v2" - env: - BUNDLE_WITH: v2 - - name: Test Mixed Pacts (Http/Kafaka/Grpc) - Pact-Ruby v2 - run: "bundle exec appraisal rack-2 rake pact:v2:spec" - env: - BUNDLE_WITH: v2 - - name: Verify Mixed Pacts (Http/Kafaka/Grpc) - Pact-Ruby v2 - run: "bundle exec appraisal rack-2 rake pact:v2:verify" - if: matrix.os != 'windows-latest' && matrix.ruby_version > '3.0' - env: - BUNDLE_WITH: v2 - - test-with-active-support: - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - ruby_version: ["3.2", "3.3", "3.4"] - os: ["ubuntu-latest","windows-latest","macos-latest"] - defaults: - run: - shell: bash - steps: - - uses: actions/checkout@v5 - - uses: ruby/setup-ruby@v1 - with: - ruby-version: ${{ matrix.ruby_version }} - bundler-cache: true - env: - BUNDLE_WITH: v2 - - run: bundle install - - run: "bundle exec appraisal install" - name: "install active support - pact-ruby" - - run: "bundle exec appraisal activesupport rake spec_with_active_support" - name: "test with active support - pact-ruby" - - run: "bundle exec rake spec:v2" - name: "test with active support - pact-ruby v2" - env: - LOAD_ACTIVE_SUPPORT: 'true' - BUNDLE_WITH: v2 + - name: Test Mixed Pacts (Http/Kafaka/Grpc) + run: "bundle exec appraisal rack-2 rake pact:spec" + - name: Verify Mixed Pacts (Http/Kafaka/Grpc) + run: "bundle exec appraisal rack-2 rake pact:verify" + if: matrix.os != 'windows-latest' \ No newline at end of file diff --git a/.rspec b/.rspec index 5f164763..dd192030 100644 --- a/.rspec +++ b/.rspec @@ -1,2 +1,4 @@ --color --format progress +--require spec_helper +--require rails_helper \ No newline at end of file diff --git a/.rspec_v2 b/.rspec_v2 deleted file mode 100644 index db0725f6..00000000 --- a/.rspec_v2 +++ /dev/null @@ -1,4 +0,0 @@ ---color ---format progress ---require spec_helper_v2 ---require rails_helper_v2 \ No newline at end of file diff --git a/Gemfile b/Gemfile index f209ed30..a09c2ed7 100644 --- a/Gemfile +++ b/Gemfile @@ -3,32 +3,26 @@ source 'https://rubygems.org' # Specify your gem's dependencies in pact.gemspec gemspec -gem "appraisal", "~> 2.5" +gem 'appraisal', '~> 2.5' if ENV['X_PACT_DEVELOPMENT'] - gem "pact-support", path: '../pact-support' - gem "pact-mock_service", path: '../pact-mock_service' - gem "pry-byebug" -end - -group :v2, optional: true do - gem "pact-ffi", "~> 0.4.28" - gem "ffi" + gem 'pact-ffi', path: '../pact-ruby-ffi' + gem 'pry-byebug' end group :local_development do - gem "pry-byebug" + gem 'pry-byebug' end group :test do gem 'faraday', '~>2.0', '<3.0' gem 'faraday-retry', '~>2.0' gem 'rackup' - gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw] + gem 'tzinfo-data', platforms: %i[windows] end -if RUBY_VERSION >= "3.4" - gem "csv" - gem "mutex_m" - gem "base64" +if RUBY_VERSION >= '3.4' + gem 'base64' + gem 'csv' + gem 'mutex_m' end diff --git a/README.md b/README.md index 0ef22c4b..cc92d3a0 100644 --- a/README.md +++ b/README.md @@ -1,367 +1,560 @@ -# Pact -![Test](https://github.com/pact-foundation/pact-ruby/workflows/Test/badge.svg) - [![Backers on Open Collective](https://opencollective.com/pact-foundation/backers/badge.svg)](#backers) - [![Sponsors on Open Collective](https://opencollective.com/pact-foundation/sponsors/badge.svg)](#sponsors) +# Pact Ruby -Define a pact between service consumers and providers, enabling "consumer driven contract" testing. +`pact-ruby v2+` implements support for the latest versions of Pact specifications: -Pact provides a fluent API for service consumers to define the HTTP requests they will make to a service provider and the HTTP responses they expect back. These expectations are used in the consumer specs to provide a mock service provider. The interactions are recorded, and played back in the service provider specs to ensure the service provider actually does provide the response the consumer expects. +- It's based on pact-ffi and pact-ruby-ffi +- It provides a convenient DSL, simplifying the writing of contract tests in Ruby/RSpec +- Writing contract tests with HTTP transports +- Writing contract tests with non-HTTP transports (for example, gRPC) +- Writing contract tests for async messages (Kafka, etc.) +- Verifying contract tests for HTTP/non-HTTP/async message transport + - V4 specification supports mixed pact interactions in a single file. -This allows testing of both sides of an integration point using fast unit tests. +## Architecture -This gem is inspired by the concept of "Consumer driven contracts". See [this article](http://martinfowler.com/articles/consumerDrivenContracts.html) by Ian Robinson for more information. +![Pact tests architecture](./pact-arch.png) -## What is it good for? - -Pact is most valuable for designing and testing integrations where you (or your team/organisation/partner organisation) control the development of both the consumer and the provider, and the requirements of the consumer are going to be used to drive the features of the provider. It is fantastic tool for developing and testing intra-organisation microservices. - -## What is it not good for? - -* Testing new or existing providers where the functionality is not being driven by the needs of the consumer (eg. public APIs) -* Testing providers where the consumer and provider teams do not have good communication channels. -* Performance and load testing. -* Functional testing of the provider - that is what the provider's own tests should do. Pact is about checking the contents and format of requests and responses. -* Situations where you cannot load data into the provider without using the API that you're actually testing (eg. public APIs). [Why?][pact-public-apis] -* Testing "pass through" APIs, where the provider merely passes on the request contents to a downstream service without validating them. [Why?][pass-through-apis] - -## Features - -* A service is mocked using an actual process running on a specified port, so javascript clients can be tested as easily as backend clients. -* "Provider states" (similar to fixtures) allow the same request to be made with a different expected response. -* Consumers specify only the fields they are interested in, allowing a provider to return more fields without breaking the pact. This allows a provider to have a different pact with a different consumer, and know which fields each cares about in a given response. -* RSpec and Minitest support for the service consumer codebase. -* Rake tasks allow pacts to be verified against a service provider codebase. -* Different versions of a consumer/provider pairs can be easily tested against each other, allowing confidence when deploying new versions of each (see the [pact_broker][pact_broker] and [pact_broker-client][pact_broker-client] gems). -* Autogenerated API documentation - need we say more? -* Autogenerated network diagrams with the [Pact Broker](https://github.com/pact-foundation/pact_broker) - -## How does it work? - -1. In the specs for the provider facing code in the consumer project, expectations are set up on a mock service provider. -1. When the specs are run, the mock service returns the expected responses. The requests, and their expected responses, are then written to a "pact" file. -1. The requests in the pact file are later replayed against the provider, and the actual responses are checked to make sure they match the expected responses. - -![Pact explanation diagram](documentation/pact_two_parts.png) - -## Why is developing and testing with Pact better than using traditional system integration tests? - -* Faster execution. -* Reliable responses from mock service reduce likelihood of flakey tests. -* Causes of failure are easier to identify as only one component is being tested at a time. -* Design of service provider is improved by considering first how the data is actually going to be used, rather than how it is most easily retrieved and serialised. -* No separate integration environment required for automated integration tests - pact tests run in standalone CI builds. -* Integration flows that would traditionally require running multiple services at the same time can be broken down and each integration point tested separately. - -## Getting help - -* Pact docs: [docs.pact.io](http://docs.pact.io) -* Ruby Pact wiki: [github.com/pact-foundation/pact-ruby/wiki](https://github.com/pact-foundation/pact-ruby/wiki) -* Slack: [slack.pact.io](http://slack.pact.io) -* Stackoverflow: [ruby pact questions](https://stackoverflow.com/questions/tagged/pact-ruby) or [general pact questions](https://stackoverflow.com/questions/tagged/pact) -* Twitter: [@pact_up](https://twitter.com/pact_up) +- DSL - implementation of RSpec-DSL for convenient writing of Pact tests +- Matchers - implementation of Pact matchers, which are convenient helpers used in consumer-DSL, encapsulating all the logic for serialization into Pact format +- Mock servers - mock servers that allow for correct execution of provider tests ## Installation -Add this line to your application's Gemfile: - - gem 'pact' - # gem 'pact-consumer-minitest' for minitest - -And then execute: - - $ bundle - -Or install it yourself as: - - $ gem install pact - -## Usage - an example scenario - -We're going to write an integration, with Pact tests, between a consumer, the Zoo App, and its provider, the Animal Service. In the Consumer project, we're going to need a model (the Alligator class) to represent the data returned from the Animal Service, and a client (the AnimalServiceClient) which will be responsible for making the HTTP calls to the Animal Service. - -![Example](example/zoo_app-animal_service.png) - -### In the Zoo App (consumer) project +The `pact/v2` namespace was introduced in `pact-ruby` v1.67.0 and moved to `pact` in v2 -#### 1. Start with your model +It introduces a suite of new depedencies, including a reliance on the `pact-ffi` and `ffi` gems. -Imagine a model class that looks something like this. The attributes for an Alligator live on a remote server, and will need to be retrieved by an HTTP call to the Animal Service. - -```ruby -class Alligator - attr_reader :name - - def initialize name - @name = name - end - - def == other - other.is_a?(Alligator) && other.name == name - end -end +```rb + gem "pact" ``` -#### 2. Create a skeleton Animal Service client class - -Imagine an Animal Service client class that looks something like this. - -```ruby -require 'httparty' +`pact-ffi` ships prebuilt binary gems, and does not support platforms outside of the released [pact_ffi](https://github.com/pact-foundation/pact-reference/tree/master/rust/pact_ffi) libraries -class AnimalServiceClient - include HTTParty - base_uri 'http://animal-service.com' +| Version | Platform | +|-----------|----------------------| +| 0.4.28.0 | x86_64-darwin | +| 0.4.28.0 | arm64-darwin | +| 0.4.28.0 | x86_64-linux | +| 0.4.28.0 | aarch64-linux | +| 0.4.28.0 | x86_64-linux-musl | +| 0.4.28.0 | aarch64-linux-musl | +| 0.4.28.0 | x64-mingw32 | +| 0.4.28.0 | x64-mingw-ucrt | - def get_alligator - # Yet to be implemented because we're doing Test First Development... - end -end -``` -#### 3. Configure the mock Animal Service +If you require a pure ruby gem, you are advised to pin to v1. -The following code will create a mock service on localhost:1234 which will respond to your application's queries over HTTP as if it were the real "Animal Service" app. It also creates a mock provider object which you will use to set up your expectations. The method name to access the mock service provider will be what ever name you give as the service argument - in this case "animal_service" +## Usage -```ruby -# In /spec/service_providers/pact_helper.rb +For each type of interaction (due to their specific features), a separate version of DSL has been implemented. However, the general principles remain the same for each type of interaction. -require 'pact/consumer/rspec' -# or require 'pact/consumer/minitest' if you are using Minitest +Place your consumer tests under -Pact.service_consumer "Zoo App" do - has_pact_with "Animal Service" do - mock_service :animal_service do - port 1234 - host "..." # optional, defaults to "localhost" - end - end -end -``` +`spec/pact/provider/**` -#### 4. Write a failing spec for the Animal Service client +**it's not an error: consumer tests contain `providers` subdirectory (because we're testing against different providers)** ```ruby -# In /spec/service_providers/animal_service_client_spec.rb - -# When using RSpec, use the metadata `:pact => true` to include all the pact functionality in your spec. -# When using Minitest, include Pact::Consumer::Minitest in your spec. - -describe AnimalServiceClient, :pact => true do - before do - # Configure your client to point to the stub service on localhost using the port you have specified - AnimalServiceClient.base_uri 'localhost:1234' - end - - subject { AnimalServiceClient.new } - - describe "get_alligator" do - - before do - animal_service.given("an alligator exists"). - upon_receiving("a request for an alligator"). - with(method: :get, path: '/alligator', query: ''). - will_respond_with( - status: 200, - headers: {'Content-Type' => 'application/json'}, - body: {name: 'Betty'} ) - end - - it "returns an alligator" do - expect(subject.get_alligator).to eq(Alligator.new('Betty')) +# Declaration of a consumer test, always include the :pact tag +# This is used in CI/CD pipelines to separate Pact tests from other RSpec tests +# Pact tests are not run as part of the general RSpec pipeline +RSpec.describe "SomePactConsumerTestForAnyTransport", :pact do + # declaration of the type of interaction - here we determine which consumer and provider interact on which transport + has_http_pact_between "CONSUMER-NAME", "PROVIDER-NAME" + # or + has_grpc_pact_between "CONSUMER-NAME", "PROVIDER-NAME" + # or + has_message_pact_between "CONSUMER-NAME", "PROVIDER-NAME" + + # the context for one of the interactions, for example GET /api/v2/stores + context "with GET /api/v2/stores" do + let(:interaction) do + # creating a new interaction - within which we describe the contract + new_interaction + # if you need to save any metadata for subsequent use by the test provider, + # for example, specify the entity ID that will need to be moved to the database in the test provider + # we use the provider states, see more at https://docs.pact.io/getting_started/provider_states + .given("UNIQUE PROVIDER STATE", key1: value1, key2: value2) + # the description of the interaction, used for identification inside the package binding, + # is optional in some cases, but it is recommended to always specify + .upon_receiving("UNIQUE INTERACTION DESCRIPTION") + # the description of the request using the matchers + # the name and parameters of the method differ for different transports + .with_request(...) + # the description of the response using the matchers + # the name and parameters of the method differ for different transports + .will_respond_with(...) + # further, there are differences for different types of transports, + # for more information, see the relevant sections of the documentation + end + + it "executes the pact test without errors" do | mock_server | + interaction.execute do + # the url of the started mock server, you should pass this into your api client in the next step + mock_server_url = mock_server.url + # here our client is called for the API being tested + # in this context, the client can be: http client, grpc client, kafka consumer + expect(make_request).to be_success + end + end end - end -end -``` - -#### 5. Run the specs - -Running the AnimalServiceClient spec will generate a pact file in the configured pact dir (`spec/pacts` by default). -Logs will be output to the configured log dir (`log` by default) that can be useful when diagnosing problems. - -Of course, the above specs will fail because the Animal Service client method is not implemented, so next, implement your provider client methods. - -#### 6. Implement the Animal Service client consumer methods - -```ruby -class AnimalServiceClient - include HTTParty - base_uri 'http://animal-service.com' - - def get_alligator - name = JSON.parse(self.class.get("/alligator").body)['name'] - Alligator.new(name) - end -end ``` -#### 7. Run the specs again. +Common DSL Methods: -Green! You now have a pact file that can be used to verify your expectations of the Animal Service provider project. +- `new_interaction` - initializes a new interaction +- `given` - allows specifying a provider state with or without parameters, for more details see +- `upon_receiving` - allows specifying the name of the interaction -Now, rinse and repeat for other likely status codes that may be returned. For example, consider how you want your client to respond to a: -* 404 (return null, or raise an error?) -* 500 (specifying that the response body should contain an error message, and ensuring that your client logs that error message will make your life much easier when things go wrong) -* 401/403 if there is authorisation. +Multiple interactions can be declared within a single rspec example, in order to call the mock server -### In the Animal Service (provider) project +- `execute_http_pact`: Use this instead of `interaction.execute` -#### 1. Create the skeleton API classes +### HTTP consumers -Create your API class using the framework of your choice (the Pact authors have a preference for [Webmachine][webmachine] and [Roar][roar]) - leave the methods unimplemented, we're doing Test First Develoment, remember? +Specific DSL methods: -#### 2. Tell your provider that it needs to honour the pact file you made earlier +- `with_request({method: string, path: string, headers: kv_hash, body: kv_hash})` - request definition +- `will_respond_with({status: int, headers: kv_hash, body: kv_hash})` - response definition -Require "pact/tasks" in your Rakefile. - -```ruby -# In Rakefile -require 'pact/tasks' -``` +More at [http_client_spec.rb](../spec/pact/providers/pact-ruby-test-app/http_client_spec.rb) -Create a `pact_helper.rb` in your service provider project. The recommended place is `spec/service_consumers/pact_helper.rb`. +### gRPC consumers -See [Verifying Pacts](https://github.com/pact-foundation/pact-ruby/wiki/Verifying-pacts) and the [Provider](documentation/configuration.md#provider) section of the Configuration documentation for more information. +Specific DSL methods: -```ruby -# In spec/service_consumers/pact_helper.rb +- `with_service(PROTO_PATH, RPC_SERVICE_AND_ACTION)` - specifies the contract used, PROTO_PATH is relative from the app root +- `with_request(request_kv_hash)` - request definition +- `will_respond_with(response_kv_hash)` - response definition -require 'pact/provider/rspec' +More at [grpc_client_spec.rb](../spec/pact/providers/pact-ruby-test-app/grpc_client_spec.rb) -Pact.service_provider "Animal Service" do +### Message consumers - honours_pact_with 'Zoo App' do +Specific DSL methods: - # This example points to a local file, however, on a real project with a continuous - # integration box, you would use a [Pact Broker](https://github.com/pact-foundation/pact_broker) or publish your pacts as artifacts, - # and point the pact_uri to the pact published by the last successful build. +- `with_headers(kv_hash)` - message-headers definition; you can use matchers +- `with_metadata(kv_hash)` - message-metadata definition (special keys are `key` and `topic`, where, respectively, you can specify the matchers for the partitioning key and the topic - pact_uri '../zoo-app/spec/pacts/zoo_app-animal_service.json' - end -end -``` +Next, the specifics are one of two options for describing the format: -#### 3. Run your failing specs +**JSON** (to describe a message in a JSON representation): - $ rake pact:verify +- `with_json_contents(kv_hash)` - message format definition -Congratulations! You now have a failing spec to develop against. +**PROTO** (to describe the message in the protobuf view): -At this stage, you'll want to be able to run your specs one at a time while you implement each feature. At the bottom of the failed pact:verify output you will see the commands to rerun each failed interaction individually. A command to run just one interaction will look like this: +- `with_proto_class(PROTO_PATH, PROTO_MESSAGE_NAME)` - specifies the contract used, PROTO_PATH is relative to the root, PROTO_MESSAGE_NAME is the name of the message used from the proto file +- `with_proto_contents(kv_hash)` - message format definition - $ rake pact:verify PACT_DESCRIPTION="a request for an alligator" PACT_PROVIDER_STATE="an alligator exists" +More at [message_spec.rb](../spec/pact/providers/pact-ruby-test-app/message.spec.rb) -#### 4. Implement enough to make your first interaction spec pass +### Kafka consumers -Rinse and repeat. +Specific DSL methods: -#### 5. Keep going til you're green +- `with_headers(kv_hash)` - message-headers definition; you can use matchers +- `with_metadata(kv_hash)` - message-metadata definition (special keys are `key` and `topic`, where, respectively, you can specify the matchers for the partitioning key and the topic -Yay! Your Animal Service provider now honours the pact it has with your Zoo App consumer. You can now have confidence that your consumer and provider will play nicely together. +Next, the specifics are one of two options for describing the format: -### Using provider states +**JSON** (to describe a message in a JSON representation): -Each interaction in a pact is verified in isolation, with no context maintained from the previous interactions. So how do you test a request that requires data to already exist on the provider? Read about provider states [here](https://github.com/pact-foundation/pact-ruby/wiki/Provider-states). +- `with_json_contents(kv_hash)` - message format definition -## Configuration +**PROTO** (to describe the message in the protobuf view): -See the [Configuration](/documentation/configuration.md) section of the documentation for options relating to thing like logging, diff formatting, and documentation generation. +- `with_proto_class(PROTO_PATH, PROTO_MESSAGE_NAME)` - specifies the contract used, PROTO_PATH is relative to the root, PROTO_MESSAGE_NAME is the name of the message used from the proto file +- `with_proto_contents(kv_hash)` - message format definition -## Pact best practices +More at [kafka_spec.rb](../spec/pact/providers/pact-ruby-test-app/kafka_spec.rb) -As in all things, there are good ways to implement Pacts, and there are not so good ways. There are also some Pact [GOTCHAS][gotchas] to beware of! Check out the [Best practices](https://github.com/pact-foundation/pact-ruby/wiki/Best-practices) section of the documentation to make sure you're not Pacting it Wrong. +Requires the following gems, to use this wrapper -## Current Pact specification version +- sbmt-kafka_consumer +- sbmt-kafka_provider -The `pact-ruby-v1` is in maintenance mode, as there has been a transition to rust-core, which is intended to be used through FFI in non-Rust stacks. +### Matchers -Pact Ruby V1 supports writing Pacts in v2, and verifying Pacts in v3 format, HOWEVER it only supports the rules that were defined in v2 (`like` and `term`). +Matchers are special helper methods that allow you to define rules for matching request/response parameters at the level of the pact manifest. +The matchers are described in the [Pact specifications](https://github.com/pact-foundation/pact-specification). In this gem, the matchers are implemented as RSpec helpers. -For Pact v3 specification features and above, please see [pact-ruby-v2](/documentation/README_V2.md) documentation. +For details of the implementation, see [matchers.rb](../lib/pact/matchers.rb) -## Docs +- `match_exactly(sample)` - match the exact value specified in the sample +- `match_type_of(sample)` - match the data type (integer, string, boolean) specified in the sample +- `match_include(sample)` - match a substring +- `match_any_string(sample)` - match any string, because of the peculiarities, null and empty strings will also be matched here +- `match_any_integer(sample)` - match any integer +- `match_any_decimal(sample)` - match any float/double +- `match_any_number(sample)` - match any integer/float/double +- `match_any_boolean(sample)` - match any true/false +- `match_uuid(sample)` - match any UUID (`match_regex` is used under the hood) +- `match_regex(regex, sample)` - match by regexp +- `match_datetime(format, sample)` - match any datetime +- `match_iso8601(sample)` - match datetime in ISO8601 (the matcher does not fully comply with ISO8601, matches only the most common variants, `match_regex` is used under the hood) +- `match_date(format, sample)` - match any date (rust datetime) +- `match_time(format, sample)` - match any time (rust datetime) +- `match_each(template)` - match all the elements of the array according to the specified template, you can use it for nested elements +- `match_each_regex(regex, sample)` - match all array elements by regex, used for arrays with string elements +- `match_each_key(template, key_matchers)` - match each hash key according to the specified template +- `match_each_value(template)` - match each hash value according to the specified template, can be used for nested elements +- `match_each_kv(template, key_matchers)` - match all the keys/values of Hash according to the specified template and key_matchers, can be used for nested elements -* [Example](example) -* [Configuration](documentation/configuration.md) -* [Terminology](https://github.com/pact-foundation/pact-ruby/wiki/Terminology) -* [Provider States](https://github.com/pact-foundation/pact-ruby/wiki/Provider-states) -* [Verifying pacts](https://github.com/pact-foundation/pact-ruby/wiki/Verifying-pacts) -* [Sharing pacts between consumer and provider](https://github.com/pact-foundation/pact-ruby/wiki/Sharing-pacts-between-consumer-and-provider) -* [Regular expressions and type matching with Pact](https://github.com/pact-foundation/pact-ruby/wiki/Regular-expressions-and-type-matching-with-Pact) -* [Frequently asked questions](https://github.com/pact-foundation/pact-ruby/wiki/FAQ) -* [Rarely asked questions](https://github.com/pact-foundation/pact-ruby/wiki/RAQ) -* [Best practices](https://github.com/pact-foundation/pact-ruby/wiki/Best-practices) -* [Troubleshooting](https://github.com/pact-foundation/pact-ruby/wiki/Troubleshooting) -* [Testing with pact diagram](https://github.com/pact-foundation/pact-ruby/wiki/Testing%20with%20pact.png) -* [News, blogs, videos and articles](https://github.com/pact-foundation/pact-ruby/wiki/News,-blogs,-videos-and-articles) +See the different uses of the matchers in [matchers_spec.rb](../spec/pact/matchers_spec.rb) -## Related libraries +### Generators -[Pact Provider Proxy](https://github.com/pact-foundation/pact-provider-proxy) - Verify a pact against a running server, allowing you to use pacts with a provider of any language. +Generators are helper methods that allow you to specify dynamic values in your contract tests. These values are generated at runtime, making your contracts more flexible and robust. Below are the available generator methods: -[Pact Broker](https://github.com/pact-foundation/pact_broker) - A pact repository. Provides endpoints to access published pacts, meaning you don't need to use messy CI URLs in your codebase. Enables cross testing of prod/head versions of your consumer and provider, allowing you to determine whether the head version of one is compatible with the production version of the other. Helps you to answer that ever so important question, "can I deploy without breaking all the things?" +For details of the implementation, see [matchers.rb](../lib/pact/generators.rb) -[Pact Broker Client](https://github.com/pact-foundation/pact_broker-client) - Contains rake tasks for publishing pacts to the pact_broker. +- `generate_random_int(min:, max:)` - Generates a random integer between the specified `min` and `max`. +- `generate_random_decimal(digits:)` - Generates a random decimal number with the specified number of `digits`. +- `generate_random_hexadecimal(digits:)` - Generates a random hexadecimal string with the specified number of `digits`. +- `generate_random_string(size:)` - Generates a random string of the specified `size`. +- `generate_uuid(example: nil)` - Generates a random UUID. Optionally, provide an `example` value. +- `generate_date(format: nil, example: nil)` - Generates a date string in the specified `format`. Optionally, provide an `example`. +- `generate_time(format: nil)` - Generates a time string in the specified `format`. +- `generate_datetime(format: nil)` - Generates a datetime string in the specified `format`. +- `generate_random_boolean` - Generates a random boolean value (`true` or `false`). +- `generate_from_provider_state(expression:, example:)` - Generates a value from the provider state using the given `expression` and `example` value. Allows templating of url and query paths with values only know at provider verification time. +- `generate_mock_server_url(regex: nil, example: nil)` - Generates a mock server URL. Optionally, specify a `regex` matches and/or an `example` value. -[Shokkenki](https://github.com/brentsnook/shokkenki) - Another Consumer Driven Contract gem written by one of Pact's original authors, Brent Snook. Shokkenki allows matchers to be composed using jsonpath expressions and allows auto-generation of mock response values based on regular expressions. +These generators can be used in your DSL definitions to provide dynamic values for requests, responses, or messages in your contract tests. -[A list of Pact implementations in other languages](https://github.com/pact-foundation/pact-ruby/wiki#implementations-in-other-languages) - JVM, .Net, Javascript and Swift +#### Generator Examples -## Links +```rb + .with_request( + method: :get, + path: generate_from_provider_state( + expression: '/alligators/${alligator_name}', + example: '/alligators/Mary'), + headers: headers) -[Simplifying microservices testing with pacts](http://dius.com.au/2014/05/19/simplifying-micro-service-testing-with-pacts/) - Ron Holshausen (one of the original pact authors) +... -[Pact specification](https://github.com/pact-foundation/pact-specification) + body: { + _links: { + :'pf:publish-provider-contract' => { + href: generate_mock_server_url( + regex: ".*(\\/provider-contracts\\/provider\\/.*\\/publish)$", + example: "/provider-contracts/provider/{provider}/publish" + ), + boolean: generate_random_boolean, + integer: generate_random_int(min: 1, max: 100), + decimal: generate_random_decimal(digits: 2), + hexidecimal: generate_random_hexadecimal(digits: 8), + string: generate_random_string(size: 10), + uuid: generate_uuid, + date: generate_date(format: "yyyyy.MMMMM.dd GGG"), + time: generate_time(), + datetime: generate_datetime(format: "%Y-%m-%dT%H:%M:%S%z") + } + } + } +``` -[Integrated tests are a scam](https://vimeo.com/80533536) - J.B. Rainsberger +## Provider verification + +Place your provider verification file under + +`spec/pact/consumers/**` + +**it's not an error: provider tests contain `consumers` subdirectory (because we're verifying against different consumer)** + +### Provider verification options + +```rb + @provider_name = provider_name + @log_level = opts[:log_level] || :info + @pact_dir = opts[:pact_dir] || nil + @provider_setup_port = opts[:provider_setup_port] || 9001 + @pact_proxy_port = opts[:pact_proxy_port] || 9002 + @pact_uri = ENV.fetch("PACT_URL", nil) || opts.fetch(:pact_uri, nil) + @publish_verification_results = ENV.fetch("PACT_PUBLISH_VERIFICATION_RESULTS", nil) == "true" || opts.fetch(:publish_verification_results, false) + @provider_version = ENV.fetch("PACT_PROVIDER_VERSION", nil) || opts.fetch(:provider_version, nil) + @provider_build_uri = ENV.fetch("PACT_PROVIDER_BUILD_URL", nil) || opts.fetch(:provider_build_uri, nil) + @provider_version_branch = ENV.fetch("PACT_PROVIDER_BRANCH", nil) || opts.fetch(:provider_version_branch, nil) + @provider_version_tags = ENV.fetch("PACT_PROVIDER_VERSION_TAGS", nil) || opts.fetch(:provider_version_tags, []) + @consumer_version_tags = ENV.fetch("PACT_CONSUMER_VERSION_TAGS", nil) || opts.fetch(:consumer_version_tags, []) + @consumer_version_selectors = ENV.fetch("PACT_CONSUMER_VERSION_SELECTORS", nil) || opts.fetch(:consumer_version_selectors, nil) + @enable_pending = ENV.fetch("PACT_VERIFIER_ENABLE_PENDING", nil) == "true" || opts.fetch(:enable_pending, false) + @include_wip_pacts_since = ENV.fetch("PACT_INCLUDE_WIP_PACTS_SINCE", nil) || opts.fetch(:include_wip_pacts_since, nil) + @fail_if_no_pacts_found = ENV.fetch("PACT_FAIL_IF_NO_PACTS_FOUND", nil) == "true" || opts.fetch(:fail_if_no_pacts_found, true) + @consumer_branch = ENV.fetch("PACT_CONSUMER_BRANCH", nil) || opts.fetch(:consumer_branch, nil) + @consumer_version = ENV.fetch("PACT_CONSUMER_VERSION", nil) || opts.fetch(:consumer_version, nil) + @consumer_name = opts[:consumer_name] + @broker_url = ENV.fetch("PACT_BROKER_BASE_URL", nil) || opts.fetch(:broker_url, nil) + @broker_username = ENV.fetch("PACT_BROKER_USERNAME", nil) || opts.fetch(:broker_username, nil) + @broker_password = ENV.fetch("PACT_BROKER_PASSWORD", nil) || opts.fetch(:broker_password, nil) + @broker_token = ENV.fetch("PACT_BROKER_TOKEN", nil) || opts.fetch(:broker_token, nil) + @verify_only = [ENV.fetch("PACT_CONSUMER_FULL_NAME", nil)].compact || opts.fetch(:verify_only, []) +``` -[Consumer Driven Contracts](http://martinfowler.com/articles/consumerDrivenContracts.html) - Ian Robinson +### Single transport providers + +```rb +# frozen_string_literal: true + +require "pact_broker" +require "pact_broker/app" +require "rspec/mocks" +include RSpec::Mocks::ExampleMethods +require_relative "../../service_consumers/hal_relation_proxy_app" + +PactBroker.configuration.base_urls = ["http://example.org"] + +pact_broker = PactBroker::App.new { |c| c.database_connection = PactBroker::TestDatabase.connection_for_test_database } +app_to_verify = HalRelationProxyApp.new(pact_broker) + +require "pact" +require "pact/rspec" +require_relative "../../service_consumers/shared_provider_states" +RSpec.describe "Verify consumers for Pact Broker", :pact do + + http_pact_provider "Pact Broker", opts: { + + # rails apps should be automatically detected + # if you need to configure your own app, you can do so here + + app: app_to_verify, + # start rackup with a different port. Useful if you already have something + # running on the default port *9292* + http_port: 9393, + + # Set the log level, default is :info + + log_level: :info, + + fail_if_no_pacts_found: true, + + # Pact Sources + + # 1. Local pacts from a directory + + # Default is pacts directory in the current working directory + # pact_dir: File.expand_path('../../../../consumer/spec/internal/pacts', __dir__), + + # 2. Broker based pacts + + # Broker credentials + + # broker_username: "pact_workshop", # can be set via PACT_BROKER_USERNAME env var + # broker_password: "pact_workshop", # can be set via PACT_BROKER_PASSWORD env var + # broker_token: "pact_workshop", # can be set via PACT_BROKER_TOKEN env var + + # Remote pact via a uri, traditionally triggered via webhooks + # when a pact that requires verification is published + + # 2a. Webhook triggered pacts + # Can be a local file or a remote URL + # Most used via webhooks + # Can be set via PACT_URL env var + # pact_uri: File.expand_path("../../../pacts/pact.json", __dir__), + pact_uri: "https://raw.githubusercontent.com/YOU54F/pact_broker-client/refs/heads/feat/pact-ruby/spec/pacts/Pact%20Broker%20Client%20V2-Pact%20Broker.json", + # pact_uri: "https://raw.githubusercontent.com/YOU54F/pact_broker-client/refs/heads/feat/pact-ruby/spec/pacts/pact_broker_client-pact_broker.json", + # pact_uri: "http://localhost:9292/pacts/provider/Pact%20Broker/consumer/Pact%20Broker%20Client/version/96532124f3a53a499276c69ff2df785b8377588e", + + # 2b. Dynamically fetched pacts from broker + + # i. Set the broker url + # broker_url: "http://localhost:9292", # can be set via PACT_BROKER_URL env var + + # ii. Set the consumer version selectors + # Consumer version selectors + # The pact broker will return the following pacts by default, if no selectors are specified + # For the recommended setup, you dont _actually_ need to specify these selectors in ruby + # consumer_version_selectors: [{"deployedOrReleased" => true},{"mainBranch" => true},{"matchingBranch" => true}], + + # iii. Set additional dynamic selection verification options + # additional dynamic selection verification options + enable_pending: true, + include_wip_pacts_since: "2021-01-01", + + # Publish verification results to the broker + publish_verification_results: ENV["PACT_PUBLISH_VERIFICATION_RESULTS"] == "true", + provider_version: `git rev-parse HEAD`.strip, + provider_version_branch: `git rev-parse --abbrev-ref HEAD`.strip, + provider_version_tags: [`git rev-parse --abbrev-ref HEAD`.strip], + # provider_build_uri: "YOUR CI URL HERE - must be a valid url", + + } + + before_state_setup do + PactBroker::TestDatabase.truncate + end -[Integration Contract Tests](http://martinfowler.com/bliki/IntegrationContractTest.html) - Martin Fowler + after_state_teardown do + PactBroker::TestDatabase.truncate + end -## Roadmap + shared_provider_states + +end +``` -See [ROADMAP.md](/ROADMAP.md). +### Multiple transport providers + +You may have a consumer pact which consumes multiple transport protocols, if they are using pact specification v4. + +In order to validate an entire pact in a single test run, you will need to configure each transport as appropriate. + +```rb +# frozen_string_literal: true + +require "pact/rspec" + +RSpec.describe "Pact::Consumers::Http", :pact do + mixed_pact_provider "pact-test-app", opts: { + http: { + http_port: 3000, + log_level: :info, + pact_dir: File.expand_path('../../pacts', __dir__), + }, + grpc: { + grpc_port: 3009 + }, + async: { + message_handlers: { + # "pet message as json" => proc do |provider_state| + # pet_id = provider_state.dig("params", "pet_id") + # with_pact_producer { |client| PetJsonProducer.new(client: client).call(pet_id) } + # end, + # "pet message as proto" => proc do |provider_state| + # pet_id = provider_state.dig("params", "pet_id") + # with_pact_producer { |client| PetProtoProducer.new(client: client).call(pet_id) } + # end + } + } + } + + handle_message "pet message as json" do |provider_state| + pet_id = provider_state.dig("params", "pet_id") + with_pact_producer { |client| PetJsonProducer.new(client: client).call(pet_id) } + end -## Contributing + handle_message "pet message as proto" do |provider_state| + pet_id = provider_state.dig("params", "pet_id") + with_pact_producer { |client| PetProtoProducer.new(client: client).call(pet_id) } + end + +end -See [CONTRIBUTING.md](/CONTRIBUTING.md). +``` -[webmachine]: https://github.com/webmachine/webmachine-ruby -[roar]: https://github.com/apotonick/roar -[pact_broker]: https://github.com/pact-foundation/pact_broker -[pact_broker-client]: https://github.com/pact-foundation/pact_broker-client -[pact-public-apis]: https://github.com/pact-foundation/pact-ruby/wiki/Why-Pact-may-not-be-the-best-tool-for-testing-public-APIs -[pass-through-apis]: https://github.com/pact-foundation/pact-ruby/wiki/Why-Pact-may-not-be-the-best-tool-for-testing-pass-through-APIs -[gotchas]: https://github.com/pact-foundation/pact-ruby/wiki/Matching-gotchas +## Development & Test -## Contributors +### Setup -This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)]. - +```shell +bundle install +``` +### Run unit tests -## Backers +```shell +bundle exec rake spec +``` -Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/pact-foundation#backer)] +### Run pact tests - +The Pact tests are not run within the general rspec pipeline, they need to be run separately, see below +#### Consumer tests -## Sponsors +```shell +bundle exec rspec -t pact spec/pact/providers/**/*_spec.rb +or +bundle exec rake pact:spec +``` -Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/pact-foundation#sponsor)] +**NOTE** If you have never run it, you need to run it at least once to generate the pact files that will be used in provider tests (below) - - - - - - - - - - +#### Provider tests +```shell +bundle exec rspec -t pact spec/pact/consumers/*_spec.rb +or +bundle exec rake pact:spec +``` +## Examples + +### Migration + +1. add `gem "pact-ffi", "~> 0.4.28"` to Gemfile, or Gemspec +2. pact ruby v2 uses activesupport classes, so you may need to add + 1. `gem 'combustion'` to load active support during tests + 1. add a pact helper to load it + + ```rb + require "combustion" + begin + Combustion.initialize! :action_controller do + config.log_level = :fatal if ENV["LOG"].to_s.empty? + end + rescue => e + # Fail fast if application couldn't be loaded + warn "💥 Failed to load the app: #{e.message}\n#{e.backtrace.join("\n")}" + exit(1) + end + ``` + +3. Add a new rake task + + - require your helper file created above + - add a tag, we will use `pact` to namespace away from our existing `pact` tagged tests + + ```rb + RSpec::Core::RakeTask.new('spec') do |task| + task.pattern = 'spec/pact/providers/**/*_spec.rb' + task.rspec_opts = ['-t pact', '--require rails_helper'] + end + ``` + +4. File paths have moved for consumer tests, and provider verification tasks + + - consumer test location + 1. pact v1 `spec/service_providers` + 2. pact v2 - `spec/pact/providers` + - provider verification location + 1. pact v1 `spec/service_consumers` + 2. pact v2 - `spec/pact/consumers` + +The following projects were designed for pact-ruby-v1 and have been migrated to pact-ruby. They can serve as an example of the work required. + +- pact broker client + - v2 +- pact broker + - v2 +- animal service + - v1 [example/animal-service](../example/animal-service/) + - v2 [example/animal-service-v2](../example/animal-service-v2/) +- zoo app + - v1 [example/zoo-app](../example/zoo-app/) + - v2 [example/zoo-app-v2](../example/zoo-app-v2/) +- message consumer/provider + - v1 + - v2 +- e2e http consumer/provider + - + - Plus http, message, grpc & mixed consumer & provider examples + +### Demos + +The demos are stored in this codebase for regression test, but exist as standalone in https://github.com/pact-foundation/pact-ruby-e2e-example + +- http consumer [http_client_spec.rb](../spec/pact/providers/pact-ruby-test-app/http_client_spec.rb) +- kafka consumer with pact-ruby wrapper [kafka_spec.rb](../spec/pact/providers/pact-ruby-test-app/kafka_spec.rb) +- message consumer [message_spec.rb](../spec/pact/providers/pact-ruby-test-app/message_spec.rb) +- plugin consumer http [plugin_matt_http_spec.rb](../spec/pact/providers/pact-ruby-test-app/plugin_matt_http_spec.rb) +- plugin consumer http [plugin_matt_sync_message_spec.rb](../spec/pact/providers/pact-ruby-test-app/plugin_matt_sync_message_spec.rb) +- plugin consumer http [plugin_matt_async_message_spec.rb](../spec/pact/providers/pact-ruby-test-app/plugin_matt_async_message_spec.rb) +- plugin consumer http [plugin_matt_http_spec.rb](../spec/pact/providers/pact-ruby-test-app/plugin_matt_http_spec.rb) +- grpc consumer with pact-ruby wrapper [grpc_client_spec.rb](../spec/pact/providers/pact-ruby-test-app/grpc_client_spec.rb) +- grpc consumer using direct plugin interface [plugin_grpc_sync_message_spec.rb](../spec/pact/providers/pact-ruby-test-app/plugin_grpc_sync_message_spec.rb) +- mixed(http/kafka/grpc) provider [multi_spec.rb](../spec/pact/consumers/multi_spec.rb) diff --git a/README_OLD.md b/README_OLD.md new file mode 100644 index 00000000..1ec32987 --- /dev/null +++ b/README_OLD.md @@ -0,0 +1,363 @@ +# Pact +![Test](https://github.com/pact-foundation/pact-ruby/workflows/Test/badge.svg) + [![Backers on Open Collective](https://opencollective.com/pact-foundation/backers/badge.svg)](#backers) + [![Sponsors on Open Collective](https://opencollective.com/pact-foundation/sponsors/badge.svg)](#sponsors) + +Define a pact between service consumers and providers, enabling "consumer driven contract" testing. + +Pact provides a fluent API for service consumers to define the HTTP requests they will make to a service provider and the HTTP responses they expect back. These expectations are used in the consumer specs to provide a mock service provider. The interactions are recorded, and played back in the service provider specs to ensure the service provider actually does provide the response the consumer expects. + +This allows testing of both sides of an integration point using fast unit tests. + +This gem is inspired by the concept of "Consumer driven contracts". See [this article](http://martinfowler.com/articles/consumerDrivenContracts.html) by Ian Robinson for more information. + +## What is it good for? + +Pact is most valuable for designing and testing integrations where you (or your team/organisation/partner organisation) control the development of both the consumer and the provider, and the requirements of the consumer are going to be used to drive the features of the provider. It is fantastic tool for developing and testing intra-organisation microservices. + +## What is it not good for? + +* Testing new or existing providers where the functionality is not being driven by the needs of the consumer (eg. public APIs) +* Testing providers where the consumer and provider teams do not have good communication channels. +* Performance and load testing. +* Functional testing of the provider - that is what the provider's own tests should do. Pact is about checking the contents and format of requests and responses. +* Situations where you cannot load data into the provider without using the API that you're actually testing (eg. public APIs). [Why?][pact-public-apis] +* Testing "pass through" APIs, where the provider merely passes on the request contents to a downstream service without validating them. [Why?][pass-through-apis] + +## Features + +* A service is mocked using an actual process running on a specified port, so javascript clients can be tested as easily as backend clients. +* "Provider states" (similar to fixtures) allow the same request to be made with a different expected response. +* Consumers specify only the fields they are interested in, allowing a provider to return more fields without breaking the pact. This allows a provider to have a different pact with a different consumer, and know which fields each cares about in a given response. +* RSpec and Minitest support for the service consumer codebase. +* Rake tasks allow pacts to be verified against a service provider codebase. +* Different versions of a consumer/provider pairs can be easily tested against each other, allowing confidence when deploying new versions of each (see the [pact_broker][pact_broker] and [pact_broker-client][pact_broker-client] gems). +* Autogenerated API documentation - need we say more? +* Autogenerated network diagrams with the [Pact Broker](https://github.com/pact-foundation/pact_broker) + +## How does it work? + +1. In the specs for the provider facing code in the consumer project, expectations are set up on a mock service provider. +1. When the specs are run, the mock service returns the expected responses. The requests, and their expected responses, are then written to a "pact" file. +1. The requests in the pact file are later replayed against the provider, and the actual responses are checked to make sure they match the expected responses. + +![Pact explanation diagram](documentation/pact_two_parts.png) + +## Why is developing and testing with Pact better than using traditional system integration tests? + +* Faster execution. +* Reliable responses from mock service reduce likelihood of flakey tests. +* Causes of failure are easier to identify as only one component is being tested at a time. +* Design of service provider is improved by considering first how the data is actually going to be used, rather than how it is most easily retrieved and serialised. +* No separate integration environment required for automated integration tests - pact tests run in standalone CI builds. +* Integration flows that would traditionally require running multiple services at the same time can be broken down and each integration point tested separately. + +## Getting help + +* Pact docs: [docs.pact.io](http://docs.pact.io) +* Ruby Pact wiki: [github.com/pact-foundation/pact-ruby/wiki](https://github.com/pact-foundation/pact-ruby/wiki) +* Slack: [slack.pact.io](http://slack.pact.io) +* Stackoverflow: [ruby pact questions](https://stackoverflow.com/questions/tagged/pact-ruby) or [general pact questions](https://stackoverflow.com/questions/tagged/pact) +* Twitter: [@pact_up](https://twitter.com/pact_up) + +## Installation + +Add this line to your application's Gemfile: + + gem 'pact' + # gem 'pact-consumer-minitest' for minitest + +And then execute: + + $ bundle + +Or install it yourself as: + + $ gem install pact + +## Usage - an example scenario + +We're going to write an integration, with Pact tests, between a consumer, the Zoo App, and its provider, the Animal Service. In the Consumer project, we're going to need a model (the Alligator class) to represent the data returned from the Animal Service, and a client (the AnimalServiceClient) which will be responsible for making the HTTP calls to the Animal Service. + +![Example](example/zoo_app-animal_service.png) + +### In the Zoo App (consumer) project + +#### 1. Start with your model + +Imagine a model class that looks something like this. The attributes for an Alligator live on a remote server, and will need to be retrieved by an HTTP call to the Animal Service. + +```ruby +class Alligator + attr_reader :name + + def initialize name + @name = name + end + + def == other + other.is_a?(Alligator) && other.name == name + end +end +``` + +#### 2. Create a skeleton Animal Service client class + +Imagine an Animal Service client class that looks something like this. + +```ruby +require 'httparty' + +class AnimalServiceClient + include HTTParty + base_uri 'http://animal-service.com' + + def get_alligator + # Yet to be implemented because we're doing Test First Development... + end +end +``` +#### 3. Configure the mock Animal Service + +The following code will create a mock service on localhost:1234 which will respond to your application's queries over HTTP as if it were the real "Animal Service" app. It also creates a mock provider object which you will use to set up your expectations. The method name to access the mock service provider will be what ever name you give as the service argument - in this case "animal_service" + +```ruby +# In /spec/service_providers/pact_helper.rb + +require 'pact/consumer/rspec' +# or require 'pact/consumer/minitest' if you are using Minitest + +Pact.service_consumer "Zoo App" do + has_pact_with "Animal Service" do + mock_service :animal_service do + port 1234 + host "..." # optional, defaults to "localhost" + end + end +end +``` + +#### 4. Write a failing spec for the Animal Service client + +```ruby +# In /spec/service_providers/animal_service_client_spec.rb + +# When using RSpec, use the metadata `:pact => true` to include all the pact functionality in your spec. +# When using Minitest, include Pact::Consumer::Minitest in your spec. + +describe AnimalServiceClient, :pact => true do + + before do + # Configure your client to point to the stub service on localhost using the port you have specified + AnimalServiceClient.base_uri 'localhost:1234' + end + + subject { AnimalServiceClient.new } + + describe "get_alligator" do + + before do + animal_service.given("an alligator exists"). + upon_receiving("a request for an alligator"). + with(method: :get, path: '/alligator', query: ''). + will_respond_with( + status: 200, + headers: {'Content-Type' => 'application/json'}, + body: {name: 'Betty'} ) + end + + it "returns an alligator" do + expect(subject.get_alligator).to eq(Alligator.new('Betty')) + end + + end + +end +``` + +#### 5. Run the specs + +Running the AnimalServiceClient spec will generate a pact file in the configured pact dir (`spec/pacts` by default). +Logs will be output to the configured log dir (`log` by default) that can be useful when diagnosing problems. + +Of course, the above specs will fail because the Animal Service client method is not implemented, so next, implement your provider client methods. + +#### 6. Implement the Animal Service client consumer methods + +```ruby +class AnimalServiceClient + include HTTParty + base_uri 'http://animal-service.com' + + def get_alligator + name = JSON.parse(self.class.get("/alligator").body)['name'] + Alligator.new(name) + end +end +``` + +#### 7. Run the specs again. + +Green! You now have a pact file that can be used to verify your expectations of the Animal Service provider project. + +Now, rinse and repeat for other likely status codes that may be returned. For example, consider how you want your client to respond to a: +* 404 (return null, or raise an error?) +* 500 (specifying that the response body should contain an error message, and ensuring that your client logs that error message will make your life much easier when things go wrong) +* 401/403 if there is authorisation. + +### In the Animal Service (provider) project + +#### 1. Create the skeleton API classes + +Create your API class using the framework of your choice (the Pact authors have a preference for [Webmachine][webmachine] and [Roar][roar]) - leave the methods unimplemented, we're doing Test First Develoment, remember? + +#### 2. Tell your provider that it needs to honour the pact file you made earlier + +Require "pact/tasks" in your Rakefile. + +```ruby +# In Rakefile +require 'pact/tasks' +``` + +Create a `pact_helper.rb` in your service provider project. The recommended place is `spec/service_consumers/pact_helper.rb`. + +See [Verifying Pacts](https://github.com/pact-foundation/pact-ruby/wiki/Verifying-pacts) and the [Provider](documentation/configuration.md#provider) section of the Configuration documentation for more information. + +```ruby +# In spec/service_consumers/pact_helper.rb + +require 'pact/provider/rspec' + +Pact.service_provider "Animal Service" do + + honours_pact_with 'Zoo App' do + + # This example points to a local file, however, on a real project with a continuous + # integration box, you would use a [Pact Broker](https://github.com/pact-foundation/pact_broker) or publish your pacts as artifacts, + # and point the pact_uri to the pact published by the last successful build. + + pact_uri '../zoo-app/spec/pacts/zoo_app-animal_service.json' + end +end +``` + +#### 3. Run your failing specs + + $ rake pact:verify + +Congratulations! You now have a failing spec to develop against. + +At this stage, you'll want to be able to run your specs one at a time while you implement each feature. At the bottom of the failed pact:verify output you will see the commands to rerun each failed interaction individually. A command to run just one interaction will look like this: + + $ rake pact:verify PACT_DESCRIPTION="a request for an alligator" PACT_PROVIDER_STATE="an alligator exists" + +#### 4. Implement enough to make your first interaction spec pass + +Rinse and repeat. + +#### 5. Keep going til you're green + +Yay! Your Animal Service provider now honours the pact it has with your Zoo App consumer. You can now have confidence that your consumer and provider will play nicely together. + +### Using provider states + +Each interaction in a pact is verified in isolation, with no context maintained from the previous interactions. So how do you test a request that requires data to already exist on the provider? Read about provider states [here](https://github.com/pact-foundation/pact-ruby/wiki/Provider-states). + +## Configuration + +See the [Configuration](/documentation/configuration.md) section of the documentation for options relating to thing like logging, diff formatting, and documentation generation. + +## Pact best practices + +As in all things, there are good ways to implement Pacts, and there are not so good ways. There are also some Pact [GOTCHAS][gotchas] to beware of! Check out the [Best practices](https://github.com/pact-foundation/pact-ruby/wiki/Best-practices) section of the documentation to make sure you're not Pacting it Wrong. + +## Current Pact specification version + +Currently, Ruby Pact supports writing Pacts in v2, and verifying Pacts in v3 format, HOWEVER it only supports the rules that were defined in v2 (`like` and `term`). If you are interested in helping add support for the v3 rules, please talk to @Beth in the `#pact-ruby` channel on our [Slack](http://slack.pact.io). + +## Docs + +* [Example](example) +* [Configuration](documentation/configuration.md) +* [Terminology](https://github.com/pact-foundation/pact-ruby/wiki/Terminology) +* [Provider States](https://github.com/pact-foundation/pact-ruby/wiki/Provider-states) +* [Verifying pacts](https://github.com/pact-foundation/pact-ruby/wiki/Verifying-pacts) +* [Sharing pacts between consumer and provider](https://github.com/pact-foundation/pact-ruby/wiki/Sharing-pacts-between-consumer-and-provider) +* [Regular expressions and type matching with Pact](https://github.com/pact-foundation/pact-ruby/wiki/Regular-expressions-and-type-matching-with-Pact) +* [Frequently asked questions](https://github.com/pact-foundation/pact-ruby/wiki/FAQ) +* [Rarely asked questions](https://github.com/pact-foundation/pact-ruby/wiki/RAQ) +* [Best practices](https://github.com/pact-foundation/pact-ruby/wiki/Best-practices) +* [Troubleshooting](https://github.com/pact-foundation/pact-ruby/wiki/Troubleshooting) +* [Testing with pact diagram](https://github.com/pact-foundation/pact-ruby/wiki/Testing%20with%20pact.png) +* [News, blogs, videos and articles](https://github.com/pact-foundation/pact-ruby/wiki/News,-blogs,-videos-and-articles) + +## Related libraries + +[Pact Provider Proxy](https://github.com/pact-foundation/pact-provider-proxy) - Verify a pact against a running server, allowing you to use pacts with a provider of any language. + +[Pact Broker](https://github.com/pact-foundation/pact_broker) - A pact repository. Provides endpoints to access published pacts, meaning you don't need to use messy CI URLs in your codebase. Enables cross testing of prod/head versions of your consumer and provider, allowing you to determine whether the head version of one is compatible with the production version of the other. Helps you to answer that ever so important question, "can I deploy without breaking all the things?" + +[Pact Broker Client](https://github.com/pact-foundation/pact_broker-client) - Contains rake tasks for publishing pacts to the pact_broker. + +[Shokkenki](https://github.com/brentsnook/shokkenki) - Another Consumer Driven Contract gem written by one of Pact's original authors, Brent Snook. Shokkenki allows matchers to be composed using jsonpath expressions and allows auto-generation of mock response values based on regular expressions. + +[A list of Pact implementations in other languages](https://github.com/pact-foundation/pact-ruby/wiki#implementations-in-other-languages) - JVM, .Net, Javascript and Swift + +## Links + +[Simplifying microservices testing with pacts](http://dius.com.au/2014/05/19/simplifying-micro-service-testing-with-pacts/) - Ron Holshausen (one of the original pact authors) + +[Pact specification](https://github.com/pact-foundation/pact-specification) + +[Integrated tests are a scam](https://vimeo.com/80533536) - J.B. Rainsberger + +[Consumer Driven Contracts](http://martinfowler.com/articles/consumerDrivenContracts.html) - Ian Robinson + +[Integration Contract Tests](http://martinfowler.com/bliki/IntegrationContractTest.html) - Martin Fowler + +## Roadmap + +See [ROADMAP.md](/ROADMAP.md). + +## Contributing + +See [CONTRIBUTING.md](/CONTRIBUTING.md). + +[webmachine]: https://github.com/webmachine/webmachine-ruby +[roar]: https://github.com/apotonick/roar +[pact_broker]: https://github.com/pact-foundation/pact_broker +[pact_broker-client]: https://github.com/pact-foundation/pact_broker-client +[pact-public-apis]: https://github.com/pact-foundation/pact-ruby/wiki/Why-Pact-may-not-be-the-best-tool-for-testing-public-APIs +[pass-through-apis]: https://github.com/pact-foundation/pact-ruby/wiki/Why-Pact-may-not-be-the-best-tool-for-testing-pass-through-APIs +[gotchas]: https://github.com/pact-foundation/pact-ruby/wiki/Matching-gotchas + +## Contributors + +This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)]. + + + +## Backers + +Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/pact-foundation#backer)] + + + + +## Sponsors + +Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/pact-foundation#sponsor)] + + + + + + + + + + + + + diff --git a/Rakefile b/Rakefile index 78539201..770e7137 100644 --- a/Rakefile +++ b/Rakefile @@ -6,5 +6,4 @@ require 'rspec/core/rake_task' Dir.glob('./lib/tasks/**/*.rake').each { |task| load task } Dir.glob('./tasks/**/*.rake').each { |task| load task } -task :default => [:spec, 'spec:provider', 'pact:tests:all'] - +task :default => [:spec, 'pact:spec', 'pact:verify'] diff --git a/config.ru b/config.ru index dfce77d5..fad0c8a3 100644 --- a/config.ru +++ b/config.ru @@ -1,3 +1,3 @@ require './spec/support/app_for_config_ru' -run AppForConfigRu \ No newline at end of file +run AppForConfigRu diff --git a/documentation/README_V2.md b/documentation/README_V2.md deleted file mode 100644 index 2ff59bc6..00000000 --- a/documentation/README_V2.md +++ /dev/null @@ -1,563 +0,0 @@ -# Pact Ruby V2 - -The `pact-ruby-v1` is in maintenance mode, as there has been a transition to rust-core, which is intended to be used through FFI in non-Rust stacks. - -`pact-ruby v2` implements support for the latest versions of Pact specifications: - -- It's based on pact-ffi and pact-ruby-ffi -- It provides a convenient DSL, simplifying the writing of contract tests in Ruby/RSpec -- Writing contract tests with HTTP transports -- Writing contract tests with non-HTTP transports (for example, gRPC) -- Writing contract tests for async messages (Kafka, etc.) -- Verifying contract tests for HTTP/non-HTTP/async message transport - - V4 specification supports mixed pact interactions in a single file. - -## Architecture - -![Pact tests architecture](./pact-v2-arch.png) - -- DSL - implementation of RSpec-DSL for convenient writing of Pact tests -- Matchers - implementation of Pact matchers, which are convenient helpers used in consumer-DSL, encapsulating all the logic for serialization into Pact format -- Mock servers - mock servers that allow for correct execution of provider tests - -## Installation - -The `pact/v2` namespace is available in `pact-ruby` v1.67.0. - -It introduces a suite of new depedencies, including a reliance on the `pact-ffi` and `ffi` gems. - -The native extensions are marked in an optional block, so they are opt in. - -```rb - gem "pact" - gem "pact-ffi", "~> 0.4.28" # add this line, to use the pact/v2 namespace -``` - -`pact-ffi` ships prebuilt binary gems, and does not support platforms outside of the released [pact_ffi](https://github.com/pact-foundation/pact-reference/tree/master/rust/pact_ffi) libraries - -| Version | Platform | -|-----------|----------------------| -| 0.4.28.0 | x86_64-darwin | -| 0.4.28.0 | arm64-darwin | -| 0.4.28.0 | x86_64-linux | -| 0.4.28.0 | aarch64-linux | -| 0.4.28.0 | x86_64-linux-musl | -| 0.4.28.0 | aarch64-linux-musl | -| 0.4.28.0 | x64-mingw32 | -| 0.4.28.0 | x64-mingw-ucrt | - -## Usage - -For each type of interaction (due to their specific features), a separate version of DSL has been implemented. However, the general principles remain the same for each type of interaction. - -Place your consumer tests under - -`spec/pact/provider/**` - -**it's not an error: consumer tests contain `providers` subdirectory (because we're testing against different providers)** - -```ruby - -# Declaration of a consumer test, always include the :pact tag -# This is used in CI/CD pipelines to separate Pact tests from other RSpec tests -# Pact tests are not run as part of the general RSpec pipeline -RSpec.describe "SomePactConsumerTestForAnyTransport", :pact do - # declaration of the type of interaction - here we determine which consumer and provider interact on which transport - has_http_pact_between "CONSUMER-NAME", "PROVIDER-NAME" - # or - has_grpc_pact_between "CONSUMER-NAME", "PROVIDER-NAME" - # or - has_message_pact_between "CONSUMER-NAME", "PROVIDER-NAME" - - # the context for one of the interactions, for example GET /api/v2/stores - context "with GET /api/v2/stores" do - let(:interaction) do - # creating a new interaction - within which we describe the contract - new_interaction - # if you need to save any metadata for subsequent use by the test provider, - # for example, specify the entity ID that will need to be moved to the database in the test provider - # we use the provider states, see more at https://docs.pact.io/getting_started/provider_states - .given("UNIQUE PROVIDER STATE", key1: value1, key2: value2) - # the description of the interaction, used for identification inside the package binding, - # is optional in some cases, but it is recommended to always specify - .upon_receiving("UNIQUE INTERACTION DESCRIPTION") - # the description of the request using the matchers - # the name and parameters of the method differ for different transports - .with_request(...) - # the description of the response using the matchers - # the name and parameters of the method differ for different transports - .will_respond_with(...) - # further, there are differences for different types of transports, - # for more information, see the relevant sections of the documentation - end - - it "executes the pact test without errors" do | mock_server | - interaction.execute do - # the url of the started mock server, you should pass this into your api client in the next step - mock_server_url = mock_server.url - # here our client is called for the API being tested - # in this context, the client can be: http client, grpc client, kafka consumer - expect(make_request).to be_success - end - end - end - end - -``` - -Common DSL Methods: - -- `new_interaction` - initializes a new interaction -- `given` - allows specifying a provider state with or without parameters, for more details see -- `upon_receiving` - allows specifying the name of the interaction - -Multiple interactions can be declared within a single rspec example, in order to call the mock server - -- `execute_http_pact`: Use this instead of `interaction.execute` - -### HTTP consumers - -Specific DSL methods: - -- `with_request({method: string, path: string, headers: kv_hash, body: kv_hash})` - request definition -- `will_respond_with({status: int, headers: kv_hash, body: kv_hash})` - response definition - -More at [http_client_spec.rb](../spec/pact/providers/pact-ruby-v2-test-app/http_client_spec.rb) - -### gRPC consumers - -Specific DSL methods: - -- `with_service(PROTO_PATH, RPC_SERVICE_AND_ACTION)` - specifies the contract used, PROTO_PATH is relative from the app root -- `with_request(request_kv_hash)` - request definition -- `will_respond_with(response_kv_hash)` - response definition - -More at [grpc_client_spec.rb](../spec/pact/providers/pact-ruby-v2-test-app/grpc_client_spec.rb) - -### Message consumers - -Specific DSL methods: - -- `with_headers(kv_hash)` - message-headers definition; you can use matchers -- `with_metadata(kv_hash)` - message-metadata definition (special keys are `key` and `topic`, where, respectively, you can specify the matchers for the partitioning key and the topic - -Next, the specifics are one of two options for describing the format: - -**JSON** (to describe a message in a JSON representation): - -- `with_json_contents(kv_hash)` - message format definition - -**PROTO** (to describe the message in the protobuf view): - -- `with_proto_class(PROTO_PATH, PROTO_MESSAGE_NAME)` - specifies the contract used, PROTO_PATH is relative to the root, PROTO_MESSAGE_NAME is the name of the message used from the proto file -- `with_proto_contents(kv_hash)` - message format definition - -More at [message_spec.rb](../spec/pact/providers/pact-ruby-v2-test-app/message.spec.rb) - -### Kafka consumers - -Specific DSL methods: - -- `with_headers(kv_hash)` - message-headers definition; you can use matchers -- `with_metadata(kv_hash)` - message-metadata definition (special keys are `key` and `topic`, where, respectively, you can specify the matchers for the partitioning key and the topic - -Next, the specifics are one of two options for describing the format: - -**JSON** (to describe a message in a JSON representation): - -- `with_json_contents(kv_hash)` - message format definition - -**PROTO** (to describe the message in the protobuf view): - -- `with_proto_class(PROTO_PATH, PROTO_MESSAGE_NAME)` - specifies the contract used, PROTO_PATH is relative to the root, PROTO_MESSAGE_NAME is the name of the message used from the proto file -- `with_proto_contents(kv_hash)` - message format definition - -More at [kafka_spec.rb](../spec/pact/providers/pact-ruby-v2-test-app/kafka_spec.rb) - -Requires the following gems, to use this wrapper - -- sbmt-kafka_consumer -- sbmt-kafka_provider - -### Matchers - -Matchers are special helper methods that allow you to define rules for matching request/response parameters at the level of the pact manifest. -The matchers are described in the [Pact specifications](https://github.com/pact-foundation/pact-specification). In this gem, the matchers are implemented as RSpec helpers. - -For details of the implementation, see [matchers.rb](../lib/pact/v2/matchers.rb) - -- `match_exactly(sample)` - match the exact value specified in the sample -- `match_type_of(sample)` - match the data type (integer, string, boolean) specified in the sample -- `match_include(sample)` - match a substring -- `match_any_string(sample)` - match any string, because of the peculiarities, null and empty strings will also be matched here -- `match_any_integer(sample)` - match any integer -- `match_any_decimal(sample)` - match any float/double -- `match_any_number(sample)` - match any integer/float/double -- `match_any_boolean(sample)` - match any true/false -- `match_uuid(sample)` - match any UUID (`match_regex` is used under the hood) -- `match_regex(regex, sample)` - match by regexp -- `match_datetime(format, sample)` - match any datetime -- `match_iso8601(sample)` - match datetime in ISO8601 (the matcher does not fully comply with ISO8601, matches only the most common variants, `match_regex` is used under the hood) -- `match_date(format, sample)` - match any date (rust datetime) -- `match_time(format, sample)` - match any time (rust datetime) -- `match_each(template)` - match all the elements of the array according to the specified template, you can use it for nested elements -- `match_each_regex(regex, sample)` - match all array elements by regex, used for arrays with string elements -- `match_each_key(template, key_matchers)` - match each hash key according to the specified template -- `match_each_value(template)` - match each hash value according to the specified template, can be used for nested elements -- `match_each_kv(template, key_matchers)` - match all the keys/values of Hash according to the specified template and key_matchers, can be used for nested elements - -See the different uses of the matchers in [matchers_spec.rb](../spec/pact/v2/matchers_spec.rb) - -### Generators - -Generators are helper methods that allow you to specify dynamic values in your contract tests. These values are generated at runtime, making your contracts more flexible and robust. Below are the available generator methods: - -For details of the implementation, see [matchers.rb](../lib/pact/v2/generators.rb) - -- `generate_random_int(min:, max:)` - Generates a random integer between the specified `min` and `max`. -- `generate_random_decimal(digits:)` - Generates a random decimal number with the specified number of `digits`. -- `generate_random_hexadecimal(digits:)` - Generates a random hexadecimal string with the specified number of `digits`. -- `generate_random_string(size:)` - Generates a random string of the specified `size`. -- `generate_uuid(example: nil)` - Generates a random UUID. Optionally, provide an `example` value. -- `generate_date(format: nil, example: nil)` - Generates a date string in the specified `format`. Optionally, provide an `example`. -- `generate_time(format: nil)` - Generates a time string in the specified `format`. -- `generate_datetime(format: nil)` - Generates a datetime string in the specified `format`. -- `generate_random_boolean` - Generates a random boolean value (`true` or `false`). -- `generate_from_provider_state(expression:, example:)` - Generates a value from the provider state using the given `expression` and `example` value. Allows templating of url and query paths with values only know at provider verification time. -- `generate_mock_server_url(regex: nil, example: nil)` - Generates a mock server URL. Optionally, specify a `regex` matches and/or an `example` value. - -These generators can be used in your DSL definitions to provide dynamic values for requests, responses, or messages in your contract tests. - -#### Generator Examples - -```rb - .with_request( - method: :get, - path: generate_from_provider_state( - expression: '/alligators/${alligator_name}', - example: '/alligators/Mary'), - headers: headers) - -... - - body: { - _links: { - :'pf:publish-provider-contract' => { - href: generate_mock_server_url( - regex: ".*(\\/provider-contracts\\/provider\\/.*\\/publish)$", - example: "/provider-contracts/provider/{provider}/publish" - ), - boolean: generate_random_boolean, - integer: generate_random_int(min: 1, max: 100), - decimal: generate_random_decimal(digits: 2), - hexidecimal: generate_random_hexadecimal(digits: 8), - string: generate_random_string(size: 10), - uuid: generate_uuid, - date: generate_date(format: "yyyyy.MMMMM.dd GGG"), - time: generate_time(), - datetime: generate_datetime(format: "%Y-%m-%dT%H:%M:%S%z") - } - } - } -``` - -## Provider verification - -Place your provider verification file under - -`spec/pact/consumers/**` - -**it's not an error: provider tests contain `consumers` subdirectory (because we're verifying against different consumer)** - -### Provider verification options - -```rb - @provider_name = provider_name - @log_level = opts[:log_level] || :info - @pact_dir = opts[:pact_dir] || nil - @provider_setup_port = opts[:provider_setup_port] || 9001 - @pact_proxy_port = opts[:pact_proxy_port] || 9002 - @pact_uri = ENV.fetch("PACT_URL", nil) || opts.fetch(:pact_uri, nil) - @publish_verification_results = ENV.fetch("PACT_PUBLISH_VERIFICATION_RESULTS", nil) == "true" || opts.fetch(:publish_verification_results, false) - @provider_version = ENV.fetch("PACT_PROVIDER_VERSION", nil) || opts.fetch(:provider_version, nil) - @provider_build_uri = ENV.fetch("PACT_PROVIDER_BUILD_URL", nil) || opts.fetch(:provider_build_uri, nil) - @provider_version_branch = ENV.fetch("PACT_PROVIDER_BRANCH", nil) || opts.fetch(:provider_version_branch, nil) - @provider_version_tags = ENV.fetch("PACT_PROVIDER_VERSION_TAGS", nil) || opts.fetch(:provider_version_tags, []) - @consumer_version_tags = ENV.fetch("PACT_CONSUMER_VERSION_TAGS", nil) || opts.fetch(:consumer_version_tags, []) - @consumer_version_selectors = ENV.fetch("PACT_CONSUMER_VERSION_SELECTORS", nil) || opts.fetch(:consumer_version_selectors, nil) - @enable_pending = ENV.fetch("PACT_VERIFIER_ENABLE_PENDING", nil) == "true" || opts.fetch(:enable_pending, false) - @include_wip_pacts_since = ENV.fetch("PACT_INCLUDE_WIP_PACTS_SINCE", nil) || opts.fetch(:include_wip_pacts_since, nil) - @fail_if_no_pacts_found = ENV.fetch("PACT_FAIL_IF_NO_PACTS_FOUND", nil) == "true" || opts.fetch(:fail_if_no_pacts_found, true) - @consumer_branch = ENV.fetch("PACT_CONSUMER_BRANCH", nil) || opts.fetch(:consumer_branch, nil) - @consumer_version = ENV.fetch("PACT_CONSUMER_VERSION", nil) || opts.fetch(:consumer_version, nil) - @consumer_name = opts[:consumer_name] - @broker_url = ENV.fetch("PACT_BROKER_BASE_URL", nil) || opts.fetch(:broker_url, nil) - @broker_username = ENV.fetch("PACT_BROKER_USERNAME", nil) || opts.fetch(:broker_username, nil) - @broker_password = ENV.fetch("PACT_BROKER_PASSWORD", nil) || opts.fetch(:broker_password, nil) - @broker_token = ENV.fetch("PACT_BROKER_TOKEN", nil) || opts.fetch(:broker_token, nil) - @verify_only = [ENV.fetch("PACT_CONSUMER_FULL_NAME", nil)].compact || opts.fetch(:verify_only, []) -``` - -### Single transport providers - -```rb -# frozen_string_literal: true - -require "pact_broker" -require "pact_broker/app" -require "rspec/mocks" -include RSpec::Mocks::ExampleMethods -require_relative "../../service_consumers/hal_relation_proxy_app" - -PactBroker.configuration.base_urls = ["http://example.org"] - -pact_broker = PactBroker::App.new { |c| c.database_connection = PactBroker::TestDatabase.connection_for_test_database } -app_to_verify = HalRelationProxyApp.new(pact_broker) - -require "pact" -require "pact/v2/rspec" -require_relative "../../service_consumers/shared_provider_states" -RSpec.describe "Verify consumers for Pact Broker", :pact_v2 do - - http_pact_provider "Pact Broker", opts: { - - # rails apps should be automatically detected - # if you need to configure your own app, you can do so here - - app: app_to_verify, - # start rackup with a different port. Useful if you already have something - # running on the default port *9292* - http_port: 9393, - - # Set the log level, default is :info - - log_level: :info, - - fail_if_no_pacts_found: true, - - # Pact Sources - - # 1. Local pacts from a directory - - # Default is pacts directory in the current working directory - # pact_dir: File.expand_path('../../../../consumer/spec/internal/pacts', __dir__), - - # 2. Broker based pacts - - # Broker credentials - - # broker_username: "pact_workshop", # can be set via PACT_BROKER_USERNAME env var - # broker_password: "pact_workshop", # can be set via PACT_BROKER_PASSWORD env var - # broker_token: "pact_workshop", # can be set via PACT_BROKER_TOKEN env var - - # Remote pact via a uri, traditionally triggered via webhooks - # when a pact that requires verification is published - - # 2a. Webhook triggered pacts - # Can be a local file or a remote URL - # Most used via webhooks - # Can be set via PACT_URL env var - # pact_uri: File.expand_path("../../../pacts/pact.json", __dir__), - pact_uri: "https://raw.githubusercontent.com/pact-foundation/pact_broker-client/refs/heads/master/spec/pacts/Pact%20Broker%20Client%20V2-Pact%20Broker.json", - # pact_uri: "https://raw.githubusercontent.com/pact-foundation/pact_broker-client/refs/heads/master/spec/pacts/pact_broker_client-pact_broker.json", - # pact_uri: "http://localhost:9292/pacts/provider/Pact%20Broker/consumer/Pact%20Broker%20Client/version/96532124f3a53a499276c69ff2df785b8377588e", - - # 2b. Dynamically fetched pacts from broker - - # i. Set the broker url - # broker_url: "http://localhost:9292", # can be set via PACT_BROKER_URL env var - - # ii. Set the consumer version selectors - # Consumer version selectors - # The pact broker will return the following pacts by default, if no selectors are specified - # For the recommended setup, you dont _actually_ need to specify these selectors in ruby - # consumer_version_selectors: [{"deployedOrReleased" => true},{"mainBranch" => true},{"matchingBranch" => true}], - - # iii. Set additional dynamic selection verification options - # additional dynamic selection verification options - enable_pending: true, - include_wip_pacts_since: "2021-01-01", - - # Publish verification results to the broker - publish_verification_results: ENV["PACT_PUBLISH_VERIFICATION_RESULTS"] == "true", - provider_version: `git rev-parse HEAD`.strip, - provider_version_branch: `git rev-parse --abbrev-ref HEAD`.strip, - provider_version_tags: [`git rev-parse --abbrev-ref HEAD`.strip], - # provider_build_uri: "YOUR CI URL HERE - must be a valid url", - - } - - before_state_setup do - PactBroker::TestDatabase.truncate - end - - after_state_teardown do - PactBroker::TestDatabase.truncate - end - - shared_provider_states - -end -``` - -### Multiple transport providers - -You may have a consumer pact which consumes multiple transport protocols, if they are using pact specification v4. - -In order to validate an entire pact in a single test run, you will need to configure each transport as appropriate. - -```rb -# frozen_string_literal: true - -require "pact/v2/rspec" - -RSpec.describe "Pact::V2::Consumers::Http", :pact_v2 do - mixed_pact_provider "pact-v2-test-app", opts: { - http: { - http_port: 3000, - log_level: :info, - pact_dir: File.expand_path('../../pacts', __dir__), - }, - grpc: { - grpc_port: 3009 - }, - async: { - message_handlers: { - # "pet message as json" => proc do |provider_state| - # pet_id = provider_state.dig("params", "pet_id") - # with_pact_producer { |client| PetJsonProducer.new(client: client).call(pet_id) } - # end, - # "pet message as proto" => proc do |provider_state| - # pet_id = provider_state.dig("params", "pet_id") - # with_pact_producer { |client| PetProtoProducer.new(client: client).call(pet_id) } - # end - } - } - } - - handle_message "pet message as json" do |provider_state| - pet_id = provider_state.dig("params", "pet_id") - with_pact_producer { |client| PetJsonProducer.new(client: client).call(pet_id) } - end - - handle_message "pet message as proto" do |provider_state| - pet_id = provider_state.dig("params", "pet_id") - with_pact_producer { |client| PetProtoProducer.new(client: client).call(pet_id) } - end - -end - -``` - -## Development & Test - -### Setup - -```shell -bundle install -``` - -### Run unit tests - -```shell -bundle exec rake spec:v2 -``` - -### Run pact tests - -The Pact tests are not run within the general rspec pipeline, they need to be run separately, see below - -#### Consumer tests - -```shell -bundle exec rspec -t pact spec/pact/providers/**/*_spec.rb -or -bundle exec rake pact:v2:spec -``` - -**NOTE** If you have never run it, you need to run it at least once to generate the pact files that will be used in provider tests (below) - -#### Provider tests - -```shell -bundle exec rspec -t pact spec/pact/consumers/*_spec.rb -or -bundle exec rake pact:v2:spec -``` - -## Examples - -### Migration - -1. add `gem "pact-ffi", "~> 0.4.28"` to Gemfile, or Gemspec -2. pact ruby v2 uses activesupport classes, so you may need to add - 1. `gem 'combustion'` to load active support during tests - 1. add a pact helper to load it - - ```rb - require "combustion" - begin - Combustion.initialize! :action_controller do - config.log_level = :fatal if ENV["LOG"].to_s.empty? - end - rescue => e - # Fail fast if application couldn't be loaded - warn "💥 Failed to load the app: #{e.message}\n#{e.backtrace.join("\n")}" - exit(1) - end - ``` - -3. Add a new rake task - - - require your helper file created above - - add a tag, we will use `pact_v2` to namespace away from our existing `pact` tagged tests - - ```rb - RSpec::Core::RakeTask.new('spec:v2') do |task| - task.pattern = 'spec/pact/providers/**/*_spec.rb' - task.rspec_opts = ['-t pact_v2', '--require rails_helper'] - end - ``` - -4. File paths have moved for consumer tests, and provider verification tasks - - - consumer test location - 1. pact v1 `spec/service_providers` - 2. pact v2 - `spec/pact/providers` - - provider verification location - 1. pact v1 `spec/service_consumers` - 2. pact v2 - `spec/pact/consumers` - -The following projects were designed for pact-ruby-v1 and have been migrated to pact-ruby-v2. They can serve as an example of the work required. - -- pact broker client - - v2 -- pact broker - - v2 -- animal service - - v1 [example/animal-service](../example/animal-service/) - - v2 [example/animal-service-v2](../example/animal-service-v2/) -- zoo app - - v1 [example/zoo-app](../example/zoo-app/) - - v2 [example/zoo-app-v2](../example/zoo-app-v2/) -- message consumer/provider - - v1 - - v2 -- e2e http consumer/provider - - - - Plus http, message, grpc & mixed consumer & provider examples - -### Demos - -The demos are stored in this codebase for regression test, but exist as standalone in https://github.com/pact-foundation/pact-ruby-e2e-example - -- http consumer [http_client_spec.rb](../spec/pact/providers/pact-ruby-v2-test-app/http_client_spec.rb) -- kafka consumer with pact-ruby wrapper [kafka_spec.rb](../spec/pact/providers/pact-ruby-v2-test-app/kafka_spec.rb) -- message consumer [message_spec.rb](../spec/pact/providers/pact-ruby-v2-test-app/message_spec.rb) -- plugin consumer http [plugin_matt_http_spec.rb](../spec/pact/providers/pact-ruby-v2-test-app/plugin_matt_http_spec.rb) -- plugin consumer http [plugin_matt_sync_message_spec.rb](../spec/pact/providers/pact-ruby-v2-test-app/plugin_matt_sync_message_spec.rb) -- plugin consumer http [plugin_matt_async_message_spec.rb](../spec/pact/providers/pact-ruby-v2-test-app/plugin_matt_async_message_spec.rb) -- plugin consumer http [plugin_matt_http_spec.rb](../spec/pact/providers/pact-ruby-v2-test-app/plugin_matt_http_spec.rb) -- grpc consumer with pact-ruby wrapper [grpc_client_spec.rb](../spec/pact/providers/pact-ruby-v2-test-app/grpc_client_spec.rb) -- grpc consumer using direct plugin interface [plugin_grpc_sync_message_spec.rb](../spec/pact/providers/pact-ruby-v2-test-app/plugin_grpc_sync_message_spec.rb) -- mixed(http/kafka/grpc) provider [multi_spec.rb](../spec/pact/consumers/multi_spec.rb) diff --git a/documentation/pact-v2-arch.png b/documentation/pact-arch.png similarity index 100% rename from documentation/pact-v2-arch.png rename to documentation/pact-arch.png diff --git a/example/animal-service-v2/Gemfile b/example/animal-service-v2/Gemfile deleted file mode 100644 index be55b69d..00000000 --- a/example/animal-service-v2/Gemfile +++ /dev/null @@ -1,19 +0,0 @@ -source 'https://rubygems.org' - -group :development, :test do - gem 'rspec' - gem 'pact', path: '../../' - gem "pact-ffi", "~> 0.4.28" # added for pact-ruby-v2 FFI support - gem 'pry' - # required for pact-ruby-v2 - gem 'combustion' - gem 'webmock' -end - -gem 'rake' -gem 'rack' -gem 'sqlite3' -gem 'sequel' -gem 'sinatra' - -gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw] \ No newline at end of file diff --git a/example/animal-service-v2/Rakefile b/example/animal-service-v2/Rakefile deleted file mode 100644 index 1573f5da..00000000 --- a/example/animal-service-v2/Rakefile +++ /dev/null @@ -1,12 +0,0 @@ -$: << File.join(File.dirname(__FILE__), "lib") - -require 'pact/tasks' - -task :default => 'pact:verify' - -require 'rspec/core/rake_task' - -RSpec::Core::RakeTask.new('pact:v2:verify') do |task| - task.pattern = 'spec/pact/consumers/*_spec.rb' - task.rspec_opts = ['-t pact_v2', '--require rails_helper'] -end \ No newline at end of file diff --git a/example/animal-service-v2/config.ru b/example/animal-service-v2/config.ru deleted file mode 100644 index c5623c9f..00000000 --- a/example/animal-service-v2/config.ru +++ /dev/null @@ -1,3 +0,0 @@ -require File.dirname(__FILE__) + '/lib/animal_service/api' - -run AnimalService::Api diff --git a/example/animal-service-v2/db/animal_db.sqlite3 b/example/animal-service-v2/db/animal_db.sqlite3 deleted file mode 100644 index bc4f88e3..00000000 Binary files a/example/animal-service-v2/db/animal_db.sqlite3 and /dev/null differ diff --git a/example/animal-service-v2/lib/animal_service/animal_repository.rb b/example/animal-service-v2/lib/animal_service/animal_repository.rb deleted file mode 100644 index f26b4a23..00000000 --- a/example/animal-service-v2/lib/animal_service/animal_repository.rb +++ /dev/null @@ -1,12 +0,0 @@ -require 'sequel' -require_relative 'db' - -module AnimalService - class AnimalRepository - - def self.find_alligator_by_name name - DATABASE[:animals].where(name: name).single_record - end - - end -end diff --git a/example/animal-service-v2/lib/animal_service/api.rb b/example/animal-service-v2/lib/animal_service/api.rb deleted file mode 100644 index 1eede90d..00000000 --- a/example/animal-service-v2/lib/animal_service/api.rb +++ /dev/null @@ -1,29 +0,0 @@ -require 'sinatra/base' -require_relative 'animal_repository' -require 'json' - -module AnimalService - - class Api < Sinatra::Base - - set :raise_errors, false - set :show_exceptions, false - - error do - e = env['sinatra.error'] - content_type :json, :charset => 'utf-8' - status 500 - {error: e.message, backtrace: e.backtrace}.to_json - end - - get '/alligators/:name' do - if (alligator = AnimalRepository.find_alligator_by_name(params[:name])) - content_type :json, :charset => 'utf-8' - alligator.to_json - else - status 404 - end - end - - end -end diff --git a/example/animal-service-v2/lib/animal_service/db.rb b/example/animal-service-v2/lib/animal_service/db.rb deleted file mode 100644 index 56185928..00000000 --- a/example/animal-service-v2/lib/animal_service/db.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'sequel' - -module AnimalService - DATABASE ||= Sequel.connect(adapter: 'sqlite', database: './db/animal_db.sqlite3') -end \ No newline at end of file diff --git a/example/animal-service/Gemfile b/example/animal-service/Gemfile index a5d31500..7dedc652 100644 --- a/example/animal-service/Gemfile +++ b/example/animal-service/Gemfile @@ -1,13 +1,19 @@ source 'https://rubygems.org' group :development, :test do - gem 'rspec' gem 'pact', path: '../../' + gem 'pact-ffi', '~> 0.4.28' # added for pact-ruby FFI support gem 'pry' + gem 'rspec' + # required for pact-ruby + gem 'combustion' + gem 'webmock' end -gem 'rake' gem 'rack' -gem 'sqlite3' +gem 'rake' gem 'sequel' gem 'sinatra' +gem 'sqlite3' + +gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw] diff --git a/example/animal-service/Rakefile b/example/animal-service/Rakefile index 273ccd29..bedad51f 100644 --- a/example/animal-service/Rakefile +++ b/example/animal-service/Rakefile @@ -1,5 +1,10 @@ -$: << File.join(File.dirname(__FILE__), "lib") +$: << File.join(File.dirname(__FILE__), 'lib') -require 'pact/tasks' +task default: 'pact:verify' -task :default => 'pact:verify' \ No newline at end of file +require 'rspec/core/rake_task' + +RSpec::Core::RakeTask.new('pact:verify') do |task| + task.pattern = 'spec/pact/consumers/*_spec.rb' + task.rspec_opts = ['-t pact', '--require rails_helper'] +end diff --git a/example/animal-service/lib/animal_service/animal_repository.rb b/example/animal-service/lib/animal_service/animal_repository.rb index f26b4a23..f054242e 100644 --- a/example/animal-service/lib/animal_service/animal_repository.rb +++ b/example/animal-service/lib/animal_service/animal_repository.rb @@ -3,10 +3,8 @@ module AnimalService class AnimalRepository - def self.find_alligator_by_name name DATABASE[:animals].where(name: name).single_record end - end end diff --git a/example/animal-service/lib/animal_service/api.rb b/example/animal-service/lib/animal_service/api.rb index 1eede90d..d9243769 100644 --- a/example/animal-service/lib/animal_service/api.rb +++ b/example/animal-service/lib/animal_service/api.rb @@ -3,9 +3,7 @@ require 'json' module AnimalService - class Api < Sinatra::Base - set :raise_errors, false set :show_exceptions, false @@ -13,7 +11,7 @@ class Api < Sinatra::Base e = env['sinatra.error'] content_type :json, :charset => 'utf-8' status 500 - {error: e.message, backtrace: e.backtrace}.to_json + { error: e.message, backtrace: e.backtrace }.to_json end get '/alligators/:name' do @@ -24,6 +22,5 @@ class Api < Sinatra::Base status 404 end end - end end diff --git a/example/animal-service/lib/animal_service/db.rb b/example/animal-service/lib/animal_service/db.rb index 56185928..3db3eb2e 100644 --- a/example/animal-service/lib/animal_service/db.rb +++ b/example/animal-service/lib/animal_service/db.rb @@ -2,4 +2,4 @@ module AnimalService DATABASE ||= Sequel.connect(adapter: 'sqlite', database: './db/animal_db.sqlite3') -end \ No newline at end of file +end diff --git a/example/animal-service-v2/spec/pact/consumers/http_spec.rb b/example/animal-service/spec/pact/consumers/http_spec.rb similarity index 87% rename from example/animal-service-v2/spec/pact/consumers/http_spec.rb rename to example/animal-service/spec/pact/consumers/http_spec.rb index 1fedb5de..05874bae 100644 --- a/example/animal-service-v2/spec/pact/consumers/http_spec.rb +++ b/example/animal-service/spec/pact/consumers/http_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'pact/v2/rspec' +require 'pact/rspec' require 'sequel' require 'animal_service/api' require 'animal_service/db' @@ -8,9 +8,9 @@ require 'rspec/mocks' include RSpec::Mocks::ExampleMethods -RSpec.describe 'Verify consumers for Bar Provider', :pact_v2 do +RSpec.describe 'Verify consumers for Bar Provider', :pact do http_pact_provider 'Animal Service', opts: { - pact_dir: File.expand_path('../../../../zoo-app-v2/spec/pacts', __dir__), + pact_dir: File.expand_path('../../../../zoo-app/spec/pacts', __dir__), http_port: 9292, app: AnimalService::Api } @@ -45,4 +45,4 @@ allow(AnimalService::AnimalRepository).to receive(:find_alligator_by_name).and_call_original end end -end \ No newline at end of file +end diff --git a/example/animal-service-v2/spec/rails_helper.rb b/example/animal-service/spec/rails_helper.rb similarity index 98% rename from example/animal-service-v2/spec/rails_helper.rb rename to example/animal-service/spec/rails_helper.rb index a8d0d351..597dbf5e 100644 --- a/example/animal-service-v2/spec/rails_helper.rb +++ b/example/animal-service/spec/rails_helper.rb @@ -9,4 +9,4 @@ # Fail fast if application couldn't be loaded warn "💥 Failed to load the app: #{e.message}\n#{e.backtrace.join("\n")}" exit(1) -end \ No newline at end of file +end diff --git a/example/animal-service/spec/service_consumers/pact_helper.rb b/example/animal-service/spec/service_consumers/pact_helper.rb deleted file mode 100644 index da2d2080..00000000 --- a/example/animal-service/spec/service_consumers/pact_helper.rb +++ /dev/null @@ -1,18 +0,0 @@ -require 'pact/provider/rspec' - -require "./spec/service_consumers/provider_states_for_zoo_app" - -Pact.service_provider 'Animal Service' do - - honours_pact_with "Zoo App" do - pact_uri '../zoo-app/spec/pacts/zoo_app-animal_service.json' - end - - ## For pact contracts from a Pact Broker - - # honours_pacts_from_pact_broker do - # pact_broker_base_url 'http://localhost:9292' - # # fail_if_no_pacts_found false # defaults to true - # end - -end diff --git a/example/animal-service/spec/service_consumers/provider_states_for_zoo_app.rb b/example/animal-service/spec/service_consumers/provider_states_for_zoo_app.rb deleted file mode 100644 index 7c180541..00000000 --- a/example/animal-service/spec/service_consumers/provider_states_for_zoo_app.rb +++ /dev/null @@ -1,26 +0,0 @@ -require 'sequel' -require 'animal_service/db' -require 'animal_service/animal_repository' - -Pact.provider_states_for "Zoo App" do - - set_up do - AnimalService::DATABASE[:animals].truncate - end - - provider_state "there is an alligator named Mary" do - set_up do - AnimalService::DATABASE[:animals].insert(name: 'Mary') - end - end - - provider_state "there is not an alligator named Mary" do - no_op - end - - provider_state "an error occurs retrieving an alligator" do - set_up do - allow(AnimalService::AnimalRepository).to receive(:find_alligator_by_name).and_raise("Argh!!!") - end - end -end \ No newline at end of file diff --git a/example/zoo-app-v2/Gemfile b/example/zoo-app-v2/Gemfile deleted file mode 100644 index 679c18f4..00000000 --- a/example/zoo-app-v2/Gemfile +++ /dev/null @@ -1,18 +0,0 @@ -source 'https://rubygems.org' - -group :development, :test do - gem 'rspec' - gem 'pact', path: '../../' - gem "pact-ffi", "~> 0.4.28" # added for pact-ruby-v2 FFI support - gem 'pact_broker-client' - gem 'pry' - # required for pact-ruby-v2 - gem 'combustion' -end - -gem 'rake' - -gem 'rack' -gem 'httparty', '>= 0.21.0' - -gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw] \ No newline at end of file diff --git a/example/zoo-app-v2/Rakefile b/example/zoo-app-v2/Rakefile deleted file mode 100644 index 7a1607ad..00000000 --- a/example/zoo-app-v2/Rakefile +++ /dev/null @@ -1,19 +0,0 @@ -require 'rspec/core/rake_task' -require 'pact_broker/client/tasks' - -$: << './lib' - -RSpec::Core::RakeTask.new(:spec) - -PactBroker::Client::PublicationTask.new do | task | - require 'zoo_app/version' - task.consumer_version = ZooApp::VERSION - task.pact_broker_base_url = "http://localhost:9292" -end - -RSpec::Core::RakeTask.new('spec:v2') do |task| - task.pattern = 'spec/pact/providers/**/*_spec.rb' - task.rspec_opts = ['-t pact_v2', '--require rails_helper'] -end - -task :default => :spec \ No newline at end of file diff --git a/example/zoo-app-v2/doc/pacts/markdown/README.md b/example/zoo-app-v2/doc/pacts/markdown/README.md deleted file mode 100644 index a037dffb..00000000 --- a/example/zoo-app-v2/doc/pacts/markdown/README.md +++ /dev/null @@ -1,3 +0,0 @@ -### Pacts for Zoo App - -* [Animal Service](Zoo%20App%20-%20Animal%20Service.md) diff --git a/example/zoo-app-v2/doc/pacts/markdown/Zoo App - Animal Service.md b/example/zoo-app-v2/doc/pacts/markdown/Zoo App - Animal Service.md deleted file mode 100644 index 19525f63..00000000 --- a/example/zoo-app-v2/doc/pacts/markdown/Zoo App - Animal Service.md +++ /dev/null @@ -1,75 +0,0 @@ -### A pact between Zoo App and Animal Service - -#### Requests from Zoo App to Animal Service - -* [A request for an alligator](#a_request_for_an_alligator_given_there_is_an_alligator_named_Mary) given there is an alligator named Mary - -* [A request for an alligator](#a_request_for_an_alligator_given_there_is_not_an_alligator_named_Mary) given there is not an alligator named Mary - -* [A request for an alligator](#a_request_for_an_alligator_given_an_error_occurs_retrieving_an_alligator) given an error occurs retrieving an alligator - -#### Interactions - - -Given **there is an alligator named Mary**, upon receiving **a request for an alligator** from Zoo App, with -```json -{ - "method": "get", - "path": "/alligators/Mary", - "headers": { - "Accept": "application/json" - } -} -``` -Animal Service will respond with: -```json -{ - "status": 200, - "headers": { - "Content-Type": "application/json;charset=utf-8" - }, - "body": { - "name": "Mary" - } -} -``` - -Given **there is not an alligator named Mary**, upon receiving **a request for an alligator** from Zoo App, with -```json -{ - "method": "get", - "path": "/alligators/Mary", - "headers": { - "Accept": "application/json" - } -} -``` -Animal Service will respond with: -```json -{ - "status": 404 -} -``` - -Given **an error occurs retrieving an alligator**, upon receiving **a request for an alligator** from Zoo App, with -```json -{ - "method": "get", - "path": "/alligators/Mary", - "headers": { - "Accept": "application/json" - } -} -``` -Animal Service will respond with: -```json -{ - "status": 500, - "headers": { - "Content-Type": "application/json;charset=utf-8" - }, - "body": { - "error": "Argh!!!" - } -} -``` diff --git a/example/zoo-app-v2/lib/zoo_app/animal_service_client.rb b/example/zoo-app-v2/lib/zoo_app/animal_service_client.rb deleted file mode 100644 index 2acde606..00000000 --- a/example/zoo-app-v2/lib/zoo_app/animal_service_client.rb +++ /dev/null @@ -1,31 +0,0 @@ -require 'httparty' -require 'zoo_app/models/alligator' - -module ZooApp - class AnimalServiceClient - - include HTTParty - base_uri 'animal-service.com' - - def self.find_alligator_by_name name - response = get("/alligators/#{name}", :headers => {'Accept' => 'application/json'}) - when_successful(response) do - ZooApp::Animals::Alligator.new(parse_body(response)) - end - end - - def self.when_successful response - if response.success? - yield - elsif response.code == 404 - nil - else - raise response.body - end - end - - def self.parse_body response - JSON.parse(response.body, {:symbolize_names => true}) - end - end -end \ No newline at end of file diff --git a/example/zoo-app-v2/lib/zoo_app/models/alligator.rb b/example/zoo-app-v2/lib/zoo_app/models/alligator.rb deleted file mode 100644 index 49233d4d..00000000 --- a/example/zoo-app-v2/lib/zoo_app/models/alligator.rb +++ /dev/null @@ -1,17 +0,0 @@ -module ZooApp - module Animals - class Alligator - - attr_reader :name - - def initialize attributes - @name = attributes[:name] - end - - def == other - other.is_a?(Alligator) && other.name == self.name - end - - end - end -end \ No newline at end of file diff --git a/example/zoo-app-v2/lib/zoo_app/version.rb b/example/zoo-app-v2/lib/zoo_app/version.rb deleted file mode 100644 index 9bbf90c0..00000000 --- a/example/zoo-app-v2/lib/zoo_app/version.rb +++ /dev/null @@ -1,3 +0,0 @@ -module ZooApp - VERSION = '1.0.0' -end \ No newline at end of file diff --git a/example/zoo-app/Gemfile b/example/zoo-app/Gemfile index 71cc235f..5d8c04b3 100644 --- a/example/zoo-app/Gemfile +++ b/example/zoo-app/Gemfile @@ -1,13 +1,18 @@ source 'https://rubygems.org' group :development, :test do - gem 'rspec' gem 'pact', path: '../../' gem 'pact_broker-client' + gem 'pact-ffi', '~> 0.4.28' # added for pact-ruby FFI support gem 'pry' + gem 'rspec' + # required for pact-ruby + gem 'combustion' end gem 'rake' +gem 'httparty', '>= 0.21.0' gem 'rack' -gem 'httparty', '>= 0.21.0' \ No newline at end of file + +gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw] diff --git a/example/zoo-app/Rakefile b/example/zoo-app/Rakefile index 6f5f3d46..ee427b40 100644 --- a/example/zoo-app/Rakefile +++ b/example/zoo-app/Rakefile @@ -5,10 +5,15 @@ $: << './lib' RSpec::Core::RakeTask.new(:spec) -PactBroker::Client::PublicationTask.new do | task | +PactBroker::Client::PublicationTask.new do |task| require 'zoo_app/version' task.consumer_version = ZooApp::VERSION task.pact_broker_base_url = "http://localhost:9292" end -task :default => :spec \ No newline at end of file +RSpec::Core::RakeTask.new('spec') do |task| + task.pattern = 'spec/pact/providers/**/*_spec.rb' + task.rspec_opts = ['-t pact', '--require rails_helper'] +end + +task :default => :spec diff --git a/example/zoo-app/lib/zoo_app/animal_service_client.rb b/example/zoo-app/lib/zoo_app/animal_service_client.rb index 2acde606..0a103ca5 100644 --- a/example/zoo-app/lib/zoo_app/animal_service_client.rb +++ b/example/zoo-app/lib/zoo_app/animal_service_client.rb @@ -3,12 +3,11 @@ module ZooApp class AnimalServiceClient - include HTTParty base_uri 'animal-service.com' def self.find_alligator_by_name name - response = get("/alligators/#{name}", :headers => {'Accept' => 'application/json'}) + response = get("/alligators/#{name}", :headers => { 'Accept' => 'application/json' }) when_successful(response) do ZooApp::Animals::Alligator.new(parse_body(response)) end @@ -25,7 +24,7 @@ def self.when_successful response end def self.parse_body response - JSON.parse(response.body, {:symbolize_names => true}) + JSON.parse(response.body, { :symbolize_names => true }) end end -end \ No newline at end of file +end diff --git a/example/zoo-app/lib/zoo_app/models/alligator.rb b/example/zoo-app/lib/zoo_app/models/alligator.rb index 49233d4d..cf1d60da 100644 --- a/example/zoo-app/lib/zoo_app/models/alligator.rb +++ b/example/zoo-app/lib/zoo_app/models/alligator.rb @@ -1,7 +1,6 @@ module ZooApp module Animals class Alligator - attr_reader :name def initialize attributes @@ -11,7 +10,6 @@ def initialize attributes def == other other.is_a?(Alligator) && other.name == self.name end - end end -end \ No newline at end of file +end diff --git a/example/zoo-app/lib/zoo_app/version.rb b/example/zoo-app/lib/zoo_app/version.rb index 9bbf90c0..5cff1b04 100644 --- a/example/zoo-app/lib/zoo_app/version.rb +++ b/example/zoo-app/lib/zoo_app/version.rb @@ -1,3 +1,3 @@ module ZooApp VERSION = '1.0.0' -end \ No newline at end of file +end diff --git a/example/zoo-app-v2/spec/pact/providers/animal_service/animal_service_client_spec.rb b/example/zoo-app/spec/pact/providers/animal_service/animal_service_client_spec.rb similarity index 88% rename from example/zoo-app-v2/spec/pact/providers/animal_service/animal_service_client_spec.rb rename to example/zoo-app/spec/pact/providers/animal_service/animal_service_client_spec.rb index c9170c21..c4023b4f 100644 --- a/example/zoo-app-v2/spec/pact/providers/animal_service/animal_service_client_spec.rb +++ b/example/zoo-app/spec/pact/providers/animal_service/animal_service_client_spec.rb @@ -1,7 +1,7 @@ -require 'pact/v2/rspec' +require 'pact/rspec' require 'zoo_app/animal_service_client' -RSpec.describe 'ZooApp::AnimalServiceClient', :pact_v2 do +RSpec.describe 'ZooApp::AnimalServiceClient', :pact do has_http_pact_between 'Zoo App', 'Animal Service', opts: { pact_specification: 'V4' } subject { ZooApp::AnimalServiceClient } @@ -20,8 +20,14 @@ super() .given('there is an alligator named {alligator_name}', { alligator_name: alligator_name }) .upon_receiving('a request for an alligator') - .with_request(method: :get, path: generate_from_provider_state(expression: '/alligators/${alligator_name}', - example: '/alligators/Mary'), headers: headers) + .with_request( + method: :get, + path: generate_from_provider_state( + expression: '/alligators/${alligator_name}', + example: '/alligators/Mary' + ), + headers: headers + ) .will_respond_with(status: 200, body: alligator_body, headers: content_headers) end diff --git a/example/zoo-app-v2/spec/pacts/Zoo App-Animal Service.json b/example/zoo-app/spec/pacts/Zoo App-Animal Service.json similarity index 99% rename from example/zoo-app-v2/spec/pacts/Zoo App-Animal Service.json rename to example/zoo-app/spec/pacts/Zoo App-Animal Service.json index 94800cea..abd65cbb 100644 --- a/example/zoo-app-v2/spec/pacts/Zoo App-Animal Service.json +++ b/example/zoo-app/spec/pacts/Zoo App-Animal Service.json @@ -117,7 +117,7 @@ } ], "metadata": { - "pact-ruby-v2": { + "pact-ruby": { "pact-ffi": "0.4.28" }, "pactRust": { diff --git a/example/zoo-app/spec/pacts/zoo_app-animal_service.json b/example/zoo-app/spec/pacts/zoo_app-animal_service.json deleted file mode 100644 index 26edd2b4..00000000 --- a/example/zoo-app/spec/pacts/zoo_app-animal_service.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "consumer": { - "name": "Zoo App" - }, - "provider": { - "name": "Animal Service" - }, - "interactions": [ - { - "description": "a request for an alligator", - "providerState": "there is an alligator named Mary", - "request": { - "method": "get", - "path": "/alligators/Mary", - "headers": { - "Accept": "application/json" - } - }, - "response": { - "status": 200, - "headers": { - "Content-Type": "application/json;charset=utf-8" - }, - "body": { - "name": "Mary" - } - } - }, - { - "description": "a request for an alligator", - "providerState": "there is not an alligator named Mary", - "request": { - "method": "get", - "path": "/alligators/Mary", - "headers": { - "Accept": "application/json" - } - }, - "response": { - "status": 404, - "headers": { - } - } - }, - { - "description": "a request for an alligator", - "providerState": "an error occurs retrieving an alligator", - "request": { - "method": "get", - "path": "/alligators/Mary", - "headers": { - "Accept": "application/json" - } - }, - "response": { - "status": 500, - "headers": { - "Content-Type": "application/json;charset=utf-8" - }, - "body": { - "error": "Argh!!!" - } - } - } - ], - "metadata": { - "pactSpecification": { - "version": "2.0.0" - } - } -} \ No newline at end of file diff --git a/example/zoo-app-v2/spec/rails_helper.rb b/example/zoo-app/spec/rails_helper.rb similarity index 99% rename from example/zoo-app-v2/spec/rails_helper.rb rename to example/zoo-app/spec/rails_helper.rb index 477edeaa..751b9a62 100644 --- a/example/zoo-app-v2/spec/rails_helper.rb +++ b/example/zoo-app/spec/rails_helper.rb @@ -1,4 +1,3 @@ - require "combustion" begin diff --git a/example/zoo-app/spec/service_providers/animal_service_client_spec.rb b/example/zoo-app/spec/service_providers/animal_service_client_spec.rb deleted file mode 100644 index c2f015c1..00000000 --- a/example/zoo-app/spec/service_providers/animal_service_client_spec.rb +++ /dev/null @@ -1,71 +0,0 @@ -require_relative 'pact_helper' -require 'zoo_app/animal_service_client' - -module ZooApp - describe AnimalServiceClient, :pact => true do - - before do - AnimalServiceClient.base_uri animal_service.mock_service_base_url - end - - describe ".find_alligator_by_name" do - context "when an alligator by the given name exists" do - - before do - animal_service.given("there is an alligator named Mary"). - upon_receiving("a request for an alligator").with( - method: :get, - path: '/alligators/Mary', - headers: {'Accept' => 'application/json'} ). - will_respond_with( - status: 200, - headers: {'Content-Type' => 'application/json;charset=utf-8'}, - body: {name: 'Mary'} - ) - end - - it "returns the alligator" do - expect(AnimalServiceClient.find_alligator_by_name("Mary")).to eq ZooApp::Animals::Alligator.new(name: 'Mary') - end - - end - - context "when an alligator by the given name does not exist" do - - before do - animal_service.given("there is not an alligator named Mary"). - upon_receiving("a request for an alligator").with( - method: :get, - path: '/alligators/Mary', - headers: {'Accept' => 'application/json'} ). - will_respond_with(status: 404) - end - - it "returns nil" do - expect(AnimalServiceClient.find_alligator_by_name("Mary")).to be_nil - end - - end - - context "when an error occurs retrieving the alligator" do - - before do - animal_service.given("an error occurs retrieving an alligator"). - upon_receiving("a request for an alligator").with( - method: :get, - path: '/alligators/Mary', - headers: {'Accept' => 'application/json'}). - will_respond_with( - status: 500, - headers: { 'Content-Type' => 'application/json;charset=utf-8'}, - body: {error: 'Argh!!!'}) - end - - it "raises an error" do - expect{ AnimalServiceClient.find_alligator_by_name("Mary") }.to raise_error /Argh/ - end - - end - end - end -end \ No newline at end of file diff --git a/example/zoo-app/spec/service_providers/pact_helper.rb b/example/zoo-app/spec/service_providers/pact_helper.rb deleted file mode 100644 index 592e71f9..00000000 --- a/example/zoo-app/spec/service_providers/pact_helper.rb +++ /dev/null @@ -1,16 +0,0 @@ -require_relative '../spec_helper' -require 'pact/consumer/rspec' - -Pact.configure do | config | - config.doc_generator = :markdown -end - -Pact.service_consumer 'Zoo App' do - has_pact_with "Animal Service" do - mock_service :animal_service do - port 8888 - pact_specification_version "2.0.0" - end - end -end - diff --git a/example/zoo-app/spec/spec_helper.rb b/example/zoo-app/spec/spec_helper.rb deleted file mode 100644 index cd642638..00000000 --- a/example/zoo-app/spec/spec_helper.rb +++ /dev/null @@ -1,6 +0,0 @@ -$: << File.expand_path("../../lib", __FILE__) - -RSpec.configure do | config | - config.color = true - config.formatter = :documentation -end \ No newline at end of file diff --git a/lib/pact.rb b/lib/pact.rb index cc77b570..01756361 100644 --- a/lib/pact.rb +++ b/lib/pact.rb @@ -1,12 +1,47 @@ -require 'pact/support' -require 'pact/version' -require 'pact/configuration' -require 'pact/consumer' -require 'pact/provider' -require 'pact/consumer_contract' - -begin - require 'pact/v2' -rescue LoadError - # "Please ensure that the 'pact-ffi' gem is included in your Gemfile for pact/v2 support." +# frozen_string_literal: true + +require 'zeitwerk' +require 'pact/ffi' + +require 'pact/railtie' if defined?(Rails::Railtie) + +module Pact + class Error < StandardError; end + + class ImplementationRequired < Error; end + + class FfiError < Error + def initialize(msg, reason, status) + super(msg) + + @msg = msg + @reason = reason + @status = status + end + + def message + "FFI error: reason: #{@reason}, status: #{@status}, message: #{@msg}" + end + end + + def self.configure + yield configuration if block_given? + end + + def self.configuration + @configuration ||= Pact::Configuration.new + end end + +loader = Zeitwerk::Loader.new +loader.push_dir(File.join(__dir__)) + +loader.tag = 'pact' + +loader.ignore("#{__dir__}/pact/version.rb") +loader.ignore("#{__dir__}/pact/rspec.rb") +loader.ignore("#{__dir__}/pact/rspec") +loader.ignore("#{__dir__}/pact/railtie.rb") unless defined?(Rails::Railtie) + +loader.setup +loader.eager_load diff --git a/lib/pact/cli.rb b/lib/pact/cli.rb deleted file mode 100755 index 5541139e..00000000 --- a/lib/pact/cli.rb +++ /dev/null @@ -1,45 +0,0 @@ -require 'thor' -require 'pact/consumer/configuration' -require 'pact/provider/configuration' - -module Pact - class CLI < Thor - def self.exit_on_failure? # Thor 1.0 deprecation guard - false - end - - desc 'verify', "Verify a pact" - method_option :pact_helper, aliases: "-h", desc: "Pact helper file", :required => true - method_option :pact_uri, aliases: "-p", desc: "Pact URI" - method_option :ignore_failures, type: :boolean, default: false, desc: "Process will always exit with exit code 0", hide: true - method_option :pact_broker_username, aliases: "-u", desc: "Pact broker user name" - method_option :pact_broker_password, aliases: "-w", desc: "Pact broker password" - method_option :pact_broker_token, aliases: "-k", desc: "Pact broker token" - method_option :backtrace, aliases: "-b", desc: "Show full backtrace", :default => false, :type => :boolean - method_option :verbose, aliases: "-v", desc: "Show verbose HTTP logging", :default => false, :type => :boolean - method_option :interactions_replay_order, aliases: "-o", - desc: "Interactions replay order: randomised or recorded (default)", - default: Pact.configuration.interactions_replay_order - method_option :description, aliases: "-d", desc: "Interaction description filter" - method_option :provider_state, aliases: "-s", desc: "Provider state filter" - method_option :interaction_index, type: :numeric, desc: "Index filter" - method_option :pact_broker_interaction_id, desc: "Pact Broker interaction ID filter" - method_option :format, aliases: "-f", banner: "FORMATTER", desc: "RSpec formatter. Defaults to custom Pact formatter. [j]son may also be used." - method_option :out, aliases: "-o", banner: "FILE", desc: "Write output to a file instead of $stdout." - - def verify - require 'pact/cli/run_pact_verification' - Cli::RunPactVerification.call(options) - end - - desc 'docs', "Generate Pact documentation in markdown" - method_option :pact_dir, desc: "Directory containing the pacts", default: Pact.configuration.pact_dir - method_option :doc_dir, desc: "Documentation directory", default: Pact.configuration.doc_dir - - def docs - require 'pact/cli/generate_pact_docs' - require 'pact/doc/generator' - Pact::Doc::Generate.call(options[:pact_dir], options[:doc_dir], [Pact::Doc::Markdown::Generator]) - end - end -end diff --git a/lib/pact/cli/generate_pact_docs.rb b/lib/pact/cli/generate_pact_docs.rb deleted file mode 100644 index 29f57f68..00000000 --- a/lib/pact/cli/generate_pact_docs.rb +++ /dev/null @@ -1,4 +0,0 @@ -require 'pact/doc/doc_file' -require 'pact/doc/generate' -require 'pact/doc/markdown/generator' -require 'pact/consumer' diff --git a/lib/pact/cli/run_pact_verification.rb b/lib/pact/cli/run_pact_verification.rb deleted file mode 100644 index daa50e94..00000000 --- a/lib/pact/cli/run_pact_verification.rb +++ /dev/null @@ -1,99 +0,0 @@ -require 'pact/cli/spec_criteria' - -module Pact - module Cli - class RunPactVerification - attr_reader :options - - def initialize options - @options = options - end - - def self.call options - new(options).call - end - - def call - configure_output - initialize_rspec - setup_load_path - load_pact_helper - run_specs - end - - private - - def initialize_rspec - # With RSpec3, if the pact_helper loads a library that adds its own formatter before we set one, - # we will get a ProgressFormatter too, and get little dots sprinkled throughout our output. - # Load a NilFormatter here to prevent that. - require 'rspec' - require 'pact/rspec' - ::RSpec.configuration.add_formatter Pact::RSpec.formatter_class.const_get('NilFormatter') - end - - def setup_load_path - require 'pact/provider/pact_spec_runner' - lib = File.join(Dir.pwd, "lib") # Assume we are running from within the project root. RSpec is smarter about this. - spec = File.join(Dir.pwd, "spec") - $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) - $LOAD_PATH.unshift(spec) if Dir.exist?(spec) && !$LOAD_PATH.include?(spec) - end - - def load_pact_helper - load options[:pact_helper] - end - - def run_specs - exit_code = if options[:pact_uri].is_a?(String) - run_with_pact_url_string - elsif options[:pact_uri] - run_with_pact_uri_object # from pact-provider-verifier - else - run_with_configured_pacts_from_pact_helper - end - exit exit_code - end - - def run_with_pact_url_string - pact_repository_uri_options = {} - pact_repository_uri_options[:username] = ENV['PACT_BROKER_USERNAME'] if ENV['PACT_BROKER_USERNAME'] - pact_repository_uri_options[:password] = ENV['PACT_BROKER_PASSWORD'] if ENV['PACT_BROKER_PASSWORD'] - pact_repository_uri_options[:token] = ENV['PACT_BROKER_TOKEN'] - pact_repository_uri_options[:username] = options[:pact_broker_username] if options[:pact_broker_username] - pact_repository_uri_options[:password] = options[:pact_broker_password] if options[:pact_broker_password] - pact_uri = ::Pact::Provider::PactURI.new(options[:pact_uri], pact_repository_uri_options) - Pact::Provider::PactSpecRunner.new([pact_uri], pact_spec_options).run - end - - def run_with_pact_uri_object - Pact::Provider::PactSpecRunner.new([options[:pact_uri]], pact_spec_options).run - end - - def run_with_configured_pacts_from_pact_helper - pact_urls = Pact.provider_world.pact_urls - Pact::Provider::PactSpecRunner.new(pact_urls, pact_spec_options).run - end - - def pact_spec_options - { - full_backtrace: options[:backtrace], - verbose: options[:verbose] || ENV['VERBOSE'] == 'true', - criteria: SpecCriteria.call(options), - format: options[:format], - out: options[:out], - ignore_failures: options[:ignore_failures], - request_customizer: options[:request_customizer] - } - end - - def configure_output - if options[:format] == 'json' && !options[:out] - # Don't want to mess up the JSON parsing with messages to stdout, so send it to stderr - require 'pact/configuration' - Pact.configuration.output_stream = Pact.configuration.error_stream - end - end - end - end -end diff --git a/lib/pact/cli/spec_criteria.rb b/lib/pact/cli/spec_criteria.rb deleted file mode 100644 index b2212af6..00000000 --- a/lib/pact/cli/spec_criteria.rb +++ /dev/null @@ -1,26 +0,0 @@ -module Pact - module Cli - class SpecCriteria - - def self.call options - criteria = {} - - criteria[:description] = Regexp.new(options[:description]) if options[:description] - criteria[:_id] = options[:pact_broker_interaction_id] if options[:pact_broker_interaction_id] - criteria[:index] = options[:interaction_index] if options[:interaction_index] - - provider_state = options[:provider_state] - - if provider_state - if provider_state.length == 0 - criteria[:provider_state] = nil #Allow PACT_PROVIDER_STATE="" to mean no provider state - else - criteria[:provider_state] = Regexp.new(provider_state) - end - end - - criteria - end - end - end -end diff --git a/lib/pact/configuration.rb b/lib/pact/configuration.rb new file mode 100644 index 00000000..c912d93b --- /dev/null +++ b/lib/pact/configuration.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Pact + class Configuration + attr_reader :before_provider_state_proc, :after_provider_state_proc + + class GlobalProviderConfigurationError < ::Pact::Error; end + + def before_provider_state_setup(&block) + raise GlobalProviderConfigurationError, 'no block given' unless block + + @before_provider_state_proc = block + end + + def after_provider_state_teardown(&block) + raise GlobalProviderConfigurationError, 'no block given' unless block + + @after_provider_state_proc = block + end + end +end diff --git a/lib/pact/consumer.rb b/lib/pact/consumer.rb index 37bd7934..10c356e2 100644 --- a/lib/pact/consumer.rb +++ b/lib/pact/consumer.rb @@ -1,7 +1,6 @@ -require 'pact/consumer_contract' -require 'pact/consumer/configuration' -require 'pact/consumer/consumer_contract_builder' -require 'pact/consumer/consumer_contract_builders' -require 'pact/consumer/interaction_builder' -require 'pact/term' -require 'pact/something_like' +# frozen_string_literal: true + +module Pact + module Consumer + end +end diff --git a/lib/pact/consumer/configuration.rb b/lib/pact/consumer/configuration.rb deleted file mode 100644 index 603754e2..00000000 --- a/lib/pact/consumer/configuration.rb +++ /dev/null @@ -1,10 +0,0 @@ -require 'pact/configuration' -require 'pact/consumer/consumer_contract_builders' -require 'pact/consumer/consumer_contract_builder' -require 'pact/consumer/configuration/service_consumer' -require 'pact/consumer/configuration/service_provider' -require 'pact/consumer/configuration/dsl' -require 'pact/consumer/configuration/configuration_extensions' - -Pact.send(:extend, Pact::Consumer::DSL) -Pact::Configuration.send(:include, Pact::Consumer::Configuration::ConfigurationExtensions) \ No newline at end of file diff --git a/lib/pact/consumer/configuration/configuration_extensions.rb b/lib/pact/consumer/configuration/configuration_extensions.rb deleted file mode 100644 index db050f9f..00000000 --- a/lib/pact/consumer/configuration/configuration_extensions.rb +++ /dev/null @@ -1,90 +0,0 @@ -require 'pact/configuration' -require 'pact/doc/markdown/generator' - -module Pact - module Consumer - module Configuration - - module ConfigurationExtensions - - DOC_GENERATORS = { markdown: Pact::Doc::Markdown::Generator } - - def doc_dir - @doc_dir ||= File.expand_path("./doc/pacts") - end - - def doc_dir= doc_dir - @doc_dir = doc_dir - end - - def reports_dir - @reports_dir ||= default_reports_dir - end - - def default_reports_dir - File.expand_path("./reports/pacts") - end - - def reports_dir= reports_dir - @reports_dir = reports_dir - end - - def add_provider_verification &block - provider_verifications << block - end - - def provider_verifications - @provider_verifications ||= [] - end - - def doc_generator= doc_generator - add_doc_generator doc_generator - end - - def add_doc_generator doc_generator - doc_generators << begin - if DOC_GENERATORS[doc_generator] - DOC_GENERATORS[doc_generator] - elsif doc_generator.respond_to?(:call) - doc_generator - else - raise "doc_generator needs to respond to call, or be in the preconfigured list: #{DOC_GENERATORS.keys}" - end - end - end - - def doc_generators - @doc_generators ||= [] - end - - def pactfile_write_mode - @pactfile_write_mode ||= :overwrite - if @pactfile_write_mode == :smart - is_rake_running? ? :overwrite : :update - else - @pactfile_write_mode - end - end - - def pactfile_write_mode= pactfile_write_mode - @pactfile_write_mode = pactfile_write_mode - end - - def pactfile_write_order - @pactfile_write_order ||= :chronological #or :alphabetical - end - - def pactfile_write_order= pactfile_write_order - @pactfile_write_order = pactfile_write_order.to_sym - end - - private - - #Would love a better way of determining this! It sure won't work on windows. - def is_rake_running? - `ps -ef | grep rake | grep #{Process.ppid} | grep -v 'grep'`.size > 0 - end - end - end - end -end diff --git a/lib/pact/consumer/configuration/dsl.rb b/lib/pact/consumer/configuration/dsl.rb deleted file mode 100644 index 3017c9f3..00000000 --- a/lib/pact/consumer/configuration/dsl.rb +++ /dev/null @@ -1,11 +0,0 @@ -require 'pact/consumer/configuration/service_consumer' - -module Pact - module Consumer - module DSL - def service_consumer name, &block - Configuration::ServiceConsumer.build(name, &block) - end - end - end -end \ No newline at end of file diff --git a/lib/pact/consumer/configuration/mock_service.rb b/lib/pact/consumer/configuration/mock_service.rb deleted file mode 100644 index 13b368c5..00000000 --- a/lib/pact/consumer/configuration/mock_service.rb +++ /dev/null @@ -1,112 +0,0 @@ -require 'pact/mock_service/version' -require 'pact/mock_service/app_manager' -require 'pact/consumer/consumer_contract_builder' -require 'pact/consumer/consumer_contract_builders' -require 'pact/consumer/world' - -module Pact - module Consumer - module Configuration - class MockService - - extend Pact::DSL - - attr_accessor :port, :host, :standalone, :verify, :provider_name, :consumer_name, :pact_specification_version - - def initialize name, consumer_name, provider_name - @name = name - @consumer_name = consumer_name - @provider_name = provider_name - @port = nil - @host = "localhost" - @standalone = false - @verify = true - @pact_specification_version = '2' - @finalized = false - end - - dsl do - def port port - self.port = port - end - - def host host - self.host = host - end - - def standalone standalone - self.standalone = standalone - end - - def verify verify - self.verify = verify - end - - def pact_specification_version pact_specification_version - self.pact_specification_version = pact_specification_version - end - end - - def finalize - raise 'Already finalized!' if @finalized - register_mock_service - configure_consumer_contract_builder - @finalized = true - end - - private - - def register_mock_service - unless standalone - url = "http://#{host}#{port.nil? ? '' : ":#{port}"}" - ret = Pact::MockService::AppManager.instance.register_mock_service_for(provider_name, url, mock_service_options) - raise "pact-mock_service(v#{Pact::MockService::VERSION}) does not support 'find available port' feature" unless ret - @port = ret - end - end - - def configure_consumer_contract_builder - consumer_contract_builder = create_consumer_contract_builder - create_consumer_contract_builders_method consumer_contract_builder - setup_verification(consumer_contract_builder) if verify - consumer_contract_builder - end - - def create_consumer_contract_builder - consumer_contract_builder_fields = { - :consumer_name => consumer_name, - :provider_name => provider_name, - :pactfile_write_mode => Pact.configuration.pactfile_write_mode, - :port => port, - :host => host, - :pact_dir => Pact.configuration.pact_dir - } - Pact::Consumer::ConsumerContractBuilder.new consumer_contract_builder_fields - end - - def setup_verification consumer_contract_builder - Pact.configuration.add_provider_verification do | example_description | - consumer_contract_builder.verify example_description - end - end - - # This makes the consumer_contract_builder available via a module method with the given identifier - # as the method name. - # I feel this should be defined somewhere else, but I'm not sure where - def create_consumer_contract_builders_method consumer_contract_builder - Pact::Consumer::ConsumerContractBuilders.send(:define_method, @name.to_sym) do - consumer_contract_builder - end - Pact.consumer_world.add_consumer_contract_builder consumer_contract_builder - end - - def mock_service_options - { - pact_specification_version: pact_specification_version, - find_available_port: port.nil?, - } - end - end - end - end -end diff --git a/lib/pact/consumer/configuration/service_consumer.rb b/lib/pact/consumer/configuration/service_consumer.rb deleted file mode 100644 index 32832705..00000000 --- a/lib/pact/consumer/configuration/service_consumer.rb +++ /dev/null @@ -1,51 +0,0 @@ -require 'pact/shared/dsl' - -module Pact - module Consumer - module Configuration - class ServiceConsumer - - extend Pact::DSL - - attr_accessor :app, :port, :name - - def initialize name - @name = name - @app = nil - @port = nil - end - - dsl do - def app app - self.app = app - end - - def port port - self.port = port - end - - def has_pact_with service_provider_name, &block - ServiceProvider.build(service_provider_name, name, &block) - end - end - - def finalize - validate - register_consumer_app if @app - end - - private - - def validate - raise "Please provide a consumer name" unless (name && !name.empty?) - raise "Please provide a port for the consumer app" if app && !port - end - - - def register_consumer_app - Pact::MockService::AppManager.instance.register app, port - end - end - end - end -end \ No newline at end of file diff --git a/lib/pact/consumer/configuration/service_provider.rb b/lib/pact/consumer/configuration/service_provider.rb deleted file mode 100644 index 6a78912f..00000000 --- a/lib/pact/consumer/configuration/service_provider.rb +++ /dev/null @@ -1,40 +0,0 @@ -require 'pact/shared/dsl' -require 'pact/consumer/configuration/mock_service' - -module Pact - module Consumer - module Configuration - class ServiceProvider - - extend Pact::DSL - - attr_accessor :service, :consumer_name, :name - - def initialize name, consumer_name - @name = name - @service = nil - @consumer_name = consumer_name - end - - dsl do - def mock_service name, &block - self.service = MockService.build(name, consumer_name, self.name, &block) - end - end - - def finalize - validate - end - - private - - def validate - raise "Please configure a service for #{name}" unless service - end - - end - - end - end - -end \ No newline at end of file diff --git a/lib/pact/consumer/consumer_contract_builder.rb b/lib/pact/consumer/consumer_contract_builder.rb deleted file mode 100644 index a431c77e..00000000 --- a/lib/pact/consumer/consumer_contract_builder.rb +++ /dev/null @@ -1,82 +0,0 @@ -require 'uri' -require 'json/add/regexp' -require 'pact/logging' -require 'pact/mock_service/client' -require 'pact/consumer/interaction_builder' - -module Pact - module Consumer - - class ConsumerContractBuilder - - include Pact::Logging - - attr_reader :mock_service_base_url - - def initialize(attributes) - @interaction_builder = nil - @consumer_contract_details = { - consumer: {name: attributes[:consumer_name]}, - provider: {name: attributes[:provider_name]}, - pactfile_write_mode: attributes[:pactfile_write_mode].to_s, - pact_dir: attributes.fetch(:pact_dir) - } - @mock_service_client = Pact::MockService::Client.new(attributes[:port], attributes[:host]) - @mock_service_base_url = "http://#{attributes[:host]}:#{attributes[:port]}" - end - - def without_writing_to_pact - interaction_builder.without_writing_to_pact - end - - def given(provider_state) - interaction_builder.given(provider_state) - end - - def upon_receiving(description) - interaction_builder.upon_receiving(description) - end - - def verify example_description - mock_service_client.verify example_description - end - - def log msg - mock_service_client.log msg - end - - def write_pact - mock_service_client.write_pact @consumer_contract_details - end - - def wait_for_interactions options = {} - wait_max_seconds = options.fetch(:wait_max_seconds, 3) - poll_interval = options.fetch(:poll_interval, 0.1) - mock_service_client.wait_for_interactions wait_max_seconds, poll_interval - end - - # @raise Pact::InvalidInteractionError - def handle_interaction_fully_defined interaction - interaction.validate! - mock_service_client.add_expected_interaction interaction #TODO: What will happen if duplicate added? - self.interaction_builder = nil - end - - private - - attr_reader :mock_service_client - attr_writer :interaction_builder - - def interaction_builder - @interaction_builder ||= - begin - interaction_builder = InteractionBuilder.new do | interaction | - handle_interaction_fully_defined(interaction) - end - interaction_builder - end - end - - end - end -end diff --git a/lib/pact/consumer/consumer_contract_builders.rb b/lib/pact/consumer/consumer_contract_builders.rb deleted file mode 100644 index aa6ef9e7..00000000 --- a/lib/pact/consumer/consumer_contract_builders.rb +++ /dev/null @@ -1,10 +0,0 @@ -module Pact - module Consumer - module ConsumerContractBuilders - # Placeholder for mock service methods which will be dynamically created - # from the Pact configuration. - - # To be included in RSpec - end - end -end \ No newline at end of file diff --git a/lib/pact/consumer/grpc_interaction_builder.rb b/lib/pact/consumer/grpc_interaction_builder.rb new file mode 100644 index 00000000..ed597862 --- /dev/null +++ b/lib/pact/consumer/grpc_interaction_builder.rb @@ -0,0 +1,192 @@ +# frozen_string_literal: true + +require "pact/ffi/sync_message_consumer" +require "pact/ffi/plugin_consumer" +require "pact/ffi/logger" + +module Pact + module Consumer + class GrpcInteractionBuilder + CONTENT_TYPE = "application/protobuf" + GRPC_CONTENT_TYPE = "application/grpc" + PROTOBUF_PLUGIN_NAME = "protobuf" + PROTOBUF_PLUGIN_VERSION = "0.6.5" + + class PluginInitError < Pact::FfiError; end + + # https://docs.rs/pact_ffi/0.4.17/pact_ffi/plugins/fn.pactffi_using_plugin.html + INIT_PLUGIN_ERRORS = { + 1 => {reason: :internal_error, status: 1, description: "A general panic was caught"}, + 2 => {reason: :plugin_load_failed, status: 2, description: "Failed to load the plugin"}, + 3 => {reason: :invalid_handle, status: 3, description: "Pact Handle is not valid"} + }.freeze + + # https://docs.rs/pact_ffi/0.4.17/pact_ffi/plugins/fn.pactffi_interaction_contents.html + CREATE_INTERACTION_ERRORS = { + 1 => {reason: :internal_error, status: 1, description: "A general panic was caught"}, + 2 => {reason: :mock_server_already_running, status: 2, description: "The mock server has already been started"}, + 3 => {reason: :invalid_handle, status: 3, description: "The interaction handle is invalid"}, + 4 => {reason: :invalid_content_type, status: 4, description: "The content type is not valid"}, + 5 => {reason: :invalid_contents, status: 5, description: "The contents JSON is not valid JSON"}, + 6 => {reason: :plugin_error, status: 6, description: "The plugin returned an error"} + }.freeze + + class CreateInteractionError < Pact::FfiError; end + + class InteractionMismatchesError < Pact::Error; end + + class InteractionBuilderError < Pact::Error; end + + def initialize(pact_config, description: nil) + @pact_config = pact_config + @description = description || "" + @proto_path = nil + @proto_include_dirs = [] + @service_name = nil + @method_name = nil + @request = nil + @response = nil + @response_meta = nil + @provider_state_meta = nil + end + + def with_service(proto_path, method, include_dirs = []) + raise InteractionBuilderError.new("invalid grpc method: cannot be blank") if method.blank? + + service_name, method_name = method.split("/") || [] + raise InteractionBuilderError.new("invalid grpc method: #{method}, should be like service/SomeMethod") if service_name.blank? || method_name.blank? + + absolute_path = File.expand_path(proto_path) + raise InteractionBuilderError.new("proto file #{proto_path} does not exist") unless File.exist?(absolute_path) + + @proto_path = absolute_path + @service_name = service_name + @method_name = method_name + @proto_include_dirs = include_dirs.map { |dir| File.expand_path(dir) } + + self + end + + def with_pact_protobuf_plugin_version(version) + raise InteractionBuilderError.new("version is required") if version.blank? + + @proto_plugin_version = version + self + end + + def given(provider_state, metadata = {}) + @provider_state_meta = {provider_state => metadata} + self + end + + def upon_receiving(description) + @description = description + self + end + + def with_request(req_hash) + @request = InteractionContents.plugin(req_hash) + self + end + + def will_respond_with(resp_hash) + @response = InteractionContents.plugin(resp_hash) + self + end + + def will_respond_with_meta(meta_hash) + @response_meta = InteractionContents.plugin(meta_hash) + self + end + + def interaction_json + result = { + "pact:proto": @proto_path, + "pact:proto-service": "#{@service_name}/#{@method_name}", + "pact:content-type": CONTENT_TYPE, + request: @request + } + + result["pact:protobuf-config"] = {additionalIncludes: @proto_include_dirs} if @proto_include_dirs.present? + + result[:response] = @response if @response.is_a?(Hash) + result[:responseMetadata] = @response_meta if @response_meta.is_a?(Hash) + + JSON.dump(result) + end + + def validate! + raise InteractionBuilderError.new("uninitialized service params, use #with_service to configure") if @proto_path.blank? || @service_name.blank? || @method_name.blank? + raise InteractionBuilderError.new("invalid request format, should be a hash") unless @request.is_a?(Hash) + raise InteractionBuilderError.new("invalid response format, should be a hash") unless @response.is_a?(Hash) || @response_meta.is_a?(Hash) + end + + def execute(&block) + raise InteractionBuilderError.new("interaction is designed to be used one-time only") if defined?(@used) + + validate! + + pact_handle = init_pact + init_plugin!(pact_handle) + + message_pact = PactFfi::SyncMessageConsumer.new_interaction(pact_handle, @description) + @provider_state_meta&.each_pair do |provider_state, meta| + if meta.present? + meta.each_pair { |k, v| PactFfi.given_with_param(message_pact, provider_state, k.to_s, v.to_s) } + else + PactFfi.given(message_pact, provider_state) + end + end + + result = PactFfi::PluginConsumer.interaction_contents(message_pact, 0, GRPC_CONTENT_TYPE, interaction_json) + if CREATE_INTERACTION_ERRORS[result].present? + error = CREATE_INTERACTION_ERRORS[result] + raise CreateInteractionError.new("There was an error while trying to add interaction \"#{@description}\"", error[:reason], error[:status]) + end + + mock_server = MockServer.create_for_grpc!(pact: pact_handle, host: @pact_config.mock_host, port: @pact_config.mock_port) + + yield(message_pact, mock_server) + + ensure + if mock_server.matched? + mock_server.write_pacts!(@pact_config.pact_dir) + else + msg = mismatches_error_msg(mock_server) + raise InteractionMismatchesError.new(msg) + end + @used = true + mock_server&.cleanup + PactFfi::PluginConsumer.cleanup_plugins(pact_handle) + PactFfi.free_pact_handle(pact_handle) + end + + private + + def mismatches_error_msg(mock_server) + rspec_example_desc = RSpec.current_example&.description + return "interaction for #{@service_name}/#{@method_name} has mismatches: #{mock_server.mismatches}" if rspec_example_desc.blank? + + "#{rspec_example_desc} has mismatches: #{mock_server.mismatches}" + end + + def init_pact + handle = PactFfi.new_pact(@pact_config.consumer_name, @pact_config.provider_name) + PactFfi.with_specification(handle, PactFfi::FfiSpecificationVersion["SPECIFICATION_VERSION_V4"]) + PactFfi.with_pact_metadata(handle, "pact-ruby", "pact-ffi", PactFfi.version) + + Pact::Native::Logger.log_to_stdout(@pact_config.log_level) + + handle + end + + def init_plugin!(pact_handle) + result = PactFfi::PluginConsumer.using_plugin(pact_handle, PROTOBUF_PLUGIN_NAME, @proto_plugin_version || PROTOBUF_PLUGIN_VERSION) + return result if INIT_PLUGIN_ERRORS[result].blank? + + error = INIT_PLUGIN_ERRORS[result] + raise PluginInitError.new("There was an error while trying to initialize plugin #{PROTOBUF_PLUGIN_NAME}/#{@proto_plugin_version || PROTOBUF_PLUGIN_VERSION}", error[:reason], error[:status]) + end + end + end +end diff --git a/lib/pact/consumer/http_interaction_builder.rb b/lib/pact/consumer/http_interaction_builder.rb new file mode 100644 index 00000000..cb383aa4 --- /dev/null +++ b/lib/pact/consumer/http_interaction_builder.rb @@ -0,0 +1,160 @@ +# frozen_string_literal: true + +require "pact/ffi/sync_message_consumer" +require "pact/ffi/plugin_consumer" +require "pact/ffi/logger" +require "json" + +module Pact + module Consumer + class HttpInteractionBuilder + + # https://docs.rs/pact_ffi/0.4.17/pact_ffi/plugins/fn.pactffi_interaction_contents.html + CREATE_INTERACTION_ERRORS = { + 1 => {reason: :internal_error, status: 1, description: "A general panic was caught"}, + 2 => {reason: :mock_server_already_running, status: 2, description: "The mock server has already been started"}, + 3 => {reason: :invalid_handle, status: 3, description: "The interaction handle is invalid"}, + 4 => {reason: :invalid_content_type, status: 4, description: "The content type is not valid"}, + 5 => {reason: :invalid_contents, status: 5, description: "The contents JSON is not valid JSON"}, + 6 => {reason: :plugin_error, status: 6, description: "The plugin returned an error"} + }.freeze + + class CreateInteractionError < Pact::FfiError; end + + class InteractionMismatchesError < Pact::Error; end + + class InteractionBuilderError < Pact::Error; end + + class << self + def create_finalizer(pact_handle) + proc { PactFfi.free_pact_handle(pact_handle) } + end + end + + def initialize(pact_config, description: nil) + @pact_config = pact_config + @description = description || "" + + @pact_handle = pact_config.pact_handle ||= init_pact + @pact_interaction = PactFfi.new_interaction(pact_handle, @description) + + ObjectSpace.define_finalizer(self, self.class.create_finalizer(pact_interaction)) + end + + def given(provider_state, metadata = {}) + if metadata.present? + PactFfi.given_with_params(pact_interaction, provider_state, JSON.dump(metadata)) + else + PactFfi.given(pact_interaction, provider_state) + end + + self + end + + def upon_receiving(description) + @description = description + PactFfi.upon_receiving(pact_interaction, @description) + self + end + + def with_request(method: nil, path: nil, query: {}, headers: {}, body: nil) + interaction_part = PactFfi::FfiInteractionPart["INTERACTION_PART_REQUEST"] + PactFfi.with_request(pact_interaction, method.to_s, format_value(path)) + + # Processing as an array of hashes, allows us to consider duplicate keys + # which should be passed to the core, at a non 0 index + if query.is_a?(Array) + key_index = Hash.new(0) + query.each do |query_item| + InteractionContents.basic(query_item).each_pair do |key, value_item| + PactFfi.with_query_parameter_v2(pact_interaction, key.to_s, key_index[key], format_value(value_item)) + key_index[key] += 1 + end + end + else + InteractionContents.basic(query).each_pair do |key, value_item| + PactFfi.with_query_parameter_v2(pact_interaction, key.to_s, 0, format_value(value_item)) + end + end + + InteractionContents.basic(headers).each_pair do |key, value_item| + PactFfi.with_header_v2(pact_interaction, interaction_part, key.to_s, 0, format_value(value_item)) + end + + if body + PactFfi.with_body(pact_interaction, interaction_part, "application/json", format_value(InteractionContents.basic(body))) + end + + self + end + + def will_respond_with(status: nil, headers: {}, body: nil) + interaction_part = PactFfi::FfiInteractionPart["INTERACTION_PART_RESPONSE"] + PactFfi.response_status(pact_interaction, status) + + InteractionContents.basic(headers).each_pair do |key, value_item| + PactFfi.with_header_v2(pact_interaction, interaction_part, key.to_s, 0, format_value(value_item)) + end + + if body + PactFfi.with_body(pact_interaction, interaction_part, "application/json", format_value(InteractionContents.basic(body))) + end + + self + end + + def execute(&block) + raise InteractionBuilderError.new("interaction is designed to be used one-time only") if defined?(@used) + + mock_server = MockServer.create_for_http!( + pact: pact_handle, host: pact_config.mock_host, port: pact_config.mock_port + ) + + yield(mock_server) + + ensure + if mock_server.matched? + mock_server.write_pacts!(pact_config.pact_dir) + else + msg = mismatches_error_msg(mock_server) + raise InteractionMismatchesError.new(msg) + end + @used = true + mock_server&.cleanup + # Reset the pact handle to allow for a new interaction to be built + # without previous interactions being included + @pact_config.reset_pact + end + + private + + attr_reader :pact_handle, :pact_interaction, :pact_config + + def mismatches_error_msg(mock_server) + rspec_example_desc = RSpec.current_example&.description + mismatches = JSON.pretty_generate(JSON.parse(mock_server.mismatches)) + mismatches_with_colored_keys = mismatches.gsub(/"([^"]+)":/) { |match| "\e[34m#{match}\e[0m" } # Blue keys / white values + + "#{rspec_example_desc} has mismatches: #{mismatches_with_colored_keys}" + end + + def init_pact + handle = PactFfi.new_pact(pact_config.consumer_name, pact_config.provider_name) + PactFfi.with_specification(handle, PactFfi::FfiSpecificationVersion["SPECIFICATION_VERSION_#{pact_config.pact_specification}"]) + PactFfi.with_pact_metadata(handle, "pact-ruby", "pact-ffi", PactFfi.version) + + Pact::Native::Logger.log_to_stdout(pact_config.log_level) + + handle + end + + def format_value(obj) + return obj if obj.is_a?(String) + + return JSON.dump({value: obj}) if obj.is_a?(Array) + + JSON.dump(obj) + end + end + end +end diff --git a/lib/pact/consumer/interaction_builder.rb b/lib/pact/consumer/interaction_builder.rb deleted file mode 100644 index 78398ea7..00000000 --- a/lib/pact/consumer/interaction_builder.rb +++ /dev/null @@ -1,45 +0,0 @@ -require 'net/http' -require 'pact/reification' -require 'pact/consumer_contract/interaction' - -module Pact - module Consumer - class InteractionBuilder - - attr_reader :interaction - - def initialize &block - @interaction = Interaction.new - @callback = block - end - - def without_writing_to_pact - interaction.metadata ||= {} - interaction.metadata[:write_to_pact] = false - self - end - - def upon_receiving description - @interaction.description = description - self - end - - def given provider_state - @interaction.provider_state = provider_state.nil? ? nil : provider_state.to_s - self - end - - def with(request_details) - interaction.request = Pact::Request::Expected.from_hash(request_details) - self - end - - def will_respond_with(response) - interaction.response = Pact::Response.new(response) - @callback.call interaction - self - end - - end - end -end diff --git a/lib/pact/consumer/interaction_contents.rb b/lib/pact/consumer/interaction_contents.rb new file mode 100644 index 00000000..04ec0e6c --- /dev/null +++ b/lib/pact/consumer/interaction_contents.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module Pact + module Consumer + class InteractionContents < Hash + BASIC_FORMAT = :basic + PLUGIN_FORMAT = :plugin + + attr_reader :format + + def self.basic(contents_hash) + new(contents_hash, BASIC_FORMAT) + end + + def self.plugin(contents_hash) + new(contents_hash, PLUGIN_FORMAT) + end + + def initialize(contents_hash, format) + init_hash(contents_hash, format).each_pair { |k, v| self[k] = v } + @format = format + end + + private + + def serialize(hash, format) + # serialize recursively + return hash if hash.is_a?(String) + + if hash.is_a?(Pact::Matchers::Base) + return hash.as_basic if format == :basic + return hash.as_plugin if format == :plugin + end + if hash.is_a?(Pact::Generators::Base) + return hash.as_basic if format == :basic + return hash.as_plugin if format == :plugin + end + hash.each_pair do |key, value| + next serialize(value, format) if value.is_a?(Hash) + next hash[key] = value.map { |v| serialize(v, format) } if value.is_a?(Array) + + if value.is_a?(Pact::Matchers::Base) + hash[key] = value.as_basic if format == :basic + hash[key] = value.as_plugin if format == :plugin + end + if value.is_a?(Pact::Generators::Base) + hash[key] = value.as_basic if format == :basic + hash[key] = value.as_plugin if format == :plugin + end + end + + hash + end + + def init_hash(hash, format) + serialize(hash.deep_dup, format) + end + end + end +end diff --git a/lib/pact/consumer/message_interaction_builder.rb b/lib/pact/consumer/message_interaction_builder.rb new file mode 100644 index 00000000..7acc7635 --- /dev/null +++ b/lib/pact/consumer/message_interaction_builder.rb @@ -0,0 +1,286 @@ +# frozen_string_literal: true + +require "pact/ffi/message_consumer" +require "pact/ffi/plugin_consumer" +require "pact/ffi/logger" + +module Pact + module Consumer + class MessageInteractionBuilder + META_CONTENT_TYPE_HEADER = "contentType" + + JSON_CONTENT_TYPE = "application/json" + PROTO_CONTENT_TYPE = "application/protobuf" + + PROTOBUF_PLUGIN_NAME = "protobuf" + PROTOBUF_PLUGIN_VERSION = "0.6.5" + + # https://docs.rs/pact_ffi/latest/pact_ffi/mock_server/handles/fn.pactffi_write_message_pact_file.html + WRITE_PACT_FILE_ERRORS = { + 1 => {reason: :file_not_accessible, status: 1, description: "The pact file was not able to be written"}, + 2 => {reason: :internal_error, status: 2, description: "The message pact for the given handle was not found"} + }.freeze + + class PluginInitError < Pact::FfiError; end + + # https://docs.rs/pact_ffi/0.4.17/pact_ffi/plugins/fn.pactffi_using_plugin.html + INIT_PLUGIN_ERRORS = { + 1 => {reason: :internal_error, status: 1, description: "A general panic was caught"}, + 2 => {reason: :plugin_load_failed, status: 2, description: "Failed to load the plugin"}, + 3 => {reason: :invalid_handle, status: 3, description: "Pact Handle is not valid"} + }.freeze + + # https://docs.rs/pact_ffi/0.4.17/pact_ffi/plugins/fn.pactffi_interaction_contents.html + CREATE_INTERACTION_ERRORS = { + 1 => {reason: :internal_error, status: 1, description: "A general panic was caught"}, + 2 => {reason: :mock_server_already_running, status: 2, description: "The mock server has already been started"}, + 3 => {reason: :invalid_handle, status: 3, description: "The interaction handle is invalid"}, + 4 => {reason: :invalid_content_type, status: 4, description: "The content type is not valid"}, + 5 => {reason: :invalid_contents, status: 5, description: "The contents JSON is not valid JSON"}, + 6 => {reason: :plugin_error, status: 6, description: "The plugin returned an error"} + }.freeze + + class CreateInteractionError < Pact::FfiError; end + + class InteractionMismatchesError < Pact::Error; end + + class InteractionBuilderError < Pact::Error; end + + def initialize(pact_config, description: nil) + @pact_config = pact_config + @description = description + + @json_contents = nil + @proto_contents = nil + @proto_path = nil + @proto_message_class = nil + @proto_include_dirs = [] + @meta = {} + @headers = {} + @provider_state_meta = nil + end + + def given(provider_state, metadata = {}) + @provider_state_meta = {provider_state => metadata} + self + end + + def upon_receiving(description) + @description = description + self + end + + def with_json_contents(contents_hash) + @json_contents = InteractionContents.basic(contents_hash) + self + end + + def with_proto_class(proto_path, message_class_name, include_dirs = []) + absolute_path = File.expand_path(proto_path) + raise InteractionBuilderError.new("proto file #{proto_path} does not exist") unless File.exist?(absolute_path) + + @proto_path = absolute_path + @proto_message_class = message_class_name + @proto_include_dirs = include_dirs.map { |dir| File.expand_path(dir) } + self + end + + def with_pact_protobuf_plugin_version(version) + raise InteractionBuilderError.new("version is required") if version.blank? + + @proto_plugin_version = version + self + end + + def with_proto_contents(contents_hash) + @proto_contents = InteractionContents.plugin(contents_hash) + self + end + + def with_metadata(meta_hash) + @meta = InteractionContents.basic(meta_hash) + self + end + + def with_headers(headers_hash) + @headers = InteractionContents.basic(headers_hash) + self + end + + def with_header(key, value) + @headers[key] = value + self + end + + def validate! + if proto_interaction? + raise InteractionBuilderError.new("proto_path / proto_message are not defined, please set ones with #with_proto_message") if @proto_contents.blank? || @proto_message_class.blank? + raise InteractionBuilderError.new("invalid request format, should be a hash") unless @proto_contents.is_a?(Hash) + else + raise InteractionBuilderError.new("invalid request format, should be a hash") unless @json_contents.is_a?(Hash) + end + raise InteractionBuilderError.new("description is required for message interactions, please set one with #upon_receiving") if @description.blank? + end + + def execute(&block) + raise InteractionBuilderError.new("interaction is designed to be used one-time only") if defined?(@used) + + validate! + pact_handle = init_pact + init_plugin!(pact_handle) if proto_interaction? + + message_pact = PactFfi::MessageConsumer.new_message_interaction(pact_handle, @description) + + configure_interaction!(message_pact) + + # strip out matchers and get raw payload/metadata + payload, metadata = fetch_reified_message(pact_handle) + configure_provider_state(message_pact, metadata) + + yield(payload, metadata) + + write_pacts!(pact_handle, @pact_config.pact_dir) + ensure + @used = true + PactFfi::MessageConsumer.free_handle(message_pact) + PactFfi::PluginConsumer.cleanup_plugins(pact_handle) + PactFfi.free_pact_handle(pact_handle) + end + + def build_interaction_json + return JSON.dump(@json_contents) unless proto_interaction? + + contents = { + "pact:proto": @proto_path, + "pact:message-type": @proto_message_class, + "pact:content-type": PROTO_CONTENT_TYPE + }.merge(@proto_contents) + + contents["pact:protobuf-config"] = {additionalIncludes: @proto_include_dirs} if @proto_include_dirs.present? + + JSON.dump(contents) + end + + private + + def write_pacts!(handle, dir) + result = PactFfi.write_message_pact_file(handle, @pact_config.pact_dir, false) + return result if WRITE_PACT_FILE_ERRORS[result].blank? + + error = WRITE_PACT_FILE_ERRORS[result] + raise WritePactsError.new("There was an error while trying to write pact file to #{dir}", error[:reason], error[:status]) + end + + def init_pact + handle = PactFfi::MessageConsumer.new_message_pact(@pact_config.consumer_name, @pact_config.provider_name) + PactFfi.with_specification(handle, PactFfi::FfiSpecificationVersion["SPECIFICATION_VERSION_V4"]) + PactFfi.with_pact_metadata(handle, "pact-ruby", "pact-ffi", PactFfi.version) + + Pact::Native::Logger.log_to_stdout(@pact_config.log_level) + + handle + end + + def fetch_reified_message(pact_handle) + iterator = PactFfi::MessageConsumer.pact_handle_get_message_iter(pact_handle) + raise InteractionBuilderError.new("cannot get message iterator: internal error") if iterator.blank? + + message_handle = PactFfi.pact_message_iter_next(iterator) + raise InteractionBuilderError.new("cannot get message from iterator: no messages") if message_handle.blank? + + contents = fetch_reified_message_body(message_handle) + meta = fetch_reified_message_headers(message_handle) + + [contents, meta.compact] + ensure + PactFfi.pact_message_iter_delete(iterator) if iterator.present? + end + + def fetch_reified_message_headers(message_handle) + meta = {"headers" => {}} + + meta[META_CONTENT_TYPE_HEADER] = PactFfi.message_find_metadata(message_handle, META_CONTENT_TYPE_HEADER) + + @meta.each_key do |key| + meta[key.to_s] = PactFfi.message_find_metadata(message_handle, key.to_s) + end + + @headers.each_key do |key| + meta["headers"][key.to_s] = PactFfi.message_find_metadata(message_handle, key.to_s) + end + + meta + end + + def configure_provider_state(message_pact, reified_metadata) + content_type = reified_metadata[META_CONTENT_TYPE_HEADER] + @provider_state_meta&.each_pair do |provider_state, meta| + if meta.present? + meta.each_pair { |k, v| PactFfi.given_with_param(message_pact, provider_state, k.to_s, v.to_s) } + PactFfi.given_with_param(message_pact, provider_state, META_CONTENT_TYPE_HEADER, content_type.to_s) if content_type + elsif content_type.present? + PactFfi.given_with_param(message_pact, provider_state, META_CONTENT_TYPE_HEADER, content_type.to_s) + else + PactFfi.given(message_pact, provider_state) + end + end + end + + def fetch_reified_message_body(message_handle) + if proto_interaction? + len = PactFfi::MessageConsumer.get_contents_length(message_handle) + ptr = PactFfi::MessageConsumer.get_contents_bin(message_handle) + return nil if ptr.blank? || len == 0 + + return String.new(ptr.read_string_length(len)) + end + + contents = PactFfi::MessageConsumer.get_contents(message_handle) + return nil if contents.blank? + + JSON.parse(contents) + end + + def configure_interaction!(message_pact) + interaction_json = build_interaction_json + + if proto_interaction? + result = PactFfi::PluginConsumer.interaction_contents(message_pact, 0, PROTO_CONTENT_TYPE, interaction_json) + if CREATE_INTERACTION_ERRORS[result].present? + error = CREATE_INTERACTION_ERRORS[result] + raise CreateInteractionError.new("There was an error while trying to add interaction \"#{@description}\"", error[:reason], error[:status]) + end + else + result = PactFfi.with_body(message_pact, 0, JSON_CONTENT_TYPE, interaction_json) + unless result + raise InteractionMismatchesError.new("There was an error while trying to add message interaction contents \"#{@description}\"") + end + end + + # meta should be configured last to avoid resetting after body is set + InteractionContents.basic(@meta.merge(@headers)).each_pair do |key, value| + PactFfi::MessageConsumer.with_metadata_v2(message_pact, key.to_s, JSON.dump(value)) + end + end + + def init_plugin!(pact_handle) + result = PactFfi::PluginConsumer.using_plugin(pact_handle, PROTOBUF_PLUGIN_NAME, @proto_plugin_version || PROTOBUF_PLUGIN_VERSION) + return result if INIT_PLUGIN_ERRORS[result].blank? + + error = INIT_PLUGIN_ERRORS[result] + raise PluginInitError.new("There was an error while trying to initialize plugin #{PROTOBUF_PLUGIN_NAME}/#{@proto_plugin_version || PROTOBUF_PLUGIN_VERSION}", error[:reason], error[:status]) + end + + def serialize_metadata(metadata_hash) + metadata = metadata_hash.deep_dup + serialize_as!(metadata, :basic) + + metadata + end + + def proto_interaction? + @proto_contents.present? + end + end + end +end diff --git a/lib/pact/consumer/mock_server.rb b/lib/pact/consumer/mock_server.rb new file mode 100644 index 00000000..b9314d88 --- /dev/null +++ b/lib/pact/consumer/mock_server.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +require "pact/ffi/mock_server" + +module Pact + module Consumer + class MockServer + attr_reader :host, :port, :transport, :handle, :url + + TRANSPORT_HTTP = "http" + TRANSPORT_GRPC = "grpc" + + class MockServerCreateError < Pact::FfiError; end + + class WritePactsError < Pact::FfiError; end + + # https://docs.rs/pact_ffi/0.4.17/pact_ffi/mock_server/fn.pactffi_create_mock_server_for_transport.html + CREATE_TRANSPORT_ERRORS = { + -1 => {reason: :invalid_handle, status: -1, description: "An invalid handle was received. Handles should be created with pactffi_new_pact"}, + -2 => {reason: :invalid_transport_json, status: -2, description: "Transport_config is not valid JSON"}, + -3 => {reason: :mock_server_not_started, status: -3, description: "The mock server could not be started"}, + -4 => {reason: :internal_error, status: -4, description: "The method panicked"}, + -5 => {reason: :invalid_host, status: -5, description: "The address is not valid"} + }.freeze + + # https://docs.rs/pact_ffi/0.4.17/pact_ffi/mock_server/fn.pactffi_write_pact_file.html + WRITE_PACT_FILE_ERRORS = { + 1 => {reason: :internal_error, status: 1, description: "A general panic was caught"}, + 2 => {reason: :file_not_accessible, status: 2, description: "The pact file was not able to be written"}, + 3 => {reason: :mock_server_not_found, status: 3, description: "A mock server with the provided port was not found"} + }.freeze + + def self.create_for_grpc!(pact:, host: "127.0.0.1", port: 0) + new(pact: pact, transport: TRANSPORT_GRPC, host: host, port: port) + end + + def self.create_for_http!(pact:, host: "127.0.0.1", port: 0) + new(pact: pact, transport: TRANSPORT_HTTP, host: host, port: port) + end + + def self.create_for_transport!(pact:, transport:, host: "127.0.0.1", port: 0) + new(pact: pact, transport: transport, host: host, port: port) + end + + def initialize(pact:, transport:, host:, port:) + + @pact = pact + @transport = transport + @host = host + @port = port + + @handle = init_transport! + # the returned handle is the port number + # we set it here, so we can consume a port number of 0 + # and allow pact to assign a random available port + @port = @handle + # construct the url for the mock server + # as a convenience for the user + @url = "#{transport}://#{host}:#{@handle}" + # TODO: handle auto-GC of native memory + # ObjectSpace.define_finalizer(self, proc do + # cleanup + # end) + end + + def write_pacts!(dir) + result = PactFfi::MockServer.write_pact_file(@handle, dir, false) + return result if WRITE_PACT_FILE_ERRORS[result].blank? + + error = WRITE_PACT_FILE_ERRORS[result] + raise WritePactsError.new("There was an error while trying to write pact file to #{dir}", error[:reason], error[:status]) + end + + def matched? + PactFfi::MockServer.matched(@handle) + end + + def mismatches + PactFfi::MockServer.mismatches(@handle) + end + + def cleanup + PactFfi::MockServer.cleanup(@handle) + end + + def cleanup_plugins + PactFfi::PluginConsumer.cleanup_plugins(@handle) + end + + def free_pact_handle + PactFfi.free_pact_handle(@handle) + end + + private + + def init_transport! + handle = PactFfi::MockServer.create_for_transport(@pact, @host, @port, @transport, nil) + # the returned handle is the port number + return handle if CREATE_TRANSPORT_ERRORS[handle].blank? + + error = CREATE_TRANSPORT_ERRORS[handle] + raise MockServerCreateError.new("There was an error while trying to create mock server for transport:#{@transport}", error[:reason], error[:status]) + end + end + end +end diff --git a/lib/pact/consumer/pact_config.rb b/lib/pact/consumer/pact_config.rb new file mode 100644 index 00000000..4f4efedc --- /dev/null +++ b/lib/pact/consumer/pact_config.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require_relative "pact_config/grpc" + +module Pact + module Consumer + module PactConfig + def self.new(transport_type, consumer_name:, provider_name:, opts: {}) + case transport_type + when :http + Http.new(consumer_name: consumer_name, provider_name: provider_name, opts: opts) + when :grpc + Grpc.new(consumer_name: consumer_name, provider_name: provider_name, opts: opts) + when :message + Message.new(consumer_name: consumer_name, provider_name: provider_name, opts: opts) + when :plugin_sync_message + PluginSyncMessage.new(consumer_name: consumer_name, provider_name: provider_name, opts: opts) + when :plugin_async_message + PluginAsyncMessage.new(consumer_name: consumer_name, provider_name: provider_name, opts: opts) + when :plugin_http + PluginHttp.new(consumer_name: consumer_name, provider_name: provider_name, opts: opts) + else + raise ArgumentError, "unknown transport_type: #{transport_type}" + end + end + end + end +end diff --git a/lib/pact/consumer/pact_config/base.rb b/lib/pact/consumer/pact_config/base.rb new file mode 100644 index 00000000..e23dda0d --- /dev/null +++ b/lib/pact/consumer/pact_config/base.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Pact + module Consumer + module PactConfig + class Base + attr_reader :consumer_name, :provider_name, :pact_dir, :log_level + + def initialize(consumer_name:, provider_name:, opts: {}) + @consumer_name = consumer_name + @provider_name = provider_name + @pact_dir = opts[:pact_dir] || (defined?(Rails) ? Rails.root.join("../pacts").to_s : "pacts") + @log_level = opts[:log_level] || :info + end + + def new_interaction(description = nil) + raise PactImplementationRequired, "#new_interaction should be implemented" + end + end + end + end +end diff --git a/lib/pact/consumer/pact_config/grpc.rb b/lib/pact/consumer/pact_config/grpc.rb new file mode 100644 index 00000000..89228295 --- /dev/null +++ b/lib/pact/consumer/pact_config/grpc.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require_relative "base" + +module Pact + module Consumer + module PactConfig + class Grpc < Base + attr_reader :mock_host, :mock_port + + def initialize(consumer_name:, provider_name:, opts: {}) + super + + @mock_host = opts[:mock_host] || "127.0.0.1" + @mock_port = opts[:mock_port] || 3009 + end + + def new_interaction(description = nil) + GrpcInteractionBuilder.new(self, description: description) + end + end + end + end +end diff --git a/lib/pact/consumer/pact_config/http.rb b/lib/pact/consumer/pact_config/http.rb new file mode 100644 index 00000000..865cbd07 --- /dev/null +++ b/lib/pact/consumer/pact_config/http.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require_relative "base" + +module Pact + module Consumer + module PactConfig + class Http < Base + attr_reader :mock_host, :mock_port, :pact_handle + + def initialize(consumer_name:, provider_name:, opts: {}) + super + + @mock_host = opts[:mock_host] || "127.0.0.1" + @mock_port = opts[:mock_port] || 0 + @log_level = opts[:log_level] || :info + @pact_specification = get_pact_specification(opts) + @pact_handle = init_pact + end + + def new_interaction(description = nil) + HttpInteractionBuilder.new(self, description: description) + end + + def reset_pact + @pact_handle = init_pact + end + + def get_pact_specification(opts) + pact_spec_version = opts[:pact_specification] || "V4" + unless pact_spec_version.match?(/^v?[1-4](\.\d+){0,2}$/i) + raise ArgumentError, "Invalid pact specification version format \n Valid versions are 1, 1.1, 2, 3, 4. Default is V4 \n V prefix is optional, and case insensitive" + end + pact_spec_version = pact_spec_version.upcase + pact_spec_version = "V#{pact_spec_version}" unless pact_spec_version.start_with?("V") + pact_spec_version = pact_spec_version.sub(/(\.0+)+$/, "") + pact_spec_version = pact_spec_version.tr(".", "_") + PactFfi::FfiSpecificationVersion["SPECIFICATION_VERSION_#{pact_spec_version.upcase}"] + end + + def init_pact + handle = PactFfi.new_pact(consumer_name, provider_name) + PactFfi.with_specification(handle, @pact_specification) + PactFfi.with_pact_metadata(handle, "pact-ruby", "pact-ffi", PactFfi.version) + + Pact::Native::Logger.log_to_stdout(@log_level) + + handle + end + end + end + end +end diff --git a/lib/pact/consumer/pact_config/message.rb b/lib/pact/consumer/pact_config/message.rb new file mode 100644 index 00000000..c7b8cee4 --- /dev/null +++ b/lib/pact/consumer/pact_config/message.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require_relative 'base' + +module Pact + module Consumer + module PactConfig + class Message < Base + def new_interaction(description = nil) + MessageInteractionBuilder.new(self, description: description) + end + end + end + end +end diff --git a/lib/pact/consumer/pact_config/plugin_async_message.rb b/lib/pact/consumer/pact_config/plugin_async_message.rb new file mode 100644 index 00000000..4d38918e --- /dev/null +++ b/lib/pact/consumer/pact_config/plugin_async_message.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require_relative "base" + +module Pact + module Consumer + module PactConfig + class PluginAsyncMessage < Base + attr_reader :mock_host, :mock_port + + def initialize(consumer_name:, provider_name:, opts: {}) + super + + @mock_host = opts[:mock_host] || "127.0.0.1" + @mock_port = opts[:mock_port] || 0 + end + + def new_interaction(description = nil) + PluginAsyncMessageInteractionBuilder.new(self, description: description) + end + end + end + end +end diff --git a/lib/pact/consumer/pact_config/plugin_http.rb b/lib/pact/consumer/pact_config/plugin_http.rb new file mode 100644 index 00000000..8c6b9222 --- /dev/null +++ b/lib/pact/consumer/pact_config/plugin_http.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require_relative "base" + +module Pact + module Consumer + module PactConfig + class PluginHttp < Base + attr_reader :mock_host, :mock_port + + def initialize(consumer_name:, provider_name:, opts: {}) + super + + @mock_host = opts[:mock_host] || "127.0.0.1" + @mock_port = opts[:mock_port] || 0 + end + + def new_interaction(description = nil) + PluginHttpInteractionBuilder.new(self, description: description) + end + end + end + end +end diff --git a/lib/pact/consumer/pact_config/plugin_sync_message.rb b/lib/pact/consumer/pact_config/plugin_sync_message.rb new file mode 100644 index 00000000..016132da --- /dev/null +++ b/lib/pact/consumer/pact_config/plugin_sync_message.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require_relative "base" + +module Pact + module Consumer + module PactConfig + class PluginSyncMessage < Base + attr_reader :mock_host, :mock_port + + def initialize(consumer_name:, provider_name:, opts: {}) + super + + @mock_host = opts[:mock_host] || "127.0.0.1" + @mock_port = opts[:mock_port] || 0 + end + + def new_interaction(description = nil) + PluginSyncMessageInteractionBuilder.new(self, description: description) + end + end + end + end +end diff --git a/lib/pact/consumer/plugin_async_message_interaction_builder.rb b/lib/pact/consumer/plugin_async_message_interaction_builder.rb new file mode 100644 index 00000000..5d8e175b --- /dev/null +++ b/lib/pact/consumer/plugin_async_message_interaction_builder.rb @@ -0,0 +1,169 @@ +# frozen_string_literal: true + +require "pact/ffi/async_message_pact" +require "pact/ffi/plugin_consumer" +require "pact/ffi/logger" + +module Pact + module Consumer + class PluginAsyncMessageInteractionBuilder + + class PluginInitError < Pact::FfiError; end + + # https://docs.rs/pact_ffi/0.4.17/pact_ffi/plugins/fn.pactffi_using_plugin.html + INIT_PLUGIN_ERRORS = { + 1 => {reason: :internal_error, status: 1, description: "A general panic was caught"}, + 2 => {reason: :plugin_load_failed, status: 2, description: "Failed to load the plugin"}, + 3 => {reason: :invalid_handle, status: 3, description: "Pact Handle is not valid"} + }.freeze + + # https://docs.rs/pact_ffi/0.4.17/pact_ffi/plugins/fn.pactffi_interaction_contents.html + CREATE_INTERACTION_ERRORS = { + 1 => {reason: :internal_error, status: 1, description: "A general panic was caught"}, + 2 => {reason: :mock_server_already_running, status: 2, description: "The mock server has already been started"}, + 3 => {reason: :invalid_handle, status: 3, description: "The interaction handle is invalid"}, + 4 => {reason: :invalid_content_type, status: 4, description: "The content type is not valid"}, + 5 => {reason: :invalid_contents, status: 5, description: "The contents JSON is not valid JSON"}, + 6 => {reason: :plugin_error, status: 6, description: "The plugin returned an error"} + }.freeze + + class CreateInteractionError < Pact::FfiError; end + + class InteractionMismatchesError < Pact::Error; end + + class InteractionBuilderError < Pact::Error; end + + def initialize(pact_config, description: nil) + @pact_config = pact_config + @description = description || "" + @contents = nil + @provider_state_meta = nil + end + + def with_plugin(plugin_name, plugin_version) + raise InteractionBuilderError.new("plugin_name is required") if plugin_name.blank? + raise InteractionBuilderError.new("plugin_version is required") if plugin_version.blank? + + @plugin_name = plugin_name + @plugin_version = plugin_version + self + end + + def given(provider_state, metadata = {}) + @provider_state_meta = {provider_state => metadata} + self + end + + def upon_receiving(description) + @description = description + self + end + + def with_contents(contents_hash) + @contents = InteractionContents.plugin(contents_hash) + self + end + + def with_content_type(content_type) + @interaction_content_type = content_type || @content_type + self + end + + def with_plugin_metadata(meta_hash) + @plugin_metadata = meta_hash + self + end + + def with_transport(transport) + @transport = transport + self + end + + def interaction_json + result = { + contents: @contents + } + result.merge!(@plugin_metadata) if @plugin_metadata.is_a?(Hash) + JSON.dump(result) + end + + def validate! + raise InteractionBuilderError.new("invalid contents format, should be a hash") unless @contents.is_a?(Hash) + end + + def execute(&block) + raise InteractionBuilderError.new("interaction is designed to be used one-time only") if defined?(@used) + + validate! + + pact_handle = init_pact + init_plugin!(pact_handle) + + interaction = PactFfi::AsyncMessageConsumer.new(pact_handle, @description) + + @provider_state_meta&.each_pair do |provider_state, meta| + if meta.present? + meta.each_pair do |k, v| + if v.nil? || (v.respond_to?(:empty?) && v.empty?) + PactFfi.given(interaction, provider_state) + else + PactFfi.given_with_param(interaction, provider_state, k.to_s, v.to_s) + end + end + else + PactFfi.given(interaction, provider_state) + end + end + + result = PactFfi.with_body(interaction, 0, @interaction_content_type, interaction_json) + if CREATE_INTERACTION_ERRORS[result].present? + error = CREATE_INTERACTION_ERRORS[result] + raise CreateInteractionError.new("There was an error while trying to add interaction \"#{@description}\"", error[:reason], error[:status]) + end + + mock_server = MockServer.create_for_transport!(pact: pact_handle, transport: @transport, host: @pact_config.mock_host, port: @pact_config.mock_port) + + yield(pact_handle, mock_server) + + ensure + if mock_server.matched? + mock_server.write_pacts!(@pact_config.pact_dir) + else + msg = mismatches_error_msg(mock_server) + raise InteractionMismatchesError.new(msg) + end + @used = true + mock_server&.cleanup + PactFfi::PluginConsumer.cleanup_plugins(pact_handle) if pact_handle + PactFfi.free_pact_handle(pact_handle) if pact_handle + end + + private + + def mismatches_error_msg(mock_server) + rspec_example_desc = RSpec.current_example&.description + return "interaction for has mismatches: #{mock_server.mismatches}" if rspec_example_desc.blank? + + "#{rspec_example_desc} has mismatches: #{mock_server.mismatches}" + end + + def init_pact + handle = PactFfi.new_pact(@pact_config.consumer_name, @pact_config.provider_name) + PactFfi.with_specification(handle, PactFfi::FfiSpecificationVersion["SPECIFICATION_VERSION_V4"]) + PactFfi.with_pact_metadata(handle, "pact-ruby", "pact-ffi", PactFfi.version) + + Pact::Native::Logger.log_to_stdout(@pact_config.log_level) + + handle + end + + def init_plugin!(pact_handle) + result = PactFfi::PluginConsumer.using_plugin(pact_handle, @plugin_name, @plugin_version) + return result if INIT_PLUGIN_ERRORS[result].blank? + + error = INIT_PLUGIN_ERRORS[result] + raise PluginInitError.new("There was an error while trying to initialize plugin #{@plugin_name}/#{@plugin_version}", error[:reason], error[:status]) + end + end + end +end diff --git a/lib/pact/consumer/plugin_http_interaction_builder.rb b/lib/pact/consumer/plugin_http_interaction_builder.rb new file mode 100644 index 00000000..e583d479 --- /dev/null +++ b/lib/pact/consumer/plugin_http_interaction_builder.rb @@ -0,0 +1,199 @@ +# frozen_string_literal: true + +require "pact/ffi/http_consumer" +require "pact/ffi/plugin_consumer" +require "pact/ffi/logger" + +module Pact + module Consumer + class PluginHttpInteractionBuilder + + class PluginInitError < Pact::FfiError; end + + # https://docs.rs/pact_ffi/0.4.17/pact_ffi/plugins/fn.pactffi_using_plugin.html + INIT_PLUGIN_ERRORS = { + 1 => {reason: :internal_error, status: 1, description: "A general panic was caught"}, + 2 => {reason: :plugin_load_failed, status: 2, description: "Failed to load the plugin"}, + 3 => {reason: :invalid_handle, status: 3, description: "Pact Handle is not valid"} + }.freeze + + # https://docs.rs/pact_ffi/0.4.17/pact_ffi/plugins/fn.pactffi_interaction_contents.html + CREATE_INTERACTION_ERRORS = { + 1 => {reason: :internal_error, status: 1, description: "A general panic was caught"}, + 2 => {reason: :mock_server_already_running, status: 2, description: "The mock server has already been started"}, + 3 => {reason: :invalid_handle, status: 3, description: "The interaction handle is invalid"}, + 4 => {reason: :invalid_content_type, status: 4, description: "The content type is not valid"}, + 5 => {reason: :invalid_contents, status: 5, description: "The contents JSON is not valid JSON"}, + 6 => {reason: :plugin_error, status: 6, description: "The plugin returned an error"} + }.freeze + + class CreateInteractionError < Pact::FfiError; end + + class InteractionMismatchesError < Pact::Error; end + + class InteractionBuilderError < Pact::Error; end + + def initialize(pact_config, description: nil) + @pact_config = pact_config + @description = description || "" + @contents = nil + @provider_state_meta = nil + end + + def with_plugin(plugin_name, plugin_version) + raise InteractionBuilderError.new("plugin_name is required") if plugin_name.blank? + raise InteractionBuilderError.new("plugin_version is required") if plugin_version.blank? + + @plugin_name = plugin_name + @plugin_version = plugin_version + self + end + + def given(provider_state, metadata = {}) + @provider_state_meta = {provider_state => metadata} + self + end + + def upon_receiving(description) + @description = description + self + end + + def with_request(method: nil, path: nil, query: {}, headers: {}, body: nil) + @request = { + method: method, + path: path, + query: query, + headers: headers, + body: body + } + self + end + + def will_respond_with(status: nil, headers: {}, body: nil) + @response = { + status: status, + headers: headers, + body: body + } + self + end + + def with_content_type(content_type) + @content_type = content_type + self + end + + + def with_plugin_metadata(meta_hash) + @plugin_metadata = meta_hash + self + end + + def with_transport(transport) + @transport = transport + self + end + + def interaction_json + result = { + request: @request, + response: @response + } + result.merge!(@plugin_metadata) if @plugin_metadata.is_a?(Hash) + JSON.dump(result) + end + + def validate! + raise InteractionBuilderError.new("invalid request format, should be a hash") unless @request.is_a?(Hash) + raise InteractionBuilderError.new("invalid response format, should be a hash") unless @response.is_a?(Hash) + end + + def execute(&block) + raise InteractionBuilderError.new("interaction is designed to be used one-time only") if defined?(@used) + + validate! + + pact_handle = init_pact + init_plugin!(pact_handle) + + interaction = PactFfi.new_interaction(pact_handle, @description) + @provider_state_meta&.each_pair do |provider_state, meta| + if meta.present? + meta.each_pair do |k, v| + if v.nil? || (v.respond_to?(:empty?) && v.empty?) + PactFfi.given(interaction, provider_state) + else + PactFfi.given_with_param(interaction, provider_state, k.to_s, v.to_s) + end + end + else + PactFfi.given(interaction, provider_state) + end + end + PactFfi::HttpConsumer.with_request(interaction, @request[:method], @request[:path]) + + result = PactFfi::PluginConsumer.interaction_contents(interaction, 0, @request[:headers]["content-type"], format_value(@request[:body])) + if CREATE_INTERACTION_ERRORS[result].present? + error = CREATE_INTERACTION_ERRORS[result] + raise CreateInteractionError.new("There was an error while trying to add interaction \"#{@description}\"", error[:reason], error[:status]) + end + result = PactFfi::PluginConsumer.interaction_contents(interaction, 1, @response[:headers]["content-type"], format_value(@response[:body])) + if CREATE_INTERACTION_ERRORS[result].present? + error = CREATE_INTERACTION_ERRORS[result] + raise CreateInteractionError.new("There was an error while trying to add interaction \"#{@description}\"", error[:reason], error[:status]) + end + mock_server = MockServer.create_for_transport!(pact: pact_handle, transport: @transport || 'http', host: @pact_config.mock_host, port: @pact_config.mock_port) + + yield(mock_server) + + ensure + if mock_server.matched? + mock_server.write_pacts!(@pact_config.pact_dir) + else + msg = mismatches_error_msg(mock_server) + raise InteractionMismatchesError.new(msg) + end + @used = true + mock_server&.cleanup + PactFfi::PluginConsumer.cleanup_plugins(pact_handle) if pact_handle + PactFfi.free_pact_handle(pact_handle) if pact_handle + end + + private + + def mismatches_error_msg(mock_server) + rspec_example_desc = RSpec.current_example&.description + return "interaction for has mismatches: #{mock_server.mismatches}" if rspec_example_desc.blank? + + "#{rspec_example_desc} has mismatches: #{mock_server.mismatches}" + end + + def init_pact + handle = PactFfi.new_pact(@pact_config.consumer_name, @pact_config.provider_name) + PactFfi.with_specification(handle, PactFfi::FfiSpecificationVersion["SPECIFICATION_VERSION_V4"]) + PactFfi.with_pact_metadata(handle, "pact-ruby", "pact-ffi", PactFfi.version) + + Pact::Native::Logger.log_to_stdout(@pact_config.log_level) + + handle + end + + def init_plugin!(pact_handle) + result = PactFfi::PluginConsumer.using_plugin(pact_handle, @plugin_name, @plugin_version) + return result if INIT_PLUGIN_ERRORS[result].blank? + + error = INIT_PLUGIN_ERRORS[result] + raise PluginInitError.new("There was an error while trying to initialize plugin #{@plugin_name}/#{@plugin_version}", error[:reason], error[:status]) + end + + def format_value(obj) + return obj if obj.is_a?(String) + + return JSON.dump({value: obj}) if obj.is_a?(Array) + + JSON.dump(obj) + end + end + end +end diff --git a/lib/pact/consumer/plugin_sync_message_interaction_builder.rb b/lib/pact/consumer/plugin_sync_message_interaction_builder.rb new file mode 100644 index 00000000..86ce2e11 --- /dev/null +++ b/lib/pact/consumer/plugin_sync_message_interaction_builder.rb @@ -0,0 +1,178 @@ +# frozen_string_literal: true + +require "pact/ffi/sync_message_consumer" +require "pact/ffi/plugin_consumer" +require "pact/ffi/logger" + +module Pact + module Consumer + class PluginSyncMessageInteractionBuilder + + class PluginInitError < Pact::FfiError; end + + # https://docs.rs/pact_ffi/0.4.17/pact_ffi/plugins/fn.pactffi_using_plugin.html + INIT_PLUGIN_ERRORS = { + 1 => {reason: :internal_error, status: 1, description: "A general panic was caught"}, + 2 => {reason: :plugin_load_failed, status: 2, description: "Failed to load the plugin"}, + 3 => {reason: :invalid_handle, status: 3, description: "Pact Handle is not valid"} + }.freeze + + # https://docs.rs/pact_ffi/0.4.17/pact_ffi/plugins/fn.pactffi_interaction_contents.html + CREATE_INTERACTION_ERRORS = { + 1 => {reason: :internal_error, status: 1, description: "A general panic was caught"}, + 2 => {reason: :mock_server_already_running, status: 2, description: "The mock server has already been started"}, + 3 => {reason: :invalid_handle, status: 3, description: "The interaction handle is invalid"}, + 4 => {reason: :invalid_content_type, status: 4, description: "The content type is not valid"}, + 5 => {reason: :invalid_contents, status: 5, description: "The contents JSON is not valid JSON"}, + 6 => {reason: :plugin_error, status: 6, description: "The plugin returned an error"} + }.freeze + + class CreateInteractionError < Pact::FfiError; end + + class InteractionMismatchesError < Pact::Error; end + + class InteractionBuilderError < Pact::Error; end + + def initialize(pact_config, description: nil) + @pact_config = pact_config + @description = description || "" + @request = nil + @response = nil + @response_meta = nil + @provider_state_meta = nil + end + + def with_plugin(plugin_name, plugin_version) + raise InteractionBuilderError.new("plugin_name is required") if plugin_name.blank? + raise InteractionBuilderError.new("plugin_version is required") if plugin_version.blank? + + @plugin_name = plugin_name + @plugin_version = plugin_version + self + end + + def given(provider_state, metadata = {}) + @provider_state_meta = {provider_state => metadata} + self + end + + def upon_receiving(description) + @description = description + self + end + + def with_request(req_hash) + @request = InteractionContents.plugin(req_hash) + self + end + + def with_content_type(content_type) + @content_type = content_type + self + end + + def will_respond_with(resp_hash) + @response = InteractionContents.plugin(resp_hash) + self + end + + def will_respond_with_meta(meta_hash) + @response_meta = InteractionContents.plugin(meta_hash) + self + end + + def with_plugin_metadata(meta_hash) + @plugin_metadata = meta_hash + self + end + + def with_transport(transport) + @transport = transport + self + end + + def interaction_json + result = { + request: @request + } + result.merge!(@plugin_metadata) if @plugin_metadata.is_a?(Hash) + + result[:response] = @response if @response.is_a?(Hash) + result[:responseMetadata] = @response_meta if @response_meta.is_a?(Hash) + + JSON.dump(result) + end + + def validate! + raise InteractionBuilderError.new("invalid request format, should be a hash") unless @request.is_a?(Hash) + raise InteractionBuilderError.new("invalid response format, should be a hash") unless @response.is_a?(Hash) || @response_meta.is_a?(Hash) + end + + def execute(&block) + raise InteractionBuilderError.new("interaction is designed to be used one-time only") if defined?(@used) + + validate! + + pact_handle = init_pact + init_plugin!(pact_handle) + + message_pact = PactFfi::SyncMessageConsumer.new_interaction(pact_handle, @description) + @provider_state_meta&.each_pair do |provider_state, meta| + if meta.present? + meta.each_pair { |k, v| PactFfi.given_with_param(message_pact, provider_state, k.to_s, v.to_s) } + else + PactFfi.given(message_pact, provider_state) + end + end + result = PactFfi::PluginConsumer.interaction_contents(message_pact, 0, @content_type, interaction_json) + if CREATE_INTERACTION_ERRORS[result].present? + error = CREATE_INTERACTION_ERRORS[result] + raise CreateInteractionError.new("There was an error while trying to add interaction \"#{@description}\"", error[:reason], error[:status]) + end + + mock_server = MockServer.create_for_transport!(pact: pact_handle, transport: @transport, host: @pact_config.mock_host, port: @pact_config.mock_port) + + yield(message_pact, mock_server) + + ensure + if mock_server.matched? + mock_server.write_pacts!(@pact_config.pact_dir) + else + msg = mismatches_error_msg(mock_server) + raise InteractionMismatchesError.new(msg) + end + @used = true + mock_server&.cleanup + PactFfi::PluginConsumer.cleanup_plugins(pact_handle) + PactFfi.free_pact_handle(pact_handle) + end + + private + + def mismatches_error_msg(mock_server) + rspec_example_desc = RSpec.current_example&.description + return "interaction for has mismatches: #{mock_server.mismatches}" if rspec_example_desc.blank? + + "#{rspec_example_desc} has mismatches: #{mock_server.mismatches}" + end + + def init_pact + handle = PactFfi.new_pact(@pact_config.consumer_name, @pact_config.provider_name) + PactFfi.with_specification(handle, PactFfi::FfiSpecificationVersion["SPECIFICATION_VERSION_V4"]) + PactFfi.with_pact_metadata(handle, "pact-ruby", "pact-ffi", PactFfi.version) + + Pact::Native::Logger.log_to_stdout(@pact_config.log_level) + + handle + end + + def init_plugin!(pact_handle) + result = PactFfi::PluginConsumer.using_plugin(pact_handle, @plugin_name, @plugin_version) + return result if INIT_PLUGIN_ERRORS[result].blank? + + error = INIT_PLUGIN_ERRORS[result] + raise PluginInitError.new("There was an error while trying to initialize plugin #{@plugin_name}/#{@plugin_version}", error[:reason], error[:status]) + end + end + end +end diff --git a/lib/pact/consumer/rspec.rb b/lib/pact/consumer/rspec.rb deleted file mode 100644 index 51af2f78..00000000 --- a/lib/pact/consumer/rspec.rb +++ /dev/null @@ -1,35 +0,0 @@ -require 'pact/consumer' -require 'pact/consumer/spec_hooks' -require 'pact/rspec' -require 'pact/helpers' - -module Pact - module Consumer - module RSpec - include Pact::Consumer::ConsumerContractBuilders - include Pact::Helpers - end - end -end - -hooks = Pact::Consumer::SpecHooks.new - -RSpec.configure do |config| - config.include Pact::Consumer::RSpec, :pact => true - - config.before :all, :pact => true do - hooks.before_all - end - - config.before :each, :pact => true do | example | - hooks.before_each Pact::RSpec.full_description(example) - end - - config.after :each, :pact => true do | example | - hooks.after_each Pact::RSpec.full_description(example) - end - - config.after :suite do - hooks.after_suite - end -end diff --git a/lib/pact/consumer/spec_hooks.rb b/lib/pact/consumer/spec_hooks.rb deleted file mode 100644 index 8fa915a5..00000000 --- a/lib/pact/consumer/spec_hooks.rb +++ /dev/null @@ -1,40 +0,0 @@ -require 'pact/doc/generate' -require 'pact/consumer/world' -require 'pact/mock_service/app_manager' -require 'pact/mock_service/client' - -module Pact - module Consumer - class SpecHooks - - def before_all - Pact::MockService::AppManager.instance.spawn_all - FileUtils.mkdir_p Pact.configuration.pact_dir - end - - def before_each example_description - Pact.consumer_world.register_pact_example_ran - Pact.configuration.logger.info "Clearing all expectations" - Pact::MockService::AppManager.instance.urls_of_mock_services.each do | url | - Pact::MockService::Client.clear_interactions url, example_description - end - end - - def after_each example_description - Pact.configuration.logger.info "Verifying interactions for #{example_description}" - Pact.configuration.provider_verifications.each do | provider_verification | - provider_verification.call example_description - end - end - - def after_suite - if Pact.consumer_world.any_pact_examples_ran? - Pact.consumer_world.consumer_contract_builders.each(&:write_pact) - Pact::Doc::Generate.call - Pact::MockService::AppManager.instance.kill_all - Pact::MockService::AppManager.instance.clear_all - end - end - end - end -end diff --git a/lib/pact/consumer/world.rb b/lib/pact/consumer/world.rb deleted file mode 100644 index 7453f71b..00000000 --- a/lib/pact/consumer/world.rb +++ /dev/null @@ -1,37 +0,0 @@ -module Pact - - def self.consumer_world - @consumer_world ||= Pact::Consumer::World.new - end - - # internal api, for testing only - def self.clear_consumer_world - @consumer_world = nil - end - - module Consumer - class World - - def initialize - @any_pact_examples_ran = false - end - - def consumer_contract_builders - @consumer_contract_builders ||= [] - end - - def add_consumer_contract_builder consumer_contract_builder - consumer_contract_builders << consumer_contract_builder - end - - def register_pact_example_ran - @any_pact_examples_ran = true - end - - def any_pact_examples_ran? - @any_pact_examples_ran - end - - end - end -end \ No newline at end of file diff --git a/lib/pact/doc/README.md b/lib/pact/doc/README.md deleted file mode 100644 index dc06fccb..00000000 --- a/lib/pact/doc/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# How to roll your own Doc Generator - -1. Create a ConsumerContractRenderer that responds to `call` and accepts a `ConsumerContract` (this is the name for the domain model of a "pact"). This should return a String. For an example, see the [Markdown::ConsumerContractRenderer][consumer_contract_renderer]. -2. Create an IndexRenderer. This allows you to create an index file for your docs. It should respond to `call` and accept the String name of the consumer, and a hash of Hash of `pact title => file_name`, and return a String. For an example, see the [Markdown::IndexRenderer][index_renderer]. -3. Create a Generator. This is responsible for the overall file generating and writing process. Copy the [Markdown::Generator][generator] and configure it with your own ConsumerContractRenderer, IndexRenderer and file details. - -If you would like to generate HTML documentation, see how the [HTMLPactRenderer][html_pact_renderer] in the Pact Broker does it. - -[consumer_contract_renderer]: https://github.com/pact-foundation/pact-ruby/blob/master/lib/pact/doc/markdown/consumer_contract_renderer.rb -[index_renderer]: https://github.com/pact-foundation/pact-ruby/blob/master/lib/pact/doc/markdown/index_renderer.rb -[generator]: https://github.com/pact-foundation/pact-ruby/blob/master/lib/pact/doc/markdown/generator.rb -[html_pact_renderer]: https://github.com/pact-foundation/pact_broker/blob/master/lib/pact_broker/api/renderers/html_pact_renderer.rb - diff --git a/lib/pact/doc/doc_file.rb b/lib/pact/doc/doc_file.rb deleted file mode 100644 index 3cd439ce..00000000 --- a/lib/pact/doc/doc_file.rb +++ /dev/null @@ -1,40 +0,0 @@ -module Pact - module Doc - - class DocFile - - def initialize consumer_contract, dir, consumer_contract_renderer, file_extension - @dir = dir - @consumer_contract = consumer_contract - @consumer_contract_renderer = consumer_contract_renderer - @file_extension = file_extension - end - - def write - File.open(path, "w") { |io| io << doc_file_contents } - end - - def title - consumer_contract.provider.name - end - - def name - "#{consumer_contract.consumer.name} - #{consumer_contract.provider.name}#{file_extension}" - end - - private - - attr_reader :dir, :consumer_contract, :consumer_contract_renderer, :file_extension - - - def path - File.join(dir, name) - end - - def doc_file_contents - consumer_contract_renderer.call(consumer_contract) - end - - end - end -end \ No newline at end of file diff --git a/lib/pact/doc/generate.rb b/lib/pact/doc/generate.rb deleted file mode 100644 index aea449ae..00000000 --- a/lib/pact/doc/generate.rb +++ /dev/null @@ -1,11 +0,0 @@ -module Pact - module Doc - class Generate - - def self.call pact_dir = Pact.configuration.pact_dir, doc_dir = Pact.configuration.doc_dir, doc_generators = Pact.configuration.doc_generators - doc_generators.each{| doc_generator| doc_generator.call pact_dir, doc_dir } - end - - end - end -end \ No newline at end of file diff --git a/lib/pact/doc/generator.rb b/lib/pact/doc/generator.rb deleted file mode 100644 index e9401f22..00000000 --- a/lib/pact/doc/generator.rb +++ /dev/null @@ -1,82 +0,0 @@ -require 'pact/doc/doc_file' -require 'fileutils' - -module Pact - module Doc - - class Generator - - def initialize pact_dir, doc_dir, options - @doc_dir = doc_dir - @pact_dir = pact_dir - @consumer_contract_renderer = options[:consumer_contract_renderer] - @doc_type = options[:doc_type] - @file_extension = options[:file_extension] - @index_renderer = options[:index_renderer] - @index_name = options[:index_name] - @after = options.fetch(:after, lambda{|pact_dir, target_dir, consumer_contracts| }) - end - - def call - ensure_target_dir_exists_and_is_clean - write_index if consumer_contracts.any? - write_doc_files - perform_after_hook - end - - private - - attr_reader :doc_dir, :pact_dir, :consumer_contract_renderer, :doc_type, :file_extension, :index_renderer, :after - - def write_index - File.open(index_file_path, "w") { |io| io << index_file_contents } - end - - def index_file_path - File.join(target_dir, "#{@index_name}#{file_extension}") - end - - def index_file_contents - index_renderer.call(consumer_contracts.first.consumer.name, index_data) - end - - def index_data - doc_files.each_with_object({}) do | doc_file, data | - data[doc_file.title] = doc_file.name - end - end - - def write_doc_files - doc_files.each(&:write) - end - - def doc_files - consumer_contracts.collect do | consumer_contract | - DocFile.new(consumer_contract, target_dir, consumer_contract_renderer, file_extension) - end - end - - def consumer_contracts - @consumer_contracts ||= begin - Dir.glob("#{pact_dir}/**").collect do |file| - Pact::ConsumerContract.from_uri file - end - end - end - - def perform_after_hook - after.call(pact_dir, target_dir, consumer_contracts) - end - - def ensure_target_dir_exists_and_is_clean - FileUtils.rm_rf target_dir - FileUtils.mkdir_p target_dir - end - - def target_dir - File.join(doc_dir, doc_type) - end - - end - end -end \ No newline at end of file diff --git a/lib/pact/doc/interaction_view_model.rb b/lib/pact/doc/interaction_view_model.rb deleted file mode 100644 index 5383b85e..00000000 --- a/lib/pact/doc/interaction_view_model.rb +++ /dev/null @@ -1,124 +0,0 @@ -require 'pact/shared/active_support_support' -require 'pact/reification' -require 'cgi' - -module Pact - module Doc - class InteractionViewModel - - include Pact::ActiveSupportSupport - - def initialize interaction, consumer_contract - @interaction = interaction - @consumer_contract = consumer_contract - end - - def id - @id ||= begin - full_desc = if has_provider_state? - "#{description} given #{interaction.provider_state}" - else - description - end - CGI.escapeHTML(full_desc.gsub(/\s+/,'_')) - end - end - - def request_method - interaction.request.method.upcase - end - - def request_path - interaction.request.path - end - - def response_status - interaction.response.status - end - - def consumer_name - markdown_escape @consumer_contract.consumer.name - end - - def provider_name - markdown_escape @consumer_contract.provider.name - end - - def has_provider_state? - @interaction.provider_state && !@interaction.provider_state.empty? - end - - def provider_state start_of_sentence = false - markdown_escape apply_capitals(@interaction.provider_state.strip, start_of_sentence) - end - - def description start_of_sentence = false - return '' unless @interaction.description - markdown_escape apply_capitals(@interaction.description.strip, start_of_sentence) - end - - def request - fix_json_formatting JSON.pretty_generate(clean_request) - end - - def response - fix_json_formatting JSON.pretty_generate(clean_response) - end - - private - - attr_reader :interaction, :consumer_contract - - def clean_request - reified_request = Reification.from_term(interaction.request) - ordered_clean_hash(reified_request).tap do | hash | - hash[:body] = reified_request[:body] if reified_request[:body] - end - end - - def clean_response - ordered_clean_hash Reification.from_term(interaction.response) - end - - # Remove empty body and headers hashes from response, and empty headers from request, - # as an empty hash means "allow anything" - it's more intuitive and cleaner to just - # remove the empty hashes from display. - def ordered_clean_hash source - ordered_keys.each_with_object({}) do |key, target| - if source.key? key - target[key] = source[key] unless value_is_an_empty_hash(source[key]) - end - end - end - - def value_is_an_empty_hash value - value.is_a?(Hash) && value.empty? - end - - def ordered_keys - [:method, :path, :query, :status, :headers, :body] - end - - def remove_key_if_empty key, hash - hash.delete(key) if hash[key].is_a?(Hash) && hash[key].empty? - end - - def apply_capitals string, start_of_sentence = false - start_of_sentence ? capitalize_first_letter(string) : lowercase_first_letter(string) - end - - def capitalize_first_letter string - string[0].upcase + string[1..-1] - end - - def lowercase_first_letter string - string[0].downcase + string[1..-1] - end - - def markdown_escape string - return nil unless string - string.gsub('*','\*').gsub('_','\_') - end - end - end -end diff --git a/lib/pact/doc/markdown/consumer_contract_renderer.rb b/lib/pact/doc/markdown/consumer_contract_renderer.rb deleted file mode 100644 index d6826967..00000000 --- a/lib/pact/doc/markdown/consumer_contract_renderer.rb +++ /dev/null @@ -1,68 +0,0 @@ -require 'pact/doc/markdown/interaction_renderer' -require 'pact/doc/sort_interactions' - -module Pact - module Doc - module Markdown - class ConsumerContractRenderer - - def initialize consumer_contract - @consumer_contract = consumer_contract - end - - def self.call consumer_contract - new(consumer_contract).call - end - - def call - title + summaries_title + summaries.join + interactions_title + full_interactions.join - end - - private - - attr_reader :consumer_contract - - def title - "### A pact between #{consumer_name} and #{provider_name}\n\n" - end - - def interaction_renderers - @interaction_renderers ||= sorted_interactions.collect{|interaction| InteractionRenderer.new interaction, @consumer_contract} - end - - def summaries_title - "#### Requests from #{consumer_name} to #{provider_name}\n\n" - end - - def interactions_title - "#### Interactions\n\n" - end - - def summaries - interaction_renderers.collect(&:render_summary) - end - - def full_interactions - interaction_renderers.collect(&:render_full_interaction) - end - - def sorted_interactions - SortInteractions.call(consumer_contract.interactions) - end - - def consumer_name - markdown_escape consumer_contract.consumer.name - end - - def provider_name - markdown_escape consumer_contract.provider.name - end - - def markdown_escape string - string.gsub('*','\*').gsub('_','\_') - end - - end - end - end -end \ No newline at end of file diff --git a/lib/pact/doc/markdown/generator.rb b/lib/pact/doc/markdown/generator.rb deleted file mode 100644 index f7cef4a5..00000000 --- a/lib/pact/doc/markdown/generator.rb +++ /dev/null @@ -1,26 +0,0 @@ -require 'pact/doc/generator' -require 'pact/doc/markdown/consumer_contract_renderer' -require 'pact/doc/markdown/index_renderer' - -module Pact - module Doc - module Markdown - class Generator < Pact::Doc::Generator - - def initialize pact_dir, doc_dir - super(pact_dir, doc_dir, - consumer_contract_renderer: ConsumerContractRenderer, - doc_type: 'markdown', - file_extension: '.md', - index_renderer: IndexRenderer, - index_name: 'README') - end - - def self.call pact_dir, doc_dir - new(pact_dir, doc_dir).call - end - - end - end - end -end \ No newline at end of file diff --git a/lib/pact/doc/markdown/index_renderer.rb b/lib/pact/doc/markdown/index_renderer.rb deleted file mode 100644 index 66ad1d9c..00000000 --- a/lib/pact/doc/markdown/index_renderer.rb +++ /dev/null @@ -1,43 +0,0 @@ -require 'erb' - -module Pact - module Doc - module Markdown - class IndexRenderer - - attr_reader :consumer_name - attr_reader :docs # Hash of pact title => file_name - - def initialize consumer_name, docs - @consumer_name = consumer_name - @docs = docs - end - - def self.call consumer_name, docs - new(consumer_name, docs).call - end - - def call - title + "\n\n" + table_of_contents + "\n" - end - - private - - def table_of_contents - docs.collect do | title, file_name | - item title, file_name - end.join("\n") - end - - def title - "### Pacts for #{consumer_name}" - end - - def item title, file_name - "* [#{title}](#{ERB::Util.url_encode(file_name)})" - end - - end - end - end -end \ No newline at end of file diff --git a/lib/pact/doc/markdown/interaction.erb b/lib/pact/doc/markdown/interaction.erb deleted file mode 100644 index 41dcd9ee..00000000 --- a/lib/pact/doc/markdown/interaction.erb +++ /dev/null @@ -1,14 +0,0 @@ - -<%= if interaction.has_provider_state? - "Given **#{interaction.provider_state}**, upon receiving" - else - "Upon receiving" - end -%> **<%= interaction.description %>** from <%= interaction.consumer_name %>, with -```json -<%= interaction.request %> -``` -<%= interaction.provider_name %> will respond with: -```json -<%= interaction.response %> -``` diff --git a/lib/pact/doc/markdown/interaction_renderer.rb b/lib/pact/doc/markdown/interaction_renderer.rb deleted file mode 100644 index 502bd69d..00000000 --- a/lib/pact/doc/markdown/interaction_renderer.rb +++ /dev/null @@ -1,43 +0,0 @@ -require 'erb' -require 'pact/doc/interaction_view_model' - -module Pact - module Doc - module Markdown - class InteractionRenderer - - attr_reader :interaction - - def initialize interaction, pact - @interaction = InteractionViewModel.new(interaction, pact) - end - - def render_summary - suffix = interaction.has_provider_state? ? " given #{interaction.provider_state}" : "" - "* [#{interaction.description(true)}](##{interaction.id})#{suffix}\n\n" - end - - def render_full_interaction - render('/interaction.erb') - end - - def render template_file - ERB.new(template_string(template_file)).result(binding) - end - - # The template file is written with only ASCII range characters, so we - # can read as UTF-8. But rendered strings must have same encoding as - # script encoding because it will joined to strings which are produced by - # string literal. - def template_string(template_file) - File.read(template_contents(template_file), external_encoding: Encoding::UTF_8).force_encoding(__ENCODING__) - end - - def template_contents(template_file) - File.dirname(__FILE__) + template_file - end - - end - end - end -end diff --git a/lib/pact/doc/sort_interactions.rb b/lib/pact/doc/sort_interactions.rb deleted file mode 100644 index 398c11f2..00000000 --- a/lib/pact/doc/sort_interactions.rb +++ /dev/null @@ -1,16 +0,0 @@ -module Pact - module Doc - class SortInteractions - - def self.call interactions - interactions.sort_by { |interaction| sortable_id(interaction) } - end - - private - - def self.sortable_id interaction - "#{interaction.description.downcase} #{interaction.response.status} #{(interaction.provider_state || '').downcase}" - end - end - end -end diff --git a/lib/pact/generators.rb b/lib/pact/generators.rb new file mode 100644 index 00000000..31adbe60 --- /dev/null +++ b/lib/pact/generators.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module Pact + module Generators + + def generate_random_int(min:, max:) + Pact::Generators::RandomIntGenerator.new(min: min, max: max) + end + def generate_random_decimal(digits:) + Pact::Generators::RandomDecimalGenerator.new(digits: digits) + end + def generate_random_hexadecimal(digits:) + Pact::Generators::RandomHexadecimalGenerator.new(digits: digits) + end + def generate_random_string(size:) + Pact::Generators::RandomStringGenerator.new(size: size) + end + + def generate_uuid(example: nil) + Pact::Generators::UuidGenerator.new(example: example) + end + + def generate_date(format: nil, example: nil) + Pact::Generators::DateGenerator.new(format: format, example: example) + end + + def generate_time(format: nil) + Pact::Generators::TimeGenerator.new(format: format) + end + + def generate_datetime(format: nil) + Pact::Generators::DateTimeGenerator.new(format: format) + end + + def generate_random_boolean + Pact::Generators::RandomBooleanGenerator.new + end + + def generate_from_provider_state(expression:, example:) + Pact::Generators::ProviderStateGenerator.new(expression: expression, example: example).as_basic + end + + def generate_mock_server_url(regex: nil, example: nil) + Pact::Generators::MockServerURLGenerator.new(regex: regex, example: example) + end + end +end diff --git a/lib/pact/generators/base.rb b/lib/pact/generators/base.rb new file mode 100644 index 00000000..cc1ffc13 --- /dev/null +++ b/lib/pact/generators/base.rb @@ -0,0 +1,285 @@ +# frozen_string_literal: true + +module Pact + module Generators + module Base + def as_basic + raise NotImplementedError, "Subclasses must implement the as_basic method" + end + end + + class RandomIntGenerator + include Base + + def initialize(min:, max:) + @min = min + @max = max + end + + def as_basic + { + "pact:matcher:type" => "integer", + "pact:generator:type" => "RandomInt", + "min" => @min, + "max" => @max, + "value" => rand(@min..@max) + } + end + end + + class RandomDecimalGenerator + include Base + + def initialize(digits:) + @digits = digits + end + + def as_basic + { + 'pact:matcher:type' => 'decimal', + "pact:generator:type" => "RandomDecimal", + "digits" => @digits, + "value" => rand.round(@digits) + } + end + end + + class RandomHexadecimalGenerator + include Base + + def initialize(digits:) + @digits = digits + end + + def as_basic + { + "pact:matcher:type" => "decimal", + "pact:generator:type" => "RandomHexadecimal", + "digits" => @digits, + "value" => SecureRandom.hex((@digits / 2.0).ceil)[0...@digits] + } + end + end + + class RandomStringGenerator + include Base + + def initialize(size:, example: nil) + @size = size + @example = example + end + + def as_basic + { + "pact:matcher:type" => "type", + "pact:generator:type" => "RandomString", + "size" => @size, + "value" => @example || SecureRandom.alphanumeric(@size) + } + end + end + + class UuidGenerator + include Base + + + def initialize(example: nil) + @example = example + @regexStr = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'; + if @example + regex = Regexp.new("^#{@regexStr}$") + unless @example.match?(regex) + raise ArgumentError, "regex: Example value '#{@example}' does not match the UUID regular expression '#{@regexStr}'" + end + end + end + + def as_basic + { + "pact:matcher:type" => "regex", + "pact:generator:type" => "Uuid", + "regex" => @regexStr, + "value" => @example || SecureRandom.uuid + } + end + end + + class DateGenerator + include Base + + def initialize(format: nil, example: nil) + @format = format || default_format + @example = example || Time.now.strftime(convert_from_java_simple_date_format(@format)) + end + + def as_basic + h = { "pact:generator:type" => type } + h["pact:matcher:type"] = matcher_type + h["format"] = @format if @format + h["value"] = @example + h + end + + def type + 'Date' + end + + def matcher_type + 'date' + end + + def default_format + 'yyyy-MM-dd' + end + + # Converts Java SimpleDateFormat to Ruby strftime format + def convert_from_java_simple_date_format(format) + f = format.dup + # Year + f.gsub!(/(? "boolean", + "pact:generator:type" => "RandomBoolean", + "value" => @example.nil? ? [true, false].sample : @example + } + end + end + + class ProviderStateGenerator + include Base + + def initialize(expression:, example:) + @expression = expression + @value = example + end + + def as_basic + { + 'pact:matcher:type' => 'type', + "pact:generator:type" => "ProviderState", + "expression" => @expression, + "value" => @value + } + end + end + + class MockServerURLGenerator + include Base + + def initialize(regex:, example:) + @regex = regex + @example = example + end + + def as_basic + { + "pact:generator:type" => "MockServerURL", + "pact:matcher:type" => "regex", + "regex" => @regex, + "example" => @example, + "value" => @example + } + end + end + end +end diff --git a/lib/pact/hal/authorization_header_redactor.rb b/lib/pact/hal/authorization_header_redactor.rb deleted file mode 100644 index a0c37537..00000000 --- a/lib/pact/hal/authorization_header_redactor.rb +++ /dev/null @@ -1,32 +0,0 @@ -require 'delegate' - -module Pact - module Hal - class AuthorizationHeaderRedactor < SimpleDelegator - def puts(*args) - __getobj__().puts(*redact_args(args)) - end - - def print(*args) - __getobj__().puts(*redact_args(args)) - end - - def <<(*args) - __getobj__().send(:<<, *redact_args(args)) - end - - private - - attr_reader :redactions - - def redact_args(args) - args.collect{ | s| redact(s) } - end - - def redact(string) - return string unless string.is_a?(String) - string.gsub(/Authorization: .*\\r\\n/, "Authorization: [redacted]\\r\\n") - end - end - end -end diff --git a/lib/pact/hal/entity.rb b/lib/pact/hal/entity.rb deleted file mode 100644 index 33490a17..00000000 --- a/lib/pact/hal/entity.rb +++ /dev/null @@ -1,110 +0,0 @@ -require 'uri' -require 'delegate' -require 'pact/hal/link' -require 'pact/errors' - -module Pact - module Hal - class RelationNotFoundError < ::Pact::Error; end - - class ErrorResponseReturned < ::Pact::Error; end - - class Entity - - def initialize(href, data, http_client, response = nil) - @href = href - @data = data - @links = (@data || {}).fetch("_links", {}) - @client = http_client - @response = response - end - - def get(key, *args) - _link(key).get(*args) - end - - def get!(key, *args) - _link(key).get!(*args) - end - - def post(key, *args) - _link(key).post(*args) - end - - def put(key, *args) - _link(key).put(*args) - end - - def can?(key) - @links.key? key.to_s - end - - def follow(key, http_method, *args) - Link.new(@links[key].merge(method: http_method), @client).run(*args) - end - - def _link(key, fallback_key = nil) - if @links[key] - Link.new(@links[key], @client) - elsif fallback_key && @links[fallback_key] - Link.new(@links[fallback_key], @client) - else - nil - end - end - - def _link!(key) - _link(key) or raise RelationNotFoundError.new("Could not find relation '#{key}' in resource at #{@href}") - end - - def success? - true - end - - def response - @response - end - - def fetch(key, fallback_key = nil) - @links[key] || (fallback_key && @links[fallback_key]) - end - - def method_missing(method_name, *args, &block) - if @data.key?(method_name.to_s) - @data[method_name.to_s] - elsif @links.key?(method_name) - Link.new(@links[method_name], @client).run(*args) - else - super - end - end - - def respond_to_missing?(method_name, include_private = false) - @data.key?(method_name) || @links.key?(method_name) - end - - def assert_success! - self - end - end - - class ErrorEntity < Entity - - def initialize(href, data, http_client, response = nil) - @href = href - @data = data - @links = {} - @client = http_client - @response = response - end - - def success? - false - end - - def assert_success! - raise ErrorResponseReturned.new("Error retrieving #{@href} status=#{response ? response.code: nil} #{response ? response.raw_body : ''}") - end - end - end -end diff --git a/lib/pact/hal/http_client.rb b/lib/pact/hal/http_client.rb deleted file mode 100644 index 42ecfefe..00000000 --- a/lib/pact/hal/http_client.rb +++ /dev/null @@ -1,146 +0,0 @@ -require 'pact/retry' -require 'pact/hal/authorization_header_redactor' -require 'net/http' -require 'rack' -require 'openssl' - -module Pact - module Hal - class RetriableHttpStatusError < StandardError - RETRIABLE_STATUS_CODES = [502, 503, 504].freeze - - attr_reader :status_code, :response_body - - def initialize status_code, response_body - @status_code = status_code - @response_body = response_body - super("HTTP #{status_code} error: #{response_body}") - end - end - - class HttpClient - attr_accessor :username, :password, :verbose, :token - - def initialize options - @username = options[:username] - @password = options[:password] - @verbose = options[:verbose] - @token = options[:token] - end - - def get href, params = {}, headers = {} - uri = URI(href) - if params && params.any? - existing_params = Rack::Utils.parse_nested_query(uri.query) - uri.query = Rack::Utils.build_nested_query(existing_params.merge(params)) - end - perform_request(create_request(uri, 'Get', nil, headers), uri) - end - - def put href, body = nil, headers = {} - uri = URI(href) - perform_request(create_request(uri, 'Put', body, headers), uri) - end - - def post href, body = nil, headers = {} - uri = URI(href) - perform_request(create_request(uri, 'Post', body, headers), uri) - end - - def create_request uri, http_method, body = nil, headers = {} - request = Net::HTTP.const_get(http_method).new(uri.request_uri) - headers.each do | key, value | - request[key] = value - end - request.body = body if body - request.basic_auth username, password if username - request['Authorization'] = "Bearer #{token}" if token - request - end - - def perform_request request, uri - response = Retry.until_true do - http = Net::HTTP.new(uri.host, uri.port, :ENV) - http.set_debug_output(output_stream) if verbose? - http.use_ssl = (uri.scheme == 'https') - http.ca_file = ENV['SSL_CERT_FILE'] if ENV['SSL_CERT_FILE'] && ENV['SSL_CERT_FILE'] != '' - http.ca_path = ENV['SSL_CERT_DIR'] if ENV['SSL_CERT_DIR'] && ENV['SSL_CERT_DIR'] != '' - - if x509_certificate? - http.cert = OpenSSL::X509::Certificate.new(x509_client_cert_file) - http.key = OpenSSL::PKey::RSA.new(x509_client_key_file) - end - - if disable_ssl_verification? - if verbose? - Pact.configuration.output_stream.puts("SSL verification is disabled") - end - http.verify_mode = OpenSSL::SSL::VERIFY_NONE - end - result = http.start do |http| - http.request request - end - - # Raise error on specific 5xx status codes to trigger retry - status_code = result.code.to_i - raise RetriableHttpStatusError.new(status_code, result.body) if RetriableHttpStatusError::RETRIABLE_STATUS_CODES.include?(status_code) - - result - end - Response.new(response) - end - - def output_stream - AuthorizationHeaderRedactor.new(Pact.configuration.output_stream) - end - - def verbose? - verbose || ENV['VERBOSE'] == 'true' - end - - def x509_certificate? - ENV['X509_CLIENT_CERT_FILE'] && ENV['X509_CLIENT_CERT_FILE'] != '' && - ENV['X509_CLIENT_KEY_FILE'] && ENV['X509_CLIENT_KEY_FILE'] != '' - end - - def x509_client_cert_file - File.read(ENV['X509_CLIENT_CERT_FILE']) - end - - def x509_client_key_file - File.read(ENV['X509_CLIENT_KEY_FILE']) - end - - def disable_ssl_verification? - ENV['PACT_DISABLE_SSL_VERIFICATION'] == 'true' || ENV['PACT_BROKER_DISABLE_SSL_VERIFICATION'] == 'true' - end - - class Response < SimpleDelegator - def body - bod = raw_body - if bod && bod != '' - JSON.parse(bod) - else - nil - end - end - - def raw_body - __getobj__().body - end - - def status - code.to_i - end - - def success? - __getobj__().code.start_with?("2") - end - - def json? - self['content-type'] && self['content-type'] =~ /json/ - end - end - end - end -end diff --git a/lib/pact/hal/link.rb b/lib/pact/hal/link.rb deleted file mode 100644 index d248b432..00000000 --- a/lib/pact/hal/link.rb +++ /dev/null @@ -1,112 +0,0 @@ -require 'erb' -require 'delegate' - -module Pact - module Hal - class Link - attr_reader :request_method, :href - - DEFAULT_GET_HEADERS = { - "Accept" => "application/hal+json" - }.freeze - - DEFAULT_POST_HEADERS = { - "Accept" => "application/hal+json", - "Content-Type" => "application/json" - }.freeze - - def initialize(attrs, http_client) - @attrs = attrs - @request_method = attrs.fetch(:method, :get).to_sym - @href = attrs.fetch('href') - @http_client = http_client - end - - def run(payload = nil) - case request_method - when :get - get(payload) - when :put - put(payload) - when :post - post(payload) - end - end - - def title_or_name - title || name - end - - def title - @attrs['title'] - end - - def name - @attrs['name'] - end - - def get(payload = {}, headers = {}) - wrap_response(href, @http_client.get(href, payload, DEFAULT_GET_HEADERS.merge(headers))) - end - - def get!(*args) - get(*args).assert_success! - end - - def put(payload = nil, headers = {}) - wrap_response(href, @http_client.put(href, payload ? payload.to_json : nil, DEFAULT_POST_HEADERS.merge(headers))) - end - - def post(payload = nil, headers = {}) - wrap_response(href, @http_client.post(href, payload ? payload.to_json : nil, DEFAULT_POST_HEADERS.merge(headers))) - end - - def post!(payload = nil, headers = {}) - post(payload, headers).assert_success! - end - - def expand(params) - expanded_url = expand_url(params, href) - new_attrs = @attrs.merge('href' => expanded_url) - Link.new(new_attrs, http_client) - end - - def with_query(query) - if query && query.any? - uri = URI(href) - existing_query_params = Rack::Utils.parse_nested_query(uri.query) - uri.query = Rack::Utils.build_nested_query(existing_query_params.merge(query)) - new_attrs = attrs.merge('href' => uri.to_s) - Link.new(new_attrs, http_client) - else - self - end - end - - private - - attr_reader :attrs, :http_client - - def wrap_response(href, http_response) - require 'pact/hal/entity' # avoid circular reference - require 'pact/hal/non_json_entity' - - if http_response.success? - if http_response.json? - Entity.new(href, http_response.body, @http_client, http_response) - else - NonJsonEntity.new(href, http_response.raw_body, @http_client, http_response) - end - else - ErrorEntity.new(href, http_response.raw_body, @http_client, http_response) - end - end - - def expand_url(params, url) - params.inject(url) do | url, (key, value) | - url.gsub('{' + key.to_s + '}', ERB::Util.url_encode(value)) - end - end - end - end -end diff --git a/lib/pact/hal/non_json_entity.rb b/lib/pact/hal/non_json_entity.rb deleted file mode 100644 index 83c96da7..00000000 --- a/lib/pact/hal/non_json_entity.rb +++ /dev/null @@ -1,28 +0,0 @@ -module Pact - module Hal - class NonJsonEntity - def initialize(href, body, http_client, response = nil) - @href = href - @body = body - @client = http_client - @response = response - end - - def success? - true - end - - def response - @response - end - - def body - @body - end - - def assert_success! - self - end - end - end -end diff --git a/lib/pact/hash_refinements.rb b/lib/pact/hash_refinements.rb deleted file mode 100644 index 3e738521..00000000 --- a/lib/pact/hash_refinements.rb +++ /dev/null @@ -1,17 +0,0 @@ -module Pact - module HashRefinements - refine Hash do - def compact - h = {} - each do |key, value| - h[key] = value unless value == nil - end - h - end unless Hash.method_defined? :compact - - def compact! - reject! {|_key, value| value == nil} - end unless Hash.method_defined? :compact! - end - end -end diff --git a/lib/pact/matchers.rb b/lib/pact/matchers.rb new file mode 100644 index 00000000..acfc5ae1 --- /dev/null +++ b/lib/pact/matchers.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +module Pact + module Matchers + PACT_SPEC_V1 = 1 + PACT_SPEC_V2 = 2 + PACT_SPEC_V3 = 3 + PACT_SPEC_V4 = 4 + + ANY_STRING_REGEX = /.*/ + UUID_REGEX = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i + + # simplified + ISO8601_REGEX = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)*(.\d{2}:\d{2})*/i + + def match_exactly(arg) + V1::Equality.new(arg) + end + + def match_type_of(arg) + V2::Type.new(arg) + end + + def match_include(arg) + V3::Include.new(arg) + end + + def match_any_string(sample = "any") + V2::Regex.new(ANY_STRING_REGEX, sample) + end + + def match_any_integer(sample = 10) + V3::Integer.new(sample) + end + + def match_any_decimal(sample = 10.0) + V3::Decimal.new(sample) + end + + def match_any_number(sample = 10.0) + V3::Number.new(sample) + end + + def match_any_boolean(sample = true) + V3::Boolean.new(sample) + end + + def match_uuid(sample = "e1d01e04-3a2b-4eed-a4fb-54f5cd257338") + V2::Regex.new(UUID_REGEX, sample) + end + + def match_regex(regex, sample) + V2::Regex.new(regex, sample) + end + + def match_datetime(format, sample) + V3::DateTime.new(format, sample) + end + + def match_iso8601(sample = "2024-08-12T12:25:00.243118+03:00") + V2::Regex.new(ISO8601_REGEX, sample) + end + + def match_date(format, sample) + V3::Date.new(format, sample) + end + + def match_time(format, sample) + V3::Time.new(format, sample) + end + + def match_each(template, min = nil) + V3::Each.new(template, min) + end + + def match_each_regex(regex, sample) + match_each_value(sample, match_regex(regex, sample)) + end + + def match_each_key(template, key_matchers) + V4::EachKey.new(key_matchers.is_a?(Array) ? key_matchers : [key_matchers], template) + end + + def match_each_value(template, value_matchers = V2::Type.new("")) + V4::EachValue.new(value_matchers.is_a?(Array) ? value_matchers : [value_matchers], template) + end + + def match_each_kv(template, key_matchers) + V4::EachKeyValue.new(key_matchers.is_a?(Array) ? key_matchers : [key_matchers], template) + end + + def match_semver(template = nil) + V3::Semver.new(template) + end + + def match_content_type(content_type, template = nil) + V3::ContentType.new(content_type, template: template) + end + + def match_not_empty(template = nil) + V4::NotEmpty.new(template) + end + + def match_status_code(template) + V4::StatusCode.new(template) + end + end +end diff --git a/lib/pact/matchers/base.rb b/lib/pact/matchers/base.rb new file mode 100644 index 00000000..afc142d9 --- /dev/null +++ b/lib/pact/matchers/base.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +module Pact + module Matchers + # see https://github.com/pact-foundation/pact-reference/blob/master/rust/pact_ffi/IntegrationJson.md + class Base + attr_reader :spec_version, :kind, :template, :opts + + class MatcherInitializationError < Pact::Error; end + + def initialize(spec_version:, kind:, template: nil, opts: {}) + @spec_version = spec_version + @kind = kind + @template = template + @opts = opts + end + + def as_basic + result = { + "pact:matcher:type" => serialize!(@kind.deep_dup, :basic) + } + result["status"] = serialize!(@opts[:status].deep_dup, :basic) if @opts[:status] + result["value"] = serialize!(@template.deep_dup, :basic) unless @template.nil? + result.merge!(serialize!(@opts.deep_dup, :basic)) + result + end + + def as_plugin + params = @opts.values.map { |v| format_primitive(v) }.join(",") + value = format_primitive(@template) unless @template.nil? + + if @template.nil? + return "matching(#{@kind}#{params.present? ? ", #{params}" : ""})" + end + + return "matching(#{@kind}, #{params}, #{value})" if params.present? + + "matching(#{@kind}, #{value})" + end + + private + + def serialize!(data, format) + # serialize complex types recursively + case data + when TrueClass, FalseClass, Numeric, String + data + when Array + data.map { |v| serialize!(v, format) } + when Hash + data.transform_values { |v| serialize!(v, format) } + when Pact::Matchers::Base + return data.as_basic if format == :basic + data.as_plugin if format == :plugin + else + data + end + end + + def format_primitive(arg) + case arg + when TrueClass, FalseClass, Numeric + arg.to_s + when String + "'#{arg}'" + else + raise "#{arg.class} is not a primitive" + end + end + end + end +end diff --git a/lib/pact/matchers/v1/equality.rb b/lib/pact/matchers/v1/equality.rb new file mode 100644 index 00000000..3c91912e --- /dev/null +++ b/lib/pact/matchers/v1/equality.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Pact + module Matchers + module V1 + class Equality < Pact::Matchers::Base + def initialize(template) + super(spec_version: Pact::Matchers::PACT_SPEC_V1, kind: 'equality', template: template) + end + + def as_plugin + "matching(equalTo, #{format_primitive(@template)})" + end + end + end + end +end diff --git a/lib/pact/matchers/v2/regex.rb b/lib/pact/matchers/v2/regex.rb new file mode 100644 index 00000000..ececf551 --- /dev/null +++ b/lib/pact/matchers/v2/regex.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Pact + module Matchers + module V2 + class Regex < Pact::Matchers::Base + def initialize(regex, template) + unless regex.is_a?(Regexp) + raise MatcherInitializationError, + "#{self.class}: #{regex} should be an instance of Regexp" + end + unless template.is_a?(String) || template.is_a?(Array) + raise MatcherInitializationError, + "#{self.class}: #{template} should be an instance of String or Array" + end + if template.is_a?(Array) && !template.all?(String) + raise MatcherInitializationError, + "#{self.class}: #{template} array values should be strings" + end + + super(spec_version: Pact::Matchers::PACT_SPEC_V2, kind: 'regex', template: template, opts: { regex: regex.to_s }) # rubocop:disable Layout/LineLength + end + end + end + end +end diff --git a/lib/pact/matchers/v2/type.rb b/lib/pact/matchers/v2/type.rb new file mode 100644 index 00000000..406df734 --- /dev/null +++ b/lib/pact/matchers/v2/type.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Pact + module Matchers + module V2 + class Type < Pact::Matchers::Base + def initialize(template) + unless template.is_a?(TrueClass) || template.is_a?(FalseClass) || template.is_a?(Numeric) || template.is_a?(String) || template.is_a?(Array) || template.is_a?(Hash) # rubocop:disable Layout/LineLength + raise MatcherInitializationError, + "#{self.class}: template is not a primitive" + end + + super(spec_version: Pact::Matchers::PACT_SPEC_V2, kind: 'type', template: template) + end + end + end + end +end diff --git a/lib/pact/matchers/v3/boolean.rb b/lib/pact/matchers/v3/boolean.rb new file mode 100644 index 00000000..b4a54993 --- /dev/null +++ b/lib/pact/matchers/v3/boolean.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Pact + module Matchers + module V3 + class Boolean < Pact::Matchers::Base + def initialize(template) + unless template.is_a?(TrueClass) || template.is_a?(FalseClass) + raise MatcherInitializationError, + "#{self.class}: #{template} should be an instance of Boolean" + end + + super(spec_version: Pact::Matchers::PACT_SPEC_V3, kind: 'boolean', template: template) + end + end + end + end +end diff --git a/lib/pact/matchers/v3/content_type.rb b/lib/pact/matchers/v3/content_type.rb new file mode 100644 index 00000000..992dc525 --- /dev/null +++ b/lib/pact/matchers/v3/content_type.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Pact + module Matchers + module V3 + class ContentType < Pact::Matchers::Base + def initialize(content_type, template: nil) + @content_type = content_type + @template = template + @opts = {} + @opts[:plugin_template] = template unless template.nil? + unless content_type.is_a?(String) && !content_type.empty? + raise MatcherInitializationError, "#{self.class}: content_type must be a non-empty String" + end + + super( + spec_version: Pact::Matchers::PACT_SPEC_V3, + kind: 'contentType', + template: content_type, + opts: @opts + ) + end + + def as_plugin + "matching(contentType, '#{@content_type}', '#{@opts[:plugin_template]}')" + end + end + end + end +end diff --git a/lib/pact/matchers/v3/date.rb b/lib/pact/matchers/v3/date.rb new file mode 100644 index 00000000..9b60f2ae --- /dev/null +++ b/lib/pact/matchers/v3/date.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Pact + module Matchers + module V3 + class Date < Pact::Matchers::Base + def initialize(format, template) + unless template.is_a?(String) + raise MatcherInitializationError, + "#{self.class}: #{format} should be an instance of String" + end + unless template.is_a?(String) + raise MatcherInitializationError, + "#{self.class}: #{template} should be an instance of String" + end + + super(spec_version: Pact::Matchers::PACT_SPEC_V3, kind: 'date', template: template, opts: { format: format }) + end + end + end + end +end diff --git a/lib/pact/matchers/v3/date_time.rb b/lib/pact/matchers/v3/date_time.rb new file mode 100644 index 00000000..8afb23e7 --- /dev/null +++ b/lib/pact/matchers/v3/date_time.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Pact + module Matchers + module V3 + class DateTime < Pact::Matchers::Base + def initialize(format, template) + unless template.is_a?(String) + raise MatcherInitializationError, + "#{self.class}: #{format} should be an instance of String" + end + unless template.is_a?(String) + raise MatcherInitializationError, + "#{self.class}: #{template} should be an instance of String" + end + + super(spec_version: Pact::Matchers::PACT_SPEC_V3, kind: 'datetime', template: template, opts: { format: format }) # rubocop:disable Layout/LineLength + end + end + end + end +end diff --git a/lib/pact/matchers/v3/decimal.rb b/lib/pact/matchers/v3/decimal.rb new file mode 100644 index 00000000..92c326fe --- /dev/null +++ b/lib/pact/matchers/v3/decimal.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Pact + module Matchers + module V3 + class Decimal < Pact::Matchers::Base + def initialize(template) + unless template.is_a?(Float) + raise MatcherInitializationError, + "#{self.class}: #{template} should be an instance of Float" + end + + super(spec_version: Pact::Matchers::PACT_SPEC_V3, kind: 'decimal', template: template) + end + end + end + end +end diff --git a/lib/pact/matchers/v3/each.rb b/lib/pact/matchers/v3/each.rb new file mode 100644 index 00000000..1f1f187e --- /dev/null +++ b/lib/pact/matchers/v3/each.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Pact + module Matchers + module V3 + class Each < Pact::Matchers::Base + def initialize(template, min) + if min.present? && min < 1 + raise MatcherInitializationError, + "#{self.class}: #{min} should be greater than 0" + end + + min_array_size = min.presence || 1 + val = template.is_a?(Array) ? template : [template] * min_array_size + + if min_array_size != val.size + raise MatcherInitializationError, + "#{self.class}: #{min} is invalid: template size is #{val.size}" + end + + super( + spec_version: Pact::Matchers::PACT_SPEC_V3, + kind: 'type', + template: val, + opts: { min: min_array_size }) + end + + def as_plugin + if @template.first.is_a?(Hash) + return { + 'pact:match' => "eachValue(matching($'SAMPLE'))", + 'SAMPLE' => serialize!(@template.first.deep_dup, :plugin) + } + end + + params = @opts.except(:min).values.map { |v| format_primitive(v) }.join(',') + value = format_primitive(@template.first) + + return "eachValue(matching(#{@kind}, #{params}, #{value}))" if params.present? + + "eachValue(matching(#{@kind}, #{value}))" + end + end + end + end +end diff --git a/lib/pact/matchers/v3/include.rb b/lib/pact/matchers/v3/include.rb new file mode 100644 index 00000000..e3d4cba6 --- /dev/null +++ b/lib/pact/matchers/v3/include.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Pact + module Matchers + module V3 + class Include < Pact::Matchers::Base + def initialize(template) + unless template.is_a?(String) + raise MatcherInitializationError, + "#{self.class}: #{template} should be an instance of String" + end + + super(spec_version: Pact::Matchers::PACT_SPEC_V3, kind: 'include', template: template) + end + end + end + end +end diff --git a/lib/pact/matchers/v3/integer.rb b/lib/pact/matchers/v3/integer.rb new file mode 100644 index 00000000..88b67133 --- /dev/null +++ b/lib/pact/matchers/v3/integer.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Pact + module Matchers + module V3 + class Integer < Pact::Matchers::Base + def initialize(template) + unless template.is_a?(::Integer) + raise MatcherInitializationError, + "#{self.class}: #{template} should be an instance of Integer" + end + + super(spec_version: Pact::Matchers::PACT_SPEC_V3, kind: 'integer', template: template) + end + end + end + end +end diff --git a/lib/pact/matchers/v3/null.rb b/lib/pact/matchers/v3/null.rb new file mode 100644 index 00000000..c8d2e272 --- /dev/null +++ b/lib/pact/matchers/v3/null.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Pact + module Matchers + module V3 + class Null < Pact::Matchers::Base + def initialize + super(spec_version: Pact::Matchers::PACT_SPEC_V3, kind: 'null', template: nil) + end + end + end + end +end diff --git a/lib/pact/matchers/v3/number.rb b/lib/pact/matchers/v3/number.rb new file mode 100644 index 00000000..7b4143c1 --- /dev/null +++ b/lib/pact/matchers/v3/number.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Pact + module Matchers + module V3 + class Number < Pact::Matchers::Base + def initialize(template) + unless template.is_a?(Numeric) + raise MatcherInitializationError, + "#{self.class}: #{template} should be an instance of Numeric" + end + + super(spec_version: Pact::Matchers::PACT_SPEC_V3, kind: 'number', template: template) + end + end + end + end +end diff --git a/lib/pact/matchers/v3/semver.rb b/lib/pact/matchers/v3/semver.rb new file mode 100644 index 00000000..fa3b7e6c --- /dev/null +++ b/lib/pact/matchers/v3/semver.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Pact + module Matchers + module V3 + class Semver < Pact::Matchers::Base + def initialize(template = nil) + @template = template + super(spec_version: Pact::Matchers::PACT_SPEC_V3, kind: 'semver', template: template) + end + + def as_plugin + if @template.nil? || @template.blank? + raise MatcherInitializationError, "#{self.class}: template must be provided when calling as_plugin" + end + + "matching(semver, '#{@template}')" + end + end + end + end +end diff --git a/lib/pact/matchers/v3/time.rb b/lib/pact/matchers/v3/time.rb new file mode 100644 index 00000000..2ae29a18 --- /dev/null +++ b/lib/pact/matchers/v3/time.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Pact + module Matchers + module V3 + class Time < Pact::Matchers::Base + def initialize(format, template) + unless template.is_a?(String) + raise MatcherInitializationError, + "#{self.class}: #{format} should be an instance of String" + end + unless template.is_a?(String) + raise MatcherInitializationError, + "#{self.class}: #{template} should be an instance of String" + end + + super(spec_version: Pact::Matchers::PACT_SPEC_V3, kind: 'time', template: template, opts: { format: format }) + end + end + end + end +end diff --git a/lib/pact/matchers/v3/values.rb b/lib/pact/matchers/v3/values.rb new file mode 100644 index 00000000..704d4b76 --- /dev/null +++ b/lib/pact/matchers/v3/values.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Pact + module Matchers + module V3 + class Values < Pact::Matchers::Base + def initialize + super(spec_version: Pact::Matchers::PACT_SPEC_V3, kind: 'values', template: nil) + end + end + end + end +end diff --git a/lib/pact/matchers/v4/each_key.rb b/lib/pact/matchers/v4/each_key.rb new file mode 100644 index 00000000..6b3fd8b6 --- /dev/null +++ b/lib/pact/matchers/v4/each_key.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Pact + module Matchers + module V4 + class EachKey < Pact::Matchers::Base + def initialize(key_matchers, template) + raise MatcherInitializationError, "#{self.class}: #{template} should be a Hash" unless template.is_a?(Hash) + + unless key_matchers.is_a?(Array) + raise MatcherInitializationError, + "#{self.class}: #{key_matchers} should be an Array" + end + unless key_matchers.all?(Pact::Matchers::Base) + raise MatcherInitializationError, + "#{self.class}: #{key_matchers} should be instances of Pact::Matchers::Base" + end + unless key_matchers.size > 0 + raise MatcherInitializationError, + "#{self.class}: #{key_matchers} size should be greater than 0" + end + + super(spec_version: Pact::Matchers::PACT_SPEC_V4, kind: 'each-key', template: template, opts: { rules: key_matchers }) # rubocop:disable Layout/LineLength + end + + def as_plugin + @opts[:rules].map do |matcher| + "eachKey(#{matcher.as_plugin})" + end.join(', ') + end + end + end + end +end diff --git a/lib/pact/matchers/v4/each_key_value.rb b/lib/pact/matchers/v4/each_key_value.rb new file mode 100644 index 00000000..58288098 --- /dev/null +++ b/lib/pact/matchers/v4/each_key_value.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Pact + module Matchers + module V4 + class EachKeyValue < Pact::Matchers::Base + def initialize(key_matchers, template) + raise MatcherInitializationError, "#{self.class}: #{template} should be a Hash" unless template.is_a?(Hash) + + unless key_matchers.is_a?(Array) + raise MatcherInitializationError, + "#{self.class}: #{key_matchers} should be an Array" + end + unless key_matchers.all?(Pact::Matchers::Base) + raise MatcherInitializationError, + "#{self.class}: #{key_matchers} should be instances of Pact::Matchers::Base" + end + + super( + spec_version: Pact::Matchers::PACT_SPEC_V4, + kind: [ + EachKey.new(key_matchers, {}), + EachValue.new([Pact::Matchers::V2::Type.new('')], {}) + ], + template: template + ) + + @key_matchers = key_matchers + end + + def as_plugin + raise MatcherInitializationError, + "#{self.class}: each-key-value is not supported in plugin syntax. Use each / each_key / each_value matchers instead" # rubocop:disable Layout/LineLength + end + end + end + end +end diff --git a/lib/pact/matchers/v4/each_value.rb b/lib/pact/matchers/v4/each_value.rb new file mode 100644 index 00000000..c715c0ee --- /dev/null +++ b/lib/pact/matchers/v4/each_value.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Pact + module Matchers + module V4 + class EachValue < Pact::Matchers::Base + def initialize(value_matchers, template) + # raise MatcherInitializationError, "#{self.class}: #{template} should be a Hash" unless template.is_a?(Hash) + unless value_matchers.is_a?(Array) + raise MatcherInitializationError, + "#{self.class}: #{value_matchers} should be an Array" + end + unless value_matchers.all?(Pact::Matchers::Base) + raise MatcherInitializationError, + "#{self.class}: #{value_matchers} should be instances of Pact::Matchers::Base" + end + unless value_matchers.size > 0 + raise MatcherInitializationError, + "#{self.class}: #{value_matchers} size should be greater than 0" + end + + super(spec_version: Pact::Matchers::PACT_SPEC_V4, kind: 'each-value', template: template, opts: { rules: value_matchers }) # rubocop:disable Layout/LineLength + end + + def as_plugin + if @template.is_a?(Hash) + return { + 'pact:match' => "eachValue(matching($'SAMPLE'))", + 'SAMPLE' => serialize!(@template.deep_dup, :plugin) + } + end + + @opts[:rules].map do |matcher| + "eachValue(#{matcher.as_plugin})" + end.join(', ') + end + end + end + end +end diff --git a/lib/pact/matchers/v4/not_empty.rb b/lib/pact/matchers/v4/not_empty.rb new file mode 100644 index 00000000..0c74491c --- /dev/null +++ b/lib/pact/matchers/v4/not_empty.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Pact + module Matchers + module V4 + class NotEmpty < Pact::Matchers::Base + def initialize(template = nil) + @template = template + super(spec_version: Pact::Matchers::PACT_SPEC_V4, kind: 'notEmpty', template: @template) + end + + def as_plugin + if @template.nil? || @template.blank? + raise MatcherInitializationError, "#{self.class}: template must be provided when calling as_plugin" + end + + "notEmpty('#{@template}')" + end + end + end + end +end diff --git a/lib/pact/matchers/v4/status_code.rb b/lib/pact/matchers/v4/status_code.rb new file mode 100644 index 00000000..83515a62 --- /dev/null +++ b/lib/pact/matchers/v4/status_code.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Pact + module Matchers + module V4 + class StatusCode < Pact::Matchers::Base + def initialize(template = nil) + super(spec_version: Pact::Matchers::PACT_SPEC_V4, kind: 'statusCode', opts: { + 'status' => template + }) + end + end + end + end +end diff --git a/lib/pact/native/blocking_verifier.rb b/lib/pact/native/blocking_verifier.rb new file mode 100644 index 00000000..49564464 --- /dev/null +++ b/lib/pact/native/blocking_verifier.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'ffi' +require 'pact/ffi/verifier' + +module Pact + module Native + module BlockingVerifier + extend FFI::Library + ffi_lib DetectOS.get_bin_path + + attach_function :execute, :pactffi_verifier_execute, %i[pointer], :int32, blocking: true + end + end +end diff --git a/lib/pact/native/logger.rb b/lib/pact/native/logger.rb new file mode 100644 index 00000000..acc6f60f --- /dev/null +++ b/lib/pact/native/logger.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'pact/ffi/logger' + +module Pact + module Native + module Logger + LOG_LEVELS = { + off: PactFfi::FfiLogLevelFilter['LOG_LEVEL_OFF'], + error: PactFfi::FfiLogLevelFilter['LOG_LEVEL_ERROR'], + warn: PactFfi::FfiLogLevelFilter['LOG_LEVEL_WARN'], + info: PactFfi::FfiLogLevelFilter['LOG_LEVEL_INFO'], + debug: PactFfi::FfiLogLevelFilter['LOG_LEVEL_DEBUG'], + trace: PactFfi::FfiLogLevelFilter['LOG_LEVEL_TRACE'] + }.freeze + + def self.log_to_stdout(log_level) + raise 'invalid log level for PactFfi::FfiLogLevelFilter' unless LOG_LEVELS.key?(log_level) + + PactFfi::Logger.log_to_stdout(LOG_LEVELS[log_level]) unless log_level == :off + end + end + end +end diff --git a/lib/pact/pact_broker.rb b/lib/pact/pact_broker.rb deleted file mode 100644 index c91f7ed2..00000000 --- a/lib/pact/pact_broker.rb +++ /dev/null @@ -1,25 +0,0 @@ -require 'pact/pact_broker/fetch_pacts' -require 'pact/pact_broker/fetch_pact_uris_for_verification' -require 'pact/provider/pact_uri' - -# -# @public Used by Pact Provider Verifier -# -module Pact - module PactBroker - extend self - - # Keep for backwards compatibility with pact-provider-verifier < 1.23.1 - def fetch_pact_uris *args - Pact::PactBroker::FetchPacts.call(*args).collect(&:uri) - end - - def fetch_pact_uris_for_verification *args - Pact::PactBroker::FetchPactURIsForVerification.call(*args) - end - - def build_pact_uri(*args) - Pact::Provider::PactURI.new(*args) - end - end -end diff --git a/lib/pact/pact_broker/fetch_pact_uris_for_verification.rb b/lib/pact/pact_broker/fetch_pact_uris_for_verification.rb deleted file mode 100644 index dfde9d41..00000000 --- a/lib/pact/pact_broker/fetch_pact_uris_for_verification.rb +++ /dev/null @@ -1,112 +0,0 @@ -require 'pact/hal/entity' -require 'pact/hal/http_client' -require 'pact/provider/pact_uri' -require 'pact/errors' -require 'pact/pact_broker/fetch_pacts' -require 'pact/pact_broker/notices' -require 'pact/pact_broker/pact_selection_description' -require "pact/hash_refinements" - -module Pact - module PactBroker - class FetchPactURIsForVerification - using Pact::HashRefinements - - include PactSelectionDescription - attr_reader :provider, :consumer_version_selectors, :provider_version_branch, :provider_version_tags, :broker_base_url, :http_client_options, :http_client, :options - - PACTS_FOR_VERIFICATION_RELATION = 'pb:provider-pacts-for-verification'.freeze - PACTS_FOR_VERIFICATION_RELATION_BETA = 'beta:provider-pacts-for-verification'.freeze - PACTS = 'pacts'.freeze - HREF = 'href'.freeze - LINKS = '_links'.freeze - SELF = 'self'.freeze - EMBEDDED = '_embedded'.freeze - - def initialize(provider, consumer_version_selectors, provider_version_branch, provider_version_tags, broker_base_url, http_client_options, options = {}) - @provider = provider - @consumer_version_selectors = consumer_version_selectors || [] - @provider_version_branch = provider_version_branch - @provider_version_tags = [*provider_version_tags] - @http_client_options = http_client_options - @broker_base_url = broker_base_url - @http_client = Pact::Hal::HttpClient.new(http_client_options) - @options = options - end - - def self.call(provider, consumer_version_selectors, provider_version_branch, provider_version_tags, broker_base_url, http_client_options, options = {}) - new(provider, consumer_version_selectors, provider_version_branch, provider_version_tags, broker_base_url, http_client_options, options).call - end - - def call - handling_no_pacts_found do - if index.can?(PACTS_FOR_VERIFICATION_RELATION) || index.can?(PACTS_FOR_VERIFICATION_RELATION_BETA) - log_message - pacts_for_verification - else - old_selectors = consumer_version_selectors.collect do | selector | - { name: selector[:tag], all: !selector[:latest], fallback: selector[:fallbackTag]} - end - # Fall back to old method of fetching pacts - FetchPacts.call(provider, old_selectors, broker_base_url, http_client_options) - end - end - end - - private - - def index - @index_entity ||= Pact::Hal::Link.new({ "href" => broker_base_url }, http_client).get.assert_success! - end - - def pacts_for_verification - pacts_for_verification_entity.response.body[EMBEDDED][PACTS].collect do | pact | - metadata = { - pending: pact["verificationProperties"]["pending"], - notices: extract_notices(pact), - short_description: pact["shortDescription"] - } - Pact::Provider::PactURI.new(pact[LINKS][SELF][HREF], http_client_options, metadata) - end - end - - def pacts_for_verification_entity - index - ._link(PACTS_FOR_VERIFICATION_RELATION, PACTS_FOR_VERIFICATION_RELATION_BETA) - .expand(provider: provider) - .post!(query) - end - - def query - q = {} - q["includePendingStatus"] = options[:include_pending_status] - q["consumerVersionSelectors"] = consumer_version_selectors if consumer_version_selectors.any? - q["providerVersionTags"] = provider_version_tags if provider_version_tags.any? - q["providerVersionBranch"] = provider_version_branch - q["includeWipPactsSince"] = options[:include_wip_pacts_since] - q.compact - end - - def extract_notices(pact) - Notices.new((pact["verificationProperties"]["notices"] || []).collect{ |notice| symbolize_keys(notice) }) - end - - def symbolize_keys(hash) - hash.each_with_object({}){ |(k,v), h| h[k.to_sym] = v } - end - - def log_message - Pact.configuration.output_stream.puts "INFO: #{pact_selection_description(provider, consumer_version_selectors, options, broker_base_url)}" - end - - def handling_no_pacts_found - pacts_found = yield - raise "No pacts found to verify" if pacts_found.empty? && options[:fail_if_no_pacts_found] != false - if pacts_found.empty? && options[:fail_if_no_pacts_found] == false - Pact.configuration.output_stream.puts "WARN: No pacts found to verify & fail_if_no_pacts_found is set to false." - end - pacts_found - end - end - end -end diff --git a/lib/pact/pact_broker/fetch_pacts.rb b/lib/pact/pact_broker/fetch_pacts.rb deleted file mode 100644 index f460dc81..00000000 --- a/lib/pact/pact_broker/fetch_pacts.rb +++ /dev/null @@ -1,103 +0,0 @@ -require 'pact/hal/entity' -require 'pact/hal/http_client' -require 'pact/provider/pact_uri' -require 'pact/errors' - -module Pact - module PactBroker - class FetchPacts - attr_reader :provider, :tags, :broker_base_url, :http_client_options, :http_client, :index_entity - - ALL_PROVIDER_TAG_RELATION = 'pb:provider-pacts-with-tag'.freeze - LATEST_PROVIDER_TAG_RELATION = 'pb:latest-provider-pacts-with-tag'.freeze - LATEST_PROVIDER_RELATION = 'pb:latest-provider-pacts'.freeze - PACTS = 'pacts'.freeze - PB_PACTS = 'pb:pacts'.freeze - HREF = 'href'.freeze - - def initialize(provider, tags, broker_base_url, http_client_options) - @provider = provider - @tags = (tags || []).collect do |tag| - if tag.is_a?(String) - { name: tag, all: false, fallback: nil } - else - tag - end - end - @http_client_options = http_client_options - @broker_base_url = broker_base_url - @http_client = Pact::Hal::HttpClient.new(http_client_options) - end - - def self.call(provider, tags, broker_base_url, http_client_options) - new(provider, tags, broker_base_url, http_client_options).call - end - - def call - log_message - if index.success? - if any_tags? - tagged_pacts_for_provider - else - latest_pacts_for_provider - end - else - raise Pact::Error.new("Error retrieving #{broker_base_url} status=#{index_entity.response.code} #{index_entity.response.raw_body}") - end - end - - private - - def any_tags? - tags && tags.any? - end - - def tagged_pacts_for_provider - tags.collect do |tag| - link = link_for(tag) - urls = pact_urls(link.expand(provider: provider, tag: tag[:name]).get) - if urls.empty? && tag[:fallback] - urls = pact_urls(link.expand(provider: provider, tag: tag[:fallback]).get) - end - urls - end.flatten - end - - def link_for(tag) - if !tag[:all] - index_entity._link!(LATEST_PROVIDER_TAG_RELATION) - else - index_entity._link!(ALL_PROVIDER_TAG_RELATION) - end - end - - def index - @index_entity ||= Pact::Hal::Link.new({ "href" => broker_base_url }, http_client).get.assert_success! - end - - def latest_pacts_for_provider - link = index_entity._link!(LATEST_PROVIDER_RELATION) - pact_urls(link.expand(provider: provider).get.assert_success!) - end - - def pact_urls(link_by_provider) - link_by_provider.assert_success!.fetch(PB_PACTS, PACTS).collect do |pact| - Pact::Provider::PactURI.new(pact[HREF], http_client_options) - end - end - - def log_message - message = "INFO: Fetching pacts for #{provider} from #{broker_base_url}" - if tags.any? - desc = tags.collect do |tag| - all_or_latest = tag[:all] ? "all" : "latest" - name = tag[:fallback] ? "#{tag[:name]} (or #{tag[:fallback]} if not found)" : tag[:name] - "#{all_or_latest} #{name}" - end.join(", ") - message << " for tags: #{desc}" - end - Pact.configuration.output_stream.puts message - end - end - end -end diff --git a/lib/pact/pact_broker/notices.rb b/lib/pact/pact_broker/notices.rb deleted file mode 100644 index 4d854bcf..00000000 --- a/lib/pact/pact_broker/notices.rb +++ /dev/null @@ -1,34 +0,0 @@ -module Pact - module PactBroker - class Notices < Array - def before_verification_notices - select { | notice | notice[:when].nil? || notice[:when].start_with?('before_verification') } - end - - def before_verification_notices_text - before_verification_notices.collect{ | notice | notice[:text] } - end - - def after_verification_notices(success, published) - select { | notice | notice[:when] == "after_verification:success_#{success}_published_#{published}" || notice[:when] == "after_verification" } - .collect do | notice | - notice.merge(:when => simplify_notice_when(notice[:when])) - end - end - - def after_verification_notices_text(success, published) - after_verification_notices(success, published).collect{ | notice | notice[:text] } - end - - def all_notices(success, published) - before_verification_notices + after_verification_notices(success, published) - end - - private - - def simplify_notice_when(when_key) - when_key.split(":").first - end - end - end -end diff --git a/lib/pact/pact_broker/pact_selection_description.rb b/lib/pact/pact_broker/pact_selection_description.rb deleted file mode 100644 index 6b6a8110..00000000 --- a/lib/pact/pact_broker/pact_selection_description.rb +++ /dev/null @@ -1,66 +0,0 @@ -module Pact - module PactBroker - module PactSelectionDescription - def pact_selection_description(provider, consumer_version_selectors, options, broker_base_url) - message = "Fetching pacts for #{provider} from #{broker_base_url} with the selection criteria: " - if consumer_version_selectors.any? - desc = consumer_version_selectors.collect do |selector| - desc = nil - if selector[:tag] - desc = !selector[:latest] ? "all for tag #{selector[:tag]}" : "latest for tag #{selector[:tag]}" - desc = "#{desc} of #{selector[:consumer]}" if selector[:consumer] - elsif selector[:branch] - desc = "latest from branch #{selector[:branch]}" - desc = "#{desc} of #{selector[:consumer]}" if selector[:consumer] - elsif selector[:mainBranch] - desc = "latest from main branch" - desc = "#{desc} of #{selector[:consumer]}" if selector[:consumer] - elsif selector[:deployed] - if selector[:environment] - desc = "currently deployed to #{selector[:environment]}" - else - desc = "currently deployed" - end - desc = "#{selector[:consumer]} #{desc}" if selector[:consumer] - elsif selector[:released] - if selector[:environment] - desc = "currently released to #{selector[:environment]}" - else - desc = "currently released" - end - desc = "#{selector[:consumer]} #{desc}" if selector[:consumer] - elsif selector[:deployedOrReleased] - if selector[:environment] - desc = "currently deployed or released to #{selector[:environment]}" - else - desc = "currently deployed or released" - end - desc = "#{selector[:consumer]} #{desc}" if selector[:consumer] - elsif selector[:environment] - desc = "currently in #{selector[:environment]}" - desc = "#{selector[:consumer]} #{desc}" if selector[:consumer] - elsif selector[:matchingBranch] - desc = "matching current branch" - desc = "#{desc} for #{selector[:consumer]}" if selector[:consumer] - elsif selector[:matchingTag] - desc = "matching tag" - desc = "#{desc} for #{selector[:consumer]}" if selector[:consumer] - else - desc = selector.to_s - end - - fallback = selector[:fallback] || selector[:fallbackTag] - desc = "#{desc} (or #{fallback} if not found)" if fallback - - desc - end.join(", ") - if options[:include_wip_pacts_since] - desc = "#{desc}, work in progress pacts created after #{options[:include_wip_pacts_since]}" - end - message << "#{desc}" - end - message - end - end - end -end diff --git a/lib/pact/project_root.rb b/lib/pact/project_root.rb deleted file mode 100644 index 35f020f2..00000000 --- a/lib/pact/project_root.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'pathname' - -module Pact - def self.project_root - @project_root ||= Pathname.new(File.expand_path('../../../',__FILE__)).freeze - end -end \ No newline at end of file diff --git a/lib/pact/provider.rb b/lib/pact/provider.rb index 6a3faaf0..3a4beb0c 100644 --- a/lib/pact/provider.rb +++ b/lib/pact/provider.rb @@ -1,3 +1,6 @@ -require 'pact/configuration' -require 'pact/provider/configuration' -require 'pact/provider/world' +# frozen_string_literal: true + +module Pact + module Provider + end +end diff --git a/lib/pact/provider/async_message_verifier.rb b/lib/pact/provider/async_message_verifier.rb new file mode 100644 index 00000000..3e62ed81 --- /dev/null +++ b/lib/pact/provider/async_message_verifier.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'pact/ffi/verifier' +require 'pact/native/logger' + +module Pact + module Provider + class AsyncMessageVerifier < BaseVerifier + PROVIDER_TRANSPORT_TYPE = 'message' + + def initialize(pact_config, mixed_config = nil) + super + + return if pact_config.is_a?(::Pact::Provider::PactConfig::Async) + + raise ArgumentError, + 'pact_config must be an instance of Pact::Provider::PactConfig::Message' + end + + private + + def add_provider_transport(pact_handle) + setup_uri = URI(@pact_config.message_setup_url) + PactFfi::Verifier.add_provider_transport(pact_handle, PROVIDER_TRANSPORT_TYPE, setup_uri.port, setup_uri.path, + '') + end + end + end +end diff --git a/lib/pact/provider/base_verifier.rb b/lib/pact/provider/base_verifier.rb new file mode 100644 index 00000000..a59b3c2a --- /dev/null +++ b/lib/pact/provider/base_verifier.rb @@ -0,0 +1,259 @@ +# frozen_string_literal: true + +require 'pact/ffi/verifier' +require 'pact/native/logger' +require 'pact/native/blocking_verifier' + +module Pact + module Provider + class BaseVerifier + PROVIDER_TRANSPORT_TYPE = nil + attr_reader :logger + + class VerificationError < Pact::FfiError; end + + class VerifierError < Pact::Error; end + + DEFAULT_CONSUMER_SELECTORS = {} + + # https://docs.rs/pact_ffi/0.4.17/pact_ffi/verifier/fn.pactffi_verify.html#errors + VERIFICATION_ERRORS = { + 1 => { reason: :verification_failed, status: 1, + description: 'The verification process failed, see output for errors' }, + 2 => { reason: :null_pointer, status: 2, description: 'A null pointer was received' }, + 3 => { reason: :internal_error, status: 3, description: 'The method panicked' }, + 4 => { reason: :invalid_arguments, status: 4, + description: 'Invalid arguments were provided to the verification process' } + }.freeze + + # env below are set up by pipeline-builder + # see paas/cicd/images/pact/pipeline-builder/-/blob/master/internal/commands/consumers-pipeline/ruby.go + def initialize(pact_config, mixed_config = nil) + unless pact_config.is_a?(::Pact::Provider::PactConfig::Base) + raise ArgumentError, + 'pact_config must be a subclass of Pact::Provider::PactConfig::Base' + end + + @pact_config = pact_config + @mixed_config = mixed_config + @logger = @pact_config.logger || Logger.new($stdout) + end + + def verify! + raise VerifierError.new('interaction is designed to be used one-time only') if defined?(@used) + + # if consumer_selectors.blank? + # logger.info("[verifier] does not need to verify consumer #{@pact_config.consumer_name}") + # return + # end + + exception = nil + pact_handle = init_pact + + start_servers! + + logger.info('[verifier] starting provider verification') + + result = Pact::Native::BlockingVerifier.execute(pact_handle) + if VERIFICATION_ERRORS[result].present? + error = VERIFICATION_ERRORS[result] + exception = VerificationError.new( + "There was an error while trying to verify provider \"#{@pact_config.provider_name}\"", error[:reason], error[:status] # rubocop:disable Layout/LineLength + ) + end + ensure + @used = true + PactFfi::Verifier.shutdown(pact_handle) if pact_handle + stop_servers + @grpc_server.stop if @grpc_server + raise exception if exception + end + + private + + def create_c_pointer_array_from_string_array(string_array) + pointers = string_array.map { |str| FFI::MemoryPointer.from_string(str) } + array_pointer = FFI::MemoryPointer.new(:pointer, pointers.size) + pointers.each_with_index do |ptr, index| + array_pointer[index].put_pointer(0, ptr) + end + array_pointer + end + + def bool_to_int(value) + value ? 1 : 0 + end + + def init_pact + handle = PactFfi::Verifier.new_for_application('pact-ruby', PactFfi.version) + set_provider_info(handle) + + if defined?(@mixed_config.grpc_config) && @mixed_config.grpc_config + @grpc_server = GrufServer.new(host: "127.0.0.1:#{@mixed_config.grpc_config.grpc_port}", + services: @mixed_config.grpc_config.grpc_services) + @grpc_server.start + PactFfi::Verifier.add_provider_transport(handle, 'grpc', @mixed_config.grpc_config.grpc_port, '', '') + end + + if defined?(@mixed_config.async_config) && @mixed_config.async_config + setup_uri = URI(@mixed_config.async_config.message_setup_url) + PactFfi::Verifier.add_provider_transport(handle, 'message', setup_uri.port, setup_uri.path, '') + end + + # TODO: add http transport? + + PactFfi::Verifier.set_provider_state(handle, @pact_config.provider_setup_url, 1, 1) + PactFfi::Verifier.set_verification_options(handle, 0, 10_000) + # pactffi_verifier_set_publish_options( + # handle: *mut VerifierHandle, + # provider_version: *const c_char, + # build_url: *const c_char, + # provider_tags: *const *const c_char, + # provider_tags_len: c_ushort, + # provider_branch: *const c_char, + # ) + c_provider_version_tags = create_c_pointer_array_from_string_array(@pact_config.provider_version_tags) + c_provider_version_tags_size = @pact_config.provider_version_tags.size + c_consumer_version_tags = create_c_pointer_array_from_string_array(@pact_config.consumer_version_tags) + c_consumer_version_tags_size = @pact_config.consumer_version_tags.size + + if @pact_config.provider_build_uri.present? + begin + URI.parse(@pact_config.provider_build_uri) + rescue URI::InvalidURIError + raise VerifierError.new('provider_build_uri is not a valid URI') + end + end + + if @pact_config.publish_verification_results == true + if @pact_config.provider_version + PactFfi::Verifier.set_publish_options(handle, @pact_config.provider_version, + @pact_config.provider_build_uri, c_provider_version_tags, c_provider_version_tags_size, @pact_config.provider_version_branch) # rubocop:disable Layout/LineLength + else + logger.warn('[verifier] - unable to publish verification results as provider version is not set') + end + end + + configure_verification_source(handle, c_provider_version_tags, c_provider_version_tags_size, + c_consumer_version_tags, c_consumer_version_tags_size) + + PactFfi::Verifier.set_no_pacts_is_error(handle, bool_to_int(@pact_config.fail_if_no_pacts_found)) + + add_provider_transport(handle) + + # the core doesnt pick up these env vars, so we need to set them here + # https://github.com/pact-foundation/pact-reference/issues/451#issuecomment-2338130587 + # PACT_DESCRIPTION + # Only validate interactions whose descriptions match this filter (regex format) + # PACT_PROVIDER_STATE + # Only validate interactions whose provider states match this filter (regex format) + # PACT_PROVIDER_NO_STATE + # Only validate interactions that have no defined provider state (true or false) + PactFfi::Verifier.set_filter_info( + handle, + ENV['PACT_DESCRIPTION'] || nil, + ENV['PACT_PROVIDER_STATE'] || nil, + bool_to_int(ENV['PACT_PROVIDER_NO_STATE'] || false) + ) + + Pact::Native::Logger.log_to_stdout(@pact_config.log_level) + + logger.info("[verifier] verification initialized for provider #{@pact_config.provider_name}, version #{@pact_config.provider_version}, transport #{self.class::PROVIDER_TRANSPORT_TYPE}") # rubocop:disable Layout/LineLength + + handle + end + + def set_provider_info(pact_handle) + # pub extern "C" fn pactffi_verifier_set_provider_info( + # handle: *mut VerifierHandle, + # name: *const c_char, + # scheme: *const c_char, + # host: *const c_char, + # port: c_ushort, + # path: *const c_char, + # ) { + PactFfi::Verifier.set_provider_info(pact_handle, @pact_config.provider_name, '', '', 0, '') + end + + def add_provider_transport(pact_handle) + raise PactImplementationRequired, 'Implement #add_provider_transport in a subclass' + end + + def start_servers! + logger.info('[verifier] starting services') + + @servers_started = true + @pact_config.start_servers + end + + def stop_servers + return unless @servers_started + + logger.info('[verifier] stopping services') + + @pact_config.stop_servers + end + + def configure_verification_source(handle, c_provider_version_tags, c_provider_version_tags_size, + c_consumer_version_tags, c_consumer_version_tags_size) + logger.info('[verifier] configuring verification source') + if @pact_config.pact_broker_proxy_url.blank? && @pact_config.pact_uri.blank? + path = @pact_config.pact_dir || (defined?(Rails) ? Rails.root.join('pacts').to_s : 'pacts') + logger.info("[verifier] pact broker url or pact uri is not set, using directory #{path} as a verification source") # rubocop:disable Layout/LineLength + return PactFfi::Verifier.add_directory_source(handle, path) + end + + if @pact_config.pact_uri.present? + if @pact_config.pact_uri.start_with?('http') + logger.info("[verifier] using pact uri #{@pact_config.pact_uri} as a verification source") + PactFfi::Verifier.url_source(handle, @pact_config.pact_uri, @pact_config.broker_username, + @pact_config.broker_password, @pact_config.broker_token) + else + logger.info("[verifier] using pact file #{@pact_config.pact_uri} as a verification source") + PactFfi::Verifier.add_file_source(handle, @pact_config.pact_uri) + end + else + logger.info("[verifier] using pact broker url #{@pact_config.broker_url} with consumer selectors: #{JSON.dump(consumer_selectors)} as a verification source") # rubocop:disable Layout/LineLength + consumer_selectors = [] if consumer_selectors.nil? + filters = consumer_selectors.map do |selector| + FFI::MemoryPointer.from_string(JSON.dump(selector).to_s) + end + filters_ptr = FFI::MemoryPointer.new(:pointer, filters.size + 1) + filters_ptr.write_array_of_pointer(filters) + PactFfi::Verifier.broker_source_with_selectors(handle, @pact_config.pact_broker_proxy_url, + @pact_config.broker_username, @pact_config.broker_password, @pact_config.broker_token, bool_to_int(@pact_config.enable_pending), @pact_config.include_wip_pacts_since, c_provider_version_tags, c_provider_version_tags_size, @pact_config.provider_version_branch, filters_ptr, consumer_selectors.size, c_consumer_version_tags, c_consumer_version_tags_size) # rubocop:disable Layout/LineLength + end + end + + def consumer_selectors + return unless @pact_config.consumer_version_selectors + + (!@pact_config.consumer_version_selectors.empty? && @pact_config.consumer_version_selectors) || @consumer_selectors # rubocop:disable Layout/LineLength + end + + def build_consumer_selectors(verify_only, consumer_name, consumer_branch) + # if verify_only and consumer_name are defined - select only needed consumer + if verify_only.present? + # select proper consumer branch if defined + if consumer_name.present? + return [] unless verify_only.include?(consumer_name) + return [{ 'branch' => consumer_branch, 'consumer' => consumer_name }] if consumer_branch.present? + + return [DEFAULT_CONSUMER_SELECTORS.merge('consumer' => consumer_name)] + end + # or default selectors + return verify_only.map { |name| DEFAULT_CONSUMER_SELECTORS.merge('consumer' => name) } + end + + # select provided consumer_name + if consumer_name.present? && consumer_branch.present? + return [{ 'branch' => consumer_branch, + 'consumer' => consumer_name }] + end + return [DEFAULT_CONSUMER_SELECTORS.merge('consumer' => consumer_name)] if consumer_name.present? + + [DEFAULT_CONSUMER_SELECTORS] + end + end + end +end diff --git a/lib/pact/provider/configuration.rb b/lib/pact/provider/configuration.rb deleted file mode 100644 index 720ce255..00000000 --- a/lib/pact/provider/configuration.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'pact/provider/configuration/dsl' -require 'pact/provider/configuration/configuration_extension' -require 'pact/provider/state/provider_state' - -Pact.send(:extend, Pact::Provider::DSL) -Pact.send(:extend, Pact::Provider::State::DSL) -Pact::Configuration.send(:include, Pact::Provider::Configuration::ConfigurationExtension) \ No newline at end of file diff --git a/lib/pact/provider/configuration/configuration_extension.rb b/lib/pact/provider/configuration/configuration_extension.rb deleted file mode 100644 index c6474f13..00000000 --- a/lib/pact/provider/configuration/configuration_extension.rb +++ /dev/null @@ -1,69 +0,0 @@ -require 'pact/provider/state/provider_state' -require 'pact/provider/state/provider_state_configured_modules' -require 'pact/provider/state/set_up' -require 'pact/provider/state/tear_down' - -module Pact - - module Provider - - module Configuration - - module ConfigurationExtension - - attr_accessor :provider_application_version - - def provider= provider - @provider = provider - end - - def provider - if defined? @provider - @provider - else - raise "Please configure your provider. See the Provider section in the README for examples." - end - end - - def config_ru_path - @config_ru_path ||= './config.ru' - end - - def config_ru_path= config_ru_path - @config_ru_path = config_ru_path - end - - def interactions_replay_order - @interactions_replay_order ||= :recorded #or :random - end - - def interactions_replay_order= interactions_replay_order - @interactions_replay_order = interactions_replay_order.to_sym - end - - def provider_state_set_up - @provider_state_set_up ||= Pact::Provider::State::SetUp - end - - def provider_state_set_up= provider_state_set_up - @provider_state_set_up = provider_state_set_up - end - - def provider_state_tear_down - @provider_state_tear_down ||= Pact::Provider::State::TearDown - end - - def provider_state_tear_down= provider_state_tear_down - @provider_state_tear_down = provider_state_tear_down - end - - def include mod - Pact::Provider::State::ProviderStateConfiguredModules.instance_eval do - include mod - end - end - - end - end - end -end diff --git a/lib/pact/provider/configuration/dsl.rb b/lib/pact/provider/configuration/dsl.rb deleted file mode 100644 index 51b7f8f2..00000000 --- a/lib/pact/provider/configuration/dsl.rb +++ /dev/null @@ -1,18 +0,0 @@ -require 'pact/provider/configuration/service_provider_dsl' -require 'pact/provider/configuration/message_provider_dsl' - -module Pact - - module Provider - - module DSL - def service_provider name, &block - Configuration::ServiceProviderDSL.build(name, &block) - end - - def message_provider name, &block - Configuration::MessageProviderDSL.build(name, &block) - end - end - end -end diff --git a/lib/pact/provider/configuration/message_provider_dsl.rb b/lib/pact/provider/configuration/message_provider_dsl.rb deleted file mode 100644 index 754dbf76..00000000 --- a/lib/pact/provider/configuration/message_provider_dsl.rb +++ /dev/null @@ -1,63 +0,0 @@ -require 'pact/provider/configuration/service_provider_dsl' - -module Pact - module Provider - module Configuration - class MessageProviderDSL < ServiceProviderDSL - class RackToMessageAdapter - def initialize(message_builder) - @message_builder = message_builder - end - - def call(env) - request_body_json = JSON.parse(env['rack.input'].read) - contents = @message_builder.call(request_body_json['description']) - [200, {"Content-Type" => "application/json"}, [{ contents: contents }.to_json]] - end - end - - def initialize name - super - @mapper_block = lambda { |args| } - end - - dsl do - def app &block - self.app_block = block - end - - def app_version application_version - self.application_version = application_version - end - - def app_version_tags tags - self.tags = tags - end - - def app_version_branch branch - self.branch = branch - end - - def publish_verification_results publish_verification_results - self.publish_verification_results = publish_verification_results - Pact::RSpec.with_rspec_2 do - Pact.configuration.error_stream.puts "WARN: Publishing of verification results is currently not supported with rspec 2. If you would like this functionality, please feel free to submit a PR!" - end - end - - def honours_pact_with consumer_name, options = {}, &block - create_pact_verification consumer_name, options, &block - end - - def honours_pacts_from_pact_broker &block - create_pact_verification_from_broker(&block) - end - - def builder &block - self.app_block = lambda { RackToMessageAdapter.new(block) } - end - end - end - end - end -end diff --git a/lib/pact/provider/configuration/pact_verification.rb b/lib/pact/provider/configuration/pact_verification.rb deleted file mode 100644 index 15c0eea2..00000000 --- a/lib/pact/provider/configuration/pact_verification.rb +++ /dev/null @@ -1,48 +0,0 @@ -require 'pact/provider/pact_verification' -require 'pact/provider/pact_uri' -require 'pact/shared/dsl' -require 'pact/provider/world' - -module Pact - module Provider - - module Configuration - - class PactVerification - - extend Pact::DSL - - attr_accessor :consumer_name, :pact_uri, :ref - - def initialize consumer_name, options = {} - @consumer_name = consumer_name - @ref = options.fetch(:ref, :head) - @pact_uri = nil - end - - dsl do - def pact_uri pact_uri, options = {} - self.pact_uri = ::Pact::Provider::PactURI.new(pact_uri, options) if pact_uri - end - end - - def finalize - validate - create_pact_verification - end - - private - - def create_pact_verification - verification = Pact::Provider::PactVerification.new(consumer_name, pact_uri, ref) - Pact.provider_world.add_pact_verification verification - end - - def validate - raise "Please provide a pact_uri for the verification" unless pact_uri - end - - end - end - end -end \ No newline at end of file diff --git a/lib/pact/provider/configuration/pact_verification_from_broker.rb b/lib/pact/provider/configuration/pact_verification_from_broker.rb deleted file mode 100644 index 71b20abe..00000000 --- a/lib/pact/provider/configuration/pact_verification_from_broker.rb +++ /dev/null @@ -1,126 +0,0 @@ -require 'pact/shared/dsl' -require 'pact/provider/world' -require 'pact/pact_broker/fetch_pact_uris_for_verification' -require 'pact/errors' -require 'pact/utils/string' - -module Pact - module Provider - module Configuration - class PactVerificationFromBroker - - extend Pact::DSL - - # If user declares a variable with the same name as one of these attributes - # in parent scope, it will clash with these ones, - # so put an underscore in front of the name to be safer. - - attr_accessor :_provider_name, :_pact_broker_base_url, :_consumer_version_tags, :_provider_version_branch, :_provider_version_tags, :_basic_auth_options, :_enable_pending, :_include_wip_pacts_since, :_verbose, :_consumer_version_selectors, :_fail_if_no_pacts_found - - def initialize(provider_name, provider_version_branch, provider_version_tags) - @_provider_name = provider_name - @_provider_version_branch = provider_version_branch - @_provider_version_tags = provider_version_tags - @_consumer_version_tags = [] - @_consumer_version_selectors = [] - @_enable_pending = false - @_include_wip_pacts_since = nil - @_verbose = false - @_fail_if_no_pacts_found = true # CLI defaults to false, unfortunately for consistency - end - - dsl do - def pact_broker_base_url pact_broker_base_url, basic_auth_options = {} - self._pact_broker_base_url = pact_broker_base_url - self._basic_auth_options = basic_auth_options - end - - def consumer_version_tags consumer_version_tags - self._consumer_version_tags = *consumer_version_tags - end - - def consumer_version_selectors consumer_version_selectors - self._consumer_version_selectors = *consumer_version_selectors - end - - def enable_pending enable_pending - self._enable_pending = enable_pending - end - - # Underlying code defaults to true if not specified - def fail_if_no_pacts_found fail_if_no_pacts_found - self._fail_if_no_pacts_found = fail_if_no_pacts_found - end - - def include_wip_pacts_since since - self._include_wip_pacts_since = if since.respond_to?(:xmlschema) - since.xmlschema - else - since - end - end - - def verbose verbose - self._verbose = verbose - end - end - - def finalize - validate - create_pact_verification - end - - private - - def create_pact_verification - fetch_pacts = Pact::PactBroker::FetchPactURIsForVerification.new( - _provider_name, - consumer_version_selectors, - _provider_version_branch, - _provider_version_tags, - _pact_broker_base_url, - _basic_auth_options.merge(verbose: _verbose), - { include_pending_status: _enable_pending, include_wip_pacts_since: _include_wip_pacts_since, fail_if_no_pacts_found: _fail_if_no_pacts_found } - ) - - Pact.provider_world.add_pact_uri_source fetch_pacts - end - - def consumer_version_selectors - convert_tags_to_selectors + convert_consumer_version_selectors - end - - def convert_tags_to_selectors - _consumer_version_tags.collect do | tag | - if tag.is_a?(Hash) - { - tag: tag.fetch(:name), - latest: !tag[:all], - fallbackTag: tag[:fallback] - } - elsif tag.is_a?(String) - { - tag: tag, - latest: true - } - else - raise Pact::Error.new("The value supplied for consumer_version_tags must be a String or a Hash. Found #{tag.class}") - end - end - end - - def convert_consumer_version_selectors - _consumer_version_selectors.collect do | selector | - selector.each_with_object({}) do | (key, value), new_selector | - new_selector[Pact::Utils::String.camelcase(key.to_s).to_sym] = value - end - end - end - - def validate - raise Pact::Error.new("Please provide a pact_broker_base_url from which to retrieve the pacts") unless _pact_broker_base_url - end - end - end - end -end \ No newline at end of file diff --git a/lib/pact/provider/configuration/service_provider_config.rb b/lib/pact/provider/configuration/service_provider_config.rb deleted file mode 100644 index 50156dc5..00000000 --- a/lib/pact/provider/configuration/service_provider_config.rb +++ /dev/null @@ -1,32 +0,0 @@ -module Pact - module Provider - module Configuration - class ServiceProviderConfig - - attr_accessor :application_version - attr_reader :branch, :build_url - - def initialize application_version, branch, tags, publish_verification_results, build_url, &app_block - @application_version = application_version - @branch = branch - @tags = [*tags] - @publish_verification_results = publish_verification_results - @app_block = app_block - @build_url = build_url - end - - def app - @app_block.call - end - - def publish_verification_results? - @publish_verification_results - end - - def tags - @tags - end - end - end - end -end \ No newline at end of file diff --git a/lib/pact/provider/configuration/service_provider_dsl.rb b/lib/pact/provider/configuration/service_provider_dsl.rb deleted file mode 100644 index 5e1aa573..00000000 --- a/lib/pact/provider/configuration/service_provider_dsl.rb +++ /dev/null @@ -1,107 +0,0 @@ -require 'pact/provider/configuration/pact_verification' -require 'pact/provider/configuration/pact_verification_from_broker' -require 'pact/provider/configuration/service_provider_config' -require 'pact/errors' - -module Pact - - module Provider - - module Configuration - - class Error < ::Pact::Error; end - - class ServiceProviderDSL - - extend Pact::DSL - - attr_accessor :name, :app_block, :application_version, :branch, :tags, :publish_verification_results, :build_url - - CONFIG_RU_APP = lambda { - unless File.exist? Pact.configuration.config_ru_path - raise "Could not find config.ru file at #{Pact.configuration.config_ru_path} Please configure the service provider app or create a config.ru file in the root directory of the project. See https://github.com/pact-foundation/pact-ruby/wiki/Verifying-pacts for more information." - end - result = Rack::Builder.parse_file(Pact.configuration.config_ru_path) - - if result.respond_to?(:first) # rack 2 - result.first - else # rack 3 - result - end - } - - def initialize name - @name = name - @publish_verification_results = false - @tags = [] - @app_block = CONFIG_RU_APP - end - - dsl do - def app &block - self.app_block = block - end - - def app_version application_version - self.application_version = application_version - end - - def app_version_tags tags - self.tags = tags - end - - def app_version_branch branch - self.branch = branch - end - - def build_url build_url - self.build_url = build_url - end - - def publish_verification_results publish_verification_results - self.publish_verification_results = publish_verification_results - Pact::RSpec.with_rspec_2 do - Pact.configuration.error_stream.puts "WARN: Publishing of verification results is currently not supported with rspec 2. If you would like this functionality, please feel free to submit a PR!" - end - end - - def honours_pact_with consumer_name, options = {}, &block - create_pact_verification consumer_name, options, &block - end - - def honours_pacts_from_pact_broker &block - create_pact_verification_from_broker(&block) - end - end - - def create_pact_verification consumer_name, options, &block - PactVerification.build(consumer_name, options, &block) - end - - def create_pact_verification_from_broker(&block) - PactVerificationFromBroker.build(name, branch, tags, &block) - end - - def finalize - validate - create_service_provider - end - - private - - def validate - raise Error.new("Please provide a name for the Provider") unless name && !name.strip.empty? - raise Error.new("Please set the app_version when publish_verification_results is true") if publish_verification_results && application_version_blank? - end - - def application_version_blank? - application_version.nil? || application_version.strip.empty? - end - - def create_service_provider - Pact.configuration.provider = ServiceProviderConfig.new(application_version, branch, tags, publish_verification_results, build_url, &@app_block) - end - end - end - end -end diff --git a/lib/pact/provider/context.rb b/lib/pact/provider/context.rb deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/pact/provider/grpc_verifier.rb b/lib/pact/provider/grpc_verifier.rb new file mode 100644 index 00000000..3272f709 --- /dev/null +++ b/lib/pact/provider/grpc_verifier.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'pact/ffi/verifier' +require 'pact/native/logger' + +module Pact + module Provider + class GrpcVerifier < BaseVerifier + PROVIDER_TRANSPORT_TYPE = 'grpc' + + def initialize(pact_config, mixed_config = nil) + super + + unless pact_config.is_a?(::Pact::Provider::PactConfig::Grpc) + raise ArgumentError, + 'pact_config must be an instance of Pact::Provider::PactConfig::Grpc' + end + + @grpc_server = GrufServer.new(host: "127.0.0.1:#{@pact_config.grpc_port}", + services: @pact_config.grpc_services, logger: @pact_config.logger) + end + + private + + def add_provider_transport(pact_handle) + PactFfi::Verifier.add_provider_transport(pact_handle, PROVIDER_TRANSPORT_TYPE, @pact_config.grpc_port, '', '') + end + + def start_servers! + super + @grpc_server.start + end + + def stop_servers + super + @grpc_server.stop + end + end + end +end diff --git a/lib/pact/provider/gruf_server.rb b/lib/pact/provider/gruf_server.rb new file mode 100644 index 00000000..e5cdaa7f --- /dev/null +++ b/lib/pact/provider/gruf_server.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +module Pact + module Provider + # inspired by Gruf::Cli::Executor + class GrufServer + SERVER_STOP_TIMEOUT_SEC = 15 + + def initialize(options = {}) + @options = options + + setup! + + @server_pid = nil + + @services = @options[:services].is_a?(Array) ? @options[:services] : [] + @logger = @options[:logger] || ::Logger.new($stdout) + end + + def start + raise "server already running, stop server before starting new one" if @thread + + @logger.info("[gruf] starting standalone server with options: #{@options}") + + @server = Gruf::Server.new(Gruf.server_options) + @services.each { |s| @server.add_service(s) } if @services.any? + @thread = Thread.new do + @logger.debug "[gruf] starting grpc server" + @server.start! + end + @server.server.wait_till_running(10) + + @logger.info("[gruf] standalone server started") + end + + def stop + @logger.info("[gruf] stopping standalone server") + + @server&.server&.stop + @thread&.join(SERVER_STOP_TIMEOUT_SEC) + @thread&.kill + + @logger.info("[gruf] standalone server stopped") + end + + ## + # Run the server + # + def run + start + + yield + rescue => e + @logger.fatal("FATAL ERROR: #{e.message} #{e.backtrace.join("\n")}") + raise + ensure + stop + end + + private + + def setup! + Gruf.server_binding_url = @options[:host] if @options[:host] + if @options[:suppress_default_interceptors] + Gruf.interceptors.remove(Gruf::Interceptors::ActiveRecord::ConnectionReset) + Gruf.interceptors.remove(Gruf::Interceptors::Instrumentation::OutputMetadataTimer) + end + Gruf.backtrace_on_error = true if @options[:backtrace_on_error] + Gruf.health_check_enabled = true if @options[:health_check] + end + end + end +end diff --git a/lib/pact/provider/help/console_text.rb b/lib/pact/provider/help/console_text.rb deleted file mode 100644 index c2e7c76d..00000000 --- a/lib/pact/provider/help/console_text.rb +++ /dev/null @@ -1,76 +0,0 @@ -require 'pact/provider/help/content' -require 'fileutils' -require 'pact/consumer/configuration' -require 'pact/provider/help/write' -require 'rainbow' - -module Pact - module Provider - module Help - class ConsoleText - - def self.call reports_dir = Pact.configuration.reports_dir, options = {color: true} - new(reports_dir || Pact.configuration.reports_dir, options).call - end - - def initialize reports_dir, options - @reports_dir = File.expand_path(reports_dir) - @options = options - end - - def call - begin - options[:color] ? ColorizeMarkdown.(help_text) : help_text - rescue Errno::ENOENT - options[:color] ? error_text_coloured : error_text_plain - end - end - - private - - attr_reader :reports_dir, :options - - def help_text - File.read(help_file_path) - end - - def help_file_path - File.join(reports_dir, Write::HELP_FILE_NAME) - end - - def error_text_plain - "Sorry, could not find help file at #{help_file_path}. Please ensure you have run `rake pact:verify`.\n" + - "If this does not fix the problem, please raise a github issues for this bug." - end - - def error_text_coloured - Rainbow(error_text_plain).red - end - - class ColorizeMarkdown - - def self.call markdown - markdown.split("\n").collect do | line | - if line.start_with?("# ") - yellow_underling line.gsub(/^# /, '') - elsif line.start_with?("* ") - green("* ") + line.gsub(/^\* /, '') - else - line - end - end.join("\n") - end - - def self.yellow_underling string - Rainbow(string).yellow.underline - end - - def self.green string - Rainbow(string).green - end - - end - end - end - end -end diff --git a/lib/pact/provider/help/content.rb b/lib/pact/provider/help/content.rb deleted file mode 100644 index 631ef28f..00000000 --- a/lib/pact/provider/help/content.rb +++ /dev/null @@ -1,38 +0,0 @@ -require 'pact/provider/help/pact_diff' - -module Pact - module Provider - module Help - class Content - - def initialize pact_sources - @pact_sources = pact_sources - end - - def text - help_text + "\n\n" + pact_diffs - end - - private - - attr_reader :pact_sources - - def help_text - temp_dir = Pact.configuration.tmp_dir - log_path = Pact.configuration.log_path - ERB.new(template_string).result(binding) - end - - def template_string - File.read(File.expand_path( '../../../templates/help.erb', __FILE__)) - end - - def pact_diffs - pact_sources.collect do | pact_json | - PactDiff.call(pact_json) - end.compact.join("\n") - end - end - end - end -end diff --git a/lib/pact/provider/help/pact_diff.rb b/lib/pact/provider/help/pact_diff.rb deleted file mode 100644 index f9677890..00000000 --- a/lib/pact/provider/help/pact_diff.rb +++ /dev/null @@ -1,43 +0,0 @@ -require 'pact/hal/entity' - -module Pact - module Provider - module Help - class PactDiff - class PrintPactDiffError < StandardError; end - - attr_reader :pact_source, :output - - def initialize pact_source - @pact_source = pact_source - end - - def self.call pact_source - new(pact_source).call - end - - def call - begin - header + "\n" + get_diff - rescue PrintPactDiffError => e - return e.message - end - end - - private - - def header - "The following changes have been made since the previous distinct version of this pact, and may be responsible for verification failure:\n" - end - - def get_diff - begin - pact_source.hal_entity._link!("pb:diff-previous-distinct").get!(nil, "Accept" => "text/plain").body - rescue StandardError => e - raise PrintPactDiffError.new("Tried to retrieve diff with previous pact, but received error #{e.class} #{e.message}.") - end - end - end - end - end -end diff --git a/lib/pact/provider/help/prompt_text.rb b/lib/pact/provider/help/prompt_text.rb deleted file mode 100644 index 5d16b347..00000000 --- a/lib/pact/provider/help/prompt_text.rb +++ /dev/null @@ -1,49 +0,0 @@ -require 'pact/consumer/configuration' -require 'rainbow' -require 'pathname' - -module Pact - module Provider - module Help - class PromptText - - def self.call reports_dir = Pact.configuration.reports_dir, options = {color: Pact.configuration.color_enabled} - new(reports_dir, options).call - end - - def initialize reports_dir, options - @reports_dir = File.expand_path(reports_dir) - @options = options - end - - def call - options[:color] ? prompt_text_colored : prompt_text_plain - end - - private - - attr_reader :reports_dir, :options - - def prompt_text_plain - "For assistance debugging failures, run `bundle exec rake pact:verify:help#{rake_args}`\n" - end - - def prompt_text_colored - Rainbow(prompt_text_plain).yellow - end - - def rake_args - if reports_dir == Pact.configuration.default_reports_dir - '' - else - "[#{relative_reports_dir}]" - end - end - - def relative_reports_dir - Pathname.new(reports_dir).relative_path_from(Pathname.new(Dir.pwd)) - end - end - end - end -end diff --git a/lib/pact/provider/help/write.rb b/lib/pact/provider/help/write.rb deleted file mode 100644 index 0931c4dd..00000000 --- a/lib/pact/provider/help/write.rb +++ /dev/null @@ -1,56 +0,0 @@ -require 'pact/provider/help/content' -require 'fileutils' -require 'pact/consumer/configuration' - -module Pact - module Provider - module Help - class Write - - HELP_FILE_NAME = 'help.md' - - def self.call pact_sources, reports_dir = Pact.configuration.reports_dir - new(pact_sources, reports_dir).call - end - - def initialize pact_sources, reports_dir - @pact_sources = pact_sources - @reports_dir = File.expand_path(reports_dir) - end - - def call - clean_reports_dir - write - rescue StandardError => e - Pact.configuration.error_stream.puts("ERROR: Error generating help output - #{e.class} #{e.message} \n" + e.backtrace.join("\n")) - end - - private - - attr_reader :reports_dir, :pact_sources - - def clean_reports_dir - raise "Cleaning report dir #{reports_dir} would delete project!" if reports_dir_contains_pwd - FileUtils.rm_rf reports_dir - FileUtils.mkdir_p reports_dir - end - - def reports_dir_contains_pwd - Dir.pwd.start_with?(reports_dir) - end - - def write - File.open(help_path, "w") { |file| file << help_text } - end - - def help_path - File.join(reports_dir, 'help.md') - end - - def help_text - Content.new(pact_sources).text - end - end - end - end -end diff --git a/lib/pact/provider/http_server.rb b/lib/pact/provider/http_server.rb new file mode 100644 index 00000000..b09a633b --- /dev/null +++ b/lib/pact/provider/http_server.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +module Pact + module Provider + # inspired by Gruf::Cli::Executor + class HttpServer + SERVER_STOP_TIMEOUT_SEC = 15 + + def initialize(options = {}) + @options = options + + @server_pid = nil + + @host = @options[:host] || "localhost" + @logger = @options[:logger] || ::Logger.new($stdout) + # allow any rack based app to be passed in, otherwise + # we will load a Rails.application + # allows for backwards compat with pact-ruby v1 + @app = @options[:app] || nil + end + + def start + raise "server already running, stop server before starting new one" if @thread + + @logger.info("[webrick] starting server with options: #{@options}") + + @thread = Thread.new do + @logger.debug "[webrick] starting http server" + + # TODO: load from config.ru, if not rails and no app provided? + # Rack 2/3 compatibility + begin + require 'rack/handler/webrick' + handler = ::Rack::Handler::WEBrick + rescue LoadError + require 'rackup/handler/webrick' + handler = Class.new(Rackup::Handler::WEBrick) + end + handler.run(@app || (defined?(Rails) ? Rails.application : nil), + Host: @options[:host], + Port: @options[:port], + Logger: @logger, + StartCallback: -> { @started = true }) do |server| + @server = server + end + end + sleep 0.001 until @started + + @logger.info("[webrick] server started") + end + + def stop + @logger.info("[webrick] stopping server") + + @server&.shutdown + @thread&.join(SERVER_STOP_TIMEOUT_SEC) + @thread&.kill + + @logger.info("[webrick] server stopped") + end + + ## + # Run the server + # + def run + start + + yield + rescue => e + @logger.fatal("FATAL ERROR: #{e.message} #{e.backtrace.join("\n")}") + raise + ensure + stop + end + end + end +end diff --git a/lib/pact/provider/http_verifier.rb b/lib/pact/provider/http_verifier.rb new file mode 100644 index 00000000..32f2846c --- /dev/null +++ b/lib/pact/provider/http_verifier.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'pact/ffi/verifier' +require 'pact/native/logger' + +module Pact + module Provider + class HttpVerifier < BaseVerifier + PROVIDER_TRANSPORT_TYPE = 'http' + + def initialize(pact_config, mixed_config = nil) + super + + unless pact_config.is_a?(::Pact::Provider::PactConfig::Http) + raise ArgumentError, + 'pact_config must be an instance of Pact::Provider::PactConfig::Http' + end + + @http_server = HttpServer.new(host: '127.0.0.1', port: @pact_config.http_port, app: @pact_config.app, + logger: @pact_config.logger) + end + + private + + def set_provider_info(pact_handle) + PactFfi::Verifier.set_provider_info(pact_handle, @pact_config.provider_name, '', '', @pact_config.http_port, '') + end + + def add_provider_transport(pact_handle) + # The http transport is already added when the `set_provider_info` method is called, + # so we don't need to explicitly add the transport here + end + + def start_servers! + super + @http_server.start + end + + def stop_servers + super + @http_server.stop + end + end + end +end diff --git a/lib/pact/provider/matchers/messages.rb b/lib/pact/provider/matchers/messages.rb deleted file mode 100644 index 1ef0419c..00000000 --- a/lib/pact/provider/matchers/messages.rb +++ /dev/null @@ -1,66 +0,0 @@ -require 'rainbow' -require 'pact/term' - -module Pact - module Matchers - module Messages - - def match_term_failure_message diff, actual, diff_formatter, color_enabled - actual_string = String === actual ? actual : actual.to_json - maybe_coloured_string = color_enabled ? Rainbow(actual_string).white : actual_string - message = "Actual: #{maybe_coloured_string}\n\n" - formatted_diff = diff_formatter.call(diff) - message + colorize_if_enabled(formatted_diff, color_enabled) - end - - def match_header_failure_message header_name, expected, actual - "Expected header \"#{header_name}\" to #{expected_desc(expected)}, but was #{actual_desc(actual)}" - end - - def expected_desc_for_it expected - case expected - when NilClass then "is nil" - when Regexp - "matches #{expected.inspect}" - when Pact::Term - "matches #{expected.matcher.inspect}" - when Pact::SomethingLike - "is an instance of #{Pact::Reification.from_term(expected).class}" - else - "equals #{expected.inspect}" - end - end - - private - - def colorize_if_enabled formatted_diff, color_enabled - if color_enabled - # RSpec wraps each line in the failure message with failure_color, turning it red. - # To ensure the lines in the diff that should be white, stay white, put an - # ANSI reset at the start of each line. - formatted_diff.split("\n").collect{ |line|"\e[0m#{line}" }.join("\n") - else - formatted_diff - end - end - - def expected_desc expected - case expected - when NilClass then "be nil" - when Regexp - "match #{expected.inspect}" - when Pact::Term - "match #{expected.matcher.inspect}" - when Pact::SomethingLike - "be an instance of #{Pact::Reification.from_term(expected).class}" - else - "equal #{expected.inspect}" - end - end - - def actual_desc actual - actual.nil? ? 'nil' : '"' + actual + '"' - end - end - end -end \ No newline at end of file diff --git a/lib/pact/provider/message_provider_servlet.rb b/lib/pact/provider/message_provider_servlet.rb new file mode 100644 index 00000000..7ec054e7 --- /dev/null +++ b/lib/pact/provider/message_provider_servlet.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require "webrick" + +module Pact + module Provider + class MessageProviderServlet < WEBrick::HTTPServlet::ProcHandler + attr_reader :logger + + CONTENT_TYPE_JSON = "application/json" + CONTENT_TYPE_PROTO = "application/protobuf" + METADATA_HEADER = "pact-message-metadata" + + def initialize(logger: nil) + super(build_proc) + + @message_handlers = {} + + @logger = logger || Logger.new($stdout) + end + + def add_message_handler(name, &block) + raise "message handler for #{name} already configured" if @message_handlers[name].present? + + @message_handlers[name] = {proc: block} + end + + private + + def build_proc + proc do |request, response| + # {"description":"message: ","providerStates":[{"name":"pet exists","params":{"pet_id":1}}]} + data = JSON.parse(request.body) + + description = data["description"] + provider_states = data["providerStates"] + + body, metadata = handle(description, provider_states) + + response.status = 200 + if body.is_a?(String) + # protobuf-serialized body + response.body = body + response.content_type = metadata[:content_type] || CONTENT_TYPE_PROTO + else + response.body = body.to_json + response.content_type = CONTENT_TYPE_JSON + end + response[METADATA_HEADER] = Base64.urlsafe_encode64(metadata.to_json) + rescue JSON::ParserError => ex + logger.error("cannot parse request: #{ex.message}") + response.status = 500 + rescue => ex + logger.error("cannot handle message request: #{ex.message}") + response.status = 500 + end + end + + def handle(description, provider_states) + handler = find_handler_for(description) + return {}, {} unless handler + + body, metadata = handler[:proc].call(provider_states&.first || {}) + unless metadata[:content_type] + # try to find content-type in provider states + content_type = provider_states&.filter_map { |state| state.dig("params", "contentType") }&.first + metadata[:content_type] = content_type if content_type + end + [body, metadata] + end + + def find_handler_for(description) + @message_handlers[description] + end + end + end +end diff --git a/lib/pact/provider/mixed_verifier.rb b/lib/pact/provider/mixed_verifier.rb new file mode 100644 index 00000000..98fc1a42 --- /dev/null +++ b/lib/pact/provider/mixed_verifier.rb @@ -0,0 +1,21 @@ +# # frozen_string_literal: true +module Pact + module Provider + # MixedVerifier coordinates verification for all present configs (async, grpc, http) + class MixedVerifier + attr_reader :mixed_config, :verifiers + + def initialize(mixed_config) + unless mixed_config.is_a?(::Pact::Provider::PactConfig::Mixed) + raise ArgumentError, 'mixed_config must be a PactConfig::Mixed' + end + + @mixed_config = mixed_config + @verifiers = [] + @verifiers << AsyncMessageVerifier.new(mixed_config.async_config) if mixed_config.async_config + @verifiers << GrpcVerifier.new(mixed_config.grpc_config) if mixed_config.grpc_config + @verifiers << HttpVerifier.new(mixed_config.http_config) if mixed_config.http_config + end + end + end +end diff --git a/lib/pact/provider/pact_broker_proxy.rb b/lib/pact/provider/pact_broker_proxy.rb new file mode 100644 index 00000000..25029b21 --- /dev/null +++ b/lib/pact/provider/pact_broker_proxy.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require 'rack-proxy' + +module Pact + module Provider + class PactBrokerProxy < Rack::Proxy + attr_reader :backend_uri, :path, :logger + + # e.g. /pacts/provider/paas-stand-seeker/consumer/paas-stand-placer/pact-version/2967a9343bd8fdd28a286c4b8322380020618892/metadata/c1tdW2VdPXByb2R1Y3Rpb24mc1tdW2N2XT03MzIy # rubocop:disable Layout/LineLength + PACT_FILE_REQUEST_PATH_REGEX = %r{/pacts/provider/.+?/consumer/.+?/pact-version/.+}.freeze + + def initialize(app = nil, opts = {}) + super + @backend_uri = URI(opts[:backend]) + @path = nil + @logger = opts[:logger] || Logger.new($stdout) + end + + def perform_request(env) + request = Rack::Request.new(env) + env['rack.timeout'] ||= ENV.fetch('PACT_BROKER_REQUEST_TIMEOUT', 5).to_i + @path = request.path + + super + end + + def rewrite_env(env) + env['HTTP_HOST'] = backend_uri.host + env + end + + def rewrite_response(triplet) + status, headers, body = triplet + + if status == '200' && PACT_FILE_REQUEST_PATH_REGEX.match?(path) + patched_body = patch_response(body.first) + + # we need to recalculate content length + headers[Rack::CONTENT_LENGTH] = patched_body.bytesize.to_s + + return [status, headers, [patched_body]] + end + + triplet + end + + private + + def patch_response(raw_body) + parsed_body = JSON.parse(raw_body) + + return body if parsed_body['consumer'].blank? || parsed_body['provider'].blank? + return body if parsed_body['interactions'].blank? + + JSON.generate(parsed_body) + rescue JSON::ParserError => e + logger.error("cannot parse broker response: #{e.message}") + end + end + end +end diff --git a/lib/pact/provider/pact_broker_proxy_runner.rb b/lib/pact/provider/pact_broker_proxy_runner.rb new file mode 100644 index 00000000..3782cc7b --- /dev/null +++ b/lib/pact/provider/pact_broker_proxy_runner.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require "webrick" + +module Pact + module Provider + class PactBrokerProxyRunner + attr_reader :logger + + + def initialize(pact_broker_host:, port: 9002, host: "127.0.0.1", pact_broker_user: nil, pact_broker_password: nil, pact_broker_token: nil, logger: nil) + @host = host + @port = port + @pact_broker_host = pact_broker_host + @pact_broker_user = pact_broker_user + @pact_broker_password = pact_broker_password + @pact_broker_token = pact_broker_token + @logger = logger || Logger.new($stdout) + + @thread = nil + end + + def proxy_url + "http://#{@host}:#{@port}" + end + + def start + raise "server already running, stop server before starting new one" if @thread + # Rack 2/3 compatibility + begin + require 'rack/handler/webrick' + handler = ::Rack::Handler::WEBrick + rescue LoadError + require 'rackup/handler/webrick' + handler = Class.new(Rackup::Handler::WEBrick) + end + @server = WEBrick::HTTPServer.new( + { BindAddress: @host, Port: @port, Logger: @logger, AccessLog: [] }, + WEBrick::Config::HTTP + ) + @server.mount("/", handler, PactBrokerProxy.new( + nil, + backend: @pact_broker_host, + streaming: false, + username: @pact_broker_user || nil, + password: @pact_broker_password || nil, + token: @pact_broker_token || nil, + logger: @logger + )) + + @thread = Thread.new do + @logger.debug "starting pact broker proxy server" + @server.start + end + end + + def stop + @logger.info("stopping pact broker proxy server") + + @server&.shutdown + @thread&.join + + @logger.info("pact broker proxy server stopped") + end + + def run + start + + yield + rescue => e + logger.fatal("FATAL ERROR: #{e.message} #{e.backtrace.join("\n")}") + raise + ensure + stop + end + end + end +end \ No newline at end of file diff --git a/lib/pact/provider/pact_config.rb b/lib/pact/provider/pact_config.rb new file mode 100644 index 00000000..2a7fdbd2 --- /dev/null +++ b/lib/pact/provider/pact_config.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +# require_relative "pact_config/grpc" + +module Pact + module Provider + module PactConfig + def self.new(transport_type, provider_name:, opts: {}) + case transport_type + when :http + Http.new(provider_name: provider_name, opts: opts) + when :grpc + Grpc.new(provider_name: provider_name, opts: opts) + when :async + Async.new(provider_name: provider_name, opts: opts) + when :mixed + Mixed.new(provider_name: provider_name, opts: opts) + else + raise ArgumentError, "unknown transport_type: #{transport_type}" + end + end + end + end +end diff --git a/lib/pact/provider/pact_config/async.rb b/lib/pact/provider/pact_config/async.rb new file mode 100644 index 00000000..7407f45f --- /dev/null +++ b/lib/pact/provider/pact_config/async.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require_relative "base" + +module Pact + module Provider + module PactConfig + class Async < Base + def initialize(provider_name:, opts: {}) + super + handlers = opts[:message_handlers] || {} + handlers.each do |name, block| + new_message_handler(name, &block) + end + end + + def new_message_handler(name, opts: {}, &block) + provider_setup_server.add_message_handler(name, &block) + end + + def new_verifier(config = nil) + AsyncMessageVerifier.new(self, config) + end + end + end + end +end + diff --git a/lib/pact/provider/pact_config/base.rb b/lib/pact/provider/pact_config/base.rb new file mode 100644 index 00000000..b88d63ac --- /dev/null +++ b/lib/pact/provider/pact_config/base.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +module Pact + module Provider + module PactConfig + class Base + attr_reader :provider_name, :provider_version, :log_level, :provider_setup_server, :provider_setup_port, :pact_proxy_port, + :consumer_branch, :consumer_version, :consumer_name, :broker_url, :broker_username, :broker_password, :verify_only, :pact_dir, + :pact_uri, :provider_version_branch, :provider_version_tags, :consumer_version_selectors, :enable_pending, :include_wip_pacts_since, + :fail_if_no_pacts_found, :provider_build_uri, :broker_token, :consumer_version_tags, :publish_verification_results, :logger + + + def initialize(provider_name:, opts: {}) + @provider_name = provider_name + @log_level = opts[:log_level] || :info + @pact_dir = opts[:pact_dir] || nil + @logger = opts[:logger] || nil + @provider_setup_port = opts[:provider_setup_port] || 9001 + @pact_proxy_port = opts[:pact_proxy_port] || 9002 + @pact_uri = ENV.fetch("PACT_URL", nil) || opts.fetch(:pact_uri, nil) + @publish_verification_results = ENV.fetch("PACT_PUBLISH_VERIFICATION_RESULTS", nil) == "true" || opts.fetch(:publish_verification_results, false) + @provider_version = ENV.fetch("PACT_PROVIDER_VERSION", nil) || opts.fetch(:provider_version, nil) + @provider_build_uri = ENV.fetch("PACT_PROVIDER_BUILD_URL", nil) || opts.fetch(:provider_build_uri, nil) + @provider_version_branch = ENV.fetch("PACT_PROVIDER_BRANCH", nil) || opts.fetch(:provider_version_branch, nil) + @provider_version_tags = ENV.fetch("PACT_PROVIDER_VERSION_TAGS", nil) || opts.fetch(:provider_version_tags, []) + @consumer_version_tags = ENV.fetch("PACT_CONSUMER_VERSION_TAGS", nil) || opts.fetch(:consumer_version_tags, []) + @consumer_version_selectors = ENV.fetch("PACT_CONSUMER_VERSION_SELECTORS", nil) || opts.fetch(:consumer_version_selectors, nil) + @enable_pending = ENV.fetch("PACT_VERIFIER_ENABLE_PENDING", nil) == "true" || opts.fetch(:enable_pending, false) + @include_wip_pacts_since = ENV.fetch("PACT_INCLUDE_WIP_PACTS_SINCE", nil) || opts.fetch(:include_wip_pacts_since, nil) + @fail_if_no_pacts_found = ENV.fetch("PACT_FAIL_IF_NO_PACTS_FOUND", nil) == "true" || opts.fetch(:fail_if_no_pacts_found, true) + @consumer_branch = ENV.fetch("PACT_CONSUMER_BRANCH", nil) || opts.fetch(:consumer_branch, nil) + @consumer_version = ENV.fetch("PACT_CONSUMER_VERSION", nil) || opts.fetch(:consumer_version, nil) + @consumer_name = opts[:consumer_name] + @broker_url = ENV.fetch("PACT_BROKER_BASE_URL", nil) || opts.fetch(:broker_url, nil) + @broker_username = ENV.fetch("PACT_BROKER_USERNAME", nil) || opts.fetch(:broker_username, nil) + @broker_password = ENV.fetch("PACT_BROKER_PASSWORD", nil) || opts.fetch(:broker_password, nil) + @broker_token = ENV.fetch("PACT_BROKER_TOKEN", nil) || opts.fetch(:broker_token, nil) + @verify_only = [ENV.fetch("PACT_CONSUMER_FULL_NAME", nil)].compact || opts.fetch(:verify_only, []) + + @provider_setup_server = opts[:provider_setup_server] || ProviderServerRunner.new(port: @provider_setup_port, logger: @logger) + if @broker_url.present? + @pact_proxy_server = PactBrokerProxyRunner.new( + port: @pact_proxy_port, + pact_broker_host: @broker_url, + pact_broker_user: @broker_username, + pact_broker_password: @broker_password, + pact_broker_token: @broker_token, + logger: @logger + ) + end + end + + + def start_servers + @provider_setup_server.start + @pact_proxy_server&.start + end + + def stop_servers + @provider_setup_server.stop + @pact_proxy_server&.stop + end + + def provider_setup_url + @provider_setup_server.state_setup_url + end + + def message_setup_url # rubocop:disable Rails/Delegate + @provider_setup_server.message_setup_url + end + + def pact_broker_proxy_url + @pact_proxy_server&.proxy_url + end + + def new_provider_state(name, opts: {}, &block) + config = ProviderStateConfiguration.new(name, opts: opts) + config.instance_eval(&block) + config.validate! + + use_hooks = !opts[:skip_hooks] + + @provider_setup_server.add_setup_state(name, use_hooks, &config.setup_proc) if config.setup_proc + @provider_setup_server.add_teardown_state(name, use_hooks, &config.teardown_proc) if config.teardown_proc + end + + def before_setup(&block) + @provider_setup_server.set_before_setup_hook(&block) + end + + def after_teardown(&block) + @provider_setup_server.set_after_teardown_hook(&block) + end + + def new_verifier + raise PactImplementationRequired, "#new_verifier should be implemented" + end + end + end + end +end diff --git a/lib/pact/provider/pact_config/grpc.rb b/lib/pact/provider/pact_config/grpc.rb new file mode 100644 index 00000000..0aba9b3e --- /dev/null +++ b/lib/pact/provider/pact_config/grpc.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require_relative "base" + +module Pact + module Provider + module PactConfig + class Grpc < Base + attr_reader :grpc_port, :grpc_services, :grpc_server + + def initialize(provider_name:, opts: {}) + super + + @grpc_port = opts[:grpc_port] || 0 + @grpc_services = opts[:grpc_services] || [] + end + + def new_verifier(config = nil) + GrpcVerifier.new(self, config) + end + end + end + end +end diff --git a/lib/pact/provider/pact_config/http.rb b/lib/pact/provider/pact_config/http.rb new file mode 100644 index 00000000..d4cb1913 --- /dev/null +++ b/lib/pact/provider/pact_config/http.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require_relative "base" + +module Pact + module Provider + module PactConfig + class Http < Base + attr_reader :http_port + attr_reader :app + + def initialize(provider_name:, opts: {}) + super + + @http_port = opts[:http_port] || 0 + @app = opts[:app] || nil + end + + def new_verifier(config = nil) + HttpVerifier.new(self, config) + end + end + end + end +end diff --git a/lib/pact/provider/pact_config/mixed.rb b/lib/pact/provider/pact_config/mixed.rb new file mode 100644 index 00000000..b278e86a --- /dev/null +++ b/lib/pact/provider/pact_config/mixed.rb @@ -0,0 +1,38 @@ +# # frozen_string_literal: true + +module Pact + module Provider + module PactConfig + # Mixed config allows composing one of each: async, grpc, http + class Mixed < Base + attr_reader :async_config, :grpc_config, :http_config + + def initialize(provider_name:, opts: {}) + super + @provider_setup_server = ProviderServerRunner.new(port: @provider_setup_port, logger: @logger) + if @broker_url.present? + @pact_proxy_server = PactBrokerProxyRunner.new( + port: @pact_proxy_port, + pact_broker_host: @broker_url, + pact_broker_user: @broker_username, + pact_broker_password: @broker_password, + pact_broker_token: @broker_token, + logger: @logger + ) + end + @http_config = opts[:http] ? Http.new(provider_name: provider_name, opts: opts[:http].merge(provider_setup_server: provider_setup_server, pact_proxy_server: @pact_proxy_server)) : nil + @grpc_config = opts[:grpc] ? Grpc.new(provider_name: provider_name, opts: opts[:grpc].merge(provider_setup_server: provider_setup_server, pact_proxy_server: @pact_proxy_server)) : nil + @async_config = opts[:async] ? Async.new(provider_name: provider_name, opts: opts[:async].merge(provider_setup_server: provider_setup_server, pact_proxy_server: @pact_proxy_server)) : nil + end + + def configs + [@async_config, @grpc_config, @http_config].compact + end + + def start_servers + end + + end + end + end +end diff --git a/lib/pact/provider/pact_helper_locator.rb b/lib/pact/provider/pact_helper_locator.rb deleted file mode 100644 index d71f936c..00000000 --- a/lib/pact/provider/pact_helper_locator.rb +++ /dev/null @@ -1,24 +0,0 @@ -module Pact - module Provider - module PactHelperLocater - PACT_HELPER_FILE_PATTERNS = [ - "spec/**/*service*consumer*/pact_helper.rb", - "spec/**/*consumer*/pact_helper.rb", - "spec/**/pact_helper.rb", - "test/**/*service*consumer*/pact_helper.rb", - "test/**/*consumer*/pact_helper.rb", - "test/**/pact_helper.rb", - "**/pact_helper.rb" - ] - - NO_PACT_HELPER_FOUND_MSG = "Please create a pact_helper.rb file that can be found using one of the following patterns: #{PACT_HELPER_FILE_PATTERNS.join(", ")}" - - def self.pact_helper_path - pact_helper_search_results = [] - PACT_HELPER_FILE_PATTERNS.find { | pattern | (pact_helper_search_results.concat(Dir.glob(pattern))).any? } - raise NO_PACT_HELPER_FOUND_MSG if pact_helper_search_results.empty? - File.join(Dir.pwd, pact_helper_search_results[0]) - end - end - end -end diff --git a/lib/pact/provider/pact_source.rb b/lib/pact/provider/pact_source.rb deleted file mode 100644 index a1a14e27..00000000 --- a/lib/pact/provider/pact_source.rb +++ /dev/null @@ -1,40 +0,0 @@ -require 'pact/consumer_contract/pact_file' -require 'pact/hal/http_client' -require 'pact/hal/entity' -require 'pact/consumer_contract' - -module Pact - module Provider - class PactSource - - attr_reader :uri # PactURI class - - def initialize uri - @uri = uri - end - - def pact_json - @pact_json ||= Pact::PactFile.read(uri.uri, uri.options) - end - - def pact_hash - @pact_hash ||= JSON.load(pact_json, nil, { max_nesting: 50 }) - end - - def pending? - uri.metadata[:pending] - end - - def consumer_contract - @consumer_contract ||= Pact::ConsumerContract.from_json(pact_json) - end - - def hal_entity - http_client_keys = [:username, :password, :token] - http_client_options = uri.options.reject{ |k, _| !http_client_keys.include?(k) } - http_client = Pact::Hal::HttpClient.new(http_client_options) - Pact::Hal::Entity.new(uri, pact_hash, http_client) - end - end - end -end diff --git a/lib/pact/provider/pact_spec_runner.rb b/lib/pact/provider/pact_spec_runner.rb deleted file mode 100644 index f0107f06..00000000 --- a/lib/pact/provider/pact_spec_runner.rb +++ /dev/null @@ -1,188 +0,0 @@ -require 'open-uri' -require 'rspec' -require 'rspec/core' -require 'rspec/core/formatters/documentation_formatter' -require 'pact/provider/pact_helper_locator' -require 'pact/project_root' -require 'pact/rspec' -require 'pact/provider/pact_source' -require 'pact/provider/help/write' -require 'pact/provider/verification_results/publish_all' -require 'pact/provider/rspec/pact_broker_formatter' -require 'pact/provider/rspec/json_formatter' -require 'pact/provider/rspec' -require 'pact/provider/rspec/calculate_exit_code' -require 'pact/utils/metrics' - -module Pact - module Provider - class PactSpecRunner - - include Pact::Provider::RSpec::ClassMethods - - attr_reader :pact_urls - attr_reader :options - - def initialize pact_urls, options = {} - @pact_urls = pact_urls - @options = options - @results = nil - end - - def run - begin - configure_rspec - initialize_specs - run_specs - ensure - ::RSpec.reset - Pact.clear_provider_world - Pact.clear_consumer_world - end - end - - private - - def configure_rspec - monkey_patch_backtrace_formatter - - config = ::RSpec.configuration - - config.color = true - config.pattern = "pattern which doesn't match any files" - config.backtrace_inclusion_patterns = [Regexp.new(Dir.getwd), /pact.*main/] - - config.extend Pact::Provider::RSpec::ClassMethods - config.include Pact::Provider::RSpec::InstanceMethods - config.include Pact::Provider::TestMethods - - if options[:silent] - config.output_stream = StringIO.new - config.error_stream = StringIO.new - else - config.error_stream = Pact.configuration.error_stream - config.output_stream = Pact.configuration.output_stream - end - - configure_output - - config.before(:suite) do - # Preload app before suite so the classes loaded in memory are consistent for - # before :each and after :each hooks. - # Otherwise the app and all its dependencies are loaded between the first before :each - # and the first after :each, leading to inconsistent behaviour - # (eg. with database_cleaner transactions) - Pact.configuration.provider.app - end - - # For the Pact::Provider::RSpec::PactBrokerFormatter - Pact.provider_world.verbose = options[:verbose] - Pact.provider_world.pact_sources = pact_sources - jsons = pact_jsons - executing_with_ruby = executing_with_ruby? - - config.after(:suite) do | suite | - Pact.provider_world.failed_examples = suite.reporter.failed_examples - Pact::Provider::Help::Write.call(Pact.provider_world.pact_sources) if executing_with_ruby - end - end - - def run_specs - exit_code = if Pact::RSpec.runner_defined? - ::RSpec::Core::Runner.run(rspec_runner_options) - else - ::RSpec::Core::CommandLine.new(NoConfigurationOptions.new) - .run(::RSpec.configuration.output_stream, ::RSpec.configuration.error_stream) - end - - if options[:ignore_failures] - 0 - else - Pact::Provider::RSpec::CalculateExitCode.call(pact_sources, Pact.provider_world.failed_examples) - end - end - - def rspec_runner_options - ["--options", Pact.project_root.join("lib/pact/provider/rspec/custom_options_file").to_s] - end - - def monkey_patch_backtrace_formatter - Pact::RSpec.with_rspec_3 do - require 'pact/provider/rspec/backtrace_formatter' - end - end - - def pact_sources - @pact_sources ||= begin - pact_urls.collect do | pact_url | - Pact::Provider::PactSource.new(pact_url) - end - end - end - - def pact_jsons - pact_sources.collect(&:pact_json) - end - - def initialize_specs - pact_sources.each do | pact_source | - spec_options = { - criteria: options[:criteria], - ignore_failures: options[:ignore_failures], - request_customizer: options[:request_customizer] - } - Pact::Utils::Metrics.report_metric("Pacts verified", "ProviderTest", "Completed") - - honour_pactfile pact_source, ordered_pact_json(pact_source.pact_json), spec_options - end - end - - def configure_output - Pact::RSpec.with_rspec_3 do - ::RSpec.configuration.add_formatter Pact::Provider::RSpec::PactBrokerFormatter, StringIO.new - end - - output = options[:out] || Pact.configuration.output_stream - if options[:format] - formatter = options[:format] == 'json' ? Pact::Provider::RSpec::JsonFormatter : options[:format] - # Send formatted output to $stdout for parsing, unless a file is specified - output = options[:out] || $stdout - ::RSpec.configuration.add_formatter formatter, output - # Don't want to mess up the JSON parsing with INFO and DEBUG messages to stdout, so send it to stderr - Pact.configuration.output_stream = Pact.configuration.error_stream if !options[:out] - else - # Sometimes the formatter set in the cli.rb get set with an output of StringIO.. don't know why - formatter_class = Pact::RSpec.formatter_class - pact_formatter = ::RSpec.configuration.formatters.find {|f| f.class == formatter_class && f.output == ::RSpec.configuration.output_stream} - ::RSpec.configuration.add_formatter(formatter_class, output) unless pact_formatter - end - - ::RSpec.configuration.full_backtrace = @options[:full_backtrace] - end - - def ordered_pact_json(pact_json) - return pact_json if Pact.configuration.interactions_replay_order == :recorded - - consumer_contract = JSON.parse(pact_json) - consumer_contract["interactions"] = consumer_contract["interactions"].shuffle - consumer_contract.to_json - end - - def class_exists? name - Kernel.const_get name - rescue NameError - false - end - - def executing_with_ruby? - ENV['PACT_EXECUTING_LANGUAGE'] == 'ruby' - end - - class NoConfigurationOptions - def method_missing(method, *args, &block) - # Do nothing! - end - end - end - end -end diff --git a/lib/pact/provider/pact_uri.rb b/lib/pact/provider/pact_uri.rb deleted file mode 100644 index 282ffe27..00000000 --- a/lib/pact/provider/pact_uri.rb +++ /dev/null @@ -1,55 +0,0 @@ -module Pact - module Provider - class PactURI - attr_reader :uri, :options, :metadata - - def initialize(uri, options = nil, metadata = nil) - @uri = uri - @options = options || {} - @metadata = metadata || {} # make sure it's not nil if nil is passed in - end - - def == other - other.is_a?(PactURI) && - uri == other.uri && - options == other.options && - metadata == other.metadata - end - - def basic_auth? - !!username && !!password - end - - def username - options[:username] - end - - def password - options[:password] - end - - def to_s - if basic_auth? && http_or_https_uri? - begin - URI(@uri).tap { |x| x.userinfo="#{username}:*****"}.to_s - rescue URI::InvalidComponentError - URI(@uri).tap { |x| x.userinfo="*****:*****"}.to_s - end - elsif personal_access_token? && http_or_https_uri? - URI(@uri).tap { |x| x.userinfo="*****"}.to_s - else - uri - end - end - - private def personal_access_token? - !!username && !password - end - - private def http_or_https_uri? - uri.start_with?('http://', 'https://') - end - - end - end -end diff --git a/lib/pact/provider/pact_verification.rb b/lib/pact/provider/pact_verification.rb deleted file mode 100644 index 7bb9dda8..00000000 --- a/lib/pact/provider/pact_verification.rb +++ /dev/null @@ -1,17 +0,0 @@ -module Pact::Provider - class PactVerification - attr_reader :consumer_name, :uri, :ref - def initialize consumer_name, uri, ref - @consumer_name = consumer_name - @uri = uri - @ref = ref - end - - def == other - other.is_a?(PactVerification) && - consumer_name == other.consumer_name && - uri == other.uri && - ref == other.ref - end - end -end \ No newline at end of file diff --git a/lib/pact/provider/print_missing_provider_states.rb b/lib/pact/provider/print_missing_provider_states.rb deleted file mode 100644 index da60e86d..00000000 --- a/lib/pact/provider/print_missing_provider_states.rb +++ /dev/null @@ -1,35 +0,0 @@ -require 'rainbow' - -module Pact - module Provider - class PrintMissingProviderStates - - # Hash of consumer names to array of names of missing provider states - def self.call missing_provider_states, output - if missing_provider_states.any? - output.puts colorize(text(missing_provider_states)) - end - end - - def self.colorize string - lines = string.split("\n") - first_line = Rainbow(lines[0]).cyan.underline - other_lines = Rainbow(lines[1..-1].join("\n")).cyan - first_line + "\n" + other_lines - end - - def self.text missing_provider_states - create_provider_states_for(missing_provider_states) - end - - def self.create_provider_states_for consumers - ERB.new(template_string).result(binding) - end - - def self.template_string - File.read(File.expand_path( '../../templates/provider_state.erb', __FILE__)) - end - - end - end -end \ No newline at end of file diff --git a/lib/pact/provider/provider_server_runner.rb b/lib/pact/provider/provider_server_runner.rb new file mode 100644 index 00000000..a7f4af41 --- /dev/null +++ b/lib/pact/provider/provider_server_runner.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +require "webrick" + +module Pact + module Provider + class ProviderServerRunner + attr_reader :logger + + SETUP_PROVIDER_STATE_PATH = "/setup-provider" + VERIFY_MESSAGE_PATH = "/verify-message" + + def initialize(port: 9001, host: "127.0.0.1", logger: nil) + @host = host + @port = port + @provider_setup_states = {} + @provider_teardown_states = {} + @logger = logger || Logger.new($stdout) + + @state_servlet = ProviderStateServlet.new(logger: @logger) + @message_servlet = MessageProviderServlet.new(logger: @logger) + @thread = nil + end + + def state_setup_url + "http://#{@host}:#{@port}#{SETUP_PROVIDER_STATE_PATH}" + end + + def message_setup_url + "http://#{@host}:#{@port}#{VERIFY_MESSAGE_PATH}" + end + + def start + raise "server already running, stop server before starting new one" if @thread + + @server = WEBrick::HTTPServer.new( + { BindAddress: @host, Port: @port, Logger: @logger, AccessLog: [] }, + WEBrick::Config::HTTP + ) + @server.mount(SETUP_PROVIDER_STATE_PATH, @state_servlet) + @server.mount(VERIFY_MESSAGE_PATH, @message_servlet) + + @thread = Thread.new do + @logger.debug "starting provider setup server" + @server.start + end + end + + def stop + @logger.info("stopping provider setup server") + + @server&.shutdown + @thread&.join + + @logger.info("provider setup server stopped") + end + + def run + start + + yield + rescue => e + logger.fatal("FATAL ERROR: #{e.message} #{e.backtrace.join("\n")}") + raise + ensure + stop + end + + def add_message_handler(state_name, &block) + @message_servlet.add_message_handler(state_name, &block) + end + + def add_setup_state(state_name, use_before_setup_hook = true, &block) + @state_servlet.add_setup_state(state_name, use_before_setup_hook, &block) + end + + def add_teardown_state(state_name, use_after_teardown_hook = true, &block) + @state_servlet.add_teardown_state(state_name, use_after_teardown_hook, &block) + end + + def set_before_setup_hook(&block) + @state_servlet.before_setup(&block) + end + + def set_after_teardown_hook(&block) + @state_servlet.after_teardown(&block) + end + end + end +end diff --git a/lib/pact/provider/provider_state_configuration.rb b/lib/pact/provider/provider_state_configuration.rb new file mode 100644 index 00000000..3f8c6b8c --- /dev/null +++ b/lib/pact/provider/provider_state_configuration.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Pact + module Provider + class ProviderStateConfiguration + attr_reader :name, :opts, :setup_proc, :teardown_proc + + class ProviderStateConfigurationError < ::Pact::Error; end + + def initialize(name, opts: {}) + @name = name + @opts = opts + @setup_proc = nil + @teardown_proc = nil + end + + def set_up(&block) + @setup_proc = block + end + + def tear_down(&block) + @teardown_proc = block + end + + def validate! + return if @setup_proc || @teardown_proc + + raise ProviderStateConfigurationError.new("no hooks configured for state #{@name}: \"provider_state\" declaration only needed if setup/teardown hooks are used for that state. Please add hooks or remove \"provider_state\" declaration") # rubocop:disable Layout/LineLength + end + end + end +end diff --git a/lib/pact/provider/provider_state_servlet.rb b/lib/pact/provider/provider_state_servlet.rb new file mode 100644 index 00000000..478dc7b4 --- /dev/null +++ b/lib/pact/provider/provider_state_servlet.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require "webrick" + +module Pact + module Provider + class ProviderStateServlet < WEBrick::HTTPServlet::ProcHandler + attr_reader :logger + + def initialize(logger: nil) + super(build_proc) + + @logger = logger || Logger.new($stdout) + + @provider_setup_states = {} + @provider_teardown_states = {} + + @before_setup_hook_proc = nil + @after_teardown_hook_proc = nil + + @global_setup_hook = ::Pact.configuration.before_provider_state_proc + @global_teardown_hook = ::Pact.configuration.after_provider_state_proc + end + + def add_setup_state(name, use_before_setup_hook, &block) + raise "provider state #{name} already configured" if @provider_setup_states[name].present? + + @provider_setup_states[name] = {proc: block, use_hooks: use_before_setup_hook} + end + + def add_teardown_state(name, use_after_teardown_hook, &block) + raise "provider state #{name} already configured" if @provider_teardown_states[name].present? + + @provider_teardown_states[name] = {proc: block, use_hooks: use_after_teardown_hook} + end + + def before_setup(&block) + @before_setup_hook_proc = block + end + + def after_teardown(&block) + @after_teardown_hook_proc = block + end + + private + + def call_setup(state_name, state_data) + logger.debug "call_setup #{state_name} with #{state_data}" + @global_setup_hook&.call + @before_setup_hook_proc&.call(state_name, state_data) if @provider_setup_states.dig(state_name, :use_hooks) + @provider_setup_states.dig(state_name, :proc)&.call(state_data) + end + + def call_teardown(state_name, state_data) + logger.debug "call_teardown #{state_name} with #{state_data}" + @provider_teardown_states.dig(state_name, :proc)&.call(state_data) + @after_teardown_hook_proc&.call(state_name, state_data) if @provider_setup_states.dig(state_name, :use_hooks) + @global_teardown_hook&.call + end + + def build_proc + proc do |request, response| + # {"action" => "setup", "params" => {"order_uuid" => "mxfcpcsfUOHO"},"state" => "order exists and can be saved"} + # {"action"=> "teardown", "params" => {"order_uuid" => "mxfcpcsfUOHO"}, "state" => "order exists and can be saved"} + data = JSON.parse(request.body) + + action = data["action"] + state_name = data["state"] + state_data = data["params"] + + logger.warn("unknown callback state action: #{action}") if action.blank? + + call_setup(state_name, state_data) if action == "setup" + call_teardown(state_name, state_data) if action == "teardown" + + response.status = 200 + rescue JSON::ParserError => ex + logger.error("cannot parse request: #{ex.message}") + response.status = 500 + end + end + end + end +end diff --git a/lib/pact/provider/request.rb b/lib/pact/provider/request.rb deleted file mode 100644 index 145fb666..00000000 --- a/lib/pact/provider/request.rb +++ /dev/null @@ -1,90 +0,0 @@ -require 'json' -require 'pact/reification' -require 'pact/shared/null_expectation' -require 'pact/generators' - -module Pact - module Provider - module Request - class Replayable - - # See https://github.com/rack/rack/blob/e7d741c6282ca4cf4e01506f5681e6e6b14c0b32/SPEC#L87-89 - NO_HTTP_PREFIX = ["CONTENT-TYPE", "CONTENT-LENGTH"] - - def initialize expected_request, state_params = nil - @expected_request = expected_request - @state_params = state_params - end - - def method - expected_request.method - end - - def path - Pact::Generators.apply_generators(expected_request, "path", expected_request.full_path, @state_params) - end - - def body - case expected_request.body - when String then expected_request.body - when NullExpectation then '' - else - generated_body - end - end - - def headers - request_headers = {} - # https://github.com/pact-foundation/pact-ruby/pull/327 - request_headers.merge!('HOST' => 'localhost') if defined?(Sinatra) - return request_headers if expected_request.headers.is_a?(Pact::NullExpectation) - - expected_request.headers.each do |key, value| - request_headers[key] = Pact::Reification.from_term(value) - end - - request_headers = Pact::Generators.apply_generators(expected_request, "header", request_headers, @state_params) - request_headers.map{ |key,value| [rack_request_header_for(key), value]}.to_h - end - - private - - attr_reader :expected_request - - def reified_body - rb = Pact::Reification.from_term(expected_request.body) - if rb.is_a?(String) - rb - else - JSON.dump(rb) - end - end - - def generated_body - result = Pact::Generators.apply_generators(expected_request, "body", reified_body, @state_params) - - case result - when Hash - result.to_json - when String - result - else - raise "Expected body to be a String or Hash, but was #{result.class} with value #{result.inspect}" - end - end - - def rack_request_header_for header - with_http_prefix(header.to_s.upcase).tr('-', '_') - end - - def rack_request_value_for value - Array(value).join("\n") - end - - def with_http_prefix header - NO_HTTP_PREFIX.include?(header) ? header : "HTTP_#{header}" - end - end - end - end -end diff --git a/lib/pact/provider/rspec.rb b/lib/pact/provider/rspec.rb deleted file mode 100644 index b498def5..00000000 --- a/lib/pact/provider/rspec.rb +++ /dev/null @@ -1,234 +0,0 @@ -require 'open-uri' -require 'pact/consumer_contract' -require 'pact/provider/rspec/matchers' -require 'pact/provider/test_methods' -require 'pact/provider/configuration' -require 'pact/provider/matchers/messages' - - -module Pact - module Provider - module RSpec - - module InstanceMethods - def app - Pact.configuration.provider.app - end - end - - module ClassMethods - EMPTY_ARRAY = [].freeze - - include ::RSpec::Core::DSL - - def honour_pactfile pact_source, pact_json, options - pact_uri = pact_source.uri - Pact.configuration.output_stream.puts "INFO: Reading pact at #{pact_uri}" - consumer_contract = Pact::ConsumerContract.from_json(pact_json) - - suffix = pact_uri.metadata[:pending] ? " [PENDING]": "" - example_group_description = "Verifying a pact between #{consumer_contract.consumer.name} and #{consumer_contract.provider.name}#{suffix}" - example_group_metadata = { pactfile_uri: pact_uri, pact_criteria: options[:criteria] } - - ::RSpec.describe example_group_description, example_group_metadata do - honour_consumer_contract consumer_contract, options.merge( - pact_json: pact_json, - pact_uri: pact_uri, - pact_source: pact_source, - consumer_contract: consumer_contract, - criteria: options[:criteria] - ) - end - end - - def honour_consumer_contract consumer_contract, options = {} - describe_consumer_contract consumer_contract, options.merge(consumer: consumer_contract.consumer.name, pact_context: InteractionContext.new) - end - - private - - def describe_consumer_contract consumer_contract, options - consumer_interactions(consumer_contract, options).tap{ |interactions| - if interactions.empty? - # If there are no interactions, the documentation formatter never fires to print this out, - # so print it out here. - Pact.configuration.output_stream.puts "DEBUG: All interactions for #{options[:pact_uri]} have been filtered out by criteria: #{options[:criteria]}" if options[:criteria] && options[:criteria].any? - end - }.each do |interaction| - describe_interaction_with_provider_state interaction, options - end - end - - def consumer_interactions(consumer_contract, options) - if options[:criteria].nil? - consumer_contract.interactions - else - consumer_contract.find_interactions(options[:criteria]) - end - end - - def describe_interaction_with_provider_state interaction, options - if interaction.provider_state - describe "Given #{interaction.provider_state}" do - describe_interaction interaction, options - end - else - describe_interaction interaction, options - end - end - - def describe_interaction interaction, options - # pact_uri and pact_interaction are used by - # Pact::Provider::RSpec::PactBrokerFormatter - - # pact_interaction_example_description is used by - # Pact::Provider::RSpec::Formatter and Pact::Provider::RSpec::Formatter2 - - # pact: verify is used to allow RSpec before and after hooks. - metadata = { - pact: :verify, - pact_interaction: interaction, - pact_interaction_example_description: interaction_description_for_rerun_command(interaction), - pact_uri: options[:pact_uri], - pact_source: options[:pact_source], - pact_ignore_failures: options[:pact_source].pending? || options[:ignore_failures], - pact_consumer_contract: options[:consumer_contract] - } - - describe description_for(interaction), metadata do - - interaction_context = InteractionContext.new - pact_context = options[:pact_context] - - before do | example | - interaction_context.run_once :before do - Pact.configuration.logger.info "Running example '#{Pact::RSpec.full_description(example)}'" - provider_states_result = set_up_provider_states interaction.provider_states, options[:consumer] - state_params = provider_states_result[interaction.provider_state]; - replay_interaction interaction, options[:request_customizer], state_params - interaction_context.last_response = last_response - end - end - - after do - interaction_context.run_once :after do - tear_down_provider_states interaction.provider_states, options[:consumer] - end - end - - if interaction.respond_to?(:message?) && interaction.message? - describe_message Pact::Response.new(interaction.response), interaction_context - else - describe "with #{interaction.request.method_and_path}" do - describe_response Pact::Response.new(interaction.response), interaction_context - end - end - end - end - - def describe_message expected_response, interaction_context - include Pact::RSpec::Matchers - extend Pact::Matchers::Messages - - - let(:expected_contents) { expected_response.body[:contents].as_json } - let(:response) { interaction_context.last_response } - let(:differ) { Pact.configuration.body_differ_for_content_type diff_content_type } - let(:diff_formatter) { Pact.configuration.diff_formatter_for_content_type diff_content_type } - let(:diff_options) { { with: differ, diff_formatter: diff_formatter } } - let(:diff_content_type) { 'application/json' } - let(:response_body) { parse_body_from_response(response) } - let(:actual_contents) { response_body['contents'] } - - it "has matching content" do | example | - if response.status != 200 - raise "An error was raised while verifying the message. The response body is: #{response.body}" - end - set_metadata(example, :pact_actual_contents, actual_contents) - expect(actual_contents).to match_term expected_contents, diff_options, example - end - end - - def describe_response expected_response, interaction_context - - describe "returns a response which" do - - include Pact::RSpec::Matchers - extend Pact::Matchers::Messages - - let(:expected_response_status) { expected_response.status } - let(:expected_response_body) { expected_response.body } - let(:response) { interaction_context.last_response } - let(:response_status) { response.status } - let(:response_body) { parse_body_from_response(response) } - let(:differ) { Pact.configuration.body_differ_for_content_type diff_content_type } - let(:diff_formatter) { Pact.configuration.diff_formatter_for_content_type diff_content_type } - let(:expected_content_type) { Pact::Headers.new(expected_response.headers || {})['Content-Type'] } - let(:actual_content_type) { response.headers['Content-Type']} - let(:diff_content_type) { String === expected_content_type ? expected_content_type : actual_content_type } # expected_content_type may be a Regexp - let(:diff_options) { { with: differ, diff_formatter: diff_formatter } } - - if expected_response.status - it "has status code #{expected_response.status}" do | example | - set_metadata(example, :pact_actual_status, response_status) - expect(response_status).to eql expected_response_status - end - end - - if expected_response.headers - describe "includes headers" do - expected_response.headers.each do |name, expected_header_value| - it "\"#{name}\" which #{expected_desc_for_it(expected_header_value)}" do | example | - set_metadata(example, :pact_actual_headers, response.headers) - header_value = response.headers[name] - expect(header_value).to match_header(name, expected_header_value) - end - end - end - end - - if expected_response.body - it "has a matching body" do | example | - set_metadata(example, :pact_actual_body, response_body) - expect(response_body).to match_term expected_response_body, diff_options, example - end - end - end - end - - def description_for interaction - interaction.provider_state ? interaction.description : interaction.description.capitalize - end - - def interaction_description_for_rerun_command interaction - description_for(interaction).capitalize + ( interaction.provider_state ? " given #{interaction.provider_state}" : "") - end - end - - # The "arrange" and "act" parts of the test really only need to be run once, - # however, stubbing is not supported in before :all, so this is a - # wee hack to enable before :all like functionality using before :each. - # In an ideal world, the test setup and execution should be quick enough for - # the difference between :all and :each to be unnoticable, but the annoying - # reality is, sometimes it does make a difference. This is for you, V! - - class InteractionContext - - attr_accessor :last_response - - def initialize - @already_run = [] - end - - def run_once hook - unless @already_run.include?(hook) - yield - @already_run << hook - end - end - - end - end - end -end - diff --git a/lib/pact/provider/rspec/backtrace_formatter.rb b/lib/pact/provider/rspec/backtrace_formatter.rb deleted file mode 100644 index d3f3eb05..00000000 --- a/lib/pact/provider/rspec/backtrace_formatter.rb +++ /dev/null @@ -1,43 +0,0 @@ -module RSpec - module Core - - # RSpec 3 has a hardwired @system_exclusion_patterns which removes everything matching /bin\// - # This causes *all* the backtrace lines to be cleaned, as rake pact:verify now shells out - # to the executable `pact verify ...` - # which then causes *all* the lines to be included as the BacktraceFormatter will - # include all lines of the backtrace if all lines were filtered out. - # This monkey patch only shows lines including bin/pact and removes the - # "show all lines if no lines would otherwise be shown" logic. - - class BacktraceFormatter - - - def format_backtrace(backtrace, options = {}) - return backtrace if options[:full_backtrace] - backtrace.map { |l| backtrace_line(l) }.compact - end - - def backtrace_line(line) - relative_path(line) unless exclude?(line) - rescue SecurityError - nil - end - - def exclude?(line) - return false if @full_backtrace - relative_line = relative_path(line) - return true unless /bin\/pact/ =~ relative_line - end - - # Copied from Metadata so a refactor can't break this overridden class - def relative_path(line) - line = line.sub(File.expand_path("."), ".") - line = line.sub(/\A([^:]+:\d+)$/, '\\1') - return nil if line == '-e:1' - line - rescue SecurityError - nil - end - end - end -end diff --git a/lib/pact/provider/rspec/calculate_exit_code.rb b/lib/pact/provider/rspec/calculate_exit_code.rb deleted file mode 100644 index 472d2599..00000000 --- a/lib/pact/provider/rspec/calculate_exit_code.rb +++ /dev/null @@ -1,18 +0,0 @@ -module Pact - module Provider - module RSpec - module CalculateExitCode - def self.call(pact_sources, failed_examples) - any_non_pending_failures = pact_sources.any? do |pact_source| - if pact_source.pending? - nil - else - failed_examples.select { |e| e.metadata[:pact_source] == pact_source }.any? - end - end - any_non_pending_failures ? 1 : 0 - end - end - end - end -end diff --git a/lib/pact/provider/rspec/custom_options_file b/lib/pact/provider/rspec/custom_options_file deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/pact/provider/rspec/formatter_rspec_2.rb b/lib/pact/provider/rspec/formatter_rspec_2.rb deleted file mode 100644 index d0b824c4..00000000 --- a/lib/pact/provider/rspec/formatter_rspec_2.rb +++ /dev/null @@ -1,76 +0,0 @@ -require 'pact/provider/print_missing_provider_states' -require 'rspec/core/formatters/documentation_formatter' -require 'rainbow' -require 'pact/provider/help/prompt_text' - -module Pact - module Provider - module RSpec - class Formatter2 < ::RSpec::Core::Formatters::DocumentationFormatter - - class NilFormatter < ::RSpec::Core::Formatters::DocumentationFormatter - def dump_commands_to_rerun_failed_examples - end - end - - def dump_commands_to_rerun_failed_examples - return if failed_examples.empty? - - print_rerun_commands - print_failure_message - print_missing_provider_states - - end - - private - - def print_rerun_commands - output.puts("\n") - interaction_rerun_commands.each do | message | - output.puts(message) - end - end - - def print_missing_provider_states - if executing_with_ruby? - PrintMissingProviderStates.call Pact.provider_world.provider_states.missing_provider_states, output - end - end - - def interaction_rerun_commands - failed_examples.collect do |example| - interaction_rerun_command_for example - end.uniq - end - - def interaction_rerun_command_for example - example_description = example.metadata[:pact_interaction_example_description] - if ENV['PACT_INTERACTION_RERUN_COMMAND'] - cmd = String.new(ENV['PACT_INTERACTION_RERUN_COMMAND']) - provider_state = example.metadata[:pact_interaction].provider_state - description = example.metadata[:pact_interaction].description - pactfile_uri = example.metadata[:pactfile_uri] - cmd.gsub!("", pactfile_uri.to_s) - cmd.gsub!("", description) - cmd.gsub!("", "#{provider_state}") - failure_color(cmd) + " " + detail_color("# #{example_description}") - else - failure_color("* #{example_description}") - end - end - - def print_failure_message - output.puts(failure_message) if executing_with_ruby? - end - - def failure_message - "\n" + Pact::Provider::Help::PromptText.() + "\n" - end - - def executing_with_ruby? - ENV['PACT_EXECUTING_LANGUAGE'] == 'ruby' - end - end - end - end -end diff --git a/lib/pact/provider/rspec/formatter_rspec_3.rb b/lib/pact/provider/rspec/formatter_rspec_3.rb deleted file mode 100644 index 5eeaf481..00000000 --- a/lib/pact/provider/rspec/formatter_rspec_3.rb +++ /dev/null @@ -1,195 +0,0 @@ -require 'pact/provider/print_missing_provider_states' -require 'rspec/core/formatters' -require 'rainbow' -require 'pact/provider/help/prompt_text' - -module Pact - module Provider - module RSpec - class Formatter < ::RSpec::Core::Formatters::DocumentationFormatter - - class NilFormatter < ::RSpec::Core::Formatters::BaseFormatter - Pact::RSpec.with_rspec_3 do - ::RSpec::Core::Formatters.register self, :start, :example_group_started, :close - end - - def dump_summary(summary) - end - end - - Pact::RSpec.with_rspec_3 do - ::RSpec::Core::Formatters.register self, :example_group_started, :example_group_finished, - :example_passed, :example_pending, :example_failed - end - - def example_group_started(notification) - # This is the metadata on the top level "Verifying a pact between X and Y" describe block - if @group_level == 0 - Pact.configuration.output_stream.puts - pact_uri = notification.group.metadata[:pactfile_uri] - ::RSpec.configuration.failure_color = pact_uri.metadata[:pending] ? :yellow : :red - - if pact_uri.metadata[:notices] - pact_uri.metadata[:notices].before_verification_notices_text.each do | text | - Pact.configuration.output_stream.puts("DEBUG: #{text}") - end - end - - criteria = notification.group.metadata[:pact_criteria] - Pact.configuration.output_stream.puts "DEBUG: Filtering interactions by: #{criteria}" if criteria && criteria.any? - end - super - end - - - def dump_summary(summary) - output.puts "\n" + colorized_totals_line(summary) - return if summary.failure_count == 0 - print_rerun_commands summary - print_failure_message - print_missing_provider_states - end - - private - - def interactions_count(summary) - summary.examples.collect{ |e| interaction_unique_key(e) }.uniq.size - end - - def failed_interactions_count(summary) - failed_interaction_examples(summary).size - end - - def pending_interactions_count(summary) - pending_interaction_examples(summary).size - end - - def failure_title summary - ::RSpec::Core::Formatters::Helpers.pluralize(failed_interactions_count(summary), "failure") - end - - def totals_line summary - line = ::RSpec::Core::Formatters::Helpers.pluralize(interactions_count(summary), "interaction") - line << ", " << failure_title(summary) - pending_count = pending_interactions_count(summary) - line << ", " << "#{pending_count} pending" if pending_count > 0 - line - end - - def colorized_totals_line(summary) - colorizer.wrap(totals_line(summary), color_for_summary(summary)) - end - - def color_for_summary summary - summary.failure_count > 0 ? ::RSpec.configuration.failure_color : ::RSpec.configuration.success_color - end - - def print_rerun_commands summary - if pending_interactions_count(summary) > 0 - set_rspec_failure_color(:yellow) - output.puts("\nPending interactions: (Failures listed here are expected and do not affect your suite's status)\n\n") - interaction_rerun_commands(pending_interaction_examples(summary)).each do | message | - output.puts(message) - end - set_rspec_failure_color(:red) - end - - if failed_interactions_count(summary) > 0 - output.puts("\nFailed interactions:\n\n") - interaction_rerun_commands(failed_interaction_examples(summary)).each do | message | - output.puts(message) - end - end - end - - def print_missing_provider_states - if executing_with_ruby? - PrintMissingProviderStates.call Pact.provider_world.provider_states.missing_provider_states, output - end - end - - def pending_interaction_examples(summary) - one_failed_example_per_interaction(summary).select do | example | - example.metadata[:pactfile_uri].metadata[:pending] - end - end - - def failed_interaction_examples(summary) - one_failed_example_per_interaction(summary).select do | example | - !example.metadata[:pactfile_uri].metadata[:pending] - end - end - - def one_failed_example_per_interaction(summary) - summary.failed_examples.group_by{| e| interaction_unique_key(e)}.values.collect(&:first) - end - - def interaction_rerun_commands examples - examples.collect do |example| - interaction_rerun_command_for example - end.compact.uniq - end - - def interaction_unique_key(example) - # pending is just to make the counting easier, it isn't required for the unique key - { - pactfile_uri: example.metadata[:pactfile_uri], - index: example.metadata[:pact_interaction].index, - } - end - - def interaction_rerun_command_for example - example_description = example.metadata[:pact_interaction_example_description] - - _id = example.metadata[:pact_interaction]._id - index = example.metadata[:pact_interaction].index - provider_state = example.metadata[:pact_interaction].provider_state - description = example.metadata[:pact_interaction].description - pactfile_uri = example.metadata[:pactfile_uri] - - if _id && ENV['PACT_INTERACTION_RERUN_COMMAND_FOR_BROKER'] - cmd = String.new(ENV['PACT_INTERACTION_RERUN_COMMAND_FOR_BROKER']) - cmd.gsub!("", example.metadata[:pactfile_uri].to_s) - cmd.gsub!("", "#{_id}") - colorizer.wrap("#{cmd} ", ::RSpec.configuration.failure_color) + colorizer.wrap("# #{example_description}", ::RSpec.configuration.detail_color) - elsif ENV['PACT_INTERACTION_RERUN_COMMAND'] - cmd = String.new(ENV['PACT_INTERACTION_RERUN_COMMAND']) - cmd.gsub!("", pactfile_uri.to_s) - cmd.gsub!("", description) - cmd.gsub!("", "#{provider_state}") - cmd.gsub!("", "#{index}") - colorizer.wrap("#{cmd} ", ::RSpec.configuration.failure_color) + colorizer.wrap("# #{example_description}", ::RSpec.configuration.detail_color) - else - message = if _id - "* #{example_description} (to re-run just this interaction, set environment variable PACT_BROKER_INTERACTION_ID=\"#{_id}\")" - else - "* #{example_description} (to re-run just this interaction, set environment variables PACT_DESCRIPTION=\"#{description}\" PACT_PROVIDER_STATE=\"#{provider_state}\")" - end - colorizer.wrap(message, ::RSpec.configuration.failure_color) - end - end - - def print_failure_message - output.puts(failure_message) if executing_with_ruby? - end - - def failure_message - "\n" + Pact::Provider::Help::PromptText.() + "\n" - end - - def colorizer - @colorizer ||= ::RSpec::Core::Formatters::ConsoleCodes - end - - def executing_with_ruby? - ENV['PACT_EXECUTING_LANGUAGE'] == 'ruby' - end - - def set_rspec_failure_color color - ::RSpec.configuration.failure_color = color - end - end - end - end -end - diff --git a/lib/pact/provider/rspec/json_formatter.rb b/lib/pact/provider/rspec/json_formatter.rb deleted file mode 100644 index e774cfdc..00000000 --- a/lib/pact/provider/rspec/json_formatter.rb +++ /dev/null @@ -1,100 +0,0 @@ -require 'rspec/core/formatters/json_formatter' - -module Pact - module Provider - module RSpec - class JsonFormatter < ::RSpec::Core::Formatters::JsonFormatter - ::RSpec::Core::Formatters.register self, :message, :dump_summary, :dump_profile, :stop, :seed, :close - - def dump_summary(summary) - super(create_custom_summary(summary)) - output_hash[:summary][:pacts] = pacts(summary) - end - - def format_example(example) - { - :id => example.id, - :interaction_index => example.metadata[:pact_interaction].index, - :description => example.description, - :full_description => example.full_description, - :status => calculate_status(example), - :file_path => example.metadata[:file_path], - :line_number => example.metadata[:line_number], - :run_time => example.execution_result.run_time, - :mismatches => extract_differences(example), - :pact_url => example.metadata[:pact_uri].uri - } - end - - def stop(notification) - output_hash[:examples] = notification.examples.map do |example| - format_example(example).tap do |hash| - e = example.exception - if e - hash[:exception] = { - class: e.class.name, - message: e.message, - } - # No point providing a backtrace for a mismatch, too much noise - if !e.is_a?(::RSpec::Expectations::ExpectationNotMetError) - hash[:exception][:backtrace] - end - end - end - end - end - - def calculate_status(example) - if example.execution_result.status == :failed && example.metadata[:pact_ignore_failures] - 'pending' - else - example.execution_result.status.to_s - end - end - - # There will most likely be only one pact associated with this RSpec execution, because - # the most likely user of this formatter is the Go implementation that parses the JSON - # and builds Go tests from them. - # If the JSON formatter is used by someone else and they have multiple pacts, all the notices - # for the pacts will be mushed together in one collection, so it will be hard to know which notice - # belongs to which pact. - def pacts(summary) - unique_pact_metadatas(summary).collect do | example_metadata | - pact_uri = example_metadata[:pact_uri] - notices = (pact_uri.metadata[:notices] && pact_uri.metadata[:notices].before_verification_notices) || [] - { - notices: notices, - url: pact_uri.uri, - consumer_name: example_metadata[:pact_consumer_contract].consumer.name, - provider_name: example_metadata[:pact_consumer_contract].provider.name, - short_description: pact_uri.metadata[:short_description] - } - end - end - - def unique_pact_metadatas(summary) - summary.examples.collect(&:metadata).group_by{ | metadata | metadata[:pact_uri].uri }.values.collect(&:first) - end - - def create_custom_summary(summary) - ::RSpec::Core::Notifications::SummaryNotification.new( - summary.duration, - summary.examples, - summary.examples.select{ | example | example.execution_result.status == :failed && !example.metadata[:pact_ignore_failures] }, - summary.examples.select{ | example | example.execution_result.status == :failed && example.metadata[:pact_ignore_failures] }, - summary.load_time, - summary.errors_outside_of_examples_count - ) - end - - def extract_differences(example) - if example.metadata[:pact_diff] - Pact::Matchers::ExtractDiffMessages.call(example.metadata[:pact_diff]).to_a - else - [] - end - end - end - end - end -end diff --git a/lib/pact/provider/rspec/matchers.rb b/lib/pact/provider/rspec/matchers.rb deleted file mode 100644 index 6b082693..00000000 --- a/lib/pact/provider/rspec/matchers.rb +++ /dev/null @@ -1,80 +0,0 @@ -require 'rspec' -require 'pact/matchers' -require 'pact/provider/matchers/messages' -require 'pact/rspec' -require 'pact/shared/json_differ' - -module Pact - module RSpec - module Matchers - module RSpec2Delegator - # For backwards compatibility with rspec-2 - def method_missing(method, *args, &block) - if method_name == :failure_message_for_should - failure_message method, *args, &block - else - super - end - end - end - - class MatchTerm - include Pact::Matchers::Messages - include RSpec2Delegator - - def initialize expected, differ, diff_formatter, example - @expected = expected - @differ = differ - @diff_formatter = diff_formatter - @example = example - end - - def matches? actual - @actual = actual - @difference = @differ.call(@expected, @actual) - unless @difference.empty? - Pact::RSpec.with_rspec_3 do - @example.metadata[:pact_diff] = @difference - end - Pact::RSpec.with_rspec_2 do - @example.example.metadata[:pact_diff] = @difference - end - end - @difference.empty? - end - - def failure_message - match_term_failure_message @difference, @actual, @diff_formatter, Pact::RSpec.color_enabled? - end - end - - def match_term expected, options, example - MatchTerm.new(expected, options.fetch(:with), options.fetch(:diff_formatter), example) - end - - class MatchHeader - include Pact::Matchers - include Pact::Matchers::Messages - include RSpec2Delegator - - def initialize header_name, expected - @header_name = header_name - @expected = expected - end - - def matches? actual - @actual = actual - diff(@expected, @actual).empty? - end - - def failure_message - match_header_failure_message @header_name, @expected, @actual - end - end - - def match_header header_name, expected - MatchHeader.new(header_name, expected) - end - end - end -end diff --git a/lib/pact/provider/rspec/pact_broker_formatter.rb b/lib/pact/provider/rspec/pact_broker_formatter.rb deleted file mode 100644 index dd29778e..00000000 --- a/lib/pact/provider/rspec/pact_broker_formatter.rb +++ /dev/null @@ -1,76 +0,0 @@ -require 'rspec/core/formatters' -require 'pact/provider/verification_results/publish_all' -require 'rainbow' -require 'pact/matchers/extract_diff_messages' - -module Pact - module Provider - module RSpec - class PactBrokerFormatter < ::RSpec::Core::Formatters::BaseFormatter - Pact::RSpec.with_rspec_3 do - ::RSpec::Core::Formatters.register self, :stop, :close - end - - attr_reader :output_hash - - def initialize(output) - super - @output_hash = {} - end - - def stop(notification) - @output_hash[:tests] = notification - .examples - .map { |example| format_example(example) } - end - - def close(_notification) - Pact::Provider::VerificationResults::PublishAll.call(Pact.provider_world.pact_sources, output_hash, { verbose: Pact.provider_world.verbose }) - end - - private - - def format_example(example) - { - testDescription: example.description, - testFullDescription: example.full_description, - status: example.execution_result.status.to_s, - interactionProviderState: example.metadata[:pact_interaction].provider_state, - interactionDescription: example.metadata[:pact_interaction].description, - pact_uri: example.metadata[:pact_uri], - pact_interaction: example.metadata[:pact_interaction] - }.tap do |hash| - if example.exception - hash[:exception] = { - class: example.exception.class.name, - message: "\e[0m#{example.exception.message}" - } - end - - if example.metadata[:pact_actual_status] - hash[:actualStatus] = example.metadata[:pact_actual_status] - end - - if example.metadata[:pact_actual_headers] - hash[:actualHeaders] = example.metadata[:pact_actual_headers] - end - - if example.metadata[:pact_actual_body] - hash[:actualBody] = example.metadata[:pact_actual_body] - end - - if example.metadata[:pact_actual_contents] - hash[:actualContents] = example.metadata[:pact_actual_contents] - end - - if example.metadata[:pact_diff] - hash[:differences] = Pact::Matchers::ExtractDiffMessages.call(example.metadata[:pact_diff]) - .to_a - .collect{ | description | { description: description } } - end - end - end - end - end - end -end diff --git a/lib/pact/provider/state/provider_state.rb b/lib/pact/provider/state/provider_state.rb deleted file mode 100644 index 92b4b1ce..00000000 --- a/lib/pact/provider/state/provider_state.rb +++ /dev/null @@ -1,180 +0,0 @@ -require 'pact/shared/dsl' -require 'pact/provider/state/provider_state_configured_modules' - -module Pact - module Provider::State - - BASE_PROVIDER_STATE_NAME = "__base_provider_state__" - - module DSL - def provider_state name, &block - ProviderStates.provider_state(name, &block).register - end - - def set_up &block - ProviderStates.base_provider_state.register.register_set_up(&block) - end - - def tear_down &block - ProviderStates.base_provider_state.register_tear_down(&block) - end - - def provider_states_for name, &block - ProviderStates.current_namespaces << name - instance_eval(&block) - ProviderStates.current_namespaces.pop - end - end - - class ProviderStates - def self.provider_state name, &block - ProviderState.build(name, current_namespaces.join('.'), &block) - end - - def self.base_provider_state - fullname = namespaced_name BASE_PROVIDER_STATE_NAME, {:for => current_namespaces.first } - provider_states[fullname] ||= ProviderState.new(BASE_PROVIDER_STATE_NAME, current_namespaces.join('.')) - provider_states[fullname] - end - - def self.register name, provider_state - provider_states[name] = provider_state - end - - def self.provider_states - @@provider_states ||= {} - end - - def self.current_namespaces - @@current_namespaces ||= [] - end - - def self.get name, options = {} - fullname = namespaced_name name, options - (provider_states[fullname] || provider_states[fullname.to_sym] || provider_states[name]) - end - - def self.get_base opts = {} - fullname = namespaced_name BASE_PROVIDER_STATE_NAME, opts - provider_states[fullname] || NoOpProviderState - end - - def self.namespaced_name name, options = {} - options[:for] ? "#{options[:for]}.#{name}" : name - end - end - - class ProviderState - - attr_accessor :name - attr_accessor :namespace - - extend Pact::DSL - - def initialize name, namespace, &block - @name = name - @namespace = namespace - @set_up_defined = false - @tear_down_defined = false - @no_op_defined = false - end - - dsl do - def set_up &block - self.register_set_up(&block) - end - - def tear_down &block - self.register_tear_down(&block) - end - - def no_op - self.register_no_op - end - end - - def register - ProviderStates.register(namespaced(name), self) - self - end - - def finalize - validate - end - - def register_set_up &block - @set_up_block = block - @set_up_defined = true - end - - def register_tear_down &block - @tear_down_block = block - @tear_down_defined = true - end - - def register_no_op - @no_op_defined = true - end - - def set_up params = {} - if @set_up_block - include_provider_state_configured_modules - instance_exec params, &@set_up_block - end - end - - def tear_down params = {} - if @tear_down_block - include_provider_state_configured_modules - instance_exec params, &@tear_down_block - end - end - - private - - attr_accessor :no_op_defined, :set_up_defined, :tear_down_defined - - def validate - if no_op_defined && set_up_defined - raise error_message_for_extra_block 'set_up' - elsif no_op_defined && tear_down_defined - raise error_message_for_extra_block 'tear_down' - elsif !(no_op_defined || set_up_defined || tear_down_defined) - raise "Please provide a set_up or tear_down block for provider state \"#{name}\". If there is no data to set up or tear down, you can use \"no_op\" instead." - end - end - - def error_message_for_extra_block block_name - "Provider state \"#{name}\" has been defined as a no_op but it also has a #{block_name} block. Please remove one or the other." - end - - def namespaced(name) - if namespace.empty? - name - else - "#{namespace}.#{name}" - end - end - - def include_provider_state_configured_modules - # Doing this at runtime means the order of the Pact configuration block - # and the provider state declarations doesn't matter. - # Using include ProviderStateConfiguredModules on the class doesn't seem to work - - # modules dynamically added to ProviderStateConfiguredModules don't seem to be - # included in the including class. - self.extend(ProviderStateConfiguredModules) unless self.singleton_class.ancestors.include?(ProviderStateConfiguredModules) - end - end - - class NoOpProviderState - - def self.set_up params = {} - - end - - def self.tear_down params = {} - - end - end - end -end diff --git a/lib/pact/provider/state/provider_state_configured_modules.rb b/lib/pact/provider/state/provider_state_configured_modules.rb deleted file mode 100644 index 5295cb96..00000000 --- a/lib/pact/provider/state/provider_state_configured_modules.rb +++ /dev/null @@ -1,15 +0,0 @@ -require 'rspec/mocks' - -module Pact - module Provider - module State - module ProviderStateConfiguredModules - - include ::RSpec::Mocks::ExampleMethods - - # Placeholder for modules configured using config.include - - end - end - end -end diff --git a/lib/pact/provider/state/provider_state_manager.rb b/lib/pact/provider/state/provider_state_manager.rb deleted file mode 100644 index 22b97c0b..00000000 --- a/lib/pact/provider/state/provider_state_manager.rb +++ /dev/null @@ -1,42 +0,0 @@ -module Pact - module Provider::State - class ProviderStateManager - - attr_reader :provider_state_name, :params, :consumer - - def initialize provider_state_name, params, consumer - @provider_state_name = provider_state_name - @params = params - @consumer = consumer - end - - def set_up_provider_state - get_global_base_provider_state.set_up(params) - get_consumer_base_provider_state.set_up(params) - if provider_state_name - get_provider_state.set_up(params) - end - end - - def tear_down_provider_state - if provider_state_name - get_provider_state.tear_down(params) - end - get_consumer_base_provider_state.tear_down(params) - get_global_base_provider_state.tear_down(params) - end - - def get_provider_state - Pact.provider_world.provider_states.get(provider_state_name, :for => consumer) - end - - def get_consumer_base_provider_state - Pact.provider_world.provider_states.get_base(:for => consumer) - end - - def get_global_base_provider_state - Pact.provider_world.provider_states.get_base - end - end - end -end diff --git a/lib/pact/provider/state/provider_state_proxy.rb b/lib/pact/provider/state/provider_state_proxy.rb deleted file mode 100644 index ced08b4b..00000000 --- a/lib/pact/provider/state/provider_state_proxy.rb +++ /dev/null @@ -1,39 +0,0 @@ -module Pact - module Provider::State - class ProviderStateProxy - - attr_reader :missing_provider_states - - def initialize - @missing_provider_states = {} - end - - def get name, options = {} - unless provider_state = ProviderStates.get(name, options) - register_missing_provider_state name, options[:for] - raise error_message name, options[:for] - end - provider_state - end - - def get_base options = {} - ProviderStates.get_base options - end - - private - - def error_message name, consumer - "Could not find provider state \"#{name}\" for consumer #{consumer}" - end - - def register_missing_provider_state name, consumer - missing_states_for(consumer) << name unless missing_states_for(consumer).include?(name) - end - - def missing_states_for consumer - @missing_provider_states[consumer] ||= [] - end - - end - end -end diff --git a/lib/pact/provider/state/set_up.rb b/lib/pact/provider/state/set_up.rb deleted file mode 100644 index 4d684961..00000000 --- a/lib/pact/provider/state/set_up.rb +++ /dev/null @@ -1,13 +0,0 @@ -require 'pact/provider/state/provider_state_manager' - -module Pact - module Provider - module State - class SetUp - def self.call provider_state_name, consumer, options = {} - State::ProviderStateManager.new(provider_state_name, options[:params], consumer).set_up_provider_state - end - end - end - end -end diff --git a/lib/pact/provider/state/tear_down.rb b/lib/pact/provider/state/tear_down.rb deleted file mode 100644 index 143e8360..00000000 --- a/lib/pact/provider/state/tear_down.rb +++ /dev/null @@ -1,13 +0,0 @@ -require 'pact/provider/state/provider_state_manager' - -module Pact - module Provider - module State - class TearDown - def self.call provider_state_name, consumer, options = {} - State::ProviderStateManager.new(provider_state_name, options[:params], consumer).tear_down_provider_state - end - end - end - end -end diff --git a/lib/pact/provider/test_methods.rb b/lib/pact/provider/test_methods.rb deleted file mode 100644 index 1619633d..00000000 --- a/lib/pact/provider/test_methods.rb +++ /dev/null @@ -1,77 +0,0 @@ -require 'pact/logging' -require 'rack/test' -require 'pact/consumer_contract/interaction' -require 'pact/provider/state/provider_state' -require 'pact/provider/state/provider_state_proxy' -require 'pact/provider/request' -require 'pact/provider/world' -require 'pact/provider/state/provider_state_manager' - -module Pact - module Provider - module TestMethods - - include Pact::Logging - include Rack::Test::Methods - - def replay_interaction interaction, request_customizer = nil, state_params = nil - request = Request::Replayable.new(interaction.request, state_params) - request = request_customizer.call(request, interaction) if request_customizer - args = [request.path, request.body, request.headers] - - logger.info "Sending #{request.method.upcase} request to path: \"#{request.path}\" with headers: #{request.headers}, see debug logs for body" - logger.debug "body :#{request.body}" - response = if self.respond_to?(:custom_request) - self.custom_request(request.method.upcase, *args) - else - self.send(request.method.downcase, *args) - end - logger.info "Received response with status: #{response.status}, headers: #{response.headers}, see debug logs for body" - logger.debug "body: #{response.body}" - end - - def parse_body_from_response rack_response - case rack_response.headers['Content-Type'] - when /json/ - # For https://github.com/pact-foundation/pact-net/issues/237 - # Only required for the pact-ruby-standalone ¯\_(ツ)_/¯ - JSON.load("[#{rack_response.body}]").first - else - rack_response.body - end - end - - def set_up_provider_states provider_states, consumer, options = {} - provider_states_result = {}; - # If there are no provider state, execute with an nil state to ensure global and base states are executed - Pact.configuration.provider_state_set_up.call(nil, consumer, options) if provider_states.nil? || provider_states.empty? - provider_states.each do | provider_state | - result = Pact.configuration.provider_state_set_up.call(provider_state.name, consumer, options.merge(params: provider_state.params)) - if result.is_a?(Hash) - provider_states_result[provider_state.name] = result - end - end - - provider_states_result - end - - def tear_down_provider_states provider_states, consumer, options = {} - # If there are no provider state, execute with an nil state to ensure global and base states are executed - Pact.configuration.provider_state_tear_down.call(nil, consumer, options) if provider_states.nil? || provider_states.empty? - provider_states.reverse_each do | provider_state | - Pact.configuration.provider_state_tear_down.call(provider_state.name, consumer, options.merge(params: provider_state.params)) - end - end - - def set_metadata example, key, value - Pact::RSpec.with_rspec_3 do - example.metadata[key] = value - end - - Pact::RSpec.with_rspec_2 do - example.example.metadata[key] = value - end - end - end - end -end diff --git a/lib/pact/provider/verification_report.rb b/lib/pact/provider/verification_report.rb deleted file mode 100644 index 7692d683..00000000 --- a/lib/pact/provider/verification_report.rb +++ /dev/null @@ -1,36 +0,0 @@ -require 'pact/consumer_contract' - -module Pact::Provider - class VerificationReport - - include Pact::FileName - - def initialize (options) - @consumer = options[:consumer] - @provider = options[:provider] - @result = options[:result] - @output = options[:output] - end - - def to_hash - { - :consumer => @consumer, - :provider => @provider, - :result => @result, - :output => @output - } - end - - def as_json options = {} - to_hash - end - - def to_json(options = {}) - as_json.to_json(options) - end - - def report_file_name - file_name("#{@consumer[:name]}_#{@consumer[:ref]}", "#{@provider[:name]}_#{@provider[:ref]}") - end - end -end diff --git a/lib/pact/provider/verification_results/create.rb b/lib/pact/provider/verification_results/create.rb deleted file mode 100644 index 44362172..00000000 --- a/lib/pact/provider/verification_results/create.rb +++ /dev/null @@ -1,88 +0,0 @@ -require 'pact/provider/verification_results/verification_result' -module Pact - module Provider - module VerificationResults - class Create - - def self.call pact_source, test_results_hash - new(pact_source, test_results_hash).call - end - - def initialize pact_source, test_results_hash - @pact_source = pact_source - @test_results_hash = test_results_hash - end - - def call - VerificationResult.new( - publishable?, - !any_failures?, - Pact.configuration.provider.application_version, - test_results_hash_for_pact_uri, - Pact.configuration.provider.build_url - ) - end - - private - - def pact_uri - @pact_uri ||= pact_source.uri - end - - def any_failures? - count_failures_for_pact_uri > 0 - end - - def publishable? - if defined?(@publishable) - @publishable - else - @publishable = pact_source.consumer_contract.interactions.all? do | interaction | - examples_for_pact_uri.any?{ |e| example_is_for_interaction?(e, interaction) } - end && examples_for_pact_uri.count > 0 - end - end - - def example_is_for_interaction?(example, interaction) - # Use the Pact Broker id if supported - if interaction._id - example[:pact_interaction]._id == interaction._id - else - # fall back to object equality (based on the field values of the interaction) - example[:pact_interaction] == interaction - end - end - - def examples_for_pact_uri - @examples_for_pact_uri ||= test_results_hash[:tests].select{ |e| e[:pact_uri] == pact_uri } - end - - def count_failures_for_pact_uri - examples_for_pact_uri.count{ |e| e[:status] != 'passed' } - end - - def test_results_hash_for_pact_uri - { - tests: examples_for_pact_uri.collect{ |e| clean_example(e) }, - summary: { - testCount: examples_for_pact_uri.size, - failureCount: count_failures_for_pact_uri - }, - metadata: { - warning: "These test results use a beta format. Do not rely on it, as it will definitely change.", - pactVerificationResultsSpecification: { - version: "1.0.0-beta.1" - } - } - } - end - - def clean_example(example) - example.reject{ |k, v| k == :pact_uri || k == :pact_interaction } - end - - attr_reader :pact_source, :test_results_hash - end - end - end -end diff --git a/lib/pact/provider/verification_results/publish.rb b/lib/pact/provider/verification_results/publish.rb deleted file mode 100644 index 671131a8..00000000 --- a/lib/pact/provider/verification_results/publish.rb +++ /dev/null @@ -1,143 +0,0 @@ -require 'json' -require 'pact/errors' -require 'pact/retry' -require 'pact/hal/entity' -require 'pact/hal/http_client' - -# TODO move this to the pact broker client - -module Pact - module Provider - module VerificationResults - class PublicationError < Pact::Error; end - - class Publish - - PUBLISH_RELATION = 'pb:publish-verification-results'.freeze - PROVIDER_RELATION = 'pb:provider'.freeze - VERSION_TAG_RELATION = 'pb:version-tag'.freeze - BRANCH_VERSION_RELATION = 'pb:branch-version'.freeze - - def self.call pact_source, verification_result, options = {} - new(pact_source, verification_result, options).call - end - - def initialize pact_source, verification_result, options = {} - @pact_source = pact_source - @verification_result = verification_result - http_client_options = pact_source.uri.options.reject{ |k, v| ![:username, :password, :token].include?(k) } - @http_client = Pact::Hal::HttpClient.new(http_client_options.merge(verbose: options[:verbose])) - @pact_entity = Pact::Hal::Entity.new(pact_source.uri, pact_source.pact_hash, http_client) - end - - def call - if can_publish_verification_results? - create_branch_version_if_configured - tag_versions_if_configured - publish_verification_results - true - else - false - end - end - - private - attr_reader :pact_source, :verification_result, :pact_entity, :http_client - - def can_publish_verification_results? - return false unless Pact.configuration.provider.publish_verification_results? - - if !pact_entity.can?(PUBLISH_RELATION) - Pact.configuration.error_stream.puts "WARN: Cannot publish verification for #{consumer_name} as there is no link named pb:publish-verification-results in the pact JSON. If you are using a pact broker, please upgrade to version 2.0.0 or later." - return false - end - - if !verification_result.publishable? - Pact.configuration.error_stream.puts "WARN: Cannot publish verification for #{consumer_name} as not all interactions have been verified. Re-run the verification without the filter parameters or environment variables to publish the verification." - return false - end - true - end - - def hacky_tag_url provider_entity - hacky_tag_url = provider_entity._link('self').href + "/versions/{version}/tags/{tag}" - Pact::Hal::Link.new('href' => hacky_tag_url) - end - - def tag_versions_if_configured - if Pact.configuration.provider.tags.any? - if pact_entity.can?(PROVIDER_RELATION) - tag_versions - else - Pact.configuration.error_stream.puts "WARN: Could not tag provider version as the pb:provider link cannot be found" - end - end - end - - def create_branch_version_if_configured - if Pact.configuration.provider.branch - branch_version_link = provider_entity._link(BRANCH_VERSION_RELATION) - if branch_version_link - version_number = Pact.configuration.provider.application_version - branch = Pact.configuration.provider.branch - - Pact.configuration.output_stream.puts "INFO: Creating #{provider_name} version #{version_number} with branch \"#{branch}\"" - branch_entity = branch_version_link.expand( - version: version_number, - branch: branch - ).put - unless branch_entity.success? - raise PublicationError.new("Error returned from tagging request: status=#{branch_entity.response.code} body=#{branch_entity.response.body}") - end - else - raise PublicationError.new("This version of the Pact Broker does not support version branches. Please update to version 2.58.0 or later.") - end - end - end - - def tag_versions - tag_link = provider_entity._link(VERSION_TAG_RELATION) || hacky_tag_url(provider_entity) - provider_application_version = Pact.configuration.provider.application_version - - Pact.configuration.provider.tags.each do | tag | - Pact.configuration.output_stream.puts "INFO: Tagging version #{provider_application_version} of #{provider_name} as #{tag.inspect}" - tag_entity = tag_link.expand(version: provider_application_version, tag: tag).put - unless tag_entity.success? - raise PublicationError.new("Error returned from tagging request: status=#{tag_entity.response.code} body=#{tag_entity.response.body}") - end - end - end - - def publish_verification_results - verification_entity = nil - begin - # The verifications resource didn't have the content_types_provided set correctly, so publishing fails if we don't have */* - verification_entity = pact_entity.post(PUBLISH_RELATION, verification_result, { "Accept" => "application/hal+json, */*" }) - rescue StandardError => e - error_message = "Failed to publish verification results due to: #{e.class} #{e.message} #{e.backtrace.join("\n")}" - raise PublicationError.new(error_message) - end - - if verification_entity.success? - new_resource_url = verification_entity._link('self').href - Pact.configuration.output_stream.puts "INFO: Verification results published to #{new_resource_url}" - else - raise PublicationError.new("Error returned from verification results publication #{verification_entity.response.code} #{verification_entity.response.body}") - end - end - - def consumer_name - pact_source.pact_hash['consumer']['name'] - end - - def provider_name - pact_source.pact_hash['provider']['name'] - end - - def provider_entity - @provider_entity ||= pact_entity.get!(PROVIDER_RELATION) - end - end - end - end -end diff --git a/lib/pact/provider/verification_results/publish_all.rb b/lib/pact/provider/verification_results/publish_all.rb deleted file mode 100644 index 94faab99..00000000 --- a/lib/pact/provider/verification_results/publish_all.rb +++ /dev/null @@ -1,50 +0,0 @@ -require'pact/provider/verification_results/create' -require'pact/provider/verification_results/publish' - -module Pact - module Provider - module VerificationResults - class PublishAll - - def self.call pact_sources, test_results_hash, options = {} - new(pact_sources, test_results_hash, options).call - end - - def initialize pact_sources, test_results_hash, options = {} - @pact_sources = pact_sources - @test_results_hash = test_results_hash - @options = options - end - - def call - verification_results.collect do | (pact_source, verification_result) | - published = false - begin - published = Publish.call(pact_source, verification_result, { verbose: options[:verbose] }) - ensure - print_after_verification_notices(pact_source, verification_result, published) - end - end - end - - private - - def verification_results - pact_sources.collect do | pact_source | - [pact_source, Create.call(pact_source, test_results_hash)] - end - end - - def print_after_verification_notices(pact_source, verification_result, published) - if pact_source.uri.metadata[:notices] - pact_source.uri.metadata[:notices].after_verification_notices_text(verification_result.success, published).each do | text | - Pact.configuration.output_stream.puts "DEBUG: #{text}" - end - end - end - - attr_reader :pact_sources, :test_results_hash, :options - end - end - end -end diff --git a/lib/pact/provider/verification_results/verification_result.rb b/lib/pact/provider/verification_results/verification_result.rb deleted file mode 100644 index 3f34df5e..00000000 --- a/lib/pact/provider/verification_results/verification_result.rb +++ /dev/null @@ -1,40 +0,0 @@ -require 'json' - -module Pact - module Provider - module VerificationResults - class VerificationResult - attr_reader :success, :provider_application_version, :test_results_hash - - def initialize publishable, success, provider_application_version, test_results_hash, build_url - @publishable = publishable - @success = success - @provider_application_version = provider_application_version - @test_results_hash = test_results_hash - @build_url = build_url - end - - def publishable? - @publishable - end - - def provider_application_version_set? - !!provider_application_version - end - - def to_json(options = {}) - { - success: success, - providerApplicationVersion: provider_application_version, - testResults: test_results_hash, - buildUrl: @build_url - }.to_json(options) - end - - def to_s - "[success: #{success}, providerApplicationVersion: #{provider_application_version}]" - end - end - end - end -end diff --git a/lib/pact/provider/world.rb b/lib/pact/provider/world.rb deleted file mode 100644 index f6168d36..00000000 --- a/lib/pact/provider/world.rb +++ /dev/null @@ -1,50 +0,0 @@ -require 'pact/provider/state/provider_state_proxy' - -module Pact - - def self.provider_world - @world ||= Pact::Provider::World.new - end - - # internal api, for testing only - def self.clear_provider_world - @world = nil - end - - module Provider - class World - - attr_accessor :pact_sources, :failed_examples, :verbose - - def provider_states - @provider_states_proxy ||= Pact::Provider::State::ProviderStateProxy.new - end - - def add_pact_verification verification - pact_verifications << verification - end - - def pact_verifications - @pact_verifications ||= [] - end - - def pact_urls - (pact_verifications.collect(&:uri) + pact_uris_from_pact_uri_sources).compact - end - - def add_pact_uri_source pact_uri_source - pact_uri_sources << pact_uri_source - end - - private - - def pact_uri_sources - @pact_uri_sources ||= [] - end - - def pact_uris_from_pact_uri_sources - pact_uri_sources.collect(&:call).flatten - end - end - end -end diff --git a/lib/pact/railtie.rb b/lib/pact/railtie.rb new file mode 100644 index 00000000..a757eb7b --- /dev/null +++ b/lib/pact/railtie.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'rails/railtie' + +module Pact + class Railtie < Rails::Railtie + rake_tasks do + load 'pact/tasks/pact.rake' + end + end +end diff --git a/lib/pact/retry.rb b/lib/pact/retry.rb deleted file mode 100644 index 242e5af3..00000000 --- a/lib/pact/retry.rb +++ /dev/null @@ -1,37 +0,0 @@ -require 'pact/errors' - -module Pact - class Retry - class RescuableError - UNRESCUEABLE = [Pact::Error] - - def self.===(e) - case e - when *UNRESCUEABLE then - false - else - true - end - end - end - - def self.until_true options = {} - max_tries = options.fetch(:times, 3) - tries = 0 - while true - begin - return yield - rescue RescuableError => e - tries += 1 - $stderr.puts "Error making request - #{e.class} #{e.message} #{e.backtrace.find {|l| l.include?('pact_provider')}}, attempt #{tries} of #{max_tries}" - raise e if max_tries == tries - sleep options - end - end - end - - def self.sleep options - Kernel.sleep options.fetch(:sleep, 5) - end - end -end \ No newline at end of file diff --git a/lib/pact/v2/rspec.rb b/lib/pact/rspec.rb similarity index 68% rename from lib/pact/v2/rspec.rb rename to lib/pact/rspec.rb index 8a8bb647..8144a0bc 100644 --- a/lib/pact/v2/rspec.rb +++ b/lib/pact/rspec.rb @@ -1,17 +1,18 @@ # frozen_string_literal: true -require "rspec" -require_relative "rspec/support/pact_consumer_helpers" -require_relative "rspec/support/pact_provider_helpers" +require 'rspec' +require 'pact' +require_relative 'rspec/support/pact_consumer_helpers' +require_relative 'rspec/support/pact_provider_helpers' RSpec.configure do |config| config.define_derived_metadata(file_path: %r{spec/pact/}) { |metadata| metadata[:pact] = true } - # it's not an error: consumer tests contain `providers` subdirectory (because we're testing against different providers) + # it's not an error: consumer tests contain `providers` subdirectory (because we're testing against different providers) # rubocop:disable Layout/LineLength config.define_derived_metadata(file_path: %r{spec/pact/providers/}) { |metadata| metadata[:pact_entity] = :consumer } # for provider tests it's the same thing: we're running tests which test consumers config.define_derived_metadata(file_path: %r{spec/pact/consumers/}) { |metadata| metadata[:pact_entity] = :provider } # exclude pact specs from generic rspec pipeline - config.filter_run_excluding :pact_v2 + config.filter_run_excluding :pact end diff --git a/lib/pact/v2/rspec/support/pact_consumer_helpers.rb b/lib/pact/rspec/support/pact_consumer_helpers.rb similarity index 83% rename from lib/pact/v2/rspec/support/pact_consumer_helpers.rb rename to lib/pact/rspec/support/pact_consumer_helpers.rb index 64f66be3..772cffbe 100644 --- a/lib/pact/v2/rspec/support/pact_consumer_helpers.rb +++ b/lib/pact/rspec/support/pact_consumer_helpers.rb @@ -3,9 +3,9 @@ require_relative "pact_message_helpers" require "json" -module PactV2ConsumerDsl - include Pact::V2::Matchers - include Pact::V2::Generators +module PactConsumerDsl + include Pact::Matchers + include Pact::Generators module ClassMethods def has_http_pact_between(consumer, provider, opts: {}) @@ -39,7 +39,7 @@ def _has_pact_between(transport_type, consumer, provider, opts: {}) # rubocop:disable RSpec/BeforeAfterAll before(:context) do - @_pact_config = Pact::V2::Consumer::PactConfig.new(transport_type, consumer_name: consumer, provider_name: provider, opts: opts) + @_pact_config = Pact::Consumer::PactConfig.new(transport_type, consumer_name: consumer, provider_name: provider, opts: opts) end # rubocop:enable RSpec/BeforeAfterAll end @@ -59,7 +59,7 @@ def pact_config def execute_http_pact raise InteractionBuilderError.new("interaction is designed to be used one-time only") if defined?(@used) - mock_server = Pact::V2::Consumer::MockServer.create_for_http!( + mock_server = Pact::Consumer::MockServer.create_for_http!( pact: pact_config.pact_handle, host: pact_config.mock_host, port: pact_config.mock_port ) @@ -70,7 +70,7 @@ def execute_http_pact mock_server.write_pacts!(pact_config.pact_dir) else msg = mismatches_error_msg(mock_server) - raise Pact::V2::Consumer::HttpInteractionBuilder::InteractionMismatchesError.new(msg) + raise Pact::Consumer::HttpInteractionBuilder::InteractionMismatchesError.new(msg) end @used = true mock_server&.cleanup @@ -88,6 +88,6 @@ def mismatches_error_msg(mock_server) end RSpec.configure do |config| - config.include PactV2ConsumerDsl, pact_entity: :consumer - config.extend PactV2ConsumerDsl::ClassMethods, pact_entity: :consumer + config.include PactConsumerDsl, pact_entity: :consumer + config.extend PactConsumerDsl::ClassMethods, pact_entity: :consumer end diff --git a/lib/pact/v2/rspec/support/pact_message_helpers.rb b/lib/pact/rspec/support/pact_message_helpers.rb similarity index 100% rename from lib/pact/v2/rspec/support/pact_message_helpers.rb rename to lib/pact/rspec/support/pact_message_helpers.rb diff --git a/lib/pact/v2/rspec/support/pact_provider_helpers.rb b/lib/pact/rspec/support/pact_provider_helpers.rb similarity index 69% rename from lib/pact/v2/rspec/support/pact_provider_helpers.rb rename to lib/pact/rspec/support/pact_provider_helpers.rb index b25de3b2..3a30828f 100644 --- a/lib/pact/v2/rspec/support/pact_provider_helpers.rb +++ b/lib/pact/rspec/support/pact_provider_helpers.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true -require_relative "pact_message_helpers" -require_relative "webmock/webmock_helpers" +require_relative 'pact_message_helpers' +require_relative 'webmock/webmock_helpers' -module PactV2ProducerDsl +module PactProducerDsl module ClassMethods - PACT_PROVIDER_NOT_DECLARED_MESSAGE = "http_pact_provider or grpc_pact_provider should be declared first" + PACT_PROVIDER_NOT_DECLARED_MESSAGE = 'http_pact_provider or grpc_pact_provider should be declared first' def http_pact_provider(provider, opts: {}) _pact_provider(:http, provider, opts: opts) @@ -26,22 +26,21 @@ def mixed_pact_provider(provider, opts: {}) def execute_mixed_pact_provider(transport_type, provider, opts: {}) raise "#{transport_type}_pact_provider is designed to be used with RSpec" unless defined?(::RSpec) raise "#{transport_type}_pact_provider has to be declared at the top level of a suite" unless top_level? - raise "mixed_pact_provider is designed to be run once per provider so cannot be declared more than once" if defined?(@_pact_config) + if defined?(@_pact_config) + raise 'mixed_pact_provider is designed to be run once per provider so cannot be declared more than once' + end - pact_config_instance = Pact::V2::Provider::PactConfig.new(transport_type, provider_name: provider, opts: opts) + pact_config_instance = Pact::Provider::PactConfig.new(transport_type, provider_name: provider, opts: opts) instance_variable_set(:@_pact_config, pact_config_instance) - # rubocop:disable RSpec/BeforeAfterAll before(:context) do # rspec allows only context ivars in specs and ignores the rest # so we use block-as-a-closure feature to save pact_config ivar reference and make it available for descendants @_pact_config = pact_config_instance end - # rubocop:enable RSpec/BeforeAfterAll - it "verifies mixed interactions with provider #{provider}" do pact_config.start_servers - # todo: call any available verifier, or exit if none specified + # TODO: call any available verifier, or exit if none specified pact_config.http_config.new_verifier(@_pact_config).verify! end end @@ -49,19 +48,18 @@ def execute_mixed_pact_provider(transport_type, provider, opts: {}) def _pact_provider(transport_type, provider, opts: {}) raise "#{transport_type}_pact_provider is designed to be used with RSpec" unless defined?(::RSpec) raise "#{transport_type}_pact_provider has to be declared at the top level of a suite" unless top_level? - raise "*_pact_provider is designed to be run once per provider so cannot be declared more than once" if defined?(@_pact_config) + if defined?(@_pact_config) + raise '*_pact_provider is designed to be run once per provider so cannot be declared more than once' + end - pact_config_instance = Pact::V2::Provider::PactConfig.new(transport_type, provider_name: provider, opts: opts) + pact_config_instance = Pact::Provider::PactConfig.new(transport_type, provider_name: provider, opts: opts) instance_variable_set(:@_pact_config, pact_config_instance) - # rubocop:disable RSpec/BeforeAfterAll before(:context) do # rspec allows only context ivars in specs and ignores the rest # so we use block-as-a-closure feature to save pact_config ivar reference and make it available for descendants @_pact_config = pact_config_instance end - # rubocop:enable RSpec/BeforeAfterAll - it "verifies interactions with provider #{provider}" do pact_config.new_verifier.verify! end @@ -69,37 +67,40 @@ def _pact_provider(transport_type, provider, opts: {}) def before_state_setup(&block) raise PACT_PROVIDER_NOT_DECLARED_MESSAGE unless pact_config + pact_config.before_setup(&block) end def after_state_teardown(&block) raise PACT_PROVIDER_NOT_DECLARED_MESSAGE unless pact_config + pact_config.after_teardown(&block) end def provider_state(name, opts: {}, &block) raise PACT_PROVIDER_NOT_DECLARED_MESSAGE unless pact_config + pact_config.new_provider_state(name, opts: opts, &block) end def handle_message(name, opts: {}, &block) - async_klass = Pact::V2::Provider::PactConfig::Async + async_klass = Pact::Provider::PactConfig::Async if defined?(@_pact_config) && - @_pact_config.respond_to?(:async_config) && - @_pact_config.async_config.is_a?(async_klass) + @_pact_config.respond_to?(:async_config) && + @_pact_config.async_config.is_a?(async_klass) @_pact_config.async_config.new_message_handler(name, opts: opts, &block) elsif pact_config && - pact_config.respond_to?(:async_config) && - pact_config.async_config.is_a?(async_klass) + pact_config.respond_to?(:async_config) && + pact_config.async_config.is_a?(async_klass) pact_config.async_config.new_message_handler(name, opts: opts, &block) elsif defined?(@_pact_config) && - @_pact_config.is_a?(async_klass) + @_pact_config.is_a?(async_klass) @_pact_config.new_message_handler(name, opts: opts, &block) elsif pact_config.is_a?(async_klass) pact_config.new_message_handler(name, opts: opts, &block) else - raise "handle_message can only be used with message_pact_provider or mixed_pact_provider with an async block" + raise 'handle_message can only be used with message_pact_provider or mixed_pact_provider with an async block' end end @@ -114,8 +115,8 @@ def pact_config end RSpec.configure do |config| - config.include PactV2ProducerDsl, pact_entity: :provider - config.extend PactV2ProducerDsl::ClassMethods, pact_entity: :provider + config.include PactProducerDsl, pact_entity: :provider + config.extend PactProducerDsl::ClassMethods, pact_entity: :provider config.around pact_entity: :provider do |example| WebmockHelpers.turned_off do diff --git a/lib/pact/v2/rspec/support/waterdrop/pact_waterdrop_client.rb b/lib/pact/rspec/support/waterdrop/pact_waterdrop_client.rb similarity index 100% rename from lib/pact/v2/rspec/support/waterdrop/pact_waterdrop_client.rb rename to lib/pact/rspec/support/waterdrop/pact_waterdrop_client.rb diff --git a/lib/pact/v2/rspec/support/webmock/webmock_helpers.rb b/lib/pact/rspec/support/webmock/webmock_helpers.rb similarity index 100% rename from lib/pact/v2/rspec/support/webmock/webmock_helpers.rb rename to lib/pact/rspec/support/webmock/webmock_helpers.rb diff --git a/lib/pact/tasks.rb b/lib/pact/tasks.rb deleted file mode 100644 index ae78b287..00000000 --- a/lib/pact/tasks.rb +++ /dev/null @@ -1,2 +0,0 @@ -load File.expand_path('../tasks/pact.rake', File.dirname(__FILE__)) -require 'pact/tasks/verification_task' diff --git a/lib/pact/tasks/pact.rake b/lib/pact/tasks/pact.rake new file mode 100644 index 00000000..89a6f86d --- /dev/null +++ b/lib/pact/tasks/pact.rake @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'rspec/core/rake_task' + +RSpec::Core::RakeTask.new(:pact).tap do |task| + task.pattern = 'spec/pact/consumers/**/*_spec.rb' + task.rspec_opts = '--require rails_helper --tag pact' +end + +namespace :pact do + desc 'Verifies the pact files' + task verify: :pact +end diff --git a/lib/pact/tasks/task_helper.rb b/lib/pact/tasks/task_helper.rb deleted file mode 100644 index 3f533d54..00000000 --- a/lib/pact/tasks/task_helper.rb +++ /dev/null @@ -1,62 +0,0 @@ -require 'pact/configuration' -require 'pact/provider/pact_helper_locator' -require 'rake/file_utils' -require 'shellwords' - -module Pact - module TaskHelper - - PACT_INTERACTION_RERUN_COMMAND = "bundle exec rake pact:verify:at[] PACT_DESCRIPTION=\"\" PACT_PROVIDER_STATE=\"\"" - PACT_INTERACTION_RERUN_COMMAND_FOR_BROKER = "bundle exec rake pact:verify:at[] PACT_BROKER_INTERACTION_ID=\"\"" - - extend self - - def execute_pact_verify pact_uri = nil, pact_helper = nil, rspec_opts = nil, verification_opts = {} - execute_cmd verify_command(pact_helper || Pact::Provider::PactHelperLocater.pact_helper_path, pact_uri, rspec_opts, verification_opts) - end - - def handle_verification_failure - exit_status = yield - abort if exit_status != 0 - end - - def verify_command pact_helper, pact_uri, rspec_opts, verification_opts - command_parts = [] - # Clear SPEC_OPTS, otherwise we can get extra formatters, creating duplicate output eg. CI Reporting. - # Allow deliberate configuration using rspec_opts in VerificationTask. - command_parts << "SPEC_OPTS=#{Shellwords.escape(rspec_opts || '')}" - command_parts << FileUtils::RUBY - command_parts << "-S pact verify" - command_parts << "--pact-helper" << Shellwords.escape(pact_helper.end_with?(".rb") ? pact_helper : pact_helper + ".rb") - (command_parts << "--pact-uri" << pact_uri) if pact_uri - command_parts << "--ignore-failures" if verification_opts[:ignore_failures] - command_parts << "--pact-broker-username" << ENV['PACT_BROKER_USERNAME'] if ENV['PACT_BROKER_USERNAME'] - command_parts << "--pact-broker-password" << ENV['PACT_BROKER_PASSWORD'] if ENV['PACT_BROKER_PASSWORD'] - command_parts << "--backtrace" if ENV['BACKTRACE'] == 'true' - command_parts << "--description #{Shellwords.escape(ENV['PACT_DESCRIPTION'])}" if ENV['PACT_DESCRIPTION'] - command_parts << "--provider-state #{Shellwords.escape(ENV['PACT_PROVIDER_STATE'])}" if ENV['PACT_PROVIDER_STATE'] - command_parts << "--pact-broker-interaction-id #{Shellwords.escape(ENV['PACT_BROKER_INTERACTION_ID'])}" if ENV['PACT_BROKER_INTERACTION_ID'] - command_parts << "--interaction-index #{Shellwords.escape(ENV['PACT_INTERACTION_INDEX'])}" if ENV['PACT_INTERACTION_INDEX'] - command_parts.flatten.join(" ") - end - - def execute_cmd command - Pact.configuration.output_stream.puts command - temporarily_set_env_var 'PACT_EXECUTING_LANGUAGE', 'ruby' do - temporarily_set_env_var 'PACT_INTERACTION_RERUN_COMMAND', PACT_INTERACTION_RERUN_COMMAND do - temporarily_set_env_var 'PACT_INTERACTION_RERUN_COMMAND_FOR_BROKER', PACT_INTERACTION_RERUN_COMMAND_FOR_BROKER do - exit_status = system(command) ? 0 : 1 - end - end - end - end - - def temporarily_set_env_var name, value - original_value = ENV[name] - ENV[name] ||= value - yield - ensure - ENV[name] = original_value - end - end -end diff --git a/lib/pact/tasks/verification_task.rb b/lib/pact/tasks/verification_task.rb deleted file mode 100644 index bac650ab..00000000 --- a/lib/pact/tasks/verification_task.rb +++ /dev/null @@ -1,95 +0,0 @@ -require 'rake/tasklib' - -=begin - To create a rake pact:verify: task - - Pact::VerificationTask.new(:head) do | pact | - pact.uri 'http://master.cd.vpc.realestate.com.au/browse/BIQ-MAS/latestSuccessful/artifact/JOB2/Pacts/mas-contract_transaction_service.json' - pact.uri 'http://master.cd.vpc.realestate.com.au/browse/BIQ-IMAGINARY-CONSUMER/latestSuccessful/artifact/JOB2/Pacts/imaginary_consumer-contract_transaction_service.json' - end - - The pact.uri may be a local file system path or a remote URL. - - To run a pact:verify:xxx task you need to define a pact_helper.rb, ideally in spec/service_consumers. - It should contain your service_provider definition, and load any provider state definition files. - It should also load all your app's dependencies (eg by calling out to spec_helper) - - Eg. - - require 'spec_helper' - require 'provider_states_for_my_consumer' - - Pact.service_provider "My Provider" do - app { TestApp.new } - end - -=end - -module Pact - class VerificationTask < ::Rake::TaskLib - - attr_reader :pact_spec_configs - attr_accessor :rspec_opts - attr_accessor :ignore_failures - attr_accessor :_pact_helper - - def initialize(name) - @rspec_opts = nil - @ignore_failures = false - @pact_spec_configs = [] - @name = name - yield self - rake_task - end - - def pact_helper(pact_helper) - @pact_spec_configs << { pact_helper: pact_helper } - end - - def uri(uri, options = {}) - @pact_spec_configs << {uri: uri, pact_helper: options[:pact_helper]} - end - - private - - attr_reader :name - - # def parse_pactfile config - # Pact::ConsumerContract.from_uri config[:uri] - # end - - # def publish_report config, output, result, provider_ref, reports_dir - # consumer_contract = parse_pactfile config - # #TODO - when checking out a historical version, provider ref will be prod, however it will think it is head. Fix this!!!! - # report = Provider::VerificationReport.new( - # :result => result, - # :output => output, - # :consumer => {:name => consumer_contract.consumer.name, :ref => name}, - # :provider => {:name => consumer_contract.provider.name, :ref => provider_ref} - # ) - - # FileUtils.mkdir_p reports_dir - # File.open("#{reports_dir}/#{report.report_file_name}", "w") { |file| file << JSON.pretty_generate(report) } - # end - - def rake_task - namespace :pact do - - desc "Verify provider against the consumer pacts for #{name}" - task "verify:#{name}" do |t, args| - - require 'pact/tasks/task_helper' - - exit_statuses = pact_spec_configs.collect do | config | - Pact::TaskHelper.execute_pact_verify config[:uri], config[:pact_helper], rspec_opts, { ignore_failures: ignore_failures } - end - - Pact::TaskHelper.handle_verification_failure do - exit_statuses.count{ | status | status != 0 } - end - - end - end - end - end -end diff --git a/lib/pact/templates/help.erb b/lib/pact/templates/help.erb deleted file mode 100644 index a5977de7..00000000 --- a/lib/pact/templates/help.erb +++ /dev/null @@ -1,22 +0,0 @@ -# For assistance debugging failures - -* The pact files have been stored locally in the following temp directory: - <%= temp_dir %> - -* The requests and responses are logged in the following log file: - <%= log_path %> - -* Add BACKTRACE=true to the `rake pact:verify` command to see the full backtrace - -* If the diff output is confusing, try using another diff formatter. - The options are :unix, :embedded and :list - - Pact.configure do | config | - config.diff_formatter = :embedded - end - - See https://github.com/pact-foundation/pact-ruby/blob/master/documentation/configuration.md#diff_formatter for examples and more information. - -* Check out https://github.com/pact-foundation/pact-ruby/wiki/Troubleshooting - -* Ask a question on stackoverflow and tag it `pact-ruby` diff --git a/lib/pact/templates/provider_state.erb b/lib/pact/templates/provider_state.erb deleted file mode 100644 index 1beb6d99..00000000 --- a/lib/pact/templates/provider_state.erb +++ /dev/null @@ -1,14 +0,0 @@ -Could not find one or more provider states. -Have you required the provider states file for this consumer in your pact_helper.rb? -If you have not yet defined these states, here is a template: -<% consumers.keys.each do | consumer_name | %> -Pact.provider_states_for "<%= consumer_name %>" do -<% consumers[consumer_name].each do | provider_state | %> - provider_state "<%= provider_state %>" do - set_up do - # Your set up code goes here - end - end -<% end %> -end -<% end %> diff --git a/lib/pact/utils/metrics.rb b/lib/pact/utils/metrics.rb deleted file mode 100644 index c7fef930..00000000 --- a/lib/pact/utils/metrics.rb +++ /dev/null @@ -1,100 +0,0 @@ -require 'securerandom' -require 'digest' -require 'socket' -require 'pact/version' -require 'net/http' - -module Pact - module Utils - class Metrics - - def self.report_metric(event, category, action, value = 1) - do_once_per_thread(:pact_metrics_message_shown) do - if track_events? - Pact.configuration.output_stream.puts "pact WARN: Please note: we are tracking events anonymously to gather important usage statistics like Pact-Ruby version - and operating system. To disable tracking, set the 'PACT_DO_NOT_TRACK' environment - variable to 'true'." - end - end - - in_thread do - begin - if track_events? - uri = URI('https://www.google-analytics.com/collect') - req = Net::HTTP::Post.new(uri) - req.set_form_data(create_tracking_event(event, category, action, value)) - - Net::HTTP.start(uri.hostname, uri.port, read_timeout:2, open_timeout:2, :use_ssl => true ) do |http| - http.request(req) - end - end - rescue StandardError => e - handle_error(e) - end - end - end - - private - - def self.handle_error e - if ENV['PACT_METRICS_DEBUG'] == 'true' - Pact.configuration.output_stream.puts("DEBUG: #{e.inspect}\n" + e.backtrace.join("\n")) - end - end - - def self.in_thread - Thread.new do - yield - end - end - - # not super safe to use the thread, but it's good enough for this usecase - def self.do_once_per_thread(key) - result = nil - if !Thread.current[key] - result = yield - end - Thread.current[key] = true - result - end - - def self.create_tracking_event(event, category, action, value) - { - "v" => 1, - "t" => "event", - "tid" => "UA-117778936-1", - "cid" => calculate_cid, - "an" => "Pact Ruby", - "av" => Pact::VERSION, - "aid" => "pact-ruby", - "aip" => 1, - "ds" => ENV['PACT_EXECUTING_LANGUAGE'] ? "client" : "cli", - "cd2" => ENV['CI'] == "true" ? "CI" : "unknown", - "cd3" => RUBY_PLATFORM, - "cd6" => ENV['PACT_EXECUTING_LANGUAGE'] || "unknown", - "cd7" => ENV['PACT_EXECUTING_LANGUAGE_VERSION'], - "el" => event, - "ec" => category, - "ea" => action, - "ev" => value - } - end - - def self.track_events? - ENV['PACT_DO_NOT_TRACK'] != 'true' - end - - def self.calculate_cid - if RUBY_PLATFORM.include? "windows" - hostname = ENV['COMPUTERNAME'] - else - hostname = ENV['HOSTNAME'] - end - if !hostname - hostname = Socket.gethostname - end - Digest::MD5.hexdigest hostname || SecureRandom.urlsafe_base64(5) - end - end - end -end diff --git a/lib/pact/utils/string.rb b/lib/pact/utils/string.rb deleted file mode 100644 index ddead806..00000000 --- a/lib/pact/utils/string.rb +++ /dev/null @@ -1,35 +0,0 @@ -# Can't use refinements because of Travelling Ruby - -module Pact - module Utils - module String - - extend self - - # ripped from rubyworks/facets, thank you - def camelcase(string, *separators) - case separators.first - when Symbol, TrueClass, FalseClass, NilClass - first_letter = separators.shift - end - - separators = ['_', '\s'] if separators.empty? - - str = string.dup - - separators.each do |s| - str = str.gsub(/(?:#{s}+)([a-z])/){ $1.upcase } - end - - case first_letter - when :upper, true - str = str.gsub(/(\A|\s)([a-z])/){ $1 + $2.upcase } - when :lower, false - str = str.gsub(/(\A|\s)([A-Z])/){ $1 + $2.downcase } - end - - str - end - end - end -end \ No newline at end of file diff --git a/lib/pact/v2.rb b/lib/pact/v2.rb deleted file mode 100644 index a8911a86..00000000 --- a/lib/pact/v2.rb +++ /dev/null @@ -1,71 +0,0 @@ -# frozen_string_literal: true - -require "zeitwerk" -require "pact/ffi" - -require "pact/v2/railtie" if defined?(Rails::Railtie) - -module Pact - module V2 - class Error < StandardError; end - - class ImplementationRequired < Error; end - - class FfiError < Error - def initialize(msg, reason, status) - super(msg) - - @msg = msg - @reason = reason - @status = status - end - - def message - "FFI error: reason: #{@reason}, status: #{@status}, message: #{@msg}" - end - end - - def self.configure - yield configuration if block_given? - end - - def self.configuration - @configuration ||= Pact::V2::Configuration.new - end - end -end - -loader = Zeitwerk::Loader.new -loader.push_dir(File.join(__dir__, "..")) - -loader.tag = "pact-v2" - -# existing pact-ruby ignores -# loader.ignore("#{__dir__}/../pact") # ignore the pact dir at the root of the repo -# loader.ignore("#{__dir__}/../pact/v2",false) # ignore the pact dir at the root of the repo -# loader.push_dir(File.join(__dir__)) - - -loader.ignore("#{__dir__}/../pact/version.rb") -loader.ignore("#{__dir__}/../pact/cli") -loader.ignore("#{__dir__}/../pact/cli.rb") -loader.ignore("#{__dir__}/../pact/consumer") -loader.ignore("#{__dir__}/../pact/consumer.rb") -loader.ignore("#{__dir__}/../pact/doc") -loader.ignore("#{__dir__}/../pact/hal") -loader.ignore("#{__dir__}/../pact/hash_refinements.rb") -loader.ignore("#{__dir__}/../pact/pact_broker") -loader.ignore("#{__dir__}/../pact/pact_broker.rb") -loader.ignore("#{__dir__}/../pact/project_root.rb") -loader.ignore("#{__dir__}/../pact/provider") -loader.ignore("#{__dir__}/../pact/provider.rb") -loader.ignore("#{__dir__}/../pact/retry.rb") -loader.ignore("#{__dir__}/../pact/tasks") -loader.ignore("#{__dir__}/../pact/tasks.rb") -loader.ignore("#{__dir__}/../pact/templates") -loader.ignore("#{__dir__}/../pact/utils") -loader.ignore("#{__dir__}/../pact/v2/rspec.rb") -loader.ignore("#{__dir__}/../pact/v2/rspec") -loader.ignore("#{__dir__}/../pact/v2/railtie.rb") unless defined?(Rails::Railtie) -loader.setup -loader.eager_load diff --git a/lib/pact/v2/configuration.rb b/lib/pact/v2/configuration.rb deleted file mode 100644 index 39bbf594..00000000 --- a/lib/pact/v2/configuration.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -module Pact - module V2 - class Configuration - attr_reader :before_provider_state_proc, :after_provider_state_proc - - class GlobalProviderConfigurationError < ::Pact::V2::Error; end - - def before_provider_state_setup(&block) - raise GlobalProviderConfigurationError, "no block given" unless block - - @before_provider_state_proc = block - end - - def after_provider_state_teardown(&block) - raise GlobalProviderConfigurationError, "no block given" unless block - - @after_provider_state_proc = block - end - end - end -end diff --git a/lib/pact/v2/consumer.rb b/lib/pact/v2/consumer.rb deleted file mode 100644 index f2e57c01..00000000 --- a/lib/pact/v2/consumer.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -module Pact - module V2 - module Consumer - end - end -end diff --git a/lib/pact/v2/consumer/grpc_interaction_builder.rb b/lib/pact/v2/consumer/grpc_interaction_builder.rb deleted file mode 100644 index 0f873f38..00000000 --- a/lib/pact/v2/consumer/grpc_interaction_builder.rb +++ /dev/null @@ -1,194 +0,0 @@ -# frozen_string_literal: true - -require "pact/ffi/sync_message_consumer" -require "pact/ffi/plugin_consumer" -require "pact/ffi/logger" - -module Pact - module V2 - module Consumer - class GrpcInteractionBuilder - CONTENT_TYPE = "application/protobuf" - GRPC_CONTENT_TYPE = "application/grpc" - PROTOBUF_PLUGIN_NAME = "protobuf" - PROTOBUF_PLUGIN_VERSION = "0.6.5" - - class PluginInitError < Pact::V2::FfiError; end - - # https://docs.rs/pact_ffi/0.4.17/pact_ffi/plugins/fn.pactffi_using_plugin.html - INIT_PLUGIN_ERRORS = { - 1 => {reason: :internal_error, status: 1, description: "A general panic was caught"}, - 2 => {reason: :plugin_load_failed, status: 2, description: "Failed to load the plugin"}, - 3 => {reason: :invalid_handle, status: 3, description: "Pact Handle is not valid"} - }.freeze - - # https://docs.rs/pact_ffi/0.4.17/pact_ffi/plugins/fn.pactffi_interaction_contents.html - CREATE_INTERACTION_ERRORS = { - 1 => {reason: :internal_error, status: 1, description: "A general panic was caught"}, - 2 => {reason: :mock_server_already_running, status: 2, description: "The mock server has already been started"}, - 3 => {reason: :invalid_handle, status: 3, description: "The interaction handle is invalid"}, - 4 => {reason: :invalid_content_type, status: 4, description: "The content type is not valid"}, - 5 => {reason: :invalid_contents, status: 5, description: "The contents JSON is not valid JSON"}, - 6 => {reason: :plugin_error, status: 6, description: "The plugin returned an error"} - }.freeze - - class CreateInteractionError < Pact::V2::FfiError; end - - class InteractionMismatchesError < Pact::V2::Error; end - - class InteractionBuilderError < Pact::V2::Error; end - - def initialize(pact_config, description: nil) - @pact_config = pact_config - @description = description || "" - @proto_path = nil - @proto_include_dirs = [] - @service_name = nil - @method_name = nil - @request = nil - @response = nil - @response_meta = nil - @provider_state_meta = nil - end - - def with_service(proto_path, method, include_dirs = []) - raise InteractionBuilderError.new("invalid grpc method: cannot be blank") if method.blank? - - service_name, method_name = method.split("/") || [] - raise InteractionBuilderError.new("invalid grpc method: #{method}, should be like service/SomeMethod") if service_name.blank? || method_name.blank? - - absolute_path = File.expand_path(proto_path) - raise InteractionBuilderError.new("proto file #{proto_path} does not exist") unless File.exist?(absolute_path) - - @proto_path = absolute_path - @service_name = service_name - @method_name = method_name - @proto_include_dirs = include_dirs.map { |dir| File.expand_path(dir) } - - self - end - - def with_pact_protobuf_plugin_version(version) - raise InteractionBuilderError.new("version is required") if version.blank? - - @proto_plugin_version = version - self - end - - def given(provider_state, metadata = {}) - @provider_state_meta = {provider_state => metadata} - self - end - - def upon_receiving(description) - @description = description - self - end - - def with_request(req_hash) - @request = InteractionContents.plugin(req_hash) - self - end - - def will_respond_with(resp_hash) - @response = InteractionContents.plugin(resp_hash) - self - end - - def will_respond_with_meta(meta_hash) - @response_meta = InteractionContents.plugin(meta_hash) - self - end - - def interaction_json - result = { - "pact:proto": @proto_path, - "pact:proto-service": "#{@service_name}/#{@method_name}", - "pact:content-type": CONTENT_TYPE, - request: @request - } - - result["pact:protobuf-config"] = {additionalIncludes: @proto_include_dirs} if @proto_include_dirs.present? - - result[:response] = @response if @response.is_a?(Hash) - result[:responseMetadata] = @response_meta if @response_meta.is_a?(Hash) - - JSON.dump(result) - end - - def validate! - raise InteractionBuilderError.new("uninitialized service params, use #with_service to configure") if @proto_path.blank? || @service_name.blank? || @method_name.blank? - raise InteractionBuilderError.new("invalid request format, should be a hash") unless @request.is_a?(Hash) - raise InteractionBuilderError.new("invalid response format, should be a hash") unless @response.is_a?(Hash) || @response_meta.is_a?(Hash) - end - - def execute(&block) - raise InteractionBuilderError.new("interaction is designed to be used one-time only") if defined?(@used) - - validate! - - pact_handle = init_pact - init_plugin!(pact_handle) - - message_pact = PactFfi::SyncMessageConsumer.new_interaction(pact_handle, @description) - @provider_state_meta&.each_pair do |provider_state, meta| - if meta.present? - meta.each_pair { |k, v| PactFfi.given_with_param(message_pact, provider_state, k.to_s, v.to_s) } - else - PactFfi.given(message_pact, provider_state) - end - end - - result = PactFfi::PluginConsumer.interaction_contents(message_pact, 0, GRPC_CONTENT_TYPE, interaction_json) - if CREATE_INTERACTION_ERRORS[result].present? - error = CREATE_INTERACTION_ERRORS[result] - raise CreateInteractionError.new("There was an error while trying to add interaction \"#{@description}\"", error[:reason], error[:status]) - end - - mock_server = MockServer.create_for_grpc!(pact: pact_handle, host: @pact_config.mock_host, port: @pact_config.mock_port) - - yield(message_pact, mock_server) - - ensure - if mock_server.matched? - mock_server.write_pacts!(@pact_config.pact_dir) - else - msg = mismatches_error_msg(mock_server) - raise InteractionMismatchesError.new(msg) - end - @used = true - mock_server&.cleanup - PactFfi::PluginConsumer.cleanup_plugins(pact_handle) - PactFfi.free_pact_handle(pact_handle) - end - - private - - def mismatches_error_msg(mock_server) - rspec_example_desc = RSpec.current_example&.description - return "interaction for #{@service_name}/#{@method_name} has mismatches: #{mock_server.mismatches}" if rspec_example_desc.blank? - - "#{rspec_example_desc} has mismatches: #{mock_server.mismatches}" - end - - def init_pact - handle = PactFfi.new_pact(@pact_config.consumer_name, @pact_config.provider_name) - PactFfi.with_specification(handle, PactFfi::FfiSpecificationVersion["SPECIFICATION_VERSION_V4"]) - PactFfi.with_pact_metadata(handle, "pact-ruby-v2", "pact-ffi", PactFfi.version) - - Pact::V2::Native::Logger.log_to_stdout(@pact_config.log_level) - - handle - end - - def init_plugin!(pact_handle) - result = PactFfi::PluginConsumer.using_plugin(pact_handle, PROTOBUF_PLUGIN_NAME, @proto_plugin_version || PROTOBUF_PLUGIN_VERSION) - return result if INIT_PLUGIN_ERRORS[result].blank? - - error = INIT_PLUGIN_ERRORS[result] - raise PluginInitError.new("There was an error while trying to initialize plugin #{PROTOBUF_PLUGIN_NAME}/#{@proto_plugin_version || PROTOBUF_PLUGIN_VERSION}", error[:reason], error[:status]) - end - end - end - end -end diff --git a/lib/pact/v2/consumer/http_interaction_builder.rb b/lib/pact/v2/consumer/http_interaction_builder.rb deleted file mode 100644 index b8ba4d1c..00000000 --- a/lib/pact/v2/consumer/http_interaction_builder.rb +++ /dev/null @@ -1,162 +0,0 @@ -# frozen_string_literal: true - -require "pact/ffi/sync_message_consumer" -require "pact/ffi/plugin_consumer" -require "pact/ffi/logger" -require "json" - -module Pact - module V2 - module Consumer - class HttpInteractionBuilder - - # https://docs.rs/pact_ffi/0.4.17/pact_ffi/plugins/fn.pactffi_interaction_contents.html - CREATE_INTERACTION_ERRORS = { - 1 => {reason: :internal_error, status: 1, description: "A general panic was caught"}, - 2 => {reason: :mock_server_already_running, status: 2, description: "The mock server has already been started"}, - 3 => {reason: :invalid_handle, status: 3, description: "The interaction handle is invalid"}, - 4 => {reason: :invalid_content_type, status: 4, description: "The content type is not valid"}, - 5 => {reason: :invalid_contents, status: 5, description: "The contents JSON is not valid JSON"}, - 6 => {reason: :plugin_error, status: 6, description: "The plugin returned an error"} - }.freeze - - class CreateInteractionError < Pact::V2::FfiError; end - - class InteractionMismatchesError < Pact::V2::Error; end - - class InteractionBuilderError < Pact::V2::Error; end - - class << self - def create_finalizer(pact_handle) - proc { PactFfi.free_pact_handle(pact_handle) } - end - end - - def initialize(pact_config, description: nil) - @pact_config = pact_config - @description = description || "" - - @pact_handle = pact_config.pact_handle ||= init_pact - @pact_interaction = PactFfi.new_interaction(pact_handle, @description) - - ObjectSpace.define_finalizer(self, self.class.create_finalizer(pact_interaction)) - end - - def given(provider_state, metadata = {}) - if metadata.present? - PactFfi.given_with_params(pact_interaction, provider_state, JSON.dump(metadata)) - else - PactFfi.given(pact_interaction, provider_state) - end - - self - end - - def upon_receiving(description) - @description = description - PactFfi.upon_receiving(pact_interaction, @description) - self - end - - def with_request(method: nil, path: nil, query: {}, headers: {}, body: nil) - interaction_part = PactFfi::FfiInteractionPart["INTERACTION_PART_REQUEST"] - PactFfi.with_request(pact_interaction, method.to_s, format_value(path)) - - # Processing as an array of hashes, allows us to consider duplicate keys - # which should be passed to the core, at a non 0 index - if query.is_a?(Array) - key_index = Hash.new(0) - query.each do |query_item| - InteractionContents.basic(query_item).each_pair do |key, value_item| - PactFfi.with_query_parameter_v2(pact_interaction, key.to_s, key_index[key], format_value(value_item)) - key_index[key] += 1 - end - end - else - InteractionContents.basic(query).each_pair do |key, value_item| - PactFfi.with_query_parameter_v2(pact_interaction, key.to_s, 0, format_value(value_item)) - end - end - - InteractionContents.basic(headers).each_pair do |key, value_item| - PactFfi.with_header_v2(pact_interaction, interaction_part, key.to_s, 0, format_value(value_item)) - end - - if body - PactFfi.with_body(pact_interaction, interaction_part, "application/json", format_value(InteractionContents.basic(body))) - end - - self - end - - def will_respond_with(status: nil, headers: {}, body: nil) - interaction_part = PactFfi::FfiInteractionPart["INTERACTION_PART_RESPONSE"] - PactFfi.response_status(pact_interaction, status) - - InteractionContents.basic(headers).each_pair do |key, value_item| - PactFfi.with_header_v2(pact_interaction, interaction_part, key.to_s, 0, format_value(value_item)) - end - - if body - PactFfi.with_body(pact_interaction, interaction_part, "application/json", format_value(InteractionContents.basic(body))) - end - - self - end - - def execute(&block) - raise InteractionBuilderError.new("interaction is designed to be used one-time only") if defined?(@used) - - mock_server = MockServer.create_for_http!( - pact: pact_handle, host: pact_config.mock_host, port: pact_config.mock_port - ) - - yield(mock_server) - - ensure - if mock_server.matched? - mock_server.write_pacts!(pact_config.pact_dir) - else - msg = mismatches_error_msg(mock_server) - raise InteractionMismatchesError.new(msg) - end - @used = true - mock_server&.cleanup - # Reset the pact handle to allow for a new interaction to be built - # without previous interactions being included - @pact_config.reset_pact - end - - private - - attr_reader :pact_handle, :pact_interaction, :pact_config - - def mismatches_error_msg(mock_server) - rspec_example_desc = RSpec.current_example&.description - mismatches = JSON.pretty_generate(JSON.parse(mock_server.mismatches)) - mismatches_with_colored_keys = mismatches.gsub(/"([^"]+)":/) { |match| "\e[34m#{match}\e[0m" } # Blue keys / white values - - "#{rspec_example_desc} has mismatches: #{mismatches_with_colored_keys}" - end - - def init_pact - handle = PactFfi.new_pact(pact_config.consumer_name, pact_config.provider_name) - PactFfi.with_specification(handle, PactFfi::FfiSpecificationVersion["SPECIFICATION_VERSION_#{pact_config.pact_specification}"]) - PactFfi.with_pact_metadata(handle, "pact-ruby-v2", "pact-ffi", PactFfi.version) - - Pact::V2::Native::Logger.log_to_stdout(pact_config.log_level) - - handle - end - - def format_value(obj) - return obj if obj.is_a?(String) - - return JSON.dump({value: obj}) if obj.is_a?(Array) - - JSON.dump(obj) - end - end - end - end -end diff --git a/lib/pact/v2/consumer/interaction_contents.rb b/lib/pact/v2/consumer/interaction_contents.rb deleted file mode 100644 index 8e17360e..00000000 --- a/lib/pact/v2/consumer/interaction_contents.rb +++ /dev/null @@ -1,62 +0,0 @@ -# frozen_string_literal: true - -module Pact - module V2 - module Consumer - class InteractionContents < Hash - BASIC_FORMAT = :basic - PLUGIN_FORMAT = :plugin - - attr_reader :format - - def self.basic(contents_hash) - new(contents_hash, BASIC_FORMAT) - end - - def self.plugin(contents_hash) - new(contents_hash, PLUGIN_FORMAT) - end - - def initialize(contents_hash, format) - init_hash(contents_hash, format).each_pair { |k, v| self[k] = v } - @format = format - end - - private - - def serialize(hash, format) - # serialize recursively - if hash.is_a?(String) - return hash - end - if hash.is_a?(Pact::V2::Matchers::Base) - return hash.as_basic if format == :basic - return hash.as_plugin if format == :plugin - end - if hash.is_a?(Pact::V2::Generators::Base) - return hash.as_basic if format == :basic - return hash.as_plugin if format == :plugin - end - hash.each_pair do |key, value| - next serialize(value, format) if value.is_a?(Hash) - next hash[key] = value.map { |v| serialize(v, format) } if value.is_a?(Array) - if value.is_a?(Pact::V2::Matchers::Base) - hash[key] = value.as_basic if format == :basic - hash[key] = value.as_plugin if format == :plugin - end - if value.is_a?(Pact::V2::Generators::Base) - hash[key] = value.as_basic if format == :basic - hash[key] = value.as_plugin if format == :plugin - end - end - - hash - end - - def init_hash(hash, format) - serialize(hash.deep_dup, format) - end - end - end - end -end diff --git a/lib/pact/v2/consumer/message_interaction_builder.rb b/lib/pact/v2/consumer/message_interaction_builder.rb deleted file mode 100644 index 11e38269..00000000 --- a/lib/pact/v2/consumer/message_interaction_builder.rb +++ /dev/null @@ -1,288 +0,0 @@ -# frozen_string_literal: true - -require "pact/ffi/message_consumer" -require "pact/ffi/plugin_consumer" -require "pact/ffi/logger" - -module Pact - module V2 - module Consumer - class MessageInteractionBuilder - META_CONTENT_TYPE_HEADER = "contentType" - - JSON_CONTENT_TYPE = "application/json" - PROTO_CONTENT_TYPE = "application/protobuf" - - PROTOBUF_PLUGIN_NAME = "protobuf" - PROTOBUF_PLUGIN_VERSION = "0.6.5" - - # https://docs.rs/pact_ffi/latest/pact_ffi/mock_server/handles/fn.pactffi_write_message_pact_file.html - WRITE_PACT_FILE_ERRORS = { - 1 => {reason: :file_not_accessible, status: 1, description: "The pact file was not able to be written"}, - 2 => {reason: :internal_error, status: 2, description: "The message pact for the given handle was not found"} - }.freeze - - class PluginInitError < Pact::V2::FfiError; end - - # https://docs.rs/pact_ffi/0.4.17/pact_ffi/plugins/fn.pactffi_using_plugin.html - INIT_PLUGIN_ERRORS = { - 1 => {reason: :internal_error, status: 1, description: "A general panic was caught"}, - 2 => {reason: :plugin_load_failed, status: 2, description: "Failed to load the plugin"}, - 3 => {reason: :invalid_handle, status: 3, description: "Pact Handle is not valid"} - }.freeze - - # https://docs.rs/pact_ffi/0.4.17/pact_ffi/plugins/fn.pactffi_interaction_contents.html - CREATE_INTERACTION_ERRORS = { - 1 => {reason: :internal_error, status: 1, description: "A general panic was caught"}, - 2 => {reason: :mock_server_already_running, status: 2, description: "The mock server has already been started"}, - 3 => {reason: :invalid_handle, status: 3, description: "The interaction handle is invalid"}, - 4 => {reason: :invalid_content_type, status: 4, description: "The content type is not valid"}, - 5 => {reason: :invalid_contents, status: 5, description: "The contents JSON is not valid JSON"}, - 6 => {reason: :plugin_error, status: 6, description: "The plugin returned an error"} - }.freeze - - class CreateInteractionError < Pact::V2::FfiError; end - - class InteractionMismatchesError < Pact::V2::Error; end - - class InteractionBuilderError < Pact::V2::Error; end - - def initialize(pact_config, description: nil) - @pact_config = pact_config - @description = description - - @json_contents = nil - @proto_contents = nil - @proto_path = nil - @proto_message_class = nil - @proto_include_dirs = [] - @meta = {} - @headers = {} - @provider_state_meta = nil - end - - def given(provider_state, metadata = {}) - @provider_state_meta = {provider_state => metadata} - self - end - - def upon_receiving(description) - @description = description - self - end - - def with_json_contents(contents_hash) - @json_contents = InteractionContents.basic(contents_hash) - self - end - - def with_proto_class(proto_path, message_class_name, include_dirs = []) - absolute_path = File.expand_path(proto_path) - raise InteractionBuilderError.new("proto file #{proto_path} does not exist") unless File.exist?(absolute_path) - - @proto_path = absolute_path - @proto_message_class = message_class_name - @proto_include_dirs = include_dirs.map { |dir| File.expand_path(dir) } - self - end - - def with_pact_protobuf_plugin_version(version) - raise InteractionBuilderError.new("version is required") if version.blank? - - @proto_plugin_version = version - self - end - - def with_proto_contents(contents_hash) - @proto_contents = InteractionContents.plugin(contents_hash) - self - end - - def with_metadata(meta_hash) - @meta = InteractionContents.basic(meta_hash) - self - end - - def with_headers(headers_hash) - @headers = InteractionContents.basic(headers_hash) - self - end - - def with_header(key, value) - @headers[key] = value - self - end - - def validate! - if proto_interaction? - raise InteractionBuilderError.new("proto_path / proto_message are not defined, please set ones with #with_proto_message") if @proto_contents.blank? || @proto_message_class.blank? - raise InteractionBuilderError.new("invalid request format, should be a hash") unless @proto_contents.is_a?(Hash) - else - raise InteractionBuilderError.new("invalid request format, should be a hash") unless @json_contents.is_a?(Hash) - end - raise InteractionBuilderError.new("description is required for message interactions, please set one with #upon_receiving") if @description.blank? - end - - def execute(&block) - raise InteractionBuilderError.new("interaction is designed to be used one-time only") if defined?(@used) - - validate! - pact_handle = init_pact - init_plugin!(pact_handle) if proto_interaction? - - message_pact = PactFfi::MessageConsumer.new_message_interaction(pact_handle, @description) - - configure_interaction!(message_pact) - - # strip out matchers and get raw payload/metadata - payload, metadata = fetch_reified_message(pact_handle) - configure_provider_state(message_pact, metadata) - - yield(payload, metadata) - - write_pacts!(pact_handle, @pact_config.pact_dir) - ensure - @used = true - PactFfi::MessageConsumer.free_handle(message_pact) - PactFfi::PluginConsumer.cleanup_plugins(pact_handle) - PactFfi.free_pact_handle(pact_handle) - end - - def build_interaction_json - return JSON.dump(@json_contents) unless proto_interaction? - - contents = { - "pact:proto": @proto_path, - "pact:message-type": @proto_message_class, - "pact:content-type": PROTO_CONTENT_TYPE - }.merge(@proto_contents) - - contents["pact:protobuf-config"] = {additionalIncludes: @proto_include_dirs} if @proto_include_dirs.present? - - JSON.dump(contents) - end - - private - - def write_pacts!(handle, dir) - result = PactFfi.write_message_pact_file(handle, @pact_config.pact_dir, false) - return result if WRITE_PACT_FILE_ERRORS[result].blank? - - error = WRITE_PACT_FILE_ERRORS[result] - raise WritePactsError.new("There was an error while trying to write pact file to #{dir}", error[:reason], error[:status]) - end - - def init_pact - handle = PactFfi::MessageConsumer.new_message_pact(@pact_config.consumer_name, @pact_config.provider_name) - PactFfi.with_specification(handle, PactFfi::FfiSpecificationVersion["SPECIFICATION_VERSION_V4"]) - PactFfi.with_pact_metadata(handle, "pact-ruby-v2", "pact-ffi", PactFfi.version) - - Pact::V2::Native::Logger.log_to_stdout(@pact_config.log_level) - - handle - end - - def fetch_reified_message(pact_handle) - iterator = PactFfi::MessageConsumer.pact_handle_get_message_iter(pact_handle) - raise InteractionBuilderError.new("cannot get message iterator: internal error") if iterator.blank? - - message_handle = PactFfi.pact_message_iter_next(iterator) - raise InteractionBuilderError.new("cannot get message from iterator: no messages") if message_handle.blank? - - contents = fetch_reified_message_body(message_handle) - meta = fetch_reified_message_headers(message_handle) - - [contents, meta.compact] - ensure - PactFfi.pact_message_iter_delete(iterator) if iterator.present? - end - - def fetch_reified_message_headers(message_handle) - meta = {"headers" => {}} - - meta[META_CONTENT_TYPE_HEADER] = PactFfi.message_find_metadata(message_handle, META_CONTENT_TYPE_HEADER) - - @meta.each_key do |key| - meta[key.to_s] = PactFfi.message_find_metadata(message_handle, key.to_s) - end - - @headers.each_key do |key| - meta["headers"][key.to_s] = PactFfi.message_find_metadata(message_handle, key.to_s) - end - - meta - end - - def configure_provider_state(message_pact, reified_metadata) - content_type = reified_metadata[META_CONTENT_TYPE_HEADER] - @provider_state_meta&.each_pair do |provider_state, meta| - if meta.present? - meta.each_pair { |k, v| PactFfi.given_with_param(message_pact, provider_state, k.to_s, v.to_s) } - PactFfi.given_with_param(message_pact, provider_state, META_CONTENT_TYPE_HEADER, content_type.to_s) if content_type - elsif content_type.present? - PactFfi.given_with_param(message_pact, provider_state, META_CONTENT_TYPE_HEADER, content_type.to_s) - else - PactFfi.given(message_pact, provider_state) - end - end - end - - def fetch_reified_message_body(message_handle) - if proto_interaction? - len = PactFfi::MessageConsumer.get_contents_length(message_handle) - ptr = PactFfi::MessageConsumer.get_contents_bin(message_handle) - return nil if ptr.blank? || len == 0 - - return String.new(ptr.read_string_length(len)) - end - - contents = PactFfi::MessageConsumer.get_contents(message_handle) - return nil if contents.blank? - - JSON.parse(contents) - end - - def configure_interaction!(message_pact) - interaction_json = build_interaction_json - - if proto_interaction? - result = PactFfi::PluginConsumer.interaction_contents(message_pact, 0, PROTO_CONTENT_TYPE, interaction_json) - if CREATE_INTERACTION_ERRORS[result].present? - error = CREATE_INTERACTION_ERRORS[result] - raise CreateInteractionError.new("There was an error while trying to add interaction \"#{@description}\"", error[:reason], error[:status]) - end - else - result = PactFfi.with_body(message_pact, 0, JSON_CONTENT_TYPE, interaction_json) - unless result - raise InteractionMismatchesError.new("There was an error while trying to add message interaction contents \"#{@description}\"") - end - end - - # meta should be configured last to avoid resetting after body is set - InteractionContents.basic(@meta.merge(@headers)).each_pair do |key, value| - PactFfi::MessageConsumer.with_metadata_v2(message_pact, key.to_s, JSON.dump(value)) - end - end - - def init_plugin!(pact_handle) - result = PactFfi::PluginConsumer.using_plugin(pact_handle, PROTOBUF_PLUGIN_NAME, @proto_plugin_version || PROTOBUF_PLUGIN_VERSION) - return result if INIT_PLUGIN_ERRORS[result].blank? - - error = INIT_PLUGIN_ERRORS[result] - raise PluginInitError.new("There was an error while trying to initialize plugin #{PROTOBUF_PLUGIN_NAME}/#{@proto_plugin_version || PROTOBUF_PLUGIN_VERSION}", error[:reason], error[:status]) - end - - def serialize_metadata(metadata_hash) - metadata = metadata_hash.deep_dup - serialize_as!(metadata, :basic) - - metadata - end - - def proto_interaction? - @proto_contents.present? - end - end - end - end -end diff --git a/lib/pact/v2/consumer/mock_server.rb b/lib/pact/v2/consumer/mock_server.rb deleted file mode 100644 index 4c088937..00000000 --- a/lib/pact/v2/consumer/mock_server.rb +++ /dev/null @@ -1,108 +0,0 @@ -# frozen_string_literal: true - -require "pact/ffi/mock_server" - -module Pact - module V2 - module Consumer - class MockServer - attr_reader :host, :port, :transport, :handle, :url - - TRANSPORT_HTTP = "http" - TRANSPORT_GRPC = "grpc" - - class MockServerCreateError < Pact::V2::FfiError; end - - class WritePactsError < Pact::V2::FfiError; end - - # https://docs.rs/pact_ffi/0.4.17/pact_ffi/mock_server/fn.pactffi_create_mock_server_for_transport.html - CREATE_TRANSPORT_ERRORS = { - -1 => {reason: :invalid_handle, status: -1, description: "An invalid handle was received. Handles should be created with pactffi_new_pact"}, - -2 => {reason: :invalid_transport_json, status: -2, description: "Transport_config is not valid JSON"}, - -3 => {reason: :mock_server_not_started, status: -3, description: "The mock server could not be started"}, - -4 => {reason: :internal_error, status: -4, description: "The method panicked"}, - -5 => {reason: :invalid_host, status: -5, description: "The address is not valid"} - }.freeze - - # https://docs.rs/pact_ffi/0.4.17/pact_ffi/mock_server/fn.pactffi_write_pact_file.html - WRITE_PACT_FILE_ERRORS = { - 1 => {reason: :internal_error, status: 1, description: "A general panic was caught"}, - 2 => {reason: :file_not_accessible, status: 2, description: "The pact file was not able to be written"}, - 3 => {reason: :mock_server_not_found, status: 3, description: "A mock server with the provided port was not found"} - }.freeze - - def self.create_for_grpc!(pact:, host: "127.0.0.1", port: 0) - new(pact: pact, transport: TRANSPORT_GRPC, host: host, port: port) - end - - def self.create_for_http!(pact:, host: "127.0.0.1", port: 0) - new(pact: pact, transport: TRANSPORT_HTTP, host: host, port: port) - end - - def self.create_for_transport!(pact:, transport:, host: "127.0.0.1", port: 0) - new(pact: pact, transport: transport, host: host, port: port) - end - - def initialize(pact:, transport:, host:, port:) - - @pact = pact - @transport = transport - @host = host - @port = port - - @handle = init_transport! - # the returned handle is the port number - # we set it here, so we can consume a port number of 0 - # and allow pact to assign a random available port - @port = @handle - # construct the url for the mock server - # as a convenience for the user - @url = "#{transport}://#{host}:#{@handle}" - # TODO: handle auto-GC of native memory - # ObjectSpace.define_finalizer(self, proc do - # cleanup - # end) - end - - def write_pacts!(dir) - result = PactFfi::MockServer.write_pact_file(@handle, dir, false) - return result if WRITE_PACT_FILE_ERRORS[result].blank? - - error = WRITE_PACT_FILE_ERRORS[result] - raise WritePactsError.new("There was an error while trying to write pact file to #{dir}", error[:reason], error[:status]) - end - - def matched? - PactFfi::MockServer.matched(@handle) - end - - def mismatches - PactFfi::MockServer.mismatches(@handle) - end - - def cleanup - PactFfi::MockServer.cleanup(@handle) - end - - def cleanup_plugins - PactFfi::PluginConsumer.cleanup_plugins(@handle) - end - - def free_pact_handle - PactFfi.free_pact_handle(@handle) - end - - private - - def init_transport! - handle = PactFfi::MockServer.create_for_transport(@pact, @host, @port, @transport, nil) - # the returned handle is the port number - return handle if CREATE_TRANSPORT_ERRORS[handle].blank? - - error = CREATE_TRANSPORT_ERRORS[handle] - raise MockServerCreateError.new("There was an error while trying to create mock server for transport:#{@transport}", error[:reason], error[:status]) - end - end - end - end -end diff --git a/lib/pact/v2/consumer/pact_config.rb b/lib/pact/v2/consumer/pact_config.rb deleted file mode 100644 index 599ef86b..00000000 --- a/lib/pact/v2/consumer/pact_config.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -require_relative "pact_config/grpc" - -module Pact - module V2 - module Consumer - module PactConfig - def self.new(transport_type, consumer_name:, provider_name:, opts: {}) - case transport_type - when :http - Http.new(consumer_name: consumer_name, provider_name: provider_name, opts: opts) - when :grpc - Grpc.new(consumer_name: consumer_name, provider_name: provider_name, opts: opts) - when :message - Message.new(consumer_name: consumer_name, provider_name: provider_name, opts: opts) - when :plugin_sync_message - PluginSyncMessage.new(consumer_name: consumer_name, provider_name: provider_name, opts: opts) - when :plugin_async_message - PluginAsyncMessage.new(consumer_name: consumer_name, provider_name: provider_name, opts: opts) - when :plugin_http - PluginHttp.new(consumer_name: consumer_name, provider_name: provider_name, opts: opts) - else - raise ArgumentError, "unknown transport_type: #{transport_type}" - end - end - end - end - end -end diff --git a/lib/pact/v2/consumer/pact_config/base.rb b/lib/pact/v2/consumer/pact_config/base.rb deleted file mode 100644 index f1250fa7..00000000 --- a/lib/pact/v2/consumer/pact_config/base.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -module Pact - module V2 - module Consumer - module PactConfig - class Base - attr_reader :consumer_name, :provider_name, :pact_dir, :log_level - - def initialize(consumer_name:, provider_name:, opts: {}) - @consumer_name = consumer_name - @provider_name = provider_name - @pact_dir = opts[:pact_dir] || (defined?(Rails) ? Rails.root.join("../pacts").to_s : "pacts") - @log_level = opts[:log_level] || :info - end - - def new_interaction(description = nil) - raise Pact::V2::ImplementationRequired, "#new_interaction should be implemented" - end - end - end - end - end -end diff --git a/lib/pact/v2/consumer/pact_config/grpc.rb b/lib/pact/v2/consumer/pact_config/grpc.rb deleted file mode 100644 index dd673c5f..00000000 --- a/lib/pact/v2/consumer/pact_config/grpc.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -require_relative "base" - -module Pact - module V2 - module Consumer - module PactConfig - class Grpc < Base - attr_reader :mock_host, :mock_port - - def initialize(consumer_name:, provider_name:, opts: {}) - super - - @mock_host = opts[:mock_host] || "127.0.0.1" - @mock_port = opts[:mock_port] || 3009 - end - - def new_interaction(description = nil) - GrpcInteractionBuilder.new(self, description: description) - end - end - end - end - end -end diff --git a/lib/pact/v2/consumer/pact_config/http.rb b/lib/pact/v2/consumer/pact_config/http.rb deleted file mode 100644 index ff0d767b..00000000 --- a/lib/pact/v2/consumer/pact_config/http.rb +++ /dev/null @@ -1,55 +0,0 @@ -# frozen_string_literal: true - -require_relative "base" - -module Pact - module V2 - module Consumer - module PactConfig - class Http < Base - attr_reader :mock_host, :mock_port, :pact_handle - - def initialize(consumer_name:, provider_name:, opts: {}) - super - - @mock_host = opts[:mock_host] || "127.0.0.1" - @mock_port = opts[:mock_port] || 0 - @log_level = opts[:log_level] || :info - @pact_specification = get_pact_specification(opts) - @pact_handle = init_pact - end - - def new_interaction(description = nil) - HttpInteractionBuilder.new(self, description: description) - end - - def reset_pact - @pact_handle = init_pact - end - - def get_pact_specification(opts) - pact_spec_version = opts[:pact_specification] || "V4" - unless pact_spec_version.match?(/^v?[1-4](\.\d+){0,2}$/i) - raise ArgumentError, "Invalid pact specification version format \n Valid versions are 1, 1.1, 2, 3, 4. Default is V4 \n V prefix is optional, and case insensitive" - end - pact_spec_version = pact_spec_version.upcase - pact_spec_version = "V#{pact_spec_version}" unless pact_spec_version.start_with?("V") - pact_spec_version = pact_spec_version.sub(/(\.0+)+$/, "") - pact_spec_version = pact_spec_version.tr(".", "_") - PactFfi::FfiSpecificationVersion["SPECIFICATION_VERSION_#{pact_spec_version.upcase}"] - end - - def init_pact - handle = PactFfi.new_pact(consumer_name, provider_name) - PactFfi.with_specification(handle, @pact_specification) - PactFfi.with_pact_metadata(handle, "pact-ruby-v2", "pact-ffi", PactFfi.version) - - Pact::V2::Native::Logger.log_to_stdout(@log_level) - - handle - end - end - end - end - end -end diff --git a/lib/pact/v2/consumer/pact_config/message.rb b/lib/pact/v2/consumer/pact_config/message.rb deleted file mode 100644 index 76aa34b9..00000000 --- a/lib/pact/v2/consumer/pact_config/message.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -require_relative "base" - -module Pact - module V2 - module Consumer - module PactConfig - class Message < Base - def new_interaction(description = nil) - MessageInteractionBuilder.new(self, description: description) - end - end - end - end - end -end diff --git a/lib/pact/v2/consumer/pact_config/plugin_async_message.rb b/lib/pact/v2/consumer/pact_config/plugin_async_message.rb deleted file mode 100644 index 1bb0f59a..00000000 --- a/lib/pact/v2/consumer/pact_config/plugin_async_message.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -require_relative "base" - -module Pact - module V2 - module Consumer - module PactConfig - class PluginAsyncMessage < Base - attr_reader :mock_host, :mock_port - - def initialize(consumer_name:, provider_name:, opts: {}) - super - - @mock_host = opts[:mock_host] || "127.0.0.1" - @mock_port = opts[:mock_port] || 0 - end - - def new_interaction(description = nil) - PluginAsyncMessageInteractionBuilder.new(self, description: description) - end - end - end - end - end -end diff --git a/lib/pact/v2/consumer/pact_config/plugin_http.rb b/lib/pact/v2/consumer/pact_config/plugin_http.rb deleted file mode 100644 index b7961cb5..00000000 --- a/lib/pact/v2/consumer/pact_config/plugin_http.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -require_relative "base" - -module Pact - module V2 - module Consumer - module PactConfig - class PluginHttp < Base - attr_reader :mock_host, :mock_port - - def initialize(consumer_name:, provider_name:, opts: {}) - super - - @mock_host = opts[:mock_host] || "127.0.0.1" - @mock_port = opts[:mock_port] || 0 - end - - def new_interaction(description = nil) - PluginHttpInteractionBuilder.new(self, description: description) - end - end - end - end - end -end diff --git a/lib/pact/v2/consumer/pact_config/plugin_sync_message.rb b/lib/pact/v2/consumer/pact_config/plugin_sync_message.rb deleted file mode 100644 index bc6c2d8c..00000000 --- a/lib/pact/v2/consumer/pact_config/plugin_sync_message.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -require_relative "base" - -module Pact - module V2 - module Consumer - module PactConfig - class PluginSyncMessage < Base - attr_reader :mock_host, :mock_port - - def initialize(consumer_name:, provider_name:, opts: {}) - super - - @mock_host = opts[:mock_host] || "127.0.0.1" - @mock_port = opts[:mock_port] || 0 - end - - def new_interaction(description = nil) - PluginSyncMessageInteractionBuilder.new(self, description: description) - end - end - end - end - end -end diff --git a/lib/pact/v2/consumer/plugin_async_message_interaction_builder.rb b/lib/pact/v2/consumer/plugin_async_message_interaction_builder.rb deleted file mode 100644 index 40b49617..00000000 --- a/lib/pact/v2/consumer/plugin_async_message_interaction_builder.rb +++ /dev/null @@ -1,171 +0,0 @@ -# frozen_string_literal: true - -require "pact/ffi/async_message_pact" -require "pact/ffi/plugin_consumer" -require "pact/ffi/logger" - -module Pact - module V2 - module Consumer - class PluginAsyncMessageInteractionBuilder - - class PluginInitError < Pact::V2::FfiError; end - - # https://docs.rs/pact_ffi/0.4.17/pact_ffi/plugins/fn.pactffi_using_plugin.html - INIT_PLUGIN_ERRORS = { - 1 => {reason: :internal_error, status: 1, description: "A general panic was caught"}, - 2 => {reason: :plugin_load_failed, status: 2, description: "Failed to load the plugin"}, - 3 => {reason: :invalid_handle, status: 3, description: "Pact Handle is not valid"} - }.freeze - - # https://docs.rs/pact_ffi/0.4.17/pact_ffi/plugins/fn.pactffi_interaction_contents.html - CREATE_INTERACTION_ERRORS = { - 1 => {reason: :internal_error, status: 1, description: "A general panic was caught"}, - 2 => {reason: :mock_server_already_running, status: 2, description: "The mock server has already been started"}, - 3 => {reason: :invalid_handle, status: 3, description: "The interaction handle is invalid"}, - 4 => {reason: :invalid_content_type, status: 4, description: "The content type is not valid"}, - 5 => {reason: :invalid_contents, status: 5, description: "The contents JSON is not valid JSON"}, - 6 => {reason: :plugin_error, status: 6, description: "The plugin returned an error"} - }.freeze - - class CreateInteractionError < Pact::V2::FfiError; end - - class InteractionMismatchesError < Pact::V2::Error; end - - class InteractionBuilderError < Pact::V2::Error; end - - def initialize(pact_config, description: nil) - @pact_config = pact_config - @description = description || "" - @contents = nil - @provider_state_meta = nil - end - - def with_plugin(plugin_name, plugin_version) - raise InteractionBuilderError.new("plugin_name is required") if plugin_name.blank? - raise InteractionBuilderError.new("plugin_version is required") if plugin_version.blank? - - @plugin_name = plugin_name - @plugin_version = plugin_version - self - end - - def given(provider_state, metadata = {}) - @provider_state_meta = {provider_state => metadata} - self - end - - def upon_receiving(description) - @description = description - self - end - - def with_contents(contents_hash) - @contents = InteractionContents.plugin(contents_hash) - self - end - - def with_content_type(content_type) - @interaction_content_type = content_type || @content_type - self - end - - def with_plugin_metadata(meta_hash) - @plugin_metadata = meta_hash - self - end - - def with_transport(transport) - @transport = transport - self - end - - def interaction_json - result = { - contents: @contents - } - result.merge!(@plugin_metadata) if @plugin_metadata.is_a?(Hash) - JSON.dump(result) - end - - def validate! - raise InteractionBuilderError.new("invalid contents format, should be a hash") unless @contents.is_a?(Hash) - end - - def execute(&block) - raise InteractionBuilderError.new("interaction is designed to be used one-time only") if defined?(@used) - - validate! - - pact_handle = init_pact - init_plugin!(pact_handle) - - interaction = PactFfi::AsyncMessageConsumer.new(pact_handle, @description) - - @provider_state_meta&.each_pair do |provider_state, meta| - if meta.present? - meta.each_pair do |k, v| - if v.nil? || (v.respond_to?(:empty?) && v.empty?) - PactFfi.given(interaction, provider_state) - else - PactFfi.given_with_param(interaction, provider_state, k.to_s, v.to_s) - end - end - else - PactFfi.given(interaction, provider_state) - end - end - - result = PactFfi.with_body(interaction, 0, @interaction_content_type, interaction_json) - if CREATE_INTERACTION_ERRORS[result].present? - error = CREATE_INTERACTION_ERRORS[result] - raise CreateInteractionError.new("There was an error while trying to add interaction \"#{@description}\"", error[:reason], error[:status]) - end - - mock_server = MockServer.create_for_transport!(pact: pact_handle, transport: @transport, host: @pact_config.mock_host, port: @pact_config.mock_port) - - yield(pact_handle, mock_server) - - ensure - if mock_server.matched? - mock_server.write_pacts!(@pact_config.pact_dir) - else - msg = mismatches_error_msg(mock_server) - raise InteractionMismatchesError.new(msg) - end - @used = true - mock_server&.cleanup - PactFfi::PluginConsumer.cleanup_plugins(pact_handle) if pact_handle - PactFfi.free_pact_handle(pact_handle) if pact_handle - end - - private - - def mismatches_error_msg(mock_server) - rspec_example_desc = RSpec.current_example&.description - return "interaction for has mismatches: #{mock_server.mismatches}" if rspec_example_desc.blank? - - "#{rspec_example_desc} has mismatches: #{mock_server.mismatches}" - end - - def init_pact - handle = PactFfi.new_pact(@pact_config.consumer_name, @pact_config.provider_name) - PactFfi.with_specification(handle, PactFfi::FfiSpecificationVersion["SPECIFICATION_VERSION_V4"]) - PactFfi.with_pact_metadata(handle, "pact-ruby-v2", "pact-ffi", PactFfi.version) - - Pact::V2::Native::Logger.log_to_stdout(@pact_config.log_level) - - handle - end - - def init_plugin!(pact_handle) - result = PactFfi::PluginConsumer.using_plugin(pact_handle, @plugin_name, @plugin_version) - return result if INIT_PLUGIN_ERRORS[result].blank? - - error = INIT_PLUGIN_ERRORS[result] - raise PluginInitError.new("There was an error while trying to initialize plugin #{@plugin_name}/#{@plugin_version}", error[:reason], error[:status]) - end - end - end - end -end diff --git a/lib/pact/v2/consumer/plugin_http_interaction_builder.rb b/lib/pact/v2/consumer/plugin_http_interaction_builder.rb deleted file mode 100644 index 0996d06a..00000000 --- a/lib/pact/v2/consumer/plugin_http_interaction_builder.rb +++ /dev/null @@ -1,201 +0,0 @@ -# frozen_string_literal: true - -require "pact/ffi/http_consumer" -require "pact/ffi/plugin_consumer" -require "pact/ffi/logger" - -module Pact - module V2 - module Consumer - class PluginHttpInteractionBuilder - - class PluginInitError < Pact::V2::FfiError; end - - # https://docs.rs/pact_ffi/0.4.17/pact_ffi/plugins/fn.pactffi_using_plugin.html - INIT_PLUGIN_ERRORS = { - 1 => {reason: :internal_error, status: 1, description: "A general panic was caught"}, - 2 => {reason: :plugin_load_failed, status: 2, description: "Failed to load the plugin"}, - 3 => {reason: :invalid_handle, status: 3, description: "Pact Handle is not valid"} - }.freeze - - # https://docs.rs/pact_ffi/0.4.17/pact_ffi/plugins/fn.pactffi_interaction_contents.html - CREATE_INTERACTION_ERRORS = { - 1 => {reason: :internal_error, status: 1, description: "A general panic was caught"}, - 2 => {reason: :mock_server_already_running, status: 2, description: "The mock server has already been started"}, - 3 => {reason: :invalid_handle, status: 3, description: "The interaction handle is invalid"}, - 4 => {reason: :invalid_content_type, status: 4, description: "The content type is not valid"}, - 5 => {reason: :invalid_contents, status: 5, description: "The contents JSON is not valid JSON"}, - 6 => {reason: :plugin_error, status: 6, description: "The plugin returned an error"} - }.freeze - - class CreateInteractionError < Pact::V2::FfiError; end - - class InteractionMismatchesError < Pact::V2::Error; end - - class InteractionBuilderError < Pact::V2::Error; end - - def initialize(pact_config, description: nil) - @pact_config = pact_config - @description = description || "" - @contents = nil - @provider_state_meta = nil - end - - def with_plugin(plugin_name, plugin_version) - raise InteractionBuilderError.new("plugin_name is required") if plugin_name.blank? - raise InteractionBuilderError.new("plugin_version is required") if plugin_version.blank? - - @plugin_name = plugin_name - @plugin_version = plugin_version - self - end - - def given(provider_state, metadata = {}) - @provider_state_meta = {provider_state => metadata} - self - end - - def upon_receiving(description) - @description = description - self - end - - def with_request(method: nil, path: nil, query: {}, headers: {}, body: nil) - @request = { - method: method, - path: path, - query: query, - headers: headers, - body: body - } - self - end - - def will_respond_with(status: nil, headers: {}, body: nil) - @response = { - status: status, - headers: headers, - body: body - } - self - end - - def with_content_type(content_type) - @content_type = content_type - self - end - - - def with_plugin_metadata(meta_hash) - @plugin_metadata = meta_hash - self - end - - def with_transport(transport) - @transport = transport - self - end - - def interaction_json - result = { - request: @request, - response: @response - } - result.merge!(@plugin_metadata) if @plugin_metadata.is_a?(Hash) - JSON.dump(result) - end - - def validate! - raise InteractionBuilderError.new("invalid request format, should be a hash") unless @request.is_a?(Hash) - raise InteractionBuilderError.new("invalid response format, should be a hash") unless @response.is_a?(Hash) - end - - def execute(&block) - raise InteractionBuilderError.new("interaction is designed to be used one-time only") if defined?(@used) - - validate! - - pact_handle = init_pact - init_plugin!(pact_handle) - - interaction = PactFfi.new_interaction(pact_handle, @description) - @provider_state_meta&.each_pair do |provider_state, meta| - if meta.present? - meta.each_pair do |k, v| - if v.nil? || (v.respond_to?(:empty?) && v.empty?) - PactFfi.given(interaction, provider_state) - else - PactFfi.given_with_param(interaction, provider_state, k.to_s, v.to_s) - end - end - else - PactFfi.given(interaction, provider_state) - end - end - PactFfi::HttpConsumer.with_request(interaction, @request[:method], @request[:path]) - - result = PactFfi::PluginConsumer.interaction_contents(interaction, 0, @request[:headers]["content-type"], format_value(@request[:body])) - if CREATE_INTERACTION_ERRORS[result].present? - error = CREATE_INTERACTION_ERRORS[result] - raise CreateInteractionError.new("There was an error while trying to add interaction \"#{@description}\"", error[:reason], error[:status]) - end - result = PactFfi::PluginConsumer.interaction_contents(interaction, 1, @response[:headers]["content-type"], format_value(@response[:body])) - if CREATE_INTERACTION_ERRORS[result].present? - error = CREATE_INTERACTION_ERRORS[result] - raise CreateInteractionError.new("There was an error while trying to add interaction \"#{@description}\"", error[:reason], error[:status]) - end - mock_server = MockServer.create_for_transport!(pact: pact_handle, transport: @transport || 'http', host: @pact_config.mock_host, port: @pact_config.mock_port) - - yield(mock_server) - - ensure - if mock_server.matched? - mock_server.write_pacts!(@pact_config.pact_dir) - else - msg = mismatches_error_msg(mock_server) - raise InteractionMismatchesError.new(msg) - end - @used = true - mock_server&.cleanup - PactFfi::PluginConsumer.cleanup_plugins(pact_handle) if pact_handle - PactFfi.free_pact_handle(pact_handle) if pact_handle - end - - private - - def mismatches_error_msg(mock_server) - rspec_example_desc = RSpec.current_example&.description - return "interaction for has mismatches: #{mock_server.mismatches}" if rspec_example_desc.blank? - - "#{rspec_example_desc} has mismatches: #{mock_server.mismatches}" - end - - def init_pact - handle = PactFfi.new_pact(@pact_config.consumer_name, @pact_config.provider_name) - PactFfi.with_specification(handle, PactFfi::FfiSpecificationVersion["SPECIFICATION_VERSION_V4"]) - PactFfi.with_pact_metadata(handle, "pact-ruby-v2", "pact-ffi", PactFfi.version) - - Pact::V2::Native::Logger.log_to_stdout(@pact_config.log_level) - - handle - end - - def init_plugin!(pact_handle) - result = PactFfi::PluginConsumer.using_plugin(pact_handle, @plugin_name, @plugin_version) - return result if INIT_PLUGIN_ERRORS[result].blank? - - error = INIT_PLUGIN_ERRORS[result] - raise PluginInitError.new("There was an error while trying to initialize plugin #{@plugin_name}/#{@plugin_version}", error[:reason], error[:status]) - end - - def format_value(obj) - return obj if obj.is_a?(String) - - return JSON.dump({value: obj}) if obj.is_a?(Array) - - JSON.dump(obj) - end - end - end - end -end diff --git a/lib/pact/v2/consumer/plugin_sync_message_interaction_builder.rb b/lib/pact/v2/consumer/plugin_sync_message_interaction_builder.rb deleted file mode 100644 index 8994f0ae..00000000 --- a/lib/pact/v2/consumer/plugin_sync_message_interaction_builder.rb +++ /dev/null @@ -1,180 +0,0 @@ -# frozen_string_literal: true - -require "pact/ffi/sync_message_consumer" -require "pact/ffi/plugin_consumer" -require "pact/ffi/logger" - -module Pact - module V2 - module Consumer - class PluginSyncMessageInteractionBuilder - - class PluginInitError < Pact::V2::FfiError; end - - # https://docs.rs/pact_ffi/0.4.17/pact_ffi/plugins/fn.pactffi_using_plugin.html - INIT_PLUGIN_ERRORS = { - 1 => {reason: :internal_error, status: 1, description: "A general panic was caught"}, - 2 => {reason: :plugin_load_failed, status: 2, description: "Failed to load the plugin"}, - 3 => {reason: :invalid_handle, status: 3, description: "Pact Handle is not valid"} - }.freeze - - # https://docs.rs/pact_ffi/0.4.17/pact_ffi/plugins/fn.pactffi_interaction_contents.html - CREATE_INTERACTION_ERRORS = { - 1 => {reason: :internal_error, status: 1, description: "A general panic was caught"}, - 2 => {reason: :mock_server_already_running, status: 2, description: "The mock server has already been started"}, - 3 => {reason: :invalid_handle, status: 3, description: "The interaction handle is invalid"}, - 4 => {reason: :invalid_content_type, status: 4, description: "The content type is not valid"}, - 5 => {reason: :invalid_contents, status: 5, description: "The contents JSON is not valid JSON"}, - 6 => {reason: :plugin_error, status: 6, description: "The plugin returned an error"} - }.freeze - - class CreateInteractionError < Pact::V2::FfiError; end - - class InteractionMismatchesError < Pact::V2::Error; end - - class InteractionBuilderError < Pact::V2::Error; end - - def initialize(pact_config, description: nil) - @pact_config = pact_config - @description = description || "" - @request = nil - @response = nil - @response_meta = nil - @provider_state_meta = nil - end - - def with_plugin(plugin_name, plugin_version) - raise InteractionBuilderError.new("plugin_name is required") if plugin_name.blank? - raise InteractionBuilderError.new("plugin_version is required") if plugin_version.blank? - - @plugin_name = plugin_name - @plugin_version = plugin_version - self - end - - def given(provider_state, metadata = {}) - @provider_state_meta = {provider_state => metadata} - self - end - - def upon_receiving(description) - @description = description - self - end - - def with_request(req_hash) - @request = InteractionContents.plugin(req_hash) - self - end - - def with_content_type(content_type) - @content_type = content_type - self - end - - def will_respond_with(resp_hash) - @response = InteractionContents.plugin(resp_hash) - self - end - - def will_respond_with_meta(meta_hash) - @response_meta = InteractionContents.plugin(meta_hash) - self - end - - def with_plugin_metadata(meta_hash) - @plugin_metadata = meta_hash - self - end - - def with_transport(transport) - @transport = transport - self - end - - def interaction_json - result = { - request: @request - } - result.merge!(@plugin_metadata) if @plugin_metadata.is_a?(Hash) - - result[:response] = @response if @response.is_a?(Hash) - result[:responseMetadata] = @response_meta if @response_meta.is_a?(Hash) - - JSON.dump(result) - end - - def validate! - raise InteractionBuilderError.new("invalid request format, should be a hash") unless @request.is_a?(Hash) - raise InteractionBuilderError.new("invalid response format, should be a hash") unless @response.is_a?(Hash) || @response_meta.is_a?(Hash) - end - - def execute(&block) - raise InteractionBuilderError.new("interaction is designed to be used one-time only") if defined?(@used) - - validate! - - pact_handle = init_pact - init_plugin!(pact_handle) - - message_pact = PactFfi::SyncMessageConsumer.new_interaction(pact_handle, @description) - @provider_state_meta&.each_pair do |provider_state, meta| - if meta.present? - meta.each_pair { |k, v| PactFfi.given_with_param(message_pact, provider_state, k.to_s, v.to_s) } - else - PactFfi.given(message_pact, provider_state) - end - end - result = PactFfi::PluginConsumer.interaction_contents(message_pact, 0, @content_type, interaction_json) - if CREATE_INTERACTION_ERRORS[result].present? - error = CREATE_INTERACTION_ERRORS[result] - raise CreateInteractionError.new("There was an error while trying to add interaction \"#{@description}\"", error[:reason], error[:status]) - end - - mock_server = MockServer.create_for_transport!(pact: pact_handle, transport: @transport, host: @pact_config.mock_host, port: @pact_config.mock_port) - - yield(message_pact, mock_server) - - ensure - if mock_server.matched? - mock_server.write_pacts!(@pact_config.pact_dir) - else - msg = mismatches_error_msg(mock_server) - raise InteractionMismatchesError.new(msg) - end - @used = true - mock_server&.cleanup - PactFfi::PluginConsumer.cleanup_plugins(pact_handle) - PactFfi.free_pact_handle(pact_handle) - end - - private - - def mismatches_error_msg(mock_server) - rspec_example_desc = RSpec.current_example&.description - return "interaction for has mismatches: #{mock_server.mismatches}" if rspec_example_desc.blank? - - "#{rspec_example_desc} has mismatches: #{mock_server.mismatches}" - end - - def init_pact - handle = PactFfi.new_pact(@pact_config.consumer_name, @pact_config.provider_name) - PactFfi.with_specification(handle, PactFfi::FfiSpecificationVersion["SPECIFICATION_VERSION_V4"]) - PactFfi.with_pact_metadata(handle, "pact-ruby-v2", "pact-ffi", PactFfi.version) - - Pact::V2::Native::Logger.log_to_stdout(@pact_config.log_level) - - handle - end - - def init_plugin!(pact_handle) - result = PactFfi::PluginConsumer.using_plugin(pact_handle, @plugin_name, @plugin_version) - return result if INIT_PLUGIN_ERRORS[result].blank? - - error = INIT_PLUGIN_ERRORS[result] - raise PluginInitError.new("There was an error while trying to initialize plugin #{@plugin_name}/#{@plugin_version}", error[:reason], error[:status]) - end - end - end - end -end diff --git a/lib/pact/v2/generators.rb b/lib/pact/v2/generators.rb deleted file mode 100644 index 3e244d47..00000000 --- a/lib/pact/v2/generators.rb +++ /dev/null @@ -1,49 +0,0 @@ -# frozen_string_literal: true - -module Pact - module V2 - module Generators - - def generate_random_int(min:, max:) - Pact::V2::Generators::RandomIntGenerator.new(min: min, max: max) - end - def generate_random_decimal(digits:) - Pact::V2::Generators::RandomDecimalGenerator.new(digits: digits) - end - def generate_random_hexadecimal(digits:) - Pact::V2::Generators::RandomHexadecimalGenerator.new(digits: digits) - end - def generate_random_string(size:) - Pact::V2::Generators::RandomStringGenerator.new(size: size) - end - - def generate_uuid(example: nil) - Pact::V2::Generators::UuidGenerator.new(example: example) - end - - def generate_date(format: nil, example: nil) - Pact::V2::Generators::DateGenerator.new(format: format, example: example) - end - - def generate_time(format: nil) - Pact::V2::Generators::TimeGenerator.new(format: format) - end - - def generate_datetime(format: nil) - Pact::V2::Generators::DateTimeGenerator.new(format: format) - end - - def generate_random_boolean - Pact::V2::Generators::RandomBooleanGenerator.new - end - - def generate_from_provider_state(expression:, example:) - Pact::V2::Generators::ProviderStateGenerator.new(expression: expression, example: example).as_basic - end - - def generate_mock_server_url(regex: nil, example: nil) - Pact::V2::Generators::MockServerURLGenerator.new(regex: regex, example: example) - end - end - end -end diff --git a/lib/pact/v2/generators/base.rb b/lib/pact/v2/generators/base.rb deleted file mode 100644 index fd21b75c..00000000 --- a/lib/pact/v2/generators/base.rb +++ /dev/null @@ -1,287 +0,0 @@ -# frozen_string_literal: true - -module Pact - module V2 - module Generators - module Base - def as_basic - raise NotImplementedError, "Subclasses must implement the as_basic method" - end - end - - class RandomIntGenerator - include Base - - def initialize(min:, max:) - @min = min - @max = max - end - - def as_basic - { - "pact:matcher:type" => "integer", - "pact:generator:type" => "RandomInt", - "min" => @min, - "max" => @max, - "value" => rand(@min..@max) - } - end - end - - class RandomDecimalGenerator - include Base - - def initialize(digits:) - @digits = digits - end - - def as_basic - { - 'pact:matcher:type' => 'decimal', - "pact:generator:type" => "RandomDecimal", - "digits" => @digits, - "value" => rand.round(@digits) - } - end - end - - class RandomHexadecimalGenerator - include Base - - def initialize(digits:) - @digits = digits - end - - def as_basic - { - "pact:matcher:type" => "decimal", - "pact:generator:type" => "RandomHexadecimal", - "digits" => @digits, - "value" => SecureRandom.hex((@digits / 2.0).ceil)[0...@digits] - } - end - end - - class RandomStringGenerator - include Base - - def initialize(size:, example: nil) - @size = size - @example = example - end - - def as_basic - { - "pact:matcher:type" => "type", - "pact:generator:type" => "RandomString", - "size" => @size, - "value" => @example || SecureRandom.alphanumeric(@size) - } - end - end - - class UuidGenerator - include Base - - - def initialize(example: nil) - @example = example - @regexStr = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'; - if @example - regex = Regexp.new("^#{@regexStr}$") - unless @example.match?(regex) - raise ArgumentError, "regex: Example value '#{@example}' does not match the UUID regular expression '#{@regexStr}'" - end - end - end - - def as_basic - { - "pact:matcher:type" => "regex", - "pact:generator:type" => "Uuid", - "regex" => @regexStr, - "value" => @example || SecureRandom.uuid - } - end - end - - class DateGenerator - include Base - - def initialize(format: nil, example: nil) - @format = format || default_format - @example = example || Time.now.strftime(convert_from_java_simple_date_format(@format)) - end - - def as_basic - h = { "pact:generator:type" => type } - h["pact:matcher:type"] = matcher_type - h["format"] = @format if @format - h["value"] = @example - h - end - - def type - 'Date' - end - - def matcher_type - 'date' - end - - def default_format - 'yyyy-MM-dd' - end - - # Converts Java SimpleDateFormat to Ruby strftime format - def convert_from_java_simple_date_format(format) - f = format.dup - # Year - f.gsub!(/(? "boolean", - "pact:generator:type" => "RandomBoolean", - "value" => @example.nil? ? [true, false].sample : @example - } - end - end - - class ProviderStateGenerator - include Base - - def initialize(expression:, example:) - @expression = expression - @value = example - end - - def as_basic - { - 'pact:matcher:type': 'type', - "pact:generator:type" => "ProviderState", - "expression" => @expression, - "value" => @value - } - end - end - - class MockServerURLGenerator - include Base - - def initialize(regex:, example:) - @regex = regex - @example = example - end - - def as_basic - { - "pact:generator:type" => "MockServerURL", - "pact:matcher:type" => "regex", - "regex" => @regex, - "example" => @example, - "value" => @example - } - end - end - end - end -end diff --git a/lib/pact/v2/matchers.rb b/lib/pact/v2/matchers.rb deleted file mode 100644 index 7886dce7..00000000 --- a/lib/pact/v2/matchers.rb +++ /dev/null @@ -1,110 +0,0 @@ -# frozen_string_literal: true - -module Pact - module V2 - module Matchers - PACT_SPEC_V1 = 1 - PACT_SPEC_V2 = 2 - PACT_SPEC_V3 = 3 - PACT_SPEC_V4 = 4 - - ANY_STRING_REGEX = /.*/ - UUID_REGEX = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i - - # simplified - ISO8601_REGEX = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)*(.\d{2}:\d{2})*/i - - def match_exactly(arg) - V1::Equality.new(arg) - end - - def match_type_of(arg) - V2::Type.new(arg) - end - - def match_include(arg) - V3::Include.new(arg) - end - - def match_any_string(sample = "any") - V2::Regex.new(ANY_STRING_REGEX, sample) - end - - def match_any_integer(sample = 10) - V3::Integer.new(sample) - end - - def match_any_decimal(sample = 10.0) - V3::Decimal.new(sample) - end - - def match_any_number(sample = 10.0) - V3::Number.new(sample) - end - - def match_any_boolean(sample = true) - V3::Boolean.new(sample) - end - - def match_uuid(sample = "e1d01e04-3a2b-4eed-a4fb-54f5cd257338") - V2::Regex.new(UUID_REGEX, sample) - end - - def match_regex(regex, sample) - V2::Regex.new(regex, sample) - end - - def match_datetime(format, sample) - V3::DateTime.new(format, sample) - end - - def match_iso8601(sample = "2024-08-12T12:25:00.243118+03:00") - V2::Regex.new(ISO8601_REGEX, sample) - end - - def match_date(format, sample) - V3::Date.new(format, sample) - end - - def match_time(format, sample) - V3::Time.new(format, sample) - end - - def match_each(template, min = nil) - V3::Each.new(template, min) - end - - def match_each_regex(regex, sample) - match_each_value(sample, match_regex(regex, sample)) - end - - def match_each_key(template, key_matchers) - V4::EachKey.new(key_matchers.is_a?(Array) ? key_matchers : [key_matchers], template) - end - - def match_each_value(template, value_matchers = V2::Type.new("")) - V4::EachValue.new(value_matchers.is_a?(Array) ? value_matchers : [value_matchers], template) - end - - def match_each_kv(template, key_matchers) - V4::EachKeyValue.new(key_matchers.is_a?(Array) ? key_matchers : [key_matchers], template) - end - - def match_semver(template = nil) - V3::Semver.new(template) - end - - def match_content_type(content_type, template = nil) - V3::ContentType.new(content_type, template: template) - end - - def match_not_empty(template = nil) - V4::NotEmpty.new(template) - end - - def match_status_code(template) - V4::StatusCode.new(template) - end - end - end -end diff --git a/lib/pact/v2/matchers/base.rb b/lib/pact/v2/matchers/base.rb deleted file mode 100644 index 1a2dd13f..00000000 --- a/lib/pact/v2/matchers/base.rb +++ /dev/null @@ -1,74 +0,0 @@ -# frozen_string_literal: true - -module Pact - module V2 - module Matchers - # see https://github.com/pact-foundation/pact-reference/blob/master/rust/pact_ffi/IntegrationJson.md - class Base - attr_reader :spec_version, :kind, :template, :opts - - class MatcherInitializationError < Pact::V2::Error; end - - def initialize(spec_version:, kind:, template: nil, opts: {}) - @spec_version = spec_version - @kind = kind - @template = template - @opts = opts - end - - def as_basic - result = { - "pact:matcher:type" => serialize!(@kind.deep_dup, :basic) - } - result["status"] = serialize!(@opts[:status].deep_dup, :basic) if @opts[:status] - result["value"] = serialize!(@template.deep_dup, :basic) unless @template.nil? - result.merge!(serialize!(@opts.deep_dup, :basic)) - result - end - - def as_plugin - params = @opts.values.map { |v| format_primitive(v) }.join(",") - value = format_primitive(@template) unless @template.nil? - - if @template.nil? - return "matching(#{@kind}#{params.present? ? ", #{params}" : ""})" - end - - return "matching(#{@kind}, #{params}, #{value})" if params.present? - - "matching(#{@kind}, #{value})" - end - - private - - def serialize!(data, format) - # serialize complex types recursively - case data - when TrueClass, FalseClass, Numeric, String - data - when Array - data.map { |v| serialize!(v, format) } - when Hash - data.transform_values { |v| serialize!(v, format) } - when Pact::V2::Matchers::Base - return data.as_basic if format == :basic - data.as_plugin if format == :plugin - else - data - end - end - - def format_primitive(arg) - case arg - when TrueClass, FalseClass, Numeric - arg.to_s - when String - "'#{arg}'" - else - raise "#{arg.class} is not a primitive" - end - end - end - end - end -end diff --git a/lib/pact/v2/matchers/v1/equality.rb b/lib/pact/v2/matchers/v1/equality.rb deleted file mode 100644 index 6ad34c21..00000000 --- a/lib/pact/v2/matchers/v1/equality.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -module Pact - module V2 - module Matchers - module V1 - class Equality < Pact::V2::Matchers::Base - def initialize(template) - super(spec_version: Pact::V2::Matchers::PACT_SPEC_V1, kind: "equality", template: template) - end - - def as_plugin - "matching(equalTo, #{format_primitive(@template)})" - end - end - end - end - end -end diff --git a/lib/pact/v2/matchers/v2/regex.rb b/lib/pact/v2/matchers/v2/regex.rb deleted file mode 100644 index 7ea56db4..00000000 --- a/lib/pact/v2/matchers/v2/regex.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -module Pact - module V2 - module Matchers - module V2 - class Regex < Pact::V2::Matchers::Base - def initialize(regex, template) - raise MatcherInitializationError, "#{self.class}: #{regex} should be an instance of Regexp" unless regex.is_a?(Regexp) - raise MatcherInitializationError, "#{self.class}: #{template} should be an instance of String or Array" unless template.is_a?(String) || template.is_a?(Array) - raise MatcherInitializationError, "#{self.class}: #{template} array values should be strings" if template.is_a?(Array) && !template.all?(String) - - super(spec_version: Pact::V2::Matchers::PACT_SPEC_V2, kind: "regex", template: template, opts: {regex: regex.to_s}) - end - end - end - end - end -end diff --git a/lib/pact/v2/matchers/v2/type.rb b/lib/pact/v2/matchers/v2/type.rb deleted file mode 100644 index 6aa1f389..00000000 --- a/lib/pact/v2/matchers/v2/type.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -module Pact - module V2 - module Matchers - module V2 - class Type < Pact::V2::Matchers::Base - def initialize(template) - raise MatcherInitializationError, "#{self.class}: template is not a primitive" unless template.is_a?(TrueClass) || template.is_a?(FalseClass) || template.is_a?(Numeric) || template.is_a?(String) || template.is_a?(Array) || template.is_a?(Hash) - - super(spec_version: Pact::V2::Matchers::PACT_SPEC_V2, kind: "type", template: template) - end - end - end - end - end -end diff --git a/lib/pact/v2/matchers/v3/boolean.rb b/lib/pact/v2/matchers/v3/boolean.rb deleted file mode 100644 index 142bf725..00000000 --- a/lib/pact/v2/matchers/v3/boolean.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -module Pact - module V2 - module Matchers - module V3 - class Boolean < Pact::V2::Matchers::Base - def initialize(template) - raise MatcherInitializationError, "#{self.class}: #{template} should be an instance of Boolean" unless template.is_a?(TrueClass) || template.is_a?(FalseClass) - - super(spec_version: Pact::V2::Matchers::PACT_SPEC_V3, kind: "boolean", template: template) - end - end - end - end - end -end diff --git a/lib/pact/v2/matchers/v3/content_type.rb b/lib/pact/v2/matchers/v3/content_type.rb deleted file mode 100644 index 0e95f42d..00000000 --- a/lib/pact/v2/matchers/v3/content_type.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -module Pact - module V2 - module Matchers - module V3 - class ContentType < Pact::V2::Matchers::Base - def initialize(content_type, template: nil) - @content_type = content_type - @template = template - @opts = {} - @opts[:plugin_template] = template unless template.nil? - unless content_type.is_a?(String) && !content_type.empty? - raise MatcherInitializationError, "#{self.class}: content_type must be a non-empty String" - end - - super( - spec_version: Pact::V2::Matchers::PACT_SPEC_V3, - kind: "contentType", - template: content_type, - opts: @opts - ) - end - - def as_plugin - "matching(contentType, '#{@content_type}', '#{@opts[:plugin_template]}')" - end - end - end - end - end -end diff --git a/lib/pact/v2/matchers/v3/date.rb b/lib/pact/v2/matchers/v3/date.rb deleted file mode 100644 index 4a8ad71f..00000000 --- a/lib/pact/v2/matchers/v3/date.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -module Pact - module V2 - module Matchers - module V3 - class Date < Pact::V2::Matchers::Base - def initialize(format, template) - raise MatcherInitializationError, "#{self.class}: #{format} should be an instance of String" unless template.is_a?(String) - raise MatcherInitializationError, "#{self.class}: #{template} should be an instance of String" unless template.is_a?(String) - - super(spec_version: Pact::V2::Matchers::PACT_SPEC_V3, kind: "date", template: template, opts: {format: format}) - end - end - end - end - end -end diff --git a/lib/pact/v2/matchers/v3/date_time.rb b/lib/pact/v2/matchers/v3/date_time.rb deleted file mode 100644 index 7cb0e265..00000000 --- a/lib/pact/v2/matchers/v3/date_time.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -module Pact - module V2 - module Matchers - module V3 - class DateTime < Pact::V2::Matchers::Base - def initialize(format, template) - raise MatcherInitializationError, "#{self.class}: #{format} should be an instance of String" unless template.is_a?(String) - raise MatcherInitializationError, "#{self.class}: #{template} should be an instance of String" unless template.is_a?(String) - - super(spec_version: Pact::V2::Matchers::PACT_SPEC_V3, kind: "datetime", template: template, opts: {format: format}) - end - end - end - end - end -end diff --git a/lib/pact/v2/matchers/v3/decimal.rb b/lib/pact/v2/matchers/v3/decimal.rb deleted file mode 100644 index 16aa2b9e..00000000 --- a/lib/pact/v2/matchers/v3/decimal.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -module Pact - module V2 - module Matchers - module V3 - class Decimal < Pact::V2::Matchers::Base - def initialize(template) - raise MatcherInitializationError, "#{self.class}: #{template} should be an instance of Float" unless template.is_a?(Float) - - super(spec_version: Pact::V2::Matchers::PACT_SPEC_V3, kind: "decimal", template: template) - end - end - end - end - end -end diff --git a/lib/pact/v2/matchers/v3/each.rb b/lib/pact/v2/matchers/v3/each.rb deleted file mode 100644 index d92fbee5..00000000 --- a/lib/pact/v2/matchers/v3/each.rb +++ /dev/null @@ -1,42 +0,0 @@ -# frozen_string_literal: true - -module Pact - module V2 - module Matchers - module V3 - class Each < Pact::V2::Matchers::Base - def initialize(template, min) - raise MatcherInitializationError, "#{self.class}: #{min} should be greater than 0" if min.present? && min < 1 - - min_array_size = min.presence || 1 - val = template.is_a?(Array) ? template : [template] * min_array_size - - raise MatcherInitializationError, "#{self.class}: #{min} is invalid: template size is #{val.size}" if min_array_size != val.size - - super( - spec_version: Pact::V2::Matchers::PACT_SPEC_V3, - kind: "type", - template: val, - opts: {min: min_array_size}) - end - - def as_plugin - if @template.first.is_a?(Hash) - return { - "pact:match" => "eachValue(matching($'SAMPLE'))", - "SAMPLE" => serialize!(@template.first.deep_dup, :plugin) - } - end - - params = @opts.except(:min).values.map { |v| format_primitive(v) }.join(",") - value = format_primitive(@template.first) - - return "eachValue(matching(#{@kind}, #{params}, #{value}))" if params.present? - - "eachValue(matching(#{@kind}, #{value}))" - end - end - end - end - end -end diff --git a/lib/pact/v2/matchers/v3/include.rb b/lib/pact/v2/matchers/v3/include.rb deleted file mode 100644 index 16d2a25b..00000000 --- a/lib/pact/v2/matchers/v3/include.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -module Pact - module V2 - module Matchers - module V3 - class Include < Pact::V2::Matchers::Base - def initialize(template) - raise MatcherInitializationError, "#{self.class}: #{template} should be an instance of String" unless template.is_a?(String) - - super(spec_version: Pact::V2::Matchers::PACT_SPEC_V3, kind: "include", template: template) - end - end - end - end - end -end diff --git a/lib/pact/v2/matchers/v3/integer.rb b/lib/pact/v2/matchers/v3/integer.rb deleted file mode 100644 index 1318ac77..00000000 --- a/lib/pact/v2/matchers/v3/integer.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -module Pact - module V2 - module Matchers - module V3 - class Integer < Pact::V2::Matchers::Base - def initialize(template) - raise MatcherInitializationError, "#{self.class}: #{template} should be an instance of Integer" unless template.is_a?(::Integer) - - super(spec_version: Pact::V2::Matchers::PACT_SPEC_V3, kind: "integer", template: template) - end - end - end - end - end -end diff --git a/lib/pact/v2/matchers/v3/null.rb b/lib/pact/v2/matchers/v3/null.rb deleted file mode 100644 index d059a163..00000000 --- a/lib/pact/v2/matchers/v3/null.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -module Pact - module V2 - module Matchers - module V3 - class Null < Pact::V2::Matchers::Base - def initialize - super(spec_version: Pact::V2::Matchers::PACT_SPEC_V3, kind: "null", template: nil) - end - - end - end - end - end -end diff --git a/lib/pact/v2/matchers/v3/number.rb b/lib/pact/v2/matchers/v3/number.rb deleted file mode 100644 index ca5d234c..00000000 --- a/lib/pact/v2/matchers/v3/number.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -module Pact - module V2 - module Matchers - module V3 - class Number < Pact::V2::Matchers::Base - def initialize(template) - raise MatcherInitializationError, "#{self.class}: #{template} should be an instance of Numeric" unless template.is_a?(Numeric) - - super(spec_version: Pact::V2::Matchers::PACT_SPEC_V3, kind: "number", template: template) - end - end - end - end - end -end diff --git a/lib/pact/v2/matchers/v3/semver.rb b/lib/pact/v2/matchers/v3/semver.rb deleted file mode 100644 index d706f82b..00000000 --- a/lib/pact/v2/matchers/v3/semver.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -module Pact - module V2 - module Matchers - module V3 - class Semver < Pact::V2::Matchers::Base - def initialize(template = nil) - @template = template - super(spec_version: Pact::V2::Matchers::PACT_SPEC_V3, kind: "semver", template: template) - end - - def as_plugin - if @template.nil? || @template.blank? - raise MatcherInitializationError, "#{self.class}: template must be provided when calling as_plugin" - end - "matching(semver, '#{@template}')" - end - end - end - end - end -end diff --git a/lib/pact/v2/matchers/v3/time.rb b/lib/pact/v2/matchers/v3/time.rb deleted file mode 100644 index 4664b905..00000000 --- a/lib/pact/v2/matchers/v3/time.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -module Pact - module V2 - module Matchers - module V3 - class Time < Pact::V2::Matchers::Base - def initialize(format, template) - raise MatcherInitializationError, "#{self.class}: #{format} should be an instance of String" unless template.is_a?(String) - raise MatcherInitializationError, "#{self.class}: #{template} should be an instance of String" unless template.is_a?(String) - - super(spec_version: Pact::V2::Matchers::PACT_SPEC_V3, kind: "time", template: template, opts: {format: format}) - end - end - end - end - end -end diff --git a/lib/pact/v2/matchers/v3/values.rb b/lib/pact/v2/matchers/v3/values.rb deleted file mode 100644 index 0ce965ff..00000000 --- a/lib/pact/v2/matchers/v3/values.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -module Pact - module V2 - module Matchers - module V3 - class Values < Pact::V2::Matchers::Base - def initialize - super(spec_version: Pact::V2::Matchers::PACT_SPEC_V3, kind: "values", template: nil) - end - - end - end - end - end -end diff --git a/lib/pact/v2/matchers/v4/each_key.rb b/lib/pact/v2/matchers/v4/each_key.rb deleted file mode 100644 index c1a18070..00000000 --- a/lib/pact/v2/matchers/v4/each_key.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -module Pact - module V2 - module Matchers - module V4 - class EachKey < Pact::V2::Matchers::Base - def initialize(key_matchers, template) - raise MatcherInitializationError, "#{self.class}: #{template} should be a Hash" unless template.is_a?(Hash) - raise MatcherInitializationError, "#{self.class}: #{key_matchers} should be an Array" unless key_matchers.is_a?(Array) - raise MatcherInitializationError, "#{self.class}: #{key_matchers} should be instances of Pact::V2::Matchers::Base" unless key_matchers.all?(Pact::V2::Matchers::Base) - raise MatcherInitializationError, "#{self.class}: #{key_matchers} size should be greater than 0" unless key_matchers.size > 0 - - super(spec_version: Pact::V2::Matchers::PACT_SPEC_V4, kind: "each-key", template: template, opts: {rules: key_matchers}) - end - - def as_plugin - @opts[:rules].map do |matcher| - "eachKey(#{matcher.as_plugin})" - end.join(", ") - end - end - end - end - end -end diff --git a/lib/pact/v2/matchers/v4/each_key_value.rb b/lib/pact/v2/matchers/v4/each_key_value.rb deleted file mode 100644 index 996cf1dc..00000000 --- a/lib/pact/v2/matchers/v4/each_key_value.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -module Pact - module V2 - module Matchers - module V4 - class EachKeyValue < Pact::V2::Matchers::Base - def initialize(key_matchers, template) - raise MatcherInitializationError, "#{self.class}: #{template} should be a Hash" unless template.is_a?(Hash) - raise MatcherInitializationError, "#{self.class}: #{key_matchers} should be an Array" unless key_matchers.is_a?(Array) - raise MatcherInitializationError, "#{self.class}: #{key_matchers} should be instances of Pact::V2::Matchers::Base" unless key_matchers.all?(Pact::V2::Matchers::Base) - - super( - spec_version: Pact::V2::Matchers::PACT_SPEC_V4, - kind: [ - EachKey.new(key_matchers, {}), - EachValue.new([Pact::V2::Matchers::V2::Type.new("")], {}) - ], - template: template - ) - - @key_matchers = key_matchers - end - - def as_plugin - raise MatcherInitializationError, "#{self.class}: each-key-value is not supported in plugin syntax. Use each / each_key / each_value matchers instead" - end - end - end - end - end -end diff --git a/lib/pact/v2/matchers/v4/each_value.rb b/lib/pact/v2/matchers/v4/each_value.rb deleted file mode 100644 index a7d90044..00000000 --- a/lib/pact/v2/matchers/v4/each_value.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -module Pact - module V2 - module Matchers - module V4 - class EachValue < Pact::V2::Matchers::Base - def initialize(value_matchers, template) - # raise MatcherInitializationError, "#{self.class}: #{template} should be a Hash" unless template.is_a?(Hash) - raise MatcherInitializationError, "#{self.class}: #{value_matchers} should be an Array" unless value_matchers.is_a?(Array) - raise MatcherInitializationError, "#{self.class}: #{value_matchers} should be instances of Pact::V2::Matchers::Base" unless value_matchers.all?(Pact::V2::Matchers::Base) - raise MatcherInitializationError, "#{self.class}: #{value_matchers} size should be greater than 0" unless value_matchers.size > 0 - - super(spec_version: Pact::V2::Matchers::PACT_SPEC_V4, kind: "each-value", template: template, opts: {rules: value_matchers}) - end - - def as_plugin - if @template.is_a?(Hash) - return { - "pact:match" => "eachValue(matching($'SAMPLE'))", - "SAMPLE" => serialize!(@template.deep_dup, :plugin) - } - end - - @opts[:rules].map do |matcher| - "eachValue(#{matcher.as_plugin})" - end.join(", ") - end - end - end - end - end -end diff --git a/lib/pact/v2/matchers/v4/not_empty.rb b/lib/pact/v2/matchers/v4/not_empty.rb deleted file mode 100644 index dfeca347..00000000 --- a/lib/pact/v2/matchers/v4/not_empty.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -module Pact - module V2 - module Matchers - module V4 - class NotEmpty < Pact::V2::Matchers::Base - def initialize(template = nil) - @template = template - super(spec_version: Pact::V2::Matchers::PACT_SPEC_V4, kind: 'notEmpty', template: @template) - end - - def as_plugin - if @template.nil? || @template.blank? - raise MatcherInitializationError, "#{self.class}: template must be provided when calling as_plugin" - end - - "notEmpty('#{@template}')" - end - end - end - end - end -end diff --git a/lib/pact/v2/matchers/v4/status_code.rb b/lib/pact/v2/matchers/v4/status_code.rb deleted file mode 100644 index efdc0837..00000000 --- a/lib/pact/v2/matchers/v4/status_code.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -module Pact - module V2 - module Matchers - module V4 - class StatusCode < Pact::V2::Matchers::Base - def initialize(template = nil) - super(spec_version: Pact::V2::Matchers::PACT_SPEC_V4, kind: 'statusCode', opts: { - 'status' => template - }) - end - end - end - end - end -end diff --git a/lib/pact/v2/native/blocking_verifier.rb b/lib/pact/v2/native/blocking_verifier.rb deleted file mode 100644 index 818e7144..00000000 --- a/lib/pact/v2/native/blocking_verifier.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -require "ffi" -require "pact/ffi/verifier" - -module Pact - module V2 - module Native - module BlockingVerifier - extend FFI::Library - ffi_lib DetectOS.get_bin_path - - attach_function :execute, :pactffi_verifier_execute, %i[pointer], :int32, blocking: true - end - end - end -end diff --git a/lib/pact/v2/native/logger.rb b/lib/pact/v2/native/logger.rb deleted file mode 100644 index 2ec88986..00000000 --- a/lib/pact/v2/native/logger.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -require "pact/ffi/logger" - -module Pact - module V2 - module Native - module Logger - LOG_LEVELS = { - off: PactFfi::FfiLogLevelFilter["LOG_LEVEL_OFF"], - error: PactFfi::FfiLogLevelFilter["LOG_LEVEL_ERROR"], - warn: PactFfi::FfiLogLevelFilter["LOG_LEVEL_WARN"], - info: PactFfi::FfiLogLevelFilter["LOG_LEVEL_INFO"], - debug: PactFfi::FfiLogLevelFilter["LOG_LEVEL_DEBUG"], - trace: PactFfi::FfiLogLevelFilter["LOG_LEVEL_TRACE"] - }.freeze - - def self.log_to_stdout(log_level) - raise "invalid log level for PactFfi::FfiLogLevelFilter" unless LOG_LEVELS.key?(log_level) - PactFfi::Logger.log_to_stdout(LOG_LEVELS[log_level]) unless log_level == :off - end - end - end - end -end diff --git a/lib/pact/v2/provider.rb b/lib/pact/v2/provider.rb deleted file mode 100644 index 973c675f..00000000 --- a/lib/pact/v2/provider.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -module Pact - module V2 - module Provider - end - end -end diff --git a/lib/pact/v2/provider/async_message_verifier.rb b/lib/pact/v2/provider/async_message_verifier.rb deleted file mode 100644 index 26a2430f..00000000 --- a/lib/pact/v2/provider/async_message_verifier.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -require "pact/ffi/verifier" -require "pact/v2/native/logger" - -module Pact - module V2 - module Provider - class AsyncMessageVerifier < BaseVerifier - PROVIDER_TRANSPORT_TYPE = "message" - - def initialize(pact_config, mixed_config = nil) - super - - raise ArgumentError, "pact_config must be an instance of Pact::V2::Provider::PactConfig::Message" unless pact_config.is_a?(::Pact::V2::Provider::PactConfig::Async) - end - - private - - def add_provider_transport(pact_handle) - setup_uri = URI(@pact_config.message_setup_url) - PactFfi::Verifier.add_provider_transport(pact_handle, PROVIDER_TRANSPORT_TYPE, setup_uri.port, setup_uri.path, "") - end - - end - end - end -end diff --git a/lib/pact/v2/provider/base_verifier.rb b/lib/pact/v2/provider/base_verifier.rb deleted file mode 100644 index ba111aaa..00000000 --- a/lib/pact/v2/provider/base_verifier.rb +++ /dev/null @@ -1,242 +0,0 @@ -# frozen_string_literal: true - -require "pact/ffi/verifier" -require "pact/v2/native/logger" -require "pact/v2/native/blocking_verifier" - -module Pact - module V2 - module Provider - class BaseVerifier - PROVIDER_TRANSPORT_TYPE = nil - attr_reader :logger - - class VerificationError < Pact::V2::FfiError; end - - class VerifierError < Pact::V2::Error; end - - DEFAULT_CONSUMER_SELECTORS = {} - - # https://docs.rs/pact_ffi/0.4.17/pact_ffi/verifier/fn.pactffi_verify.html#errors - VERIFICATION_ERRORS = { - 1 => {reason: :verification_failed, status: 1, description: "The verification process failed, see output for errors"}, - 2 => {reason: :null_pointer, status: 2, description: "A null pointer was received"}, - 3 => {reason: :internal_error, status: 3, description: "The method panicked"}, - 4 => {reason: :invalid_arguments, status: 4, description: "Invalid arguments were provided to the verification process"} - }.freeze - - # env below are set up by pipeline-builder - # see paas/cicd/images/pact/pipeline-builder/-/blob/master/internal/commands/consumers-pipeline/ruby.go - def initialize(pact_config, mixed_config = nil) - raise ArgumentError, "pact_config must be a subclass of Pact::V2::Provider::PactConfig::Base" unless pact_config.is_a?(::Pact::V2::Provider::PactConfig::Base) - @pact_config = pact_config - @mixed_config = mixed_config - @logger = @pact_config.logger || Logger.new($stdout) - end - - def verify! - raise VerifierError.new("interaction is designed to be used one-time only") if defined?(@used) - - # if consumer_selectors.blank? - # logger.info("[verifier] does not need to verify consumer #{@pact_config.consumer_name}") - # return - # end - - exception = nil - pact_handle = init_pact - - start_servers! - - logger.info("[verifier] starting provider verification") - - result = Pact::V2::Native::BlockingVerifier.execute(pact_handle) - if VERIFICATION_ERRORS[result].present? - error = VERIFICATION_ERRORS[result] - exception = VerificationError.new("There was an error while trying to verify provider \"#{@pact_config.provider_name}\"", error[:reason], error[:status]) - end - ensure - @used = true - PactFfi::Verifier.shutdown(pact_handle) if pact_handle - stop_servers - @grpc_server.stop if @grpc_server - raise exception if exception - end - - private - - def create_c_pointer_array_from_string_array(string_array) - pointers = string_array.map { |str| FFI::MemoryPointer.from_string(str) } - array_pointer = FFI::MemoryPointer.new(:pointer, pointers.size) - pointers.each_with_index do |ptr, index| - array_pointer[index].put_pointer(0, ptr) - end - array_pointer - end - - def bool_to_int(value) - value ? 1 : 0 - end - - def init_pact - handle = PactFfi::Verifier.new_for_application("pact-ruby-v2", PactFfi.version) - set_provider_info(handle) - - if defined?(@mixed_config.grpc_config) && @mixed_config.grpc_config - @grpc_server = GrufServer.new(host: "127.0.0.1:#{@mixed_config.grpc_config.grpc_port}", services: @mixed_config.grpc_config.grpc_services) - @grpc_server.start - PactFfi::Verifier.add_provider_transport(handle, "grpc", @mixed_config.grpc_config.grpc_port, "", "") - end - - if defined?(@mixed_config.async_config) && @mixed_config.async_config - setup_uri = URI(@mixed_config.async_config.message_setup_url) - PactFfi::Verifier.add_provider_transport(handle, "message", setup_uri.port, setup_uri.path, "") - end - - # todo: add http transport? - - PactFfi::Verifier.set_provider_state(handle, @pact_config.provider_setup_url, 1, 1) - PactFfi::Verifier.set_verification_options(handle, 0, 10000) - # pactffi_verifier_set_publish_options( - # handle: *mut VerifierHandle, - # provider_version: *const c_char, - # build_url: *const c_char, - # provider_tags: *const *const c_char, - # provider_tags_len: c_ushort, - # provider_branch: *const c_char, - # ) - c_provider_version_tags = create_c_pointer_array_from_string_array(@pact_config.provider_version_tags) - c_provider_version_tags_size = @pact_config.provider_version_tags.size - c_consumer_version_tags = create_c_pointer_array_from_string_array(@pact_config.consumer_version_tags) - c_consumer_version_tags_size = @pact_config.consumer_version_tags.size - - if @pact_config.provider_build_uri.present? - begin - URI.parse(@pact_config.provider_build_uri) - rescue URI::InvalidURIError - raise VerifierError.new("provider_build_uri is not a valid URI") - end - end - - if @pact_config.publish_verification_results == true - if @pact_config.provider_version - PactFfi::Verifier.set_publish_options(handle, @pact_config.provider_version, @pact_config.provider_build_uri, c_provider_version_tags, c_provider_version_tags_size, @pact_config.provider_version_branch) - else - logger.warn("[verifier] - unable to publish verification results as provider version is not set") - end - end - - configure_verification_source(handle, c_provider_version_tags, c_provider_version_tags_size, c_consumer_version_tags, c_consumer_version_tags_size) - - PactFfi::Verifier.set_no_pacts_is_error(handle, bool_to_int(@pact_config.fail_if_no_pacts_found)) - - add_provider_transport(handle) - - # the core doesnt pick up these env vars, so we need to set them here - # https://github.com/pact-foundation/pact-reference/issues/451#issuecomment-2338130587 - # PACT_DESCRIPTION - # Only validate interactions whose descriptions match this filter (regex format) - # PACT_PROVIDER_STATE - # Only validate interactions whose provider states match this filter (regex format) - # PACT_PROVIDER_NO_STATE - # Only validate interactions that have no defined provider state (true or false) - PactFfi::Verifier.set_filter_info( - handle, - ENV["PACT_DESCRIPTION"] || nil, - ENV["PACT_PROVIDER_STATE"] || nil, - bool_to_int(ENV["PACT_PROVIDER_NO_STATE"] || false) - ) - - Pact::V2::Native::Logger.log_to_stdout(@pact_config.log_level) - - logger.info("[verifier] verification initialized for provider #{@pact_config.provider_name}, version #{@pact_config.provider_version}, transport #{self.class::PROVIDER_TRANSPORT_TYPE}") - - handle - end - - def set_provider_info(pact_handle) - # pub extern "C" fn pactffi_verifier_set_provider_info( - # handle: *mut VerifierHandle, - # name: *const c_char, - # scheme: *const c_char, - # host: *const c_char, - # port: c_ushort, - # path: *const c_char, - # ) { - PactFfi::Verifier.set_provider_info(pact_handle, @pact_config.provider_name, "", "", 0, "") - end - - def add_provider_transport(pact_handle) - raise Pact::V2::ImplementationRequired, "Implement #add_provider_transport in a subclass" - end - - def start_servers! - logger.info("[verifier] starting services") - - @servers_started = true - @pact_config.start_servers - end - - def stop_servers - return unless @servers_started - - logger.info("[verifier] stopping services") - - @pact_config.stop_servers - end - - def configure_verification_source(handle, c_provider_version_tags, c_provider_version_tags_size, c_consumer_version_tags, c_consumer_version_tags_size) - logger.info("[verifier] configuring verification source") - if @pact_config.pact_broker_proxy_url.blank? && @pact_config.pact_uri.blank? - # todo support non rail apps - path = @pact_config.pact_dir || (defined?(Rails) ? Rails.root.join("pacts").to_s : "pacts") - logger.info("[verifier] pact broker url or pact uri is not set, using directory #{path} as a verification source") - return PactFfi::Verifier.add_directory_source(handle, path) - end - - if @pact_config.pact_uri.present? - if @pact_config.pact_uri.start_with?("http") - logger.info("[verifier] using pact uri #{@pact_config.pact_uri} as a verification source") - PactFfi::Verifier.url_source(handle, @pact_config.pact_uri, @pact_config.broker_username, @pact_config.broker_password, @pact_config.broker_token) - else - logger.info("[verifier] using pact file #{@pact_config.pact_uri} as a verification source") - PactFfi::Verifier.add_file_source(handle, @pact_config.pact_uri) - end - else - logger.info("[verifier] using pact broker url #{@pact_config.broker_url} with consumer selectors: #{JSON.dump(consumer_selectors)} as a verification source") - consumer_selectors = [] if consumer_selectors.nil? - filters = consumer_selectors.map do |selector| - FFI::MemoryPointer.from_string(JSON.dump(selector).to_s) - end - filters_ptr = FFI::MemoryPointer.new(:pointer, filters.size + 1) - filters_ptr.write_array_of_pointer(filters) - PactFfi::Verifier.broker_source_with_selectors(handle, @pact_config.pact_broker_proxy_url, @pact_config.broker_username, @pact_config.broker_password, @pact_config.broker_token, bool_to_int(@pact_config.enable_pending), @pact_config.include_wip_pacts_since, c_provider_version_tags, c_provider_version_tags_size, @pact_config.provider_version_branch, filters_ptr, consumer_selectors.size, c_consumer_version_tags, c_consumer_version_tags_size) - end - end - - def consumer_selectors - (!@pact_config.consumer_version_selectors.empty? && @pact_config.consumer_version_selectors) || @consumer_selectors if @pact_config.consumer_version_selectors - end - - def build_consumer_selectors(verify_only, consumer_name, consumer_branch) - # if verify_only and consumer_name are defined - select only needed consumer - if verify_only.present? - # select proper consumer branch if defined - if consumer_name.present? - return [] unless verify_only.include?(consumer_name) - return [{"branch" => consumer_branch, "consumer" => consumer_name}] if consumer_branch.present? - return [DEFAULT_CONSUMER_SELECTORS.merge("consumer" => consumer_name)] - end - # or default selectors - return verify_only.map { |name| DEFAULT_CONSUMER_SELECTORS.merge("consumer" => name) } - end - - # select provided consumer_name - return [{"branch" => consumer_branch, "consumer" => consumer_name}] if consumer_name.present? && consumer_branch.present? - return [DEFAULT_CONSUMER_SELECTORS.merge("consumer" => consumer_name)] if consumer_name.present? - - [DEFAULT_CONSUMER_SELECTORS] - end - end - end - end -end diff --git a/lib/pact/v2/provider/grpc_verifier.rb b/lib/pact/v2/provider/grpc_verifier.rb deleted file mode 100644 index c16a75c3..00000000 --- a/lib/pact/v2/provider/grpc_verifier.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -require "pact/ffi/verifier" -require "pact/v2/native/logger" - -module Pact - module V2 - module Provider - class GrpcVerifier < BaseVerifier - PROVIDER_TRANSPORT_TYPE = "grpc" - - def initialize(pact_config, mixed_config = nil) - super - - raise ArgumentError, "pact_config must be an instance of Pact::V2::Provider::PactConfig::Grpc" unless pact_config.is_a?(::Pact::V2::Provider::PactConfig::Grpc) - @grpc_server = GrufServer.new(host: "127.0.0.1:#{@pact_config.grpc_port}", services: @pact_config.grpc_services, logger: @pact_config.logger) - end - - private - - def add_provider_transport(pact_handle) - PactFfi::Verifier.add_provider_transport(pact_handle, PROVIDER_TRANSPORT_TYPE, @pact_config.grpc_port, "", "") - end - - - def start_servers! - super - @grpc_server.start - end - - def stop_servers - super - @grpc_server.stop - end - end - end - end -end diff --git a/lib/pact/v2/provider/gruf_server.rb b/lib/pact/v2/provider/gruf_server.rb deleted file mode 100644 index 925df4b3..00000000 --- a/lib/pact/v2/provider/gruf_server.rb +++ /dev/null @@ -1,75 +0,0 @@ -# frozen_string_literal: true - -module Pact - module V2 - module Provider - # inspired by Gruf::Cli::Executor - class GrufServer - SERVER_STOP_TIMEOUT_SEC = 15 - - def initialize(options = {}) - @options = options - - setup! - - @server_pid = nil - - @services = @options[:services].is_a?(Array) ? @options[:services] : [] - @logger = @options[:logger] || ::Logger.new($stdout) - end - - def start - raise "server already running, stop server before starting new one" if @thread - - @logger.info("[gruf] starting standalone server with options: #{@options}") - - @server = Gruf::Server.new(Gruf.server_options) - @services.each { |s| @server.add_service(s) } if @services.any? - @thread = Thread.new do - @logger.debug "[gruf] starting grpc server" - @server.start! - end - @server.server.wait_till_running(10) - - @logger.info("[gruf] standalone server started") - end - - def stop - @logger.info("[gruf] stopping standalone server") - - @server&.server&.stop - @thread&.join(SERVER_STOP_TIMEOUT_SEC) - @thread&.kill - - @logger.info("[gruf] standalone server stopped") - end - - ## - # Run the server - # - def run - start - - yield - rescue => e - @logger.fatal("FATAL ERROR: #{e.message} #{e.backtrace.join("\n")}") - raise - ensure - stop - end - - private - - def setup! - Gruf.server_binding_url = @options[:host] if @options[:host] - if @options[:suppress_default_interceptors] - Gruf.interceptors.remove(Gruf::Interceptors::ActiveRecord::ConnectionReset) - Gruf.interceptors.remove(Gruf::Interceptors::Instrumentation::OutputMetadataTimer) - end - Gruf.backtrace_on_error = true if @options[:backtrace_on_error] - Gruf.health_check_enabled = true if @options[:health_check] - end - end - end - end -end diff --git a/lib/pact/v2/provider/http_server.rb b/lib/pact/v2/provider/http_server.rb deleted file mode 100644 index 6d87c0af..00000000 --- a/lib/pact/v2/provider/http_server.rb +++ /dev/null @@ -1,79 +0,0 @@ -# frozen_string_literal: true - -module Pact - module V2 - module Provider - # inspired by Gruf::Cli::Executor - class HttpServer - SERVER_STOP_TIMEOUT_SEC = 15 - - def initialize(options = {}) - @options = options - - @server_pid = nil - - @host = @options[:host] || "localhost" - @logger = @options[:logger] || ::Logger.new($stdout) - # allow any rack based app to be passed in, otherwise - # we will load a Rails.application - # allows for backwards compat with pact-ruby v1 - @app = @options[:app] || nil - end - - def start - raise "server already running, stop server before starting new one" if @thread - - @logger.info("[webrick] starting server with options: #{@options}") - - @thread = Thread.new do - @logger.debug "[webrick] starting http server" - - # TODO: load from config.ru, if not rails and no app provided? - # Rack 2/3 compatibility - begin - require 'rack/handler/webrick' - handler = ::Rack::Handler::WEBrick - rescue LoadError - require 'rackup/handler/webrick' - handler = Class.new(Rackup::Handler::WEBrick) - end - handler.run(@app || (defined?(Rails) ? Rails.application : nil), - Host: @options[:host], - Port: @options[:port], - Logger: @logger, - StartCallback: -> { @started = true }) do |server| - @server = server - end - end - sleep 0.001 until @started - - @logger.info("[webrick] server started") - end - - def stop - @logger.info("[webrick] stopping server") - - @server&.shutdown - @thread&.join(SERVER_STOP_TIMEOUT_SEC) - @thread&.kill - - @logger.info("[webrick] server stopped") - end - - ## - # Run the server - # - def run - start - - yield - rescue => e - @logger.fatal("FATAL ERROR: #{e.message} #{e.backtrace.join("\n")}") - raise - ensure - stop - end - end - end - end -end diff --git a/lib/pact/v2/provider/http_verifier.rb b/lib/pact/v2/provider/http_verifier.rb deleted file mode 100644 index 3d207af2..00000000 --- a/lib/pact/v2/provider/http_verifier.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true - -require "pact/ffi/verifier" -require "pact/v2/native/logger" - -module Pact - module V2 - module Provider - class HttpVerifier < BaseVerifier - PROVIDER_TRANSPORT_TYPE = "http" - - def initialize(pact_config, mixed_config = nil) - super - - raise ArgumentError, "pact_config must be an instance of Pact::V2::Provider::PactConfig::Http" unless pact_config.is_a?(::Pact::V2::Provider::PactConfig::Http) - @http_server = HttpServer.new(host: "127.0.0.1", port: @pact_config.http_port, app: @pact_config.app, logger: @pact_config.logger) - end - - private - - def set_provider_info(pact_handle) - PactFfi::Verifier.set_provider_info(pact_handle, @pact_config.provider_name, "", "", @pact_config.http_port, "") - end - - def add_provider_transport(pact_handle) - # The http transport is already added when the `set_provider_info` method is called, - # so we don't need to explicitly add the transport here - end - - - def start_servers! - super - @http_server.start - end - - def stop_servers - super - @http_server.stop - end - end - end - end -end diff --git a/lib/pact/v2/provider/message_provider_servlet.rb b/lib/pact/v2/provider/message_provider_servlet.rb deleted file mode 100644 index 708fc5a1..00000000 --- a/lib/pact/v2/provider/message_provider_servlet.rb +++ /dev/null @@ -1,79 +0,0 @@ -# frozen_string_literal: true - -require "webrick" - -module Pact - module V2 - module Provider - class MessageProviderServlet < WEBrick::HTTPServlet::ProcHandler - attr_reader :logger - - CONTENT_TYPE_JSON = "application/json" - CONTENT_TYPE_PROTO = "application/protobuf" - METADATA_HEADER = "pact-message-metadata" - - def initialize(logger: nil) - super(build_proc) - - @message_handlers = {} - - @logger = logger || Logger.new($stdout) - end - - def add_message_handler(name, &block) - raise "message handler for #{name} already configured" if @message_handlers[name].present? - - @message_handlers[name] = {proc: block} - end - - private - - def build_proc - proc do |request, response| - # {"description":"message: ","providerStates":[{"name":"pet exists","params":{"pet_id":1}}]} - data = JSON.parse(request.body) - - description = data["description"] - provider_states = data["providerStates"] - - body, metadata = handle(description, provider_states) - - response.status = 200 - if body.is_a?(String) - # protobuf-serialized body - response.body = body - response.content_type = metadata[:content_type] || CONTENT_TYPE_PROTO - else - response.body = body.to_json - response.content_type = CONTENT_TYPE_JSON - end - response[METADATA_HEADER] = Base64.urlsafe_encode64(metadata.to_json) - rescue JSON::ParserError => ex - logger.error("cannot parse request: #{ex.message}") - response.status = 500 - rescue => ex - logger.error("cannot handle message request: #{ex.message}") - response.status = 500 - end - end - - def handle(description, provider_states) - handler = find_handler_for(description) - return {}, {} unless handler - - body, metadata = handler[:proc].call(provider_states&.first || {}) - unless metadata[:content_type] - # try to find content-type in provider states - content_type = provider_states&.filter_map { |state| state.dig("params", "contentType") }&.first - metadata[:content_type] = content_type if content_type - end - [body, metadata] - end - - def find_handler_for(description) - @message_handlers[description] - end - end - end - end -end diff --git a/lib/pact/v2/provider/mixed_verifier.rb b/lib/pact/v2/provider/mixed_verifier.rb deleted file mode 100644 index 2b5c8581..00000000 --- a/lib/pact/v2/provider/mixed_verifier.rb +++ /dev/null @@ -1,22 +0,0 @@ -# # frozen_string_literal: true -module Pact - module V2 - module Provider - # MixedVerifier coordinates verification for all present configs (async, grpc, http) - class MixedVerifier - attr_reader :mixed_config, :verifiers - - def initialize(mixed_config) - unless mixed_config.is_a?(::Pact::V2::Provider::PactConfig::Mixed) - raise ArgumentError, "mixed_config must be a PactConfig::Mixed" - end - @mixed_config = mixed_config - @verifiers = [] - @verifiers << AsyncMessageVerifier.new(mixed_config.async_config) if mixed_config.async_config - @verifiers << GrpcVerifier.new(mixed_config.grpc_config) if mixed_config.grpc_config - @verifiers << HttpVerifier.new(mixed_config.http_config) if mixed_config.http_config - end - end - end - end -end diff --git a/lib/pact/v2/provider/pact_broker_proxy.rb b/lib/pact/v2/provider/pact_broker_proxy.rb deleted file mode 100644 index 942b5f72..00000000 --- a/lib/pact/v2/provider/pact_broker_proxy.rb +++ /dev/null @@ -1,66 +0,0 @@ -# frozen_string_literal: true - -require "rack-proxy" - -module Pact - module V2 - module Provider - class PactBrokerProxy < Rack::Proxy - attr_reader :backend_uri, :path, :logger - - # e.g. /pacts/provider/paas-stand-seeker/consumer/paas-stand-placer/pact-version/2967a9343bd8fdd28a286c4b8322380020618892/metadata/c1tdW2VdPXByb2R1Y3Rpb24mc1tdW2N2XT03MzIy - PACT_FILE_REQUEST_PATH_REGEX = %r{/pacts/provider/.+?/consumer/.+?/pact-version/.+}.freeze - - def initialize(app = nil, opts = {}) - super - @backend_uri = URI(opts[:backend]) - @path = nil - @logger = opts[:logger] || Logger.new($stdout) - end - - def perform_request(env) - request = Rack::Request.new(env) - env["rack.timeout"] ||= ENV.fetch("PACT_BROKER_REQUEST_TIMEOUT", 5).to_i - @path = request.path - - super - end - - def rewrite_env(env) - env["HTTP_HOST"] = backend_uri.host - env - end - - def rewrite_response(triplet) - status, headers, body = triplet - - if status == "200" && PACT_FILE_REQUEST_PATH_REGEX.match?(path) - patched_body = patch_response(body.first) - - # we need to recalculate content length - headers[Rack::CONTENT_LENGTH] = patched_body.bytesize.to_s - - return [status, headers, [patched_body]] - end - - triplet - end - - private - - def patch_response(raw_body) - parsed_body = JSON.parse(raw_body) - - return body if parsed_body["consumer"].blank? || parsed_body["provider"].blank? - return body if parsed_body["interactions"].blank? - - - JSON.generate(parsed_body) - rescue JSON::ParserError => ex - logger.error("cannot parse broker response: #{ex.message}") - end - - end - end - end -end diff --git a/lib/pact/v2/provider/pact_broker_proxy_runner.rb b/lib/pact/v2/provider/pact_broker_proxy_runner.rb deleted file mode 100644 index 2843585e..00000000 --- a/lib/pact/v2/provider/pact_broker_proxy_runner.rb +++ /dev/null @@ -1,80 +0,0 @@ -# frozen_string_literal: true - -require "webrick" - -module Pact - module V2 - module Provider - class PactBrokerProxyRunner - attr_reader :logger - - - def initialize(pact_broker_host:, port: 9002, host: "127.0.0.1", pact_broker_user: nil, pact_broker_password: nil, pact_broker_token: nil, logger: nil) - @host = host - @port = port - @pact_broker_host = pact_broker_host - @pact_broker_user = pact_broker_user - @pact_broker_password = pact_broker_password - @pact_broker_token = pact_broker_token - @logger = logger || Logger.new($stdout) - - @thread = nil - end - - def proxy_url - "http://#{@host}:#{@port}" - end - - def start - raise "server already running, stop server before starting new one" if @thread - # Rack 2/3 compatibility - begin - require 'rack/handler/webrick' - handler = ::Rack::Handler::WEBrick - rescue LoadError - require 'rackup/handler/webrick' - handler = Class.new(Rackup::Handler::WEBrick) - end - @server = WEBrick::HTTPServer.new( - { BindAddress: @host, Port: @port, Logger: @logger, AccessLog: [] }, - WEBrick::Config::HTTP - ) - @server.mount("/", handler, PactBrokerProxy.new( - nil, - backend: @pact_broker_host, - streaming: false, - username: @pact_broker_user || nil, - password: @pact_broker_password || nil, - token: @pact_broker_token || nil, - logger: @logger - )) - - @thread = Thread.new do - @logger.debug "starting pact broker proxy server" - @server.start - end - end - - def stop - @logger.info("stopping pact broker proxy server") - - @server&.shutdown - @thread&.join - - @logger.info("pact broker proxy server stopped") - end - - def run - start - - yield - rescue => e - logger.fatal("FATAL ERROR: #{e.message} #{e.backtrace.join("\n")}") - raise - ensure - stop - end - end - end - end -end diff --git a/lib/pact/v2/provider/pact_config.rb b/lib/pact/v2/provider/pact_config.rb deleted file mode 100644 index f64e9310..00000000 --- a/lib/pact/v2/provider/pact_config.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -# require_relative "pact_config/grpc" - -module Pact - module V2 - module Provider - module PactConfig - def self.new(transport_type, provider_name:, opts: {}) - case transport_type - when :http - Http.new(provider_name: provider_name, opts: opts) - when :grpc - Grpc.new(provider_name: provider_name, opts: opts) - when :async - Async.new(provider_name: provider_name, opts: opts) - when :mixed - Mixed.new(provider_name: provider_name, opts: opts) - else - raise ArgumentError, "unknown transport_type: #{transport_type}" - end - end - end - end - end -end diff --git a/lib/pact/v2/provider/pact_config/async.rb b/lib/pact/v2/provider/pact_config/async.rb deleted file mode 100644 index 50808678..00000000 --- a/lib/pact/v2/provider/pact_config/async.rb +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true - -require_relative "base" - -module Pact - module V2 - module Provider - module PactConfig - class Async < Base - def initialize(provider_name:, opts: {}) - super - handlers = opts[:message_handlers] || {} - handlers.each do |name, block| - new_message_handler(name, &block) - end - end - - def new_message_handler(name, opts: {}, &block) - provider_setup_server.add_message_handler(name, &block) - end - - def new_verifier(config = nil) - AsyncMessageVerifier.new(self, config) - end - end - end - end - end -end diff --git a/lib/pact/v2/provider/pact_config/base.rb b/lib/pact/v2/provider/pact_config/base.rb deleted file mode 100644 index ac64bf79..00000000 --- a/lib/pact/v2/provider/pact_config/base.rb +++ /dev/null @@ -1,103 +0,0 @@ -# frozen_string_literal: true - -module Pact - module V2 - module Provider - module PactConfig - class Base - attr_reader :provider_name, :provider_version, :log_level, :provider_setup_server, :provider_setup_port, :pact_proxy_port, - :consumer_branch, :consumer_version, :consumer_name, :broker_url, :broker_username, :broker_password, :verify_only, :pact_dir, - :pact_uri, :provider_version_branch, :provider_version_tags, :consumer_version_selectors, :enable_pending, :include_wip_pacts_since, - :fail_if_no_pacts_found, :provider_build_uri, :broker_token, :consumer_version_tags, :publish_verification_results, :logger - - - def initialize(provider_name:, opts: {}) - @provider_name = provider_name - @log_level = opts[:log_level] || :info - @pact_dir = opts[:pact_dir] || nil - @logger = opts[:logger] || nil - @provider_setup_port = opts[:provider_setup_port] || 9001 - @pact_proxy_port = opts[:pact_proxy_port] || 9002 - @pact_uri = ENV.fetch("PACT_URL", nil) || opts.fetch(:pact_uri, nil) - @publish_verification_results = ENV.fetch("PACT_PUBLISH_VERIFICATION_RESULTS", nil) == "true" || opts.fetch(:publish_verification_results, false) - @provider_version = ENV.fetch("PACT_PROVIDER_VERSION", nil) || opts.fetch(:provider_version, nil) - @provider_build_uri = ENV.fetch("PACT_PROVIDER_BUILD_URL", nil) || opts.fetch(:provider_build_uri, nil) - @provider_version_branch = ENV.fetch("PACT_PROVIDER_BRANCH", nil) || opts.fetch(:provider_version_branch, nil) - @provider_version_tags = ENV.fetch("PACT_PROVIDER_VERSION_TAGS", nil) || opts.fetch(:provider_version_tags, []) - @consumer_version_tags = ENV.fetch("PACT_CONSUMER_VERSION_TAGS", nil) || opts.fetch(:consumer_version_tags, []) - @consumer_version_selectors = ENV.fetch("PACT_CONSUMER_VERSION_SELECTORS", nil) || opts.fetch(:consumer_version_selectors, nil) - @enable_pending = ENV.fetch("PACT_VERIFIER_ENABLE_PENDING", nil) == "true" || opts.fetch(:enable_pending, false) - @include_wip_pacts_since = ENV.fetch("PACT_INCLUDE_WIP_PACTS_SINCE", nil) || opts.fetch(:include_wip_pacts_since, nil) - @fail_if_no_pacts_found = ENV.fetch("PACT_FAIL_IF_NO_PACTS_FOUND", nil) == "true" || opts.fetch(:fail_if_no_pacts_found, true) - @consumer_branch = ENV.fetch("PACT_CONSUMER_BRANCH", nil) || opts.fetch(:consumer_branch, nil) - @consumer_version = ENV.fetch("PACT_CONSUMER_VERSION", nil) || opts.fetch(:consumer_version, nil) - @consumer_name = opts[:consumer_name] - @broker_url = ENV.fetch("PACT_BROKER_BASE_URL", nil) || opts.fetch(:broker_url, nil) - @broker_username = ENV.fetch("PACT_BROKER_USERNAME", nil) || opts.fetch(:broker_username, nil) - @broker_password = ENV.fetch("PACT_BROKER_PASSWORD", nil) || opts.fetch(:broker_password, nil) - @broker_token = ENV.fetch("PACT_BROKER_TOKEN", nil) || opts.fetch(:broker_token, nil) - @verify_only = [ENV.fetch("PACT_CONSUMER_FULL_NAME", nil)].compact || opts.fetch(:verify_only, []) - - @provider_setup_server = opts[:provider_setup_server] || ProviderServerRunner.new(port: @provider_setup_port, logger: @logger) - if @broker_url.present? - @pact_proxy_server = PactBrokerProxyRunner.new( - port: @pact_proxy_port, - pact_broker_host: @broker_url, - pact_broker_user: @broker_username, - pact_broker_password: @broker_password, - pact_broker_token: @broker_token, - logger: @logger - ) - end - end - - - def start_servers - @provider_setup_server.start - @pact_proxy_server&.start - end - - def stop_servers - @provider_setup_server.stop - @pact_proxy_server&.stop - end - - def provider_setup_url - @provider_setup_server.state_setup_url - end - - def message_setup_url # rubocop:disable Rails/Delegate - @provider_setup_server.message_setup_url - end - - def pact_broker_proxy_url - @pact_proxy_server&.proxy_url - end - - def new_provider_state(name, opts: {}, &block) - config = ProviderStateConfiguration.new(name, opts: opts) - config.instance_eval(&block) - config.validate! - - use_hooks = !opts[:skip_hooks] - - @provider_setup_server.add_setup_state(name, use_hooks, &config.setup_proc) if config.setup_proc - @provider_setup_server.add_teardown_state(name, use_hooks, &config.teardown_proc) if config.teardown_proc - end - - def before_setup(&block) - @provider_setup_server.set_before_setup_hook(&block) - end - - def after_teardown(&block) - @provider_setup_server.set_after_teardown_hook(&block) - end - - def new_verifier - raise Pact::V2::ImplementationRequired, "#new_verifier should be implemented" - end - end - end - end - end -end diff --git a/lib/pact/v2/provider/pact_config/grpc.rb b/lib/pact/v2/provider/pact_config/grpc.rb deleted file mode 100644 index ea3b98d3..00000000 --- a/lib/pact/v2/provider/pact_config/grpc.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -require_relative "base" - -module Pact - module V2 - module Provider - module PactConfig - class Grpc < Base - attr_reader :grpc_port, :grpc_services, :grpc_server - - def initialize(provider_name:, opts: {}) - super - - @grpc_port = opts[:grpc_port] || 0 - @grpc_services = opts[:grpc_services] || [] - end - - def new_verifier(config = nil) - GrpcVerifier.new(self, config) - end - end - end - end - end -end diff --git a/lib/pact/v2/provider/pact_config/http.rb b/lib/pact/v2/provider/pact_config/http.rb deleted file mode 100644 index 212ed1d8..00000000 --- a/lib/pact/v2/provider/pact_config/http.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -require_relative "base" - -module Pact - module V2 - module Provider - module PactConfig - class Http < Base - attr_reader :http_port - attr_reader :app - - def initialize(provider_name:, opts: {}) - super - - @http_port = opts[:http_port] || 0 - @app = opts[:app] || nil - end - - def new_verifier(config = nil) - HttpVerifier.new(self, config) - end - end - end - end - end -end diff --git a/lib/pact/v2/provider/pact_config/mixed.rb b/lib/pact/v2/provider/pact_config/mixed.rb deleted file mode 100644 index c36da3fb..00000000 --- a/lib/pact/v2/provider/pact_config/mixed.rb +++ /dev/null @@ -1,40 +0,0 @@ -# # frozen_string_literal: true - -module Pact - module V2 - module Provider - module PactConfig - # Mixed config allows composing one of each: async, grpc, http - class Mixed < Base - attr_reader :async_config, :grpc_config, :http_config - - def initialize(provider_name:, opts: {}) - super - @provider_setup_server = ProviderServerRunner.new(port: @provider_setup_port, logger: @logger) - if @broker_url.present? - @pact_proxy_server = PactBrokerProxyRunner.new( - port: @pact_proxy_port, - pact_broker_host: @broker_url, - pact_broker_user: @broker_username, - pact_broker_password: @broker_password, - pact_broker_token: @broker_token, - logger: @logger - ) - end - @http_config = opts[:http] ? Http.new(provider_name: provider_name, opts: opts[:http].merge(provider_setup_server: provider_setup_server, pact_proxy_server: @pact_proxy_server)) : nil - @grpc_config = opts[:grpc] ? Grpc.new(provider_name: provider_name, opts: opts[:grpc].merge(provider_setup_server: provider_setup_server, pact_proxy_server: @pact_proxy_server)) : nil - @async_config = opts[:async] ? Async.new(provider_name: provider_name, opts: opts[:async].merge(provider_setup_server: provider_setup_server, pact_proxy_server: @pact_proxy_server)) : nil - end - - def configs - [@async_config, @grpc_config, @http_config].compact - end - - def start_servers - end - - end - end - end - end -end diff --git a/lib/pact/v2/provider/provider_server_runner.rb b/lib/pact/v2/provider/provider_server_runner.rb deleted file mode 100644 index bfe5674d..00000000 --- a/lib/pact/v2/provider/provider_server_runner.rb +++ /dev/null @@ -1,92 +0,0 @@ -# frozen_string_literal: true - -require "webrick" - -module Pact - module V2 - module Provider - class ProviderServerRunner - attr_reader :logger - - SETUP_PROVIDER_STATE_PATH = "/setup-provider" - VERIFY_MESSAGE_PATH = "/verify-message" - - def initialize(port: 9001, host: "127.0.0.1", logger: nil) - @host = host - @port = port - @provider_setup_states = {} - @provider_teardown_states = {} - @logger = logger || Logger.new($stdout) - - @state_servlet = ProviderStateServlet.new(logger: @logger) - @message_servlet = MessageProviderServlet.new(logger: @logger) - @thread = nil - end - - def state_setup_url - "http://#{@host}:#{@port}#{SETUP_PROVIDER_STATE_PATH}" - end - - def message_setup_url - "http://#{@host}:#{@port}#{VERIFY_MESSAGE_PATH}" - end - - def start - raise "server already running, stop server before starting new one" if @thread - - @server = WEBrick::HTTPServer.new( - { BindAddress: @host, Port: @port, Logger: @logger, AccessLog: [] }, - WEBrick::Config::HTTP - ) - @server.mount(SETUP_PROVIDER_STATE_PATH, @state_servlet) - @server.mount(VERIFY_MESSAGE_PATH, @message_servlet) - - @thread = Thread.new do - @logger.debug "starting provider setup server" - @server.start - end - end - - def stop - @logger.info("stopping provider setup server") - - @server&.shutdown - @thread&.join - - @logger.info("provider setup server stopped") - end - - def run - start - - yield - rescue => e - logger.fatal("FATAL ERROR: #{e.message} #{e.backtrace.join("\n")}") - raise - ensure - stop - end - - def add_message_handler(state_name, &block) - @message_servlet.add_message_handler(state_name, &block) - end - - def add_setup_state(state_name, use_before_setup_hook = true, &block) - @state_servlet.add_setup_state(state_name, use_before_setup_hook, &block) - end - - def add_teardown_state(state_name, use_after_teardown_hook = true, &block) - @state_servlet.add_teardown_state(state_name, use_after_teardown_hook, &block) - end - - def set_before_setup_hook(&block) - @state_servlet.before_setup(&block) - end - - def set_after_teardown_hook(&block) - @state_servlet.after_teardown(&block) - end - end - end - end -end diff --git a/lib/pact/v2/provider/provider_state_configuration.rb b/lib/pact/v2/provider/provider_state_configuration.rb deleted file mode 100644 index e4b53cb5..00000000 --- a/lib/pact/v2/provider/provider_state_configuration.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -module Pact - module V2 - module Provider - class ProviderStateConfiguration - attr_reader :name, :opts, :setup_proc, :teardown_proc - - class ProviderStateConfigurationError < ::Pact::V2::Error; end - - def initialize(name, opts: {}) - @name = name - @opts = opts - @setup_proc = nil - @teardown_proc = nil - end - - def set_up(&block) - @setup_proc = block - end - - def tear_down(&block) - @teardown_proc = block - end - - def validate! - raise ProviderStateConfigurationError.new("no hooks configured for state #{@name}: \"provider_state\" declaration only needed if setup/teardown hooks are used for that state. Please add hooks or remove \"provider_state\" declaration") unless @setup_proc || @teardown_proc - end - end - end - end -end diff --git a/lib/pact/v2/provider/provider_state_servlet.rb b/lib/pact/v2/provider/provider_state_servlet.rb deleted file mode 100644 index 0d01a7e7..00000000 --- a/lib/pact/v2/provider/provider_state_servlet.rb +++ /dev/null @@ -1,86 +0,0 @@ -# frozen_string_literal: true - -require "webrick" - -module Pact - module V2 - module Provider - class ProviderStateServlet < WEBrick::HTTPServlet::ProcHandler - attr_reader :logger - - def initialize(logger: nil) - super(build_proc) - - @logger = logger || Logger.new($stdout) - - @provider_setup_states = {} - @provider_teardown_states = {} - - @before_setup_hook_proc = nil - @after_teardown_hook_proc = nil - - @global_setup_hook = ::Pact::V2.configuration.before_provider_state_proc - @global_teardown_hook = ::Pact::V2.configuration.after_provider_state_proc - end - - def add_setup_state(name, use_before_setup_hook, &block) - raise "provider state #{name} already configured" if @provider_setup_states[name].present? - - @provider_setup_states[name] = {proc: block, use_hooks: use_before_setup_hook} - end - - def add_teardown_state(name, use_after_teardown_hook, &block) - raise "provider state #{name} already configured" if @provider_teardown_states[name].present? - - @provider_teardown_states[name] = {proc: block, use_hooks: use_after_teardown_hook} - end - - def before_setup(&block) - @before_setup_hook_proc = block - end - - def after_teardown(&block) - @after_teardown_hook_proc = block - end - - private - - def call_setup(state_name, state_data) - logger.debug "call_setup #{state_name} with #{state_data}" - @global_setup_hook&.call - @before_setup_hook_proc&.call(state_name, state_data) if @provider_setup_states.dig(state_name, :use_hooks) - @provider_setup_states.dig(state_name, :proc)&.call(state_data) - end - - def call_teardown(state_name, state_data) - logger.debug "call_teardown #{state_name} with #{state_data}" - @provider_teardown_states.dig(state_name, :proc)&.call(state_data) - @after_teardown_hook_proc&.call(state_name, state_data) if @provider_setup_states.dig(state_name, :use_hooks) - @global_teardown_hook&.call - end - - def build_proc - proc do |request, response| - # {"action" => "setup", "params" => {"order_uuid" => "mxfcpcsfUOHO"},"state" => "order exists and can be saved"} - # {"action"=> "teardown", "params" => {"order_uuid" => "mxfcpcsfUOHO"}, "state" => "order exists and can be saved"} - data = JSON.parse(request.body) - - action = data["action"] - state_name = data["state"] - state_data = data["params"] - - logger.warn("unknown callback state action: #{action}") if action.blank? - - call_setup(state_name, state_data) if action == "setup" - call_teardown(state_name, state_data) if action == "teardown" - - response.status = 200 - rescue JSON::ParserError => ex - logger.error("cannot parse request: #{ex.message}") - response.status = 500 - end - end - end - end - end -end diff --git a/lib/pact/v2/railtie.rb b/lib/pact/v2/railtie.rb deleted file mode 100644 index 01980bfe..00000000 --- a/lib/pact/v2/railtie.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -require "rails/railtie" - -module Pact - module V2 - class Railtie < Rails::Railtie - rake_tasks do - load "pact/v2/tasks/pact.rake" - end - end - end -end diff --git a/lib/pact/v2/tasks/pact.rake b/lib/pact/v2/tasks/pact.rake deleted file mode 100644 index e0c555cb..00000000 --- a/lib/pact/v2/tasks/pact.rake +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -require "rspec/core/rake_task" - -RSpec::Core::RakeTask.new(:pact_v2).tap do |task| - task.pattern = "spec/pact/consumers/**/*_spec.rb" - task.rspec_opts = "--require rails_helper_v2 --tag pact_v2" -end - -namespace :pact_v2 do - desc "Verifies the pact files" - task verify: :pact_v2 -end diff --git a/lib/pact/version.rb b/lib/pact/version.rb index d09cd3bd..93869cbb 100644 --- a/lib/pact/version.rb +++ b/lib/pact/version.rb @@ -1,4 +1,4 @@ # Remember to bump pact-provider-proxy when this changes major version module Pact - VERSION = "1.67.5" + VERSION = "2.0.0-alpha.1" end diff --git a/lib/tasks/pact.rake b/lib/tasks/pact.rake deleted file mode 100644 index 2d2cf117..00000000 --- a/lib/tasks/pact.rake +++ /dev/null @@ -1,34 +0,0 @@ - -namespace :pact do - - desc "Verifies the pact files configured in the pact_helper.rb against this service provider." - task :verify do - - require 'pact/tasks/task_helper' - - include Pact::TaskHelper - - handle_verification_failure do - execute_pact_verify - end - end - - desc "Verifies the pact at the given URI against this service provider." - task 'verify:at', :pact_uri do | t, args | - require 'rainbow' - require 'pact/tasks/task_helper' - - include Pact::TaskHelper - - abort(Rainbow("Please provide a pact URI. eg. rake pact:verify:at[../my-consumer/spec/pacts/my_consumer-my_provider.json]").red) unless args[:pact_uri] - handle_verification_failure do - execute_pact_verify args[:pact_uri] - end - end - - desc "Get help debugging pact:verify failures." - task 'verify:help', :reports_dir do | t, args | - require 'pact/provider/help/console_text' - puts Pact::Provider::Help::ConsoleText.(args[:reports_dir]) - end -end diff --git a/pact.gemspec b/pact.gemspec index be862d8f..d9a139ac 100644 --- a/pact.gemspec +++ b/pact.gemspec @@ -1,96 +1,73 @@ -lib = File.expand_path("lib", __dir__) +lib = File.expand_path('lib', __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'pact/version' Gem::Specification.new do |gem| - gem.name = "pact" + gem.name = 'pact' gem.version = Pact::VERSION - gem.authors = ["James Fraser", "Sergei Matheson", "Brent Snook", "Ronald Holshausen", "Beth Skurrie"] - gem.email = ["james.fraser@alumni.swinburne.edu", "sergei.matheson@gmail.com", "brent@fuglylogic.com", "uglyog@gmail.com", "bskurrie@dius.com.au"] - gem.description = %q{Enables consumer driven contract testing, providing a mock service and DSL for the consumer project, and interaction playback and verification for the service provider project.} - gem.summary = %q{Enables consumer driven contract testing, providing a mock service and DSL for the consumer project, and interaction playback and verification for the service provider project.} - gem.homepage = "https://github.com/pact-foundation/pact-ruby" + gem.authors = ['James Fraser', 'Sergei Matheson', 'Brent Snook', 'Ronald Holshausen', 'Beth Skurrie'] + gem.email = ['james.fraser@alumni.swinburne.edu', 'sergei.matheson@gmail.com', 'brent@fuglylogic.com', + 'uglyog@gmail.com', 'bskurrie@dius.com.au'] + gem.description = 'Enables consumer driven contract testing, providing a mock service and DSL for the consumer project, and interaction playback and verification for the service provider project.' # rubocop:disable Layout/LineLength + gem.summary = 'Enables consumer driven contract testing, providing a mock service and DSL for the consumer project, and interaction playback and verification for the service provider project.' # rubocop:disable Layout/LineLength + gem.homepage = 'https://github.com/pact-foundation/pact-ruby' gem.required_ruby_version = '>= 2.0' gem.files = `git ls-files bin lib pact.gemspec CHANGELOG.md LICENSE.txt`.split($/) gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) } gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) - gem.require_paths = ["lib"] + gem.require_paths = ['lib'] gem.license = 'MIT' gem.metadata = { - 'changelog_uri' => 'https://github.com/pact-foundation/pact-ruby/blob/master/CHANGELOG.md', - 'source_code_uri' => 'https://github.com/pact-foundation/pact-ruby', - 'bug_tracker_uri' => 'https://github.com/pact-foundation/pact-ruby/issues', + 'changelog_uri' => 'https://github.com/pact-foundation/pact-ruby/blob/master/CHANGELOG.md', + 'source_code_uri' => 'https://github.com/pact-foundation/pact-ruby', + 'bug_tracker_uri' => 'https://github.com/pact-foundation/pact-ruby/issues', 'documentation_uri' => 'https://github.com/pact-foundation/pact-ruby/blob/master/README.md' } - # Shared dev dependencies between v1 and v2 - gem.add_development_dependency 'rake', '~> 13.0' - gem.add_development_dependency 'faraday', '~>2.0', '<3.0' - gem.add_development_dependency 'webmock', '~> 3.0' - - # Shared runtime dependencies between v1 and v2 - gem.add_runtime_dependency 'rspec', '~> 3.0' - - # Pact v2 dependencies - # Core dependencies (code loading) - gem.add_dependency "zeitwerk", "~> 2.3" + gem.add_dependency 'zeitwerk', '~> 2.3' # For Pact support via Pact Rust Core - # moved to optional group in Gemfile, to allow opt in for users who want Pact v2 - # and to support pact-ruby-standalone which cannot package the ffi for all supported platforms - # gem.add_dependency "pact-ffi", "~> 0.4.28" + gem.add_dependency 'pact-ffi', '~> 0.4.28' # For Provider Side Verification - gem.add_dependency "rack" - gem.add_dependency "rack-proxy" - gem.add_dependency "webrick", '~> 1.8' + gem.add_dependency 'rack' + gem.add_dependency 'rack-proxy' + gem.add_dependency 'webrick', '~> 1.8' # For Rails support, including testing non rails apps - gem.add_development_dependency "combustion", ">= 1.3" + gem.add_development_dependency 'combustion', '>= 1.3' + # std gems dropped from various ruby versions and not updated in deps + gem.add_development_dependency 'ostruct' # For Kafka support unless RUBY_PLATFORM =~ /win32|x64-mingw32|x64-mingw-ucrt/ # windows does not support librdkafka - gem.add_development_dependency "sbmt-kafka_consumer", ">= 2.0.1" - gem.add_development_dependency "sbmt-kafka_producer", ">= 1.0" + gem.add_development_dependency 'sbmt-kafka_consumer', '>= 2.0.1' + gem.add_development_dependency 'sbmt-kafka_producer', '>= 1.0' end if ENV['X_PACT_DEVELOPMENT_RDKAFKA'] == 'true' # darwin-arm64 prebuilt gems available from 0.20.0 - gem.add_development_dependency "karafka-rdkafka", ">= 0.20.0" - end + gem.add_development_dependency 'karafka-rdkafka', '>= 0.20.0' + end # For gRPC support - gem.add_development_dependency "gruf", ">= 2.18" - gem.add_development_dependency "gruf-rspec", ">= 0.6.0" + gem.add_development_dependency 'gruf', '>= 2.18' + gem.add_development_dependency 'gruf-rspec', '>= 0.6.0' # Testing tools - gem.add_development_dependency "rspec" - gem.add_development_dependency "rspec-rails" - gem.add_development_dependency "rspec_junit_formatter" - gem.add_development_dependency "vcr", ">= 6.0" + gem.add_development_dependency 'rspec' + gem.add_development_dependency 'rspec_junit_formatter' + gem.add_development_dependency 'rspec-rails' + gem.add_development_dependency 'vcr', '>= 6.0' # Development and linting tools - gem.add_development_dependency "appraisal", ">= 2.4" - gem.add_development_dependency "bundler", ">= 2.2" - gem.add_development_dependency "rubocop" - gem.add_development_dependency "rubocop-rspec" - gem.add_development_dependency "rubocop-rails" - gem.add_development_dependency "rubocop-performance" - gem.add_development_dependency "standard", ">= 1.35.1" - - - # Pact v1 dependencies - gem.add_runtime_dependency 'rack-test', '>= 0.6.3', '< 3.0.0' - gem.add_runtime_dependency 'thor', '>= 0.20', '< 2.0' - gem.add_runtime_dependency "rainbow", '~> 3.1' - - gem.add_runtime_dependency "pact-support" , "~> 1.21", ">=1.21.2" - gem.add_runtime_dependency 'pact-mock_service', '~> 3.0', '>= 3.3.1' - gem.add_development_dependency 'fakefs', '2.4' - gem.add_development_dependency 'hashie', '~> 5.0' - gem.add_development_dependency 'faraday-multipart', '~> 1.0' - gem.add_development_dependency 'conventional-changelog', '~> 1.3' + gem.add_development_dependency 'appraisal', '>= 2.4' gem.add_development_dependency 'bump', '~> 0.5' - gem.add_development_dependency 'pact-message', '~> 0.8' - gem.add_development_dependency 'rspec-its', '~> 1.3' - # gem.add_development_dependency 'webrick', '~> 1.8' # webrick is a runtime dependency of pact v2, so included above - gem.add_development_dependency 'ostruct' - -end \ No newline at end of file + gem.add_development_dependency 'bundler', '>= 2.2' + gem.add_development_dependency 'conventional-changelog', '~> 1.3' + gem.add_development_dependency 'faraday', '~>2.0', '<3.0' + gem.add_development_dependency 'rake', '~> 13.0' + gem.add_development_dependency 'rubocop' + gem.add_development_dependency 'rubocop-performance' + gem.add_development_dependency 'rubocop-rails' + gem.add_development_dependency 'rubocop-rspec' + gem.add_development_dependency 'standard', '>= 1.35.1' + gem.add_development_dependency 'webmock', '~> 3.0' +end diff --git a/spec/features/consumer_with_file_upload_spec.rb b/spec/features/consumer_with_file_upload_spec.rb deleted file mode 100644 index 2d23e4c4..00000000 --- a/spec/features/consumer_with_file_upload_spec.rb +++ /dev/null @@ -1,83 +0,0 @@ -require 'net/http' -require 'pact/consumer' -require 'pact/consumer/rspec' -require 'faraday' -require 'faraday/multipart' -load 'pact/consumer/world.rb' - -describe "A consumer with a file upload", :pact => true do - - before :all do - Pact.clear_configuration - Pact.clear_consumer_world - Pact.service_consumer "Consumer with a file upload" do - has_pact_with "A file upload service" do - mock_service :file_upload_service do - verify false - port 7777 - end - end - end - end - - let(:file_to_upload) { File.absolute_path("./spec/support/text.txt") } - let(:payload) { { file: Faraday::UploadIO.new(file_to_upload, 'text/plain') } } - - let(:connection) do - Faraday.new(file_upload_service.mock_service_base_url + "/files") do |builder| - builder.request :multipart - builder.request :url_encoded - builder.adapter :net_http - end - end - - let(:do_request) { connection.post { |req| req.body = payload } } - - let(:body) do - "-------------RubyMultipartPost-05e76cbc2adb42ac40344eb9b35e98bc\r\nContent-Disposition: form-data; name=\"file\"; filename=\"text.txt\"\r\nContent-Length: 14\r\nContent-Type: text/plain\r\nContent-Transfer-Encoding: binary\r\n\r\n#{File.read(file_to_upload)}\r\n-------------RubyMultipartPost-05e76cbc2adb42ac40344eb9b35e98bc--\r\n" - end - - describe "when the content matches" do - it "returns the mocked response and verification passes" do - file_upload_service. - upon_receiving("a request to upload a file").with({ - method: :post, - path: '/files', - body: body, - headers: { - "Content-Type" => Pact.term(/multipart\/form\-data/, "multipart/form-data; boundary=-----------RubyMultipartPost-05e76cbc2adb42ac40344eb9b35e98bc"), - "Content-Length" => Pact.like("299") - } - }). - will_respond_with({ - status: 200 - }) - - do_request - - file_upload_service.verify("when the content matches") - end - end - - describe "when the content does not match" do - it "the verification fails" do - file_upload_service. - upon_receiving("a request to upload another file").with({ - method: :post, - path: '/files', - body: body.gsub('text.txt', 'wrong.txt'), - headers: { - "Content-Type" => Pact.term(/multipart\/form\-data/, "multipart/form-data; boundary=-----------RubyMultipartPost-05e76cbc2adb42ac40344eb9b35e98bc"), - "Content-Length" => Pact.like("299") - } - }). - will_respond_with({ - status: 200 - }) - - do_request - - expect { file_upload_service.verify("when the content matches") }.to raise_error /do not match/ - end - end -end diff --git a/spec/features/consumption_spec.rb b/spec/features/consumption_spec.rb deleted file mode 100644 index 16b7d97d..00000000 --- a/spec/features/consumption_spec.rb +++ /dev/null @@ -1,113 +0,0 @@ -require 'net/http' -require 'pact/consumer' -require 'pact/consumer/rspec' -load 'pact/consumer/world.rb' - -describe "A service consumer side of a pact", :pact => true do - - before do - Pact.clear_configuration - Pact.clear_consumer_world - end - - describe "blah" do - describe "thing" do - - it "goes a little something like this" do - - Pact.service_consumer "Consumer" do - has_pact_with "Alice Service" do - mock_service :alice_service do - verify true - port 8888 - end - end - - has_pact_with "Bob" do - mock_service :bob_service do - verify false - port 4321 - end - end - end - - alice_service. - upon_receiving("a retrieve Mallory request").with({ - method: :get, - path: '/mallory' - }). - will_respond_with({ - status: 200, - headers: { 'Content-Type' => 'text/html' }, - body: term(/Mallory/, 'That is some good Mallory.') - }) - - bob_service. - upon_receiving('a create donut request').with({ - method: :post, - path: '/donuts', - body: { - "name" => term(/Bob/, 'Bob') - }, - headers: {'Accept' => 'text/plain', "Content-Type" => 'application/json'} - }). - will_respond_with({ - status: 201, - body: 'Donut created.' - }) - bob_service. - upon_receiving('a delete charlie request').with({ - method: :delete, - path: '/charlie' - }). - will_respond_with({ - status: 200, - body: /deleted/ - }) - bob_service. - upon_receiving('an update alligators request').with({ - method: :put, - path: '/alligators', - body: [{"name" => 'Roger' }] - }). - will_respond_with({ - status: 200, - body: [{"name" => "Roger", "age" => 20}] - }) - - - alice_response = Net::HTTP.get_response(URI('http://localhost:8888/mallory')) - - expect(alice_response.code).to eql '200' - expect(alice_response['Content-Type']).to eql 'text/html' - expect(alice_response.body).to eql 'That is some good Mallory.' - - uri = URI('http://localhost:4321/donuts') - post_req = Net::HTTP::Post.new(uri.path) - post_req['Accept'] = "text/plain" - post_req['Content-Type'] = "application/json" - post_req.body = {"name" => "Bobby"}.to_json - bob_post_response = Net::HTTP.start(uri.hostname, uri.port) do |http| - http.request post_req - end - - expect(bob_post_response.code).to eql '201' - expect(bob_post_response.body).to eql 'Donut created.' - - uri = URI('http://localhost:4321/alligators') - post_req = Net::HTTP::Put.new(uri.path) - post_req['Content-Type'] = "application/json" - post_req.body = [{"name" => "Roger"}].to_json - bob_post_response = Net::HTTP.start(uri.hostname, uri.port) do |http| - http.request post_req - end - - expect(bob_post_response.code).to eql '200' - expect(bob_post_response.body).to eql([{"name" => "Roger", "age" => 20}].to_json) - - expect{ bob_service.verify('goes a little something like this') }.to raise_error /do not match/ - end - - end - end -end diff --git a/spec/features/foo_bar_spec.rb b/spec/features/foo_bar_spec.rb deleted file mode 100644 index ce424d45..00000000 --- a/spec/features/foo_bar_spec.rb +++ /dev/null @@ -1,47 +0,0 @@ -require 'net/http' -require 'pact/consumer' -require 'pact/consumer/rspec' -load 'pact/consumer/world.rb' - -# Use this test along with `rake pact:verify:foobar` to debug end to end issues. -# The Bar app is in spec/support/bar_pact_helper.rb - -describe "Bar", :pact => true do - - it "can retrieve a thing" do - - Pact.clear_configuration - Pact.clear_consumer_world - - Pact.service_consumer "Foo" do - has_pact_with "Bar" do - mock_service :bar_service do - pact_specification_version "2" - host "127.0.0.1" - port 4638 - end - end - end - - bar_service. - upon_receiving("a retrieve thing request").with({ - method: :get, - path: '/thing' - }). - will_respond_with({ - status: 200, - headers: { 'Content-Type' => 'application/json' }, - body: { - name: "Thing 1" - } - }) - - bar_response = Net::HTTP.get_response(URI('http://localhost:4638/thing')) - - expect(bar_response.code).to eql '200' - expect(JSON.parse(bar_response.body)).to eq "name" => "Thing 1" - - puts bar_service.write_pact - end - -end \ No newline at end of file diff --git a/spec/features/production_spec.rb b/spec/features/production_spec.rb deleted file mode 100644 index c2afde97..00000000 --- a/spec/features/production_spec.rb +++ /dev/null @@ -1,163 +0,0 @@ -require 'spec_helper' -require 'pact/provider/rspec' -require 'pact/consumer_contract' -require 'features/provider_states/zebras' -require 'pact/provider/pact_source' - - -module Pact::Provider - - describe "A service production side of a pact" do - - class ServiceUnderTest - - def call(env) - case env['PATH_INFO'] - when '/donuts' - [201, {'Content-Type' => 'application/json'}, { message: "Donut created." }.to_json] - when '/charlie' - [200, {'Content-Type' => 'application/json'}, { message: "Your charlie has been deleted" }.to_json] - end - end - - end - - class ServiceUnderTestWithFixture - - def find_zebra_names - #simulate loading data from a database - data = JSON.load(File.read('tmp/a_mock_database.json')) - data.collect{ | zebra | zebra['name'] } - end - - def call(env) - case env['PATH_INFO'] - when "/zebra_names" - [200, {'Content-Type' => 'application/json'}, { names: find_zebra_names }.to_json] - end - end - - end - - pact = Pact::ConsumerContract.from_json <<-EOS - { - "consumer" : { "name" : "a consumer"}, - "provider" : { "name" : "a provider"}, - "interactions" : [ - { - "description": "donut creation request", - "request": { - "method": "post", - "path": "/donuts" - }, - "response": { - "body": {"message": "Donut created."}, - "status": 201 - } - }, - { - "description": "charlie deletion request", - "request": { - "method": "delete", - "path": "/charlie" - }, - "response": { - "body": { - "message": { - "json_class": "Regexp", - "o": 0, - "s": "deleted" - } - }, - "status": 200 - } - } - ] - } - EOS - - before :all do - Pact.service_provider "My Provider" do - app { ServiceUnderTest.new } - end - end - - pact_uri = Pact::Provider::PactURI.new("http://dummy-uri") - pact_source = Pact::Provider::PactSource.new(pact_uri) - - honour_consumer_contract pact, pact_uri: pact_uri, pact_source: pact_source - - end - - describe "with a provider_state" do - - context "that is a symbol" do - consumer_contract = Pact::ConsumerContract.from_json <<-EOS - { - "consumer" : { "name" : "the-wild-beast-store"}, - "provider" : { "name" : "provider"}, - "interactions" : [ - { - "description": "donut creation request", - "request": { - "method": "delete", - "path": "/zebra_names" - }, - "response": { - "body": {"names": ["Jason", "Sarah"]}, - "status": 200 - }, - "provider_state" : "the_zebras_are_here" - } - ] - } - EOS - - before :all do - Pact.service_provider "My Provider" do - app { ServiceUnderTestWithFixture.new } - end - end - - pact_uri = Pact::Provider::PactURI.new("http://dummy-uri") - pact_source = Pact::Provider::PactSource.new(pact_uri) - - honour_consumer_contract consumer_contract, pact_uri: pact_uri, pact_source: pact_source - end - - context "that is a string" do - consumer_contract = Pact::ConsumerContract.from_json <<-EOS - { - "consumer" : { "name" : "some consumer"}, - "provider" : { "name" : "provider"}, - "interactions" : [ - { - "description": "donut creation request", - "request": { - "method": "post", - "path": "/zebra_names" - }, - "response": { - "body": {"names": ["Mark", "Gertrude"]}, - "status": 200 - }, - "provider_state" : "some other zebras are here" - } - ] - } - EOS - - before :all do - Pact.service_provider "ServiceUnderTestWithFixture" do - app { ServiceUnderTestWithFixture.new } - end - end - - pact_uri = Pact::Provider::PactURI.new("http://dummy-uri") - pact_source = Pact::Provider::PactSource.new(pact_uri) - - honour_consumer_contract consumer_contract, pact_uri: pact_uri, pact_source: pact_source - end - - end -end diff --git a/spec/features/provider_states/zebras.rb b/spec/features/provider_states/zebras.rb deleted file mode 100644 index 916bcf6d..00000000 --- a/spec/features/provider_states/zebras.rb +++ /dev/null @@ -1,28 +0,0 @@ -require 'json' -require 'fileutils' - -Pact.provider_states_for 'the-wild-beast-store' do - - provider_state :the_zebras_are_here do - set_up do - FileUtils.mkdir_p 'tmp' - some_data = [{'name' => 'Jason'},{'name' => 'Sarah'}] - File.open("tmp/a_mock_database.json", "w") { |file| file << some_data.to_json } - end - - tear_down do - FileUtils.rm_rf("tmp/a_mock_database.json") - end - end -end - -Pact.provider_state "some other zebras are here" do - set_up do - some_data = [{'name' => 'Mark'},{'name' => 'Gertrude'}] - File.open("tmp/a_mock_database.json", "w") { |file| file << some_data.to_json } - end - - tear_down do - FileUtils.rm_rf("tmp/a_mock_database.json") - end -end diff --git a/spec/fixtures/certificates/ca_cert.pem b/spec/fixtures/certificates/ca_cert.pem deleted file mode 100644 index ad49d99f..00000000 --- a/spec/fixtures/certificates/ca_cert.pem +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDATCCAemgAwIBAgIUWfQF2Mh+eFd3q+cSVgekpaMTh9MwDQYJKoZIhvcNAQEL -BQAwDzENMAsGA1UEAwwETXlDQTAgFw0yMzA5MTkxMTA2MjZaGA8yMTIzMDgyNjEx -MDYyNlowDzENMAsGA1UEAwwETXlDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC -AQoCggEBAK9Qha2OdeFrSCUqiYRUBngNLn8PRGDaKPWmjd+3WOWJNM1RNgFfGpKY -nxYJp4J6eW7aeQ6o94Q+QOZp+Yxm6thrtvjRbcEafAore4EwC4tjXvoFoy+mKwzm -njlJw+ha3TsMAqD3GGDLF7uDnmliURRo8TOmJ++Mwss9Uhb5p9LArjWXa3sV8da+ -gsxP2aTgBZfznUhNKDGUfezYa5UEbHQ869rA1PAqL3tOC2M5LTX08C2PlzzLOF5S -gBzicV1PPDkmkbxKmFV+D8LmkwWNsRhrzZ6TIxYoXIRhziS7JuYOGU7G0+6ZKpIP -mo7WXSoSrd7GL5PQJzlHKCsTckd4so0CAwEAAaNTMFEwHQYDVR0OBBYEFCeovNXs -r1mcbprFaLyll+LrBJmQMB8GA1UdIwQYMBaAFCeovNXsr1mcbprFaLyll+LrBJmQ -MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAHOsbZ0iDiKiRU8Q -hIAav056dboPjTK19Q736DUD6oCbTbvecfxMv/wu9LmYGW5jt/DWP6s+jDYhcPpj -c3U03pPKCnvsG5z60ZgmNSqzyVAVPW17UVdw/ZnkKK/SFxYgYQaF/1g6opS2Zana -4aBGypqqGoD4KE+DAnRjuuCUpiz3zXwGd86auajY6soMlLNnVXteVa/whW6IZ84x -w4LISeMGUr+MXw9ye4WhcZYKZ4vwJdUYst2PA0pDuGwBDbGnrYloGm2BSpaHXUUo -XrwKFFkIxcK63IpAhoceTJpyfjI1BSmItfjEwToOUu6xDBsHLNiH6BKstSxk0DfX -01PHz2I= ------END CERTIFICATE----- diff --git a/spec/fixtures/certificates/ca_cert.srl b/spec/fixtures/certificates/ca_cert.srl deleted file mode 100644 index 5a951072..00000000 --- a/spec/fixtures/certificates/ca_cert.srl +++ /dev/null @@ -1 +0,0 @@ -494F82D5FE5055D2C9C64941C421085B59521071 diff --git a/spec/fixtures/certificates/ca_key.pem b/spec/fixtures/certificates/ca_key.pem deleted file mode 100644 index c29f42ee..00000000 --- a/spec/fixtures/certificates/ca_key.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCvUIWtjnXha0gl -KomEVAZ4DS5/D0Rg2ij1po3ft1jliTTNUTYBXxqSmJ8WCaeCenlu2nkOqPeEPkDm -afmMZurYa7b40W3BGnwKK3uBMAuLY176BaMvpisM5p45ScPoWt07DAKg9xhgyxe7 -g55pYlEUaPEzpifvjMLLPVIW+afSwK41l2t7FfHWvoLMT9mk4AWX851ITSgxlH3s -2GuVBGx0POvawNTwKi97TgtjOS019PAtj5c8yzheUoAc4nFdTzw5JpG8SphVfg/C -5pMFjbEYa82ekyMWKFyEYc4kuybmDhlOxtPumSqSD5qO1l0qEq3exi+T0Cc5Rygr -E3JHeLKNAgMBAAECggEAF2EHQqWB24V2rIYnVT9DUZUobdyiWMF0aYtEK4uuzjAQ -RjpzQkGQMJvWc0DnAW5wbTOzUHIrTTZkFJYYp6boiziUwPUPduCfnqznySBCxIbZ -mUFRNBSBHzT4mq6B8qV+D9bChFFkrdvHlsOu8gzLaouyxsQnWo8MlxU0B55UHrWc -nqIsPKVBeBtiF7c7eyZtpKmYgmWN8hnPzTZ2rtCL/BS3p2+/O+fFJKuul58Yo4t6 -bmMCPN5C6HxNhB6ADHm3lPVU3ap5g3a/4UHqVJ8c2SGKfAx6C1PgbajxiA74qMLS -YOhMXzc3jSLmakqvSmVhQFJhFt7drbbGtx4oD3+XPQKBgQDj1k7O2A0yJRQPtvQJ -A1m+H5fmynMnH6XuQuO8WzqCsDsE786EAG6AzY562SMEQrQ0zgpFx0A9ZmECNaOZ -28OnzcA5xGKQh5dD0ou9lvRHXEavu7fYCrAG+wlQTo1eRHUDOAN4pQPoZ9r3bz1M -tnGtG3rak4KemAsoX8aSy59ZswKBgQDE/C+eu012vzjyr2J1W0Gdms7fh5CWzMp8 -hCHk+kmLCY4DHIaUv0tT3IXGKebRH+PZObE3zZ5Hx2QXPjFQWsyTkd9D2tRIWHaZ -ZpKPBLxYJJuBc3YWZM1qC2ZcRyvv1NgtNUFpB5xOGIUL3/QsfcOE25kC7Z21aN+e -uXSi3CkivwKBgGFHSZLLcKbuaehjx0Jp6dFhj+v8mLolqyVV7gKoOQ0/zZNICLcX -sBbSrXkKaQcSq/q31m8Aqg8NPXJCEL5KtPlawi5oCWWIXy+YIA4s+9PUNGIoFlDq -D0qLuOhPAdE0DXn4WpMScd6zKSzolBXC+DpfN09IGEc6x9jPO+vFgR49AoGABPiw -YvsrK1IMJ+PRQlD5SPb9PZr4RTYJ7jaPfG3sqTumf+Gaa+qgBg/MuIGaN7DsWTEh -jdz8n6cimYuSRwrjmt3VmqrNLL4+0ARMsptV/Yt++TdmxY3puUFsZevN6hGfGxT6 -/6GXikkIIpKWYQETjCjWpcJFdqyc6C6aCPoxd5UCgYEA1B4AdDgxhZgXhz24sKM7 -aX2aY4glBsEZ7dxaqpqvwmsSshvdfudjuFxo5jjMKV2C9JmwrCGML9O6MvSP03n8 -B3R543JqKqWLTaSROHkcoil+LdIV9w7jrMBildOCHSDXwuM8Pl7YObIdKMq4pVwe -87n9/ZihlrKGaZ8utMrrGmc= ------END PRIVATE KEY----- diff --git a/spec/fixtures/certificates/client_cert.pem b/spec/fixtures/certificates/client_cert.pem deleted file mode 100644 index c97f425f..00000000 --- a/spec/fixtures/certificates/client_cert.pem +++ /dev/null @@ -1,17 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICrDCCAZQCFElPgtX+UFXSycZJQcQhCFtZUhBxMA0GCSqGSIb3DQEBCwUAMA8x -DTALBgNVBAMMBE15Q0EwIBcNMjMwOTE5MTEwNzM1WhgPMjEyMzA4MjYxMTA3MzVa -MBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC -AQoCggEBAJj6DF+bu0DhXmkBaC2+CkoqNlsO+LzW9bZnNCQk0Jw99fgCGTLifU3N -eyAhKgHs+V3G/9ULbMrxYMSQ/psrrXpS7FM9xtA0WZ0VAg7Oi4WEi+wueE0R1GmO -NMuCVT2JCYd5uDh8+mrWoVqb9L4xIsy0kaV0Nnl+NX1zDvHXUHzfo3T3roaxRbd6 -N92qNPzrj8TviwbapT0bo4GKwTCOO1ewPFGCjsWEeLZ4p2UfbOzW/zjIBEUD8Kqg -FOht48y9J6XG3Tb61/7neT0xj6E7cn6hGSzuiIM/oZbtuUt72VDgbLbOrS02oHTz -YmC9tVL35Qvfgzrqw0DEv7zpm/3iG0sCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA -nndLK/t6+dmoAwg4K7pdo8xqUEDnUx8K7sU2whZvLEUM+mO+jWOe3USHjR3aXYnU -OjNhN90/TAy5wlIK6U2C36nHyZJUeScxuiaVwErwayE+GgwYmw9R7HVofgcVfTve -IpjyrT7mDOCMYjkHgZv1dSHQTcc6uclaw7SgywEEjxjCNSJCN+WPjxCdcuno0td8 -i7F6FL7FeOiP1mtQrTo42Tq+knerUc55CbTW4anbQfL+6TFEVCPJKduLHFieGB0k -BFilUR3JD2t8/f4fIilQ6FrMZpUzKcLbgW9cjts8mxq0zNV+z6lISgKbdxZFQp+2 -fvyYdnoNLP0YeRI6j9x1pg== ------END CERTIFICATE----- diff --git a/spec/fixtures/certificates/key.pem b/spec/fixtures/certificates/key.pem deleted file mode 100644 index e0879e33..00000000 --- a/spec/fixtures/certificates/key.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCY+gxfm7tA4V5p -AWgtvgpKKjZbDvi81vW2ZzQkJNCcPfX4Ahky4n1NzXsgISoB7Pldxv/VC2zK8WDE -kP6bK616UuxTPcbQNFmdFQIOzouFhIvsLnhNEdRpjjTLglU9iQmHebg4fPpq1qFa -m/S+MSLMtJGldDZ5fjV9cw7x11B836N0966GsUW3ejfdqjT864/E74sG2qU9G6OB -isEwjjtXsDxRgo7FhHi2eKdlH2zs1v84yARFA/CqoBTobePMvSelxt02+tf+53k9 -MY+hO3J+oRks7oiDP6GW7blLe9lQ4Gy2zq0tNqB082JgvbVS9+UL34M66sNAxL+8 -6Zv94htLAgMBAAECggEAF2l9Z0yANgfH2S478XQ6Qut+8iSycMQ9SrM0yatQufjJ -ojFABgefwb6G733j3fOUnoOMN+DNv6l9c9f0/26J2ETEomC8ArVgWagTboyx0bdd -asIZ60GlTppS/ipuPUKx0KgSR6Lo+FzsyN9Bb7I5bzbba4UDqUhli1OGoACh8tpS -pyhD58C0nWBCYUjgkB2ilVoguQnnTvYC0VDbGOWK1P8bw0to810mkKTyv7ztifW2 -lHUwTe8vbQk7jY52+crvtgVZWNaXEdma3ivDSDHUjK3WLmPw9MFgVSMVYFLDZUQN -Btd7PyBSkjeHOzoS5b9l4qnjn2vhObpjrT5PZT6TMQKBgQDI+xrzk351kyqezHuZ -Bqo5CqEN3BHvwKALh3DA3uxHVaLqOo/yALv86yHgue/9ksaxxDwufAnVvcg5eEEh -XIsZrfKBIaNV5umqJAkbCbx4hVCX9mE45THv3Nc7XhiHuZZpUHb1i7qcab1lly5Y -7lFoCd5dCQJUoBf3/9Bw76OjRwKBgQDC2sba56V0dahFxTE3bOIRR2HIYWNfPv0a -7ejiNSHVHGTLrEfnya5ZcerT0j6QNA2IQcKw5ovPKn2xgjGlfPWgtBAz55r+lfU2 -+/6CRf8v6tu9FdPs7RDHxBuicOGQlQGSAH2+tfcY9ZCB8wcdGYB3v5ko0OFsFdZY -+fJOIt4h3QKBgByFJanzADsHC0FFmzR38afujjQ9Sn5PQ2bfbWyxNa5ZxKigbtTU -rdiSNViCij/dmDyZsECYcXzXVZZyLivhygt217bjYx5JilcOjgw8MXaY1Hr8B4ff -Xlq/Z/uQusJn36RKOtdVYMHZb3r/HSCZkQvGeruRD7eakEwtDRM5rmr5AoGAZFt9 -s90/ED5RDq5DbQJ9ZNzY9fWC0tmETsxd97PZ2wMmvufamPz8+UB86+ALLQZCOf10 -otv7AhYmarhdjZhQghZ7ieAtqhXeGBWtvbcDedCCoF6PqiVnURwmB4IQCwFTr7jl -CsZ5n7dKWEOtVEWALyzVW3pJv/t3TJhfPfMjaVkCgYAxmC4/jmBCLmQZ3eWbmZHx -X7N2qAI7Cu2JVi1Wut4WnBgFNynYH+kt67LZSQ9Jf9lHDnlBe5gOTvF5/8UeoTMv -MGI4R4WJ6ezWV12ugbmKAvzHB/SiJ9U0ph78ibejCxW3gomuDzY1T+xF56kCKXJ0 -uPaEN0rPMT6wMEegJHaE8A== ------END PRIVATE KEY----- diff --git a/spec/fixtures/certificates/server.csr b/spec/fixtures/certificates/server.csr deleted file mode 100644 index d8020bb5..00000000 --- a/spec/fixtures/certificates/server.csr +++ /dev/null @@ -1,15 +0,0 @@ ------BEGIN CERTIFICATE REQUEST----- -MIICWTCCAUECAQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0B -AQEFAAOCAQ8AMIIBCgKCAQEAmPoMX5u7QOFeaQFoLb4KSio2Ww74vNb1tmc0JCTQ -nD31+AIZMuJ9Tc17ICEqAez5Xcb/1QtsyvFgxJD+myutelLsUz3G0DRZnRUCDs6L -hYSL7C54TRHUaY40y4JVPYkJh3m4OHz6atahWpv0vjEizLSRpXQ2eX41fXMO8ddQ -fN+jdPeuhrFFt3o33ao0/OuPxO+LBtqlPRujgYrBMI47V7A8UYKOxYR4tninZR9s -7Nb/OMgERQPwqqAU6G3jzL0npcbdNvrX/ud5PTGPoTtyfqEZLO6Igz+hlu25S3vZ -UOBsts6tLTagdPNiYL21UvflC9+DOurDQMS/vOmb/eIbSwIDAQABoAAwDQYJKoZI -hvcNAQELBQADggEBAEz74PiDtYCL1XiZV4On0l5jRjBrKTVEAnjEtWgygy9V6U1d -BYE3AxwsdTUygl/cS2i3g8U2yZGQ1ZAh/qHq0sHB6TDePLmNSEiksP7KOJwXU9vO -/pCS9qbOYcWucLlQpnHxySpUlcxFWmrl33pMaNCzxxLN1q3eRbNmxoxACI/+vZsX -M6sm2fhhw6yZkU7D04BDgSwsddW8ApDqbtwbndyv/ZL13xjG9yow8noSF7uxGQnn -UnVFMGVGp3I6M/E3VFIwRvUYA1MJeqh9tLIEItlGmqkrQmxOnMvXKzJnQ9nK1KBq -2gaBXdvbabkXKAHnV0tYbDmZXvTO+7Ci7wgapNU= ------END CERTIFICATE REQUEST----- diff --git a/spec/fixtures/certificates/unsigned_cert.pem b/spec/fixtures/certificates/unsigned_cert.pem deleted file mode 100644 index ea787127..00000000 --- a/spec/fixtures/certificates/unsigned_cert.pem +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDCzCCAfOgAwIBAgIUN2/oKOttkdOretzyqc+Zv8IqpT8wDQYJKoZIhvcNAQEL -BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTIzMDkxOTExMjEzM1oYDzIxMjMw -ODI2MTEyMTMzWjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB -AQUAA4IBDwAwggEKAoIBAQD30tqrKrHa0p1RsGlDc7lUUS/ZF2/7ZtNWe0gRuuum -6l/X5H8F+Ay1cO8DirGx3s/LPpj7DwvjjKo2eE+wcO2v/R5S+uPL4Bm0o+bPGwZP -vw+XMMgBZUsNSMER6DUliP5bHQ/8TCXWpfP3rLJ9QitOAX/rD9bVrOs3g3I0uf2A -RZ0O40//5q9fiXRC3PAfPbX7XdyI9Mr3duwmAW+nK2Gbd98ut27PkO0Fze27Xtk2 -EdIh3u5pajK/ub8rf5vyfk+c/6pcN9kMakPtlgIR/eqzTkfRWyIpMoFn/X8VumUQ -X4ylj1SfSs+K47GBjrqknEh1BYlblW8WKg5cUjx/r/b/AgMBAAGjUzBRMB0GA1Ud -DgQWBBQUEefafoC0qDhzThhEBMMwr/C5FTAfBgNVHSMEGDAWgBQUEefafoC0qDhz -ThhEBMMwr/C5FTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQDn -dfkjZtgSdbsEPhUMfUlhZWXqxtLDQBoxM7xF+i5WC6w3yHpN/teA8SqA8CYiPb9d -5rNfnmJLP4PeyfTu6Pc0EJpQsmK19i9z0FPrA7bqPIzgF4U4R1eQ5mvTzlNoGkp3 -1gnjDdwtTq0RFfuvHKm5EqECKX+hBEJKMiviEH/mGqQuoycpKifZ5WRTQonnWjGe -BVkhdn4Psp83EWdnD/yQbo1XEbYRtsaPM4Dozr6uKbeq9Zbu+xDO9Uw4mTE/WSfb -t4AXqOLDRafOP9w3twlFH2ZQxqpSaqXo8z1RkS9jtCm69JcDnsePKqkhesToMZAz -2cylIQmuuNIRGLmCRsVK ------END CERTIFICATE----- diff --git a/spec/fixtures/certificates/unsigned_key.pem b/spec/fixtures/certificates/unsigned_key.pem deleted file mode 100644 index c7fab0c3..00000000 --- a/spec/fixtures/certificates/unsigned_key.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQD30tqrKrHa0p1R -sGlDc7lUUS/ZF2/7ZtNWe0gRuuum6l/X5H8F+Ay1cO8DirGx3s/LPpj7DwvjjKo2 -eE+wcO2v/R5S+uPL4Bm0o+bPGwZPvw+XMMgBZUsNSMER6DUliP5bHQ/8TCXWpfP3 -rLJ9QitOAX/rD9bVrOs3g3I0uf2ARZ0O40//5q9fiXRC3PAfPbX7XdyI9Mr3duwm -AW+nK2Gbd98ut27PkO0Fze27Xtk2EdIh3u5pajK/ub8rf5vyfk+c/6pcN9kMakPt -lgIR/eqzTkfRWyIpMoFn/X8VumUQX4ylj1SfSs+K47GBjrqknEh1BYlblW8WKg5c -Ujx/r/b/AgMBAAECggEAR4aYzBwndvOgqioTR3+H9tjzyWFlVZbo2iX8t/lN+D/e -562wJ6Xe7SMqKMiH3sFjEdMATj2afdNkcRIqVc9SGqAgd2yoAHiukp9Xh2DSYoPP -WSCgKR72GWBtMODnLe0rFFr/+R51MU12a35xiYtmej4ekFZi+ArPXJdYh/VCQBnG -BGF+EnUJqCAOXLz9zG3FoYVBWu071vEnpBtfblbHYfY2/o5CSoORkxcput6XDHxO -7pOXN7IRt7DJZ0goda3OwZQ9suKyTLcOxa8cA+DteVP6cvh4u2l+ZMxUIYK5B1eh -VAmJkIbcbAaz/SxyO9E2gWraz+pu6ArOGY/0krcBmQKBgQD9BlVtx7QTVzu+nLzF -2++cB0LTsTD9T9rlMfIuQMBIOywmsivyvUDr9SjOPqICQUIRufHKqCgHs4TH7Ifs -4AxyUEwQMG4xuYh3nU5eZlpUEjzUWbbe7o0NjhaJ4ZlUBvzHBcgptUlKTwvwamMs -pnzQxWlFXFuh+pxPPdSXZVmWJQKBgQD6vN0pO/xc3bAqHSCavX95NBjUhFvkAsoo -T8tfv2qoAN8RhI2/N8prix6tJk3AdhzdLmmMktv3MBDXd3cgLgmXQTYHIijWXPlF -/WXWmZXK0E9fiDjfXI9eB7E237fYGOaSobOhLOLoHcuL0kndps67QP2BhtXhYB88 -1We7LoJVUwKBgQCnF1qtH5d0ukPTEdC73Q0z/buM7tPKRMTqXHxxPQN9782tVDYf -nAlWiVTENqpoUM4fxKq/SSL+SvfhyvrMW/z8NLi2bDUpEzviufg58N+v60dOeFyC -hgiSLgYGUfweeGrPx6qymGxo7SCWSLtrjhqZB/UIAADnTAeTcOKGhECQHQKBgAlM -A29J+BuBZMzK87CJIjbeRaVrmvSjXdeMzd+o+01ratn9bjwO14SRTfvhlbRzLLLO -y78YmutZbuZuWY5p5pUjJ9uv2o/INr3vnV0NqM4yVx8Vr/YoOnCkHGAKf4iVs8bw -E/b/8RHmOOvgSjjbvIKY8E1jMH8Az2e0CfqYyOBdAoGAOlhTefyBGgAWFHqH/l4p -ThbWupIMsw1ZXlArwBnTfsUFuz0Yq7B+0tqrV8lhS3P4/0jI2yWnzhluDk62clwz -Xg187V85Ylagshsjv60mP5qBEF4N7Nf5fP2w6+GjMU+YiHEBsgGGt+2jPgKeCGQW -IlV3ym59oL+wGyN9OK3z+aw= ------END PRIVATE KEY----- diff --git a/spec/fixtures/vcr/pact-broker/pact_data.yml b/spec/fixtures/vcr/pact-broker/pact_data.yml index daf5896b..1f6cf0fe 100644 --- a/spec/fixtures/vcr/pact-broker/pact_data.yml +++ b/spec/fixtures/vcr/pact-broker/pact_data.yml @@ -56,7 +56,7 @@ http_interactions: {\n int32 id = 1;\n string name = 2;\n enum Status {\n PENDING = 0;\n COMPLETED = 1;\n CANCELED = 2;\n PROCESSED = 3;\n }\n Status status = 3;\n google.type.Money price = 4;\n}\n\nmessage OrderRequest {\n string uuid = 1;\n}\n\nmessage OrderResponse - {\n Order order = 1;\n}\n"}},"name":"protobuf","version":"0.5.5"}],"pact-ruby-v2":{"pact-ffi":"0.4.7"}},"provider":{"name":"paas-stand-seeker"},"createdAt":"2024-08-01T13:24:10+00:00","_links":{"self":{"title":"Pact","name":"Pact + {\n Order order = 1;\n}\n"}},"name":"protobuf","version":"0.5.5"}],"pact-ruby":{"pact-ffi":"0.4.7"}},"provider":{"name":"paas-stand-seeker"},"createdAt":"2024-08-01T13:24:10+00:00","_links":{"self":{"title":"Pact","name":"Pact between paas-stand-placer (98c66ec6) and paas-stand-seeker","href":"https://example.org/pacts/provider/paas-stand-seeker/consumer/paas-stand-placer/version/98c66ec6"},"pb:consumer":{"title":"Consumer","name":"paas-stand-placer","href":"https://example.org/pacticipants/paas-stand-placer"},"pb:consumer-version":{"title":"Consumer version","name":"98c66ec6","href":"https://example.org/pacticipants/paas-stand-placer/versions/98c66ec6"},"pb:consumer-versions":[{"title":"Consumer version","name":"98c66ec6","href":"https://example.org/pacticipants/paas-stand-placer/versions/98c66ec6"}],"pb:provider":{"title":"Provider","name":"paas-stand-seeker","href":"https://example.org/pacticipants/paas-stand-seeker"},"pb:pact-version":{"title":"Pact diff --git a/spec/integration/cli_docs_spec.rb b/spec/integration/cli_docs_spec.rb deleted file mode 100644 index 0a02ef12..00000000 --- a/spec/integration/cli_docs_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -require 'open3' -require 'support/cli' -require 'fileutils' - -describe "running the pact docs CLI", skip_windows: true do - - include Pact::Support::CLI - - before do - FileUtils.rm_rf "tmp/docs" - end - - let(:command) { "bundle exec bin/pact docs --pact-dir spec/support/docs --doc-dir tmp/docs" } - - it "writes some docs" do - execute_command command - expect(Dir.glob("tmp/docs/**/*").size).to_not eq 0 - end -end diff --git a/spec/integration/cli_spec.rb b/spec/integration/cli_spec.rb deleted file mode 100644 index 2fafd1ce..00000000 --- a/spec/integration/cli_spec.rb +++ /dev/null @@ -1,96 +0,0 @@ -require 'open3' -require 'support/cli' - -describe "running the pact verify CLI", skip_windows: true do - - include Pact::Support::CLI - - # Running this under RSpec 2 gives different output - let(:expected_test_output) { %r{(6 examples|2 interactions), 0 failures} } - - describe "running a failing test with --backtrace" do - let(:command) do - [ - "bundle exec bin/pact verify", - "--pact-uri spec/support/test_app_fail.json", - "--pact-helper spec/support/pact_helper.rb", - "--backtrace 2>&1" - ].join(" ") - end - it "displays the full backtrace" do - execute_command command, with: [/describe_interaction/] - end - end - - describe "running a failing test without --backtrace" do - let(:command) do - [ - "bundle exec bin/pact verify", - "--pact-uri spec/support/test_app_fail.json", - "--pact-helper spec/support/pact_helper.rb 2>&1" - ].join(" ") - end - xit "does not display the full backtrace - need to fix test to work with rspec2" do - execute_command command, without: [/describe_interaction/] - end - end - - describe "running with json output and an output path specified" do - before do - FileUtils.rm_rf 'tmp/foo.json' - end - - let(:command) do - [ - "bundle exec bin/pact verify", - "--pact-uri spec/support/test_app_pass.json", - "--pact-helper spec/support/pact_helper.rb", - "--format json", - "--out tmp/foo.json" - ].join(" ") - end - - it "formats the output as json to the specified file" do - output = `#{command}` - expect(JSON.parse(File.read('tmp/foo.json'))["examples"].size).to be > 1 - expect(output).to_not match expected_test_output - end - end - - describe "running with json output and no output path specified" do - let(:command) do - [ - "bundle exec bin/pact verify", - "--pact-uri spec/support/test_app_pass.json", - "--pact-helper spec/support/pact_helper.rb", - "--format json" - ].join(" ") - end - - it "formats the output as json to stdout" do - output = `#{command}` - expect(JSON.parse(output)["examples"].size).to be > 1 - end - end - - describe "running with an output path specified" do - before do - FileUtils.rm_rf 'tmp/foo.out' - end - - let(:command) do - [ - "bundle exec bin/pact verify", - "--pact-uri spec/support/test_app_pass.json", - "--pact-helper spec/support/pact_helper.rb", - "--out tmp/foo.out" - ].join(" ") - end - - it "writes the output to the specified path and not to stdout" do - output = `#{command}` - expect(File.read('tmp/foo.out')).to match expected_test_output - expect(output).to_not match expected_test_output - end - end -end diff --git a/spec/integration/consumer_async_request_spec.rb b/spec/integration/consumer_async_request_spec.rb deleted file mode 100644 index 6f41331f..00000000 --- a/spec/integration/consumer_async_request_spec.rb +++ /dev/null @@ -1,46 +0,0 @@ -require 'spec_helper' -require 'pact/consumer' -require 'pact/consumer/rspec' -load 'pact/consumer/world.rb' - -describe "A service consumer side of a pact", :pact => true do - - context "with an asynchronous interaction with provider" do - before do - Pact.clear_configuration - - Pact.service_consumer "Consumer" do - has_pact_with "Zebra Service" do - mock_service :zebra_service do - verify true - port 1239 - end - end - end - end - - it "goes like this" do - zebra_service. - given(:the_zebras_are_here). - upon_receiving("a retrieve Mallory request"). - with({ - method: :get, - path: '/mallory' - }). - will_respond_with({status: 200}) - - async_interaction { Net::HTTP.get_response(URI('http://localhost:1239/mallory')) } - - zebra_service.wait_for_interactions wait_max_seconds: 1, poll_interval: 0.1 - end - - def async_interaction - Thread.new do - sleep 0.2 - yield - end - end - - end - -end diff --git a/spec/integration/consumer_more_than_one_matching_interaction_spec.rb b/spec/integration/consumer_more_than_one_matching_interaction_spec.rb deleted file mode 100644 index 720a2a1b..00000000 --- a/spec/integration/consumer_more_than_one_matching_interaction_spec.rb +++ /dev/null @@ -1,49 +0,0 @@ -require 'pact/consumer' -require 'pact/consumer/rspec' -load 'pact/consumer/world.rb' - -describe "A service consumer side of a pact", :pact => true do - - context "with more than one matching interaction found" do - let(:expected_response) do - {"message"=>"Multiple interaction found for GET /path", "matching_interactions"=>[{"description"=>"a request", "request"=>{"method"=>"get", "path"=>"/path", "body"=>{"a"=>"some body"}, "headers"=>{"Content-Type"=>"application/json"}}}, {"description"=>"an identical request", "request"=>{"method"=>"get", "path"=>"/path", "body"=>{"a"=>"some body"}, "headers"=>{"Content-Type"=>"application/json"}}}]} - end - - it "returns an error" do - Pact.clear_configuration - Pact.clear_consumer_world - - Pact.service_consumer "Consumer" do - has_pact_with "Mary Service" do - mock_service :mary_service do - verify false - port 1237 - end - end - end - - mary_service - .given("something") - .upon_receiving("a request") - .with(method: 'get', path: '/path', body: {a: 'some body'}, headers: {'Content-Type' => 'application/json'}) - .will_respond_with(status: 200) - - - mary_service - .upon_receiving("an identical request") - .with(method: 'get', path: '/path', body: {a: 'some body'}, headers: {'Content-Type' => 'application/json'}) - .will_respond_with(status: 200) - - uri = URI('http://localhost:1237/path') - post_req = Net::HTTP::Get.new(uri.path) - post_req['Content-Type'] = "application/json" - post_req.body = {a: "some body"}.to_json - response = Net::HTTP.start(uri.hostname, uri.port) do |http| - http.request post_req - end - - expect(JSON.load(response.body)).to eq expected_response - end - - end -end diff --git a/spec/integration/consumer_no_matching_interaction_spec.rb b/spec/integration/consumer_no_matching_interaction_spec.rb deleted file mode 100644 index 42799d47..00000000 --- a/spec/integration/consumer_no_matching_interaction_spec.rb +++ /dev/null @@ -1,53 +0,0 @@ -require 'pact/consumer' -require 'pact/consumer/rspec' -load 'pact/consumer/world.rb' - -describe "A service consumer side of a pact", :pact => true do - context "with no matching interaction found" do - - let(:expected_response) do - { - "message"=>"No interaction found for GET /path", - "interaction_diffs"=>[{ - "description" => "a request that will not be properly matched", - "provider_state" => "something", - "body"=>{ - "a"=>{ - "EXPECTED"=>"some body", - "ACTUAL"=>"not matching body" - } - } - }] - } - end - - it "returns an error" do - Pact.clear_configuration - - Pact.service_consumer "Consumer" do - has_pact_with "Mary Service" do - mock_service :mary_service do - verify false - port 1236 - end - end - end - - mary_service - .given("something") - .upon_receiving("a request that will not be properly matched") - .with(method: 'get', path: '/path', body: {a: 'some body'}, headers: {'Content-Type' => 'application/json'}) - .will_respond_with(status: 200) - - uri = URI('http://localhost:1236/path') - post_req = Net::HTTP::Get.new(uri.path) - post_req['Content-Type'] = "application/json" - post_req.body = {a: "not matching body"}.to_json - response = Net::HTTP.start(uri.hostname, uri.port) do |http| - http.request post_req - end - - expect(JSON.load(response.body)).to eq expected_response - end - end -end diff --git a/spec/integration/consumer_with_a_provider_state_spec.rb b/spec/integration/consumer_with_a_provider_state_spec.rb deleted file mode 100644 index 30d111d9..00000000 --- a/spec/integration/consumer_with_a_provider_state_spec.rb +++ /dev/null @@ -1,49 +0,0 @@ -require 'pact/consumer' -require 'pact/consumer/rspec' -load 'pact/consumer/world.rb' -require 'faraday' - -describe "A service consumer side of a pact", :pact => true do - context "with a provider state" do - before do - Pact.clear_configuration - - Pact.service_consumer "Consumer" do - has_pact_with "Zebra Service" do - mock_service :zebra_service do - verify false - port 1235 - end - end - end - end - - let(:body) { 'That is some good Mallory.' } - let(:zebra_header) { '*.zebra.com' } - - it "goes like this" do - zebra_service. - given(:the_zebras_are_here). - upon_receiving("a retrieve Mallory request").with( - method: :get, - path: '/mallory', - headers: {'Accept' => 'text/html'} - ). - will_respond_with( - status: 200, - headers: { - 'Content-Type' => 'text/html', - 'Zebra-Origin' => term(/\*/, zebra_header) - }, - body: term(/Mallory/, body) - ) - - response = Faraday.get(zebra_service.mock_service_base_url + "/mallory", nil, {'Accept' => 'text/html'}) - expect(response.body).to eq body - expect(response.headers['Zebra-Origin']).to eq zebra_header - - interactions = Pact::ConsumerContract.from_json(zebra_service.write_pact).interactions - expect(interactions.first.provider_state).to eq("the_zebras_are_here") - end - end -end diff --git a/spec/integration/consumer_with_form_hash_spec.rb b/spec/integration/consumer_with_form_hash_spec.rb deleted file mode 100644 index 24c4660e..00000000 --- a/spec/integration/consumer_with_form_hash_spec.rb +++ /dev/null @@ -1,62 +0,0 @@ -require 'spec_helper' -require 'net/http' -require 'pact/consumer' -require 'pact/consumer/rspec' -require 'faraday' -load 'pact/consumer/world.rb' - -describe "A service consumer side of a pact", :pact => true do - - let(:body) { 'That is some good Mallory.' } - - context 'submitting a form specified as a Hash' do - - before :all do - Pact.clear_configuration - - Pact.service_consumer "Consumer" do - has_pact_with "Zebra Service" do - mock_service :zebra_service_4 do - port 1245 - end - end - end - end - - before do - - zebra_service_4. - given("the zebras like using forms"). - upon_receiving("a create Mallory request").with({ - method: :post, - path: '/mallory', - headers: {'Content-Type' => 'application/x-www-form-urlencoded'}, - body: { - param1: term('woger', /w/), - param2: 'penguin' - } - }). - will_respond_with({ - status: 200 - }) - - end - - let(:url) { zebra_service_4.mock_service_base_url + "/mallory" } - let(:response) { Faraday.post url, param2: 'penguin', param1: 'wiffle' } - let(:pact_json) { response; zebra_service_4.write_pact } - - it "matches form data" do - expect(response.status).to eq 200 - end - - it "does not include any Pact::Terms" do - expect(pact_json).to_not include "Pact::Term" - end - - it "includes the reified form" do - expect(pact_json).to include "param1=woger" - end - - end -end diff --git a/spec/integration/consumer_with_form_spec.rb b/spec/integration/consumer_with_form_spec.rb deleted file mode 100644 index 1ac3f2f1..00000000 --- a/spec/integration/consumer_with_form_spec.rb +++ /dev/null @@ -1,50 +0,0 @@ -require 'spec_helper' -require 'net/http' -require 'pact/consumer' -require 'pact/consumer/rspec' -require 'faraday' -load 'pact/consumer/world.rb' - -describe "A service consumer side of a pact", :pact => true do - - let(:body) { 'That is some good Mallory.' } - - context 'submitting a form' do - - before :all do - Pact.clear_configuration - - Pact.service_consumer "Consumer" do - has_pact_with "Zebra Service" do - mock_service :zebra_service_3 do - port 1243 - end - end - end - end - - before do - - zebra_service_3. - given("the zebras like using forms"). - upon_receiving("a create Mallory request").with({ - method: :post, - path: '/mallory', - headers: {'Content-Type' => 'application/x-www-form-urlencoded'}, - body: "param1=wiffle¶m2=penguin" - }). - will_respond_with({ - status: 200 - }) - - end - - let(:url) { zebra_service_3.mock_service_base_url + "/mallory" } - - it "matches form data" do - response = Faraday.post url, param2: 'penguin', param1: 'wiffle' - expect(response.status).to eq 200 - end - - end -end diff --git a/spec/integration/consumer_with_pact_term_header.rb b/spec/integration/consumer_with_pact_term_header.rb deleted file mode 100644 index 2c2c868d..00000000 --- a/spec/integration/consumer_with_pact_term_header.rb +++ /dev/null @@ -1,37 +0,0 @@ -require 'pact/consumer' -require 'pact/consumer/rspec' -load 'pact/consumer/world.rb' -require 'faraday' - -describe "A service consumer side of a pact", :pact => true do - context "when the header is a Pact::Term" do - before do - Pact.clear_configuration - - Pact.service_consumer "Consumer" do - has_pact_with "Zebra Service" do - mock_service :another_zebra_service_for_term_header do - port 4445 - end - end - end - end - - let(:body) { 'That is some good Mallory.' } - - it "matches using the term" do - another_zebra_service_for_term_header. - upon_receiving("a request to save an alligator").with( - method: :put, - path: '/alligators/John', - headers: { - 'Content-Type' => term(/json/, 'application/json') - } - ). - will_respond_with(status: 200) - - response = Faraday.put(another_zebra_service_for_term_header.mock_service_base_url + "/alligators/John", nil, {'Content-Type' => 'foo/json'}) - expect(response.status).to eq 200 - end - end -end diff --git a/spec/integration/consumer_with_pact_term_in_path.rb b/spec/integration/consumer_with_pact_term_in_path.rb deleted file mode 100644 index 6e9d9628..00000000 --- a/spec/integration/consumer_with_pact_term_in_path.rb +++ /dev/null @@ -1,37 +0,0 @@ -require 'pact/consumer' -require 'pact/consumer/rspec' -load 'pact/consumer/world.rb' -require 'faraday' - -describe "A service consumer side of a pact", :pact => true do - context "with a provider state" do - before do - Pact.clear_configuration - - Pact.service_consumer "Consumer" do - has_pact_with "Zebra Service" do - mock_service :another_zebra_service do - port 4444 - end - end - end - end - - let(:body) { 'That is some good Mallory.' } - - it "goes like this" do - another_zebra_service. - upon_receiving("a request for an alligator").with( - method: :get, - path: term(/alligators\/.*/, '/alligators/Mary'), - ). - will_respond_with(status: 200) - - response = Faraday.get(another_zebra_service.mock_service_base_url + "/alligators/John") - expect(response.status).to eq 200 - - interactions = Pact::ConsumerContract.from_json(another_zebra_service.write_pact).interactions - expect(interactions.first.request.path).to eq('/alligators/Mary') - end - end -end diff --git a/spec/integration/consumer_with_params_hash_spec.rb b/spec/integration/consumer_with_params_hash_spec.rb deleted file mode 100644 index 9c1b54db..00000000 --- a/spec/integration/consumer_with_params_hash_spec.rb +++ /dev/null @@ -1,123 +0,0 @@ -require 'spec_helper' -require 'net/http' -require 'pact/consumer' -require 'pact/consumer/rspec' -require 'faraday' -load 'pact/consumer/world.rb' - -describe "A service consumer side of a pact", :pact => true do - - # Helper to make Faraday requests. - # Faraday::FlatParamsEncoder may only be needed with our current version of Faraday 0.9 - # and ensures that when there are multiple parameters of the same name, they are encoded properly. e.g. colour=blue&colour=green - def faraday_mallory(base_url, params, headers = {}) - Faraday.new( - base_url, - request: { params_encoder: Faraday::FlatParamsEncoder } - ).get '/mallory', params, { 'Accept' => 'application/json' }.merge(headers) - end - - let(:body) { 'That is some good Mallory.' } - - context 'When expecting multiple instances of the same parameter in the query' do - - before :all do - Pact.clear_configuration - - Pact.service_consumer "Consumer" do - has_pact_with "Zebra Service" do - mock_service :zebra_service2 do - verify false - port 1241 - end - end - end - end - - before do - - zebra_service2 - .given(:the_zebras_are_here) - .upon_receiving("a retrieve Mallory request") - .with( - method: :get, - path: '/mallory', - headers: { 'Accept' => 'application/json' }, - query: { colour: 'brown', size: ['small', 'large'] } - ) - .will_respond_with( - status: 200, - headers: { 'Content-Type' => 'application/json' }, - body: term(/Mallory/, body) - ) - end - - it "matches when all instances are provided" do - response = faraday_mallory(zebra_service2.mock_service_base_url, size: ['small', 'large'], colour: 'brown') - expect(response.body).to eq body - - interactions = Pact::ConsumerContract.from_json(zebra_service2.write_pact).interactions - expect(interactions.first.provider_state).to eq("the_zebras_are_here") - end - - it "does not match when only the first instance is provided" do - response = Faraday.get(zebra_service2.mock_service_base_url + "/mallory?colour=brown&size=small", nil, 'Accept' => 'application/json') - expect(response.body).not_to eq body - end - - it "does not match when only the last instance is provided" do - response = Faraday.get(zebra_service2.mock_service_base_url + "/mallory?colour=brown&size=large", nil, 'Accept' => 'application/json') - expect(response.body).not_to eq body - end - - it "does not match when they are out of order" do - response = faraday_mallory(zebra_service2.mock_service_base_url, size: ['large', 'small'], colour: 'brown') - expect(response.body).not_to eq body - end - end - - context "and a complex request matching Pact Terms and multiple instances of the same parameter" do - - before :all do - Pact.clear_configuration - Pact.service_consumer "Consumer" do - has_pact_with "Zebra Service" do - mock_service :zebra_service do - verify false - port 1242 - end - end - end - end - - before do - zebra_service. - given(:the_zebras_are_here). - upon_receiving("a retrieve Mallory request"). - with({ - method: :get, - path: '/mallory', - headers: { 'Accept' => 'application/json', 'Content-Type' => 'application/x-www-form-urlencoded' }, - query: { size: ['small', term(/med.*/, 'medium'), 'large'], colour: 'brown', weight: '5' } - }). - will_respond_with({ - status: 200, - headers: { 'Content-Type' => 'application/json' }, - body: term(/Mallory/, body) - }) - end - - let(:response) do - faraday_mallory( - zebra_service.mock_service_base_url, - { weight: 5, size: ['small','medium','large'], colour: 'brown' }, - { 'Content-Type' => 'application/x-www-form-urlencoded' } - ) - end - - it "goes like this" do - expect(response.body).to eq body - end - - end -end diff --git a/spec/integration/consumer_with_v2_matching.rb b/spec/integration/consumer_with_v2_matching.rb deleted file mode 100644 index 5448e106..00000000 --- a/spec/integration/consumer_with_v2_matching.rb +++ /dev/null @@ -1,46 +0,0 @@ -require 'pact/consumer' -require 'pact/consumer/rspec' -load 'pact/consumer/world.rb' -require 'faraday' - -describe "When the pact_specification_version is set to 2", :pact => true do - - before do - Pact.clear_configuration - - Pact.service_consumer "Consumer" do - has_pact_with "Zebra Service" do - mock_service :zebra_service do - verify false - port 1235 - pact_specification_version '2' - end - end - end - end - - let(:body) { 'That is some good Mallory.' } - - it "writes the pact with v2 matching rules" do - zebra_service. - given(:the_zebras_are_here). - upon_receiving("a retrieve Mallory request").with( - method: :get, - path: '/mallory', - headers: {'Accept' => 'text/html'} - ). - will_respond_with( - status: 200, - headers: { - 'Content-Type' => 'text/html' - }, - body: term(/Mallory/, body) - ) - - response = Faraday.get(zebra_service.mock_service_base_url + "/mallory", nil, {'Accept' => 'text/html'}) - expect(response.body).to eq body - pact_hash = JSON.parse(zebra_service.write_pact) - expect(pact_hash['interactions'][0]['response']['matchingRules']).to be_instance_of(Hash) - end - -end diff --git a/spec/integration/executing_verify_from_wrapper_language_spec.rb b/spec/integration/executing_verify_from_wrapper_language_spec.rb deleted file mode 100644 index 53d8e519..00000000 --- a/spec/integration/executing_verify_from_wrapper_language_spec.rb +++ /dev/null @@ -1,27 +0,0 @@ -RSpec.describe "executing pact verify", skip_windows: true do - let(:command) { "bundle exec rake pact:verify:test_app:fail > /dev/null" } - let(:reports_dir) { 'tmp/spec_reports' } # The config for this is in spec/support/pact_helper.rb - - before do - FileUtils.rm_rf reports_dir - end - - after do - FileUtils.rm_rf reports_dir - end - - context "from ruby" do - it "creates a reports dir" do - system({}, command) - expect(File.exist?(reports_dir)).to be true - end - end - - context "with a wrapper language" do - it "does not create a reports dir" do - system({'PACT_EXECUTING_LANGUAGE' => 'foo'}, command) - - expect(File.exist?(reports_dir)).to be false - end - end -end diff --git a/spec/integration/pact/consumer_configuration_spec.rb b/spec/integration/pact/consumer_configuration_spec.rb deleted file mode 100644 index 272e3cdf..00000000 --- a/spec/integration/pact/consumer_configuration_spec.rb +++ /dev/null @@ -1,66 +0,0 @@ -require 'spec_helper' -require 'pact/configuration' -require 'pact/consumer/configuration' - -describe "consumer side" do - describe "configure" do - - class TestHelper - include Pact::Consumer::ConsumerContractBuilders - end - - let(:application) { double("App")} - let(:world) { Pact::Consumer::World.new } - - before do - Pact.clear_configuration - Pact::MockService::AppManager.instance.clear_all - # Don't want processes actually spawning - allow_any_instance_of(Pact::MockService::AppRegistration).to receive(:spawn) - allow(Pact).to receive(:consumer_world).and_return(world) - - my_app = application - - Pact.service_consumer "My Consumer" do - app my_app - port 1111 - - has_pact_with "My Service" do - mock_service :my_service do - port 8888 - standalone true - end - end - - has_pact_with "My Other Service" do - mock_service :my_other_service do - port 1235 - standalone false - end - end - end - - end - - describe "providers" do - - subject { TestHelper.new.my_service } - - it "should have defined methods in MockServices for the providers" do - expect(subject).to be_instance_of(Pact::Consumer::ConsumerContractBuilder) - end - - context "when standalone is true" do - it "is not registerd with the AppManager" do - expect(Pact::MockService::AppManager.instance.app_registered_on?(8888)).to eq false - end - end - - context "when standalone is false" do - it "should register the MockServices on their given ports if they are not" do - expect(Pact::MockService::AppManager.instance.app_registered_on?(1235)).to eq true - end - end - end - end -end \ No newline at end of file diff --git a/spec/integration/pact/provider_configuration_spec.rb b/spec/integration/pact/provider_configuration_spec.rb deleted file mode 100644 index f28c2b27..00000000 --- a/spec/integration/pact/provider_configuration_spec.rb +++ /dev/null @@ -1,25 +0,0 @@ -require 'spec_helper' -require 'pact/provider/rspec' - -describe "provider side" do - describe "configure" do - - class TestHelper - include Pact::Provider::RSpec::InstanceMethods - end - - let(:application) { double("App")} - - before do - app_block = ->{ application } - Pact.service_provider "My Provider" do - app &app_block - end - end - - it "makes the app available to the tests" do - expect(TestHelper.new.app).to be(application) - end - - end -end \ No newline at end of file diff --git a/spec/integration/publish_verification_spec.rb b/spec/integration/publish_verification_spec.rb deleted file mode 100644 index 311b3dd3..00000000 --- a/spec/integration/publish_verification_spec.rb +++ /dev/null @@ -1,77 +0,0 @@ -require 'pact/provider/verification_results/publish_all' -require 'pact/provider/pact_uri' - -describe "publishing verifications" do - before do - allow(Pact.configuration).to receive(:provider).and_return(provider_configuration) - allow($stdout).to receive(:puts) - end - - let(:provider_configuration) do - double('provider_configuration', - application_version: '1.2.3', - publish_verification_results?: true, - branch: nil, - tags: [], - build_url: 'http://ci/build/1') - end - - let(:pact_sources) do - [instance_double('Pact::Provider::PactSource', consumer_contract: consumer_contract, pact_hash: pact_hash, uri: pact_uri)] - end - - let(:pact_uri) do - instance_double('Pact::Provider::PactURI', uri: 'pact.json', options: {}, metadata: metadata) - end - - let(:consumer_contract) { instance_double('Pact::ConsumerContract', interactions: [pact_interaction])} - let(:pact_interaction) { instance_double('Pact::Interaction', _id: "1") } - - let(:metadata) { { notices: notices} } - let(:notices) { instance_double('Pact::PactBroker::Notices', after_verification_notices_text: ['hello'] ) } - - let(:pact_hash) do - { - 'interactions' => [{}], - '_links' => { - 'pb:publish-verification-results' => { - 'href' => 'http://publish/' - } - } - } - end - - let(:created_verification_body) do - { - '_links' => { - 'self' => { - 'href' => 'http://created' - } - } - }.to_json - end - - let(:test_results_hash) do - { - tests: [ - { - testDescription: '1', - status: 'passed', - pact_uri: pact_uri, - pact_interaction: pact_interaction - } - ] - } - end - - subject { Pact::Provider::VerificationResults::PublishAll.call(pact_sources, test_results_hash) } - - let!(:request) do - stub_request(:post, 'http://publish').to_return(status: 200, headers: {'Content-Type' => 'application/hal+json'}, body: created_verification_body) - end - - it "publishes the results" do - subject - expect(request).to have_been_made - end -end diff --git a/spec/internal/app/consumers/test_message_consumer.rb b/spec/internal/app/consumers/test_message_consumer.rb index 0d5eaa30..f1267bba 100644 --- a/spec/internal/app/consumers/test_message_consumer.rb +++ b/spec/internal/app/consumers/test_message_consumer.rb @@ -1,9 +1,7 @@ class TestMessageConsumer - def consume_message(message) puts "Message consumed" puts message.to_json message end - - end \ No newline at end of file +end diff --git a/spec/internal/app/producers/pet_json_producer.rb b/spec/internal/app/producers/pet_json_producer.rb index be426642..3a36e6e5 100644 --- a/spec/internal/app/producers/pet_json_producer.rb +++ b/spec/internal/app/producers/pet_json_producer.rb @@ -31,6 +31,6 @@ def call(pet_id) } } - sync_publish(pet, headers: {"identity-key" => uuid}) + sync_publish(pet, headers: { "identity-key" => uuid }) end end diff --git a/spec/internal/app/producers/pet_proto_producer.rb b/spec/internal/app/producers/pet_proto_producer.rb index 141b5423..6bfff838 100644 --- a/spec/internal/app/producers/pet_proto_producer.rb +++ b/spec/internal/app/producers/pet_proto_producer.rb @@ -25,6 +25,6 @@ def call(pet_id) ) ] ) - sync_publish(serializer.encode(message), headers: {"identity-key" => uuid}) + sync_publish(serializer.encode(message), headers: { "identity-key" => uuid }) end end diff --git a/spec/internal/app/rpc/pet_store/pet_store_controller.rb b/spec/internal/app/rpc/pet_store/pet_store_controller.rb index 5cb069ae..b3a67da8 100644 --- a/spec/internal/app/rpc/pet_store/pet_store_controller.rb +++ b/spec/internal/app/rpc/pet_store/pet_store_controller.rb @@ -6,7 +6,7 @@ class PetStoreController < Gruf::Controllers::Base def pet_by_id req = request.message - PetStore::Grpc::PetStore::V1::PetResponse.new(pet: {id: req.id, name: "Jack"}, metadata: request.metadata) + PetStore::Grpc::PetStore::V1::PetResponse.new(pet: { id: req.id, name: "Jack" }, metadata: request.metadata) end end end diff --git a/spec/internal/config/kafka_consumer.yml b/spec/internal/config/kafka_consumer.yml index 6f0c645b..968cd5a1 100644 --- a/spec/internal/config/kafka_consumer.yml +++ b/spec/internal/config/kafka_consumer.yml @@ -4,7 +4,7 @@ default: &default sasl_mechanism: <%= ENV.fetch('KAFKA_SASL_DSN'){ 'SCRAM-SHA-512:kafka_login:kafka_password' }.split(':').first %> sasl_username: <%= ENV.fetch('KAFKA_SASL_DSN'){ 'SCRAM-SHA-512:kafka_login:kafka_password' }.split(':').second %> sasl_password: <%= ENV.fetch('KAFKA_SASL_DSN'){ 'SCRAM-SHA-512:kafka_login:kafka_password' }.split(':').last %> - client_id: pact-ruby-v2-test-app + client_id: pact-ruby-test-app kafka: servers: <%= ENV.fetch('KAFKA_BROKERS'){ 'kafka:9092' } %> consumer_groups: diff --git a/spec/internal/pkg/client/pet_store/grpc/pet_store_pb.rb b/spec/internal/pkg/client/pet_store/grpc/pet_store_pb.rb index 1d193706..a6972b7f 100644 --- a/spec/internal/pkg/client/pet_store/grpc/pet_store_pb.rb +++ b/spec/internal/pkg/client/pet_store/grpc/pet_store_pb.rb @@ -1,10 +1,11 @@ # frozen_string_literal: true + # Generated by the protocol buffer compiler. DO NOT EDIT! # source: spec/internal/deps/services/pet_store/grpc/pet_store.proto require 'google/protobuf' -descriptor_data = "\n:spec/internal/deps/services/pet_store/grpc/pet_store.proto\x12\x0cpet_store.v1\"U\n\x03Pet\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0c\n\x04tags\x18\x03 \x03(\t\x12&\n\x06\x63olors\x18\x04 \x03(\x0b\x32\x16.pet_store.v1.PetColor\"\x9d\x01\n\x08PetColor\x12\x13\n\x0b\x64\x65scription\x18\x01 \x01(\t\x12\x0c\n\x04link\x18\x02 \x01(\t\x12\x12\n\nrelates_to\x18\x03 \x03(\t\x12/\n\x05\x63olor\x18\x04 \x01(\x0e\x32 .pet_store.v1.PetColor.BaseColor\")\n\tBaseColor\x12\x07\n\x03RED\x10\x00\x12\t\n\x05GREEN\x10\x01\x12\x08\n\x04\x42LUE\x10\x02\"\x1c\n\x0ePetByIdRequest\x12\n\n\x02id\x18\x01 \x01(\x05\"\x99\x01\n\x0bPetResponse\x12\x1e\n\x03pet\x18\x01 \x01(\x0b\x32\x11.pet_store.v1.Pet\x12\x39\n\x08metadata\x18\x02 \x03(\x0b\x32\'.pet_store.v1.PetResponse.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x32J\n\x04Pets\x12\x42\n\x07PetById\x12\x1c.pet_store.v1.PetByIdRequest\x1a\x19.pet_store.v1.PetResponseB\x1f\xea\x02\x1cPetStore::Grpc::PetStore::V1b\x06proto3" +descriptor_data = "\n:spec/internal/deps/services/pet_store/grpc/pet_store.proto\x12\x0cpet_store.v1\"U\n\x03Pet\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0c\n\x04tags\x18\x03 \x03(\t\x12&\n\x06\x63olors\x18\x04 \x03(\x0b\x32\x16.pet_store.v1.PetColor\"\x9d\x01\n\x08PetColor\x12\x13\n\x0b\x64\x65scription\x18\x01 \x01(\t\x12\x0c\n\x04link\x18\x02 \x01(\t\x12\x12\n\nrelates_to\x18\x03 \x03(\t\x12/\n\x05\x63olor\x18\x04 \x01(\x0e\x32 .pet_store.v1.PetColor.BaseColor\")\n\tBaseColor\x12\x07\n\x03RED\x10\x00\x12\t\n\x05GREEN\x10\x01\x12\x08\n\x04\x42LUE\x10\x02\"\x1c\n\x0ePetByIdRequest\x12\n\n\x02id\x18\x01 \x01(\x05\"\x99\x01\n\x0bPetResponse\x12\x1e\n\x03pet\x18\x01 \x01(\x0b\x32\x11.pet_store.v1.Pet\x12\x39\n\x08metadata\x18\x02 \x03(\x0b\x32\'.pet_store.v1.PetResponse.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x32J\n\x04Pets\x12\x42\n\x07PetById\x12\x1c.pet_store.v1.PetByIdRequest\x1a\x19.pet_store.v1.PetResponseB\x1f\xea\x02\x1cPetStore::Grpc::PetStore::V1b\x06proto3" # rubocop:disable Layout/LineLength pool = Google::Protobuf::DescriptorPool.generated_pool pool.add_serialized_file(descriptor_data) @@ -13,11 +14,11 @@ module PetStore module Grpc module PetStore module V1 - Pet = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("pet_store.v1.Pet").msgclass - PetColor = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("pet_store.v1.PetColor").msgclass - PetColor::BaseColor = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("pet_store.v1.PetColor.BaseColor").enummodule - PetByIdRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("pet_store.v1.PetByIdRequest").msgclass - PetResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("pet_store.v1.PetResponse").msgclass + Pet = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("pet_store.v1.Pet").msgclass # rubocop:disable Layout/LineLength + PetColor = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("pet_store.v1.PetColor").msgclass # rubocop:disable Layout/LineLength + PetColor::BaseColor = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("pet_store.v1.PetColor.BaseColor").enummodule # rubocop:disable Layout/LineLength + PetByIdRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("pet_store.v1.PetByIdRequest").msgclass # rubocop:disable Layout/LineLength + PetResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("pet_store.v1.PetResponse").msgclass # rubocop:disable Layout/LineLength end end end diff --git a/spec/internal/pkg/client/pet_store/grpc/pet_store_services_pb.rb b/spec/internal/pkg/client/pet_store/grpc/pet_store_services_pb.rb index bcf7f0b0..9bdc4d1f 100644 --- a/spec/internal/pkg/client/pet_store/grpc/pet_store_services_pb.rb +++ b/spec/internal/pkg/client/pet_store/grpc/pet_store_services_pb.rb @@ -10,7 +10,6 @@ module PetStore module V1 module Pets class Service - include ::GRPC::GenericService self.marshal_class_method = :encode diff --git a/spec/internal/pkg/server/pet_store_pb.rb b/spec/internal/pkg/server/pet_store_pb.rb index 7d902c77..64ff670a 100644 --- a/spec/internal/pkg/server/pet_store_pb.rb +++ b/spec/internal/pkg/server/pet_store_pb.rb @@ -5,7 +5,7 @@ require "google/protobuf" -descriptor_data = "\n:spec/internal/deps/services/pet_store/grpc/pet_store.proto\x12\x0cpet_store.v1\"U\n\x03Pet\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0c\n\x04tags\x18\x03 \x03(\t\x12&\n\x06\x63olors\x18\x04 \x03(\x0b\x32\x16.pet_store.v1.PetColor\"\x9d\x01\n\x08PetColor\x12\x13\n\x0b\x64\x65scription\x18\x01 \x01(\t\x12\x0c\n\x04link\x18\x02 \x01(\t\x12\x12\n\nrelates_to\x18\x03 \x03(\t\x12/\n\x05\x63olor\x18\x04 \x01(\x0e\x32 .pet_store.v1.PetColor.BaseColor\")\n\tBaseColor\x12\x07\n\x03RED\x10\x00\x12\t\n\x05GREEN\x10\x01\x12\x08\n\x04\x42LUE\x10\x02\"\x1c\n\x0ePetByIdRequest\x12\n\n\x02id\x18\x01 \x01(\x05\"\x99\x01\n\x0bPetResponse\x12\x1e\n\x03pet\x18\x01 \x01(\x0b\x32\x11.pet_store.v1.Pet\x12\x39\n\x08metadata\x18\x02 \x03(\x0b\x32'.pet_store.v1.PetResponse.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x32J\n\x04Pets\x12\x42\n\x07PetById\x12\x1c.pet_store.v1.PetByIdRequest\x1a\x19.pet_store.v1.PetResponseB\x1f\xea\x02\x1cPetStore::Grpc::PetStore::V1b\x06proto3" +descriptor_data = "\n:spec/internal/deps/services/pet_store/grpc/pet_store.proto\x12\x0cpet_store.v1\"U\n\x03Pet\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0c\n\x04tags\x18\x03 \x03(\t\x12&\n\x06\x63olors\x18\x04 \x03(\x0b\x32\x16.pet_store.v1.PetColor\"\x9d\x01\n\x08PetColor\x12\x13\n\x0b\x64\x65scription\x18\x01 \x01(\t\x12\x0c\n\x04link\x18\x02 \x01(\t\x12\x12\n\nrelates_to\x18\x03 \x03(\t\x12/\n\x05\x63olor\x18\x04 \x01(\x0e\x32 .pet_store.v1.PetColor.BaseColor\")\n\tBaseColor\x12\x07\n\x03RED\x10\x00\x12\t\n\x05GREEN\x10\x01\x12\x08\n\x04\x42LUE\x10\x02\"\x1c\n\x0ePetByIdRequest\x12\n\n\x02id\x18\x01 \x01(\x05\"\x99\x01\n\x0bPetResponse\x12\x1e\n\x03pet\x18\x01 \x01(\x0b\x32\x11.pet_store.v1.Pet\x12\x39\n\x08metadata\x18\x02 \x03(\x0b\x32'.pet_store.v1.PetResponse.MetadataEntry\x1a/\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x32J\n\x04Pets\x12\x42\n\x07PetById\x12\x1c.pet_store.v1.PetByIdRequest\x1a\x19.pet_store.v1.PetResponseB\x1f\xea\x02\x1cPetStore::Grpc::PetStore::V1b\x06proto3" # rubocop:disable Layout/LineLength pool = Google::Protobuf::DescriptorPool.generated_pool pool.add_serialized_file(descriptor_data) @@ -14,11 +14,11 @@ module PetStore module Grpc module PetStore module V1 - Pet = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("pet_store.v1.Pet").msgclass - PetColor = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("pet_store.v1.PetColor").msgclass - PetColor::BaseColor = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("pet_store.v1.PetColor.BaseColor").enummodule - PetByIdRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("pet_store.v1.PetByIdRequest").msgclass - PetResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("pet_store.v1.PetResponse").msgclass + Pet = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("pet_store.v1.Pet").msgclass # rubocop:disable Layout/LineLength + PetColor = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("pet_store.v1.PetColor").msgclass # rubocop:disable Layout/LineLength + PetColor::BaseColor = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("pet_store.v1.PetColor.BaseColor").enummodule # rubocop:disable Layout/LineLength + PetByIdRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("pet_store.v1.PetByIdRequest").msgclass # rubocop:disable Layout/LineLength + PetResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("pet_store.v1.PetResponse").msgclass # rubocop:disable Layout/LineLength end end end diff --git a/spec/v2/pact/configuration_spec.rb b/spec/lib/configuration_spec.rb similarity index 64% rename from spec/v2/pact/configuration_spec.rb rename to spec/lib/configuration_spec.rb index 2930a1d8..a17ccb8f 100644 --- a/spec/v2/pact/configuration_spec.rb +++ b/spec/lib/configuration_spec.rb @@ -1,25 +1,25 @@ # frozen_string_literal: true -RSpec.describe Pact::V2::Configuration do +RSpec.describe Pact::Configuration do subject(:config) { described_class.new } - describe "#before_provider_state_setup" do - it "raises if block is not given" do + describe '#before_provider_state_setup' do + it 'raises if block is not given' do expect { config.before_provider_state_setup }.to raise_error(/no block given/) end - it "configures setup block" do + it 'configures setup block' do config.before_provider_state_setup {} expect(config.before_provider_state_proc).to be_instance_of(Proc) end end - describe "#after_provider_state_teardown" do - it "raises if block is not given" do + describe '#after_provider_state_teardown' do + it 'raises if block is not given' do expect { config.after_provider_state_teardown }.to raise_error(/no block given/) end - it "configures teardown block" do + it 'configures teardown block' do config.after_provider_state_teardown {} expect(config.after_provider_state_proc).to be_instance_of(Proc) end diff --git a/spec/lib/consumer/grpc_interaction_builder_spec.rb b/spec/lib/consumer/grpc_interaction_builder_spec.rb new file mode 100644 index 00000000..0a83065c --- /dev/null +++ b/spec/lib/consumer/grpc_interaction_builder_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +RSpec.describe Pact::Consumer::GrpcInteractionBuilder do + subject { described_class.new(nil) } + + let(:proto_path) { Rails.root.join('deps/services/pet_store/grpc/pet_store.proto').to_s } + let(:builder) do + subject + .with_service(proto_path, 'Pets/PetById') + .with_request(param: 'some data') + .will_respond_with(result: 'some data') + end + + it 'builds proper json' do + result = JSON.parse(builder.interaction_json) + expect(result).to eq( + 'pact:content-type' => 'application/protobuf', + 'pact:proto' => File.expand_path(proto_path).to_s, + 'pact:proto-service' => 'Pets/PetById', + 'request' => { + 'param' => 'some data' + }, + 'response' => { + 'result' => 'some data' + } + ) + end +end diff --git a/spec/lib/consumer/interaction_contents_spec.rb b/spec/lib/consumer/interaction_contents_spec.rb new file mode 100644 index 00000000..52fc2659 --- /dev/null +++ b/spec/lib/consumer/interaction_contents_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +RSpec.describe Pact::Consumer::InteractionContents do + include Pact::Matchers + + let(:contents) do + { + str: match_any_string('str'), + bool: match_any_boolean(true), + num: match_any_number(1), + nested: match_each( + { + a: 1, + b: '2' + } + ) + } + end + + context 'with plugin interaction' do + it 'serializes properly to json' do + expect(described_class.plugin(contents).to_json) + .to eq("{\"str\":\"matching(regex, '(?-mix:.*)', 'str')\",\"bool\":\"matching(boolean, true)\",\"num\":\"matching(number, 1)\",\"nested\":{\"pact:match\":\"eachValue(matching($'SAMPLE'))\",\"SAMPLE\":{\"a\":1,\"b\":\"2\"}}}") # rubocop:disable Layout/LineLength + end + end + + context 'with basic interaction' do + it 'serializes properly to json' do + expect(described_class.basic(contents).to_json) + .to eq('{"str":{"pact:matcher:type":"regex","value":"str","regex":"(?-mix:.*)"},"bool":{"pact:matcher:type":"boolean","value":true},"num":{"pact:matcher:type":"number","value":1},"nested":{"pact:matcher:type":"type","value":[{"a":1,"b":"2"}],"min":1}}') # rubocop:disable Layout/LineLength + end + end +end diff --git a/spec/lib/consumer/message_interaction_builder_spec.rb b/spec/lib/consumer/message_interaction_builder_spec.rb new file mode 100644 index 00000000..c73d592b --- /dev/null +++ b/spec/lib/consumer/message_interaction_builder_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +RSpec.describe Pact::Consumer::MessageInteractionBuilder do + subject { described_class.new(nil) } + + context 'when proto message is used' do + let(:proto_path) { 'spec/internal/deps/services/pet_store/grpc/pet_store.proto' } + let(:builder) do + subject + .upon_receiving('message as proto') + .with_proto_class(proto_path, 'Pet') + .with_proto_contents(id: 1) + end + + it 'builds proper json' do + result = JSON.parse(builder.build_interaction_json) + expect(result).to eq( + 'pact:content-type' => 'application/protobuf', + 'pact:message-type' => 'Pet', + 'pact:proto' => File.expand_path(proto_path).to_s, + 'id' => 1 + ) + end + end + + context 'when json message is used' do + let(:proto_path) { 'spec/internal/deps/services/pet_store/grpc/pet_store.proto' } + let(:builder) do + subject + .upon_receiving('message as proto') + .with_json_contents(id: 1) + end + + it 'builds proper json' do + result = JSON.parse(builder.build_interaction_json) + expect(result).to eq('id' => 1) + end + end +end diff --git a/spec/lib/generators_spec.rb b/spec/lib/generators_spec.rb new file mode 100644 index 00000000..a65de13d --- /dev/null +++ b/spec/lib/generators_spec.rb @@ -0,0 +1,213 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'pact/generators' + +module Pact + module Generators + RSpec.describe RandomIntGenerator do + subject { described_class.new(min: 1, max: 10) } + + describe '#as_basic' do + it 'returns the correct hash' do + expect(subject.as_basic).to match({ + 'pact:matcher:type' => 'integer', + 'pact:generator:type' => 'RandomInt', + 'min' => 1, + 'max' => 10, + 'value' => a_value_between(1, 10) + }) + end + end + end + + RSpec.describe RandomDecimalGenerator do + subject { described_class.new(digits: 5) } + + describe '#as_basic' do + it 'returns the correct hash' do + expect(subject.as_basic).to match({ + 'pact:matcher:type' => 'decimal', + 'pact:generator:type' => 'RandomDecimal', + 'digits' => 5, + 'value' => a_value_between(0.00001, 0.99999) + }) + end + end + end + + RSpec.describe RandomHexadecimalGenerator do + subject { described_class.new(digits: 8) } + + describe '#as_basic' do + it 'returns the correct hash' do + expect(subject.as_basic).to match({ + 'pact:matcher:type' => 'decimal', + 'pact:generator:type' => 'RandomHexadecimal', + 'digits' => 8, + 'value' => match(/[0-9a-f]{8}/) + }) + end + end + end + + RSpec.describe RandomStringGenerator do + subject { described_class.new(size: 12) } + + describe '#as_basic' do + it 'returns the correct hash' do + expect(subject.as_basic).to match({ + 'pact:matcher:type' => 'type', + 'pact:generator:type' => 'RandomString', + 'size' => 12, + 'value' => match(/[a-zA-Z0-9]{12}/) + }) + end + end + end + + RSpec.describe UuidGenerator do + subject { described_class.new } + + describe '#as_basic' do + it 'returns the correct hash' do + expect(subject.as_basic).to match({ + 'pact:generator:type' => 'Uuid', + 'pact:matcher:type' => 'regex', + 'regex' => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}', + 'value' => match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/) # rubocop:disable Layout/LineLength + }) + end + end + end + + RSpec.describe DateGenerator do + context 'with format' do + subject { described_class.new(format: 'yyyy-MM-dd') } + + it 'returns the correct hash' do + expect(subject.as_basic).to match({ + 'pact:matcher:type' => 'date', + 'pact:generator:type' => 'Date', + 'format' => 'yyyy-MM-dd', + 'value' => match(/\d{4}-\d{2}-\d{2}/) + }) + end + end + + context 'without format' do + subject { described_class.new } + + it 'returns the correct hash' do + expect(subject.as_basic).to match({ + 'format' => 'yyyy-MM-dd', + 'pact:generator:type' => 'Date', + 'pact:matcher:type' => 'date', + 'value' => match(/\d{4}-\d{2}-\d{2}/) + }) + end + end + end + + RSpec.describe TimeGenerator do + context 'with format' do + subject { described_class.new(format: 'HH:mm:ss') } + + it 'returns the correct hash' do + expect(subject.as_basic).to match({ + 'pact:generator:type' => 'Time', + 'pact:matcher:type' => 'time', + 'format' => 'HH:mm:ss', + 'value' => match(/\d{2}:\d{2}:\d{2}/) + }) + end + end + + context 'without format' do + subject { described_class.new } + + it 'returns the correct hash' do + expect(subject.as_basic).to match({ + 'format' => 'HH:mm', + 'pact:generator:type' => 'Time', + 'pact:matcher:type' => 'time', + 'value' => match(/\d{2}:\d{2}/) + }) + end + end + end + + RSpec.describe DateTimeGenerator do + context 'with format' do + subject { described_class.new(format: "yyyy-MM-dd'T'HH:mm:ssZ") } + + it 'returns the correct hash' do + expect(subject.as_basic).to match({ + 'pact:generator:type' => 'DateTime', + 'pact:matcher:type' => 'datetime', + 'format' => "yyyy-MM-dd'T'HH:mm:ssZ", + 'value' => match(/\d{4}-\d{2}-\d{2}'T'\d{2}:\d{2}:\d{2}\+\d{4}/) + }) + end + end + + context 'without format' do + subject { described_class.new } + + it 'returns the correct hash' do + expect(subject.as_basic).to match({ + 'format' => 'yyyy-MM-dd HH:mm', + 'pact:generator:type' => 'DateTime', + 'pact:matcher:type' => 'datetime', + 'value' => match(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}/) + }) + end + end + end + + RSpec.describe RandomBooleanGenerator do + subject { described_class.new } + + describe '#as_basic' do + it 'returns the correct hash' do + match({ + 'pact:generator:type' => 'RandomBoolean', + 'pact:matcher:type' => 'boolean', + 'value' => satisfy { |v| [true, false].include?(v) } + }) + end + end + end + + RSpec.describe ProviderStateGenerator do + subject { described_class.new(expression: '/alligators/${alligator_name}', example: '/alligators/Mary') } + + describe '#as_basic' do + it 'returns the correct hash' do + expect(subject.as_basic).to eq({ + 'pact:generator:type' => 'ProviderState', + 'pact:matcher:type' => 'type', + 'expression' => '/alligators/${alligator_name}', + 'value' => '/alligators/Mary' + }) + end + end + end + + RSpec.describe MockServerURLGenerator do + subject { described_class.new(regex: 'http://localhost:\\d+', example: 'http://localhost:1234') } + + describe '#as_basic' do + it 'returns the correct hash' do + expect(subject.as_basic).to eq({ + 'pact:generator:type' => 'MockServerURL', + 'pact:matcher:type' => 'regex', + 'regex' => 'http://localhost:\\d+', + 'example' => 'http://localhost:1234', + 'value' => 'http://localhost:1234' + }) + end + end + end + end +end diff --git a/spec/lib/matchers_spec.rb b/spec/lib/matchers_spec.rb new file mode 100644 index 00000000..d7d1b1d7 --- /dev/null +++ b/spec/lib/matchers_spec.rb @@ -0,0 +1,491 @@ +# frozen_string_literal: true + +RSpec.describe Pact::Matchers do + subject(:test_class) { Class.new { extend Pact::Matchers } } + + context 'with basic format serialization' do + it 'properly builds matcher for UUID' do + expect(test_class.match_uuid.as_basic).to eq({ + 'pact:matcher:type' => 'regex', + 'value' => 'e1d01e04-3a2b-4eed-a4fb-54f5cd257338', + :regex => '(?i-mx:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})' # rubocop:disable Layout/LineLength + }) + end + + it 'properly builds matcher for regex' do + expect(test_class.match_regex(/(A-Z){1,3}/, 'ABC').as_basic).to eq({ + 'pact:matcher:type' => 'regex', + 'value' => 'ABC', + :regex => '(?-mix:(A-Z){1,3})' + }) + end + + it 'properly builds matcher for datetime' do + expect(test_class.match_datetime('yyyy-MM-dd HH:mm:ssZZZZZ', '2020-05-21 16:44:32+10:00').as_basic).to eq({ + 'pact:matcher:type' => 'datetime', # rubocop:disable Layout/LineLength + 'value' => '2020-05-21 16:44:32+10:00', # rubocop:disable Layout/LineLength + :format => 'yyyy-MM-dd HH:mm:ssZZZZZ' # rubocop:disable Layout/LineLength + }) + end + + it 'properly builds matcher for iso8601' do + expect(test_class.match_iso8601('2020-05-21T16:44:32').as_basic).to eq({ + 'pact:matcher:type' => 'regex', + 'value' => '2020-05-21T16:44:32', + :regex => '(?i-mx:\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)*(.\\d{2}:\\d{2})*)' # rubocop:disable Layout/LineLength + }) + end + + it 'properly builds matcher for date' do + expect(test_class.match_date('yyyy-MM-dd', '2020-05-21').as_basic).to eq({ + 'pact:matcher:type' => 'date', + 'value' => '2020-05-21', + :format => 'yyyy-MM-dd' + }) + end + + it 'properly builds matcher for time' do + expect(test_class.match_time('HH:mm:ss', '16:44:32').as_basic).to eq({ + 'pact:matcher:type' => 'time', + 'value' => '16:44:32', + :format => 'HH:mm:ss' + }) + end + + it 'properly builds matcher for include' do + expect(test_class.match_include('some string').as_basic).to eq({ + 'pact:matcher:type' => 'include', + 'value' => 'some string' + }) + end + + it 'properly builds matcher for any string' do + expect(test_class.match_any_string.as_basic).to eq({ + 'pact:matcher:type' => 'regex', + 'value' => 'any', + :regex => '(?-mix:.*)' + }) + expect(test_class.match_any_string('').as_basic).to eq({ + 'pact:matcher:type' => 'regex', + 'value' => '', + :regex => '(?-mix:.*)' + }) + end + + it 'properly builds matcher for boolean values' do + expect(test_class.match_any_boolean.as_basic).to eq({ + 'pact:matcher:type' => 'boolean', + 'value' => true + }) + end + + it 'properly builds matcher for integer values' do + expect(test_class.match_any_integer.as_basic).to eq({ + 'pact:matcher:type' => 'integer', + 'value' => 10 + }) + end + + it 'properly builds matcher for float values' do + expect(test_class.match_any_decimal.as_basic).to eq({ + 'pact:matcher:type' => 'decimal', + 'value' => 10.0 + }) + end + + it 'properly builds matcher for exact values' do + expect(test_class.match_exactly('some arg').as_basic).to eq({ + 'pact:matcher:type' => 'equality', + 'value' => 'some arg' + }) + expect(test_class.match_exactly(1).as_basic).to eq({ + 'pact:matcher:type' => 'equality', + 'value' => 1 + }) + expect(test_class.match_exactly(true).as_basic).to eq({ + 'pact:matcher:type' => 'equality', + 'value' => true + }) + end + + it 'properly builds typed matcher' do + expect(test_class.match_type_of(1).as_basic).to eq({ + 'pact:matcher:type' => 'type', + 'value' => 1 + }) + expect { test_class.match_type_of(Object.new).as_basic }.to raise_error(/is not a primitive/) + end + + it 'properly builds each matcher' do + expect(test_class.match_each(1).as_basic).to eq({ + 'pact:matcher:type' => 'type', + 'value' => [1], + :min => 1 + }) + expect(test_class.match_each(true).as_basic).to eq({ + 'pact:matcher:type' => 'type', + 'value' => [true], + :min => 1 + }) + expect(test_class.match_each('some').as_basic).to eq({ + 'pact:matcher:type' => 'type', + 'value' => ['some'], + :min => 1 + }) + expect(test_class.match_each( + { + str: test_class.match_any_string('str'), + bool: test_class.match_any_boolean(true), + num: test_class.match_any_number(1), + nested: test_class.match_each( + { + a: 1, + b: '2' + } + ) + } + ).as_basic).to eq({ + 'pact:matcher:type' => 'type', + 'value' => [ + { + str: { + 'pact:matcher:type' => 'regex', + :regex => '(?-mix:.*)', + 'value' => 'str' + }, + bool: { + 'pact:matcher:type' => 'boolean', + 'value' => true + }, + num: { + 'pact:matcher:type' => 'number', + 'value' => 1 + }, + nested: { + 'pact:matcher:type' => 'type', + 'value' => [ + { a: 1, b: '2' } + ], + :min => 1 + } + } + ], + :min => 1 + }) + end + + it 'properly builds each-key matcher' do + expect(test_class.match_each_key({ 'some-key' => 'value' }, + test_class.match_regex(/\w+-\w+/, 'some-key')).as_basic).to eq( + { + 'pact:matcher:type' => 'each-key', + :rules => [ + { + 'pact:matcher:type' => 'regex', + :regex => '(?-mix:\\w+-\\w+)', + 'value' => 'some-key' + } + ], + 'value' => { 'some-key' => 'value' } + } + ) + expect(test_class.match_each_key({ 'some-key' => { 'value1' => 1, 'value2' => 2 } }, + test_class.match_regex(/\w+-\w+/, 'some-key')).as_basic).to eq( + { + 'pact:matcher:type' => 'each-key', + :rules => [ + { + 'pact:matcher:type' => 'regex', + :regex => '(?-mix:\\w+-\\w+)', + 'value' => 'some-key' + } + ], + 'value' => { 'some-key' => { 'value1' => 1, 'value2' => 2 } } + } + ) + end + + it 'properly builds each-value matcher' do + expect(test_class.match_each_value({ 'some-key' => 'value' }, + test_class.match_regex(/\w+/, 'value')).as_basic).to eq( + { + 'pact:matcher:type' => 'each-value', + :rules => [ + { + 'pact:matcher:type' => 'regex', + :regex => '(?-mix:\\w+)', + 'value' => 'value' + } + ], + 'value' => { 'some-key' => 'value' } + } + ) + expect(test_class.match_each_value( + { 'some-key' => { 'value1' => test_class.match_any_string('1'), 'value2' => test_class.match_any_number(2) } }, + test_class.match_regex(/\w+-\w+/, 'some-key') + ).as_basic).to eq( + { + 'pact:matcher:type' => 'each-value', + :rules => [ + { + 'pact:matcher:type' => 'regex', + :regex => '(?-mix:\\w+-\\w+)', + 'value' => 'some-key' + } + ], + 'value' => { + 'some-key' => { + 'value1' => { + 'pact:matcher:type' => 'regex', + :regex => '(?-mix:.*)', + 'value' => '1' + }, + 'value2' => { + 'pact:matcher:type' => 'number', + 'value' => 2 + } + } + } + } + ) + end + + it 'properly builds each-key-value matcher' do + expect(test_class.match_each_kv( + { + 'some-key' => { + 'value1' => test_class.match_any_string('1') + } + }, test_class.match_regex(/\w+/, 'value') + ).as_basic).to eq({ + 'pact:matcher:type' => [ + { + 'pact:matcher:type' => 'each-key', + :rules => [ + { + 'pact:matcher:type' => 'regex', + :regex => '(?-mix:\\w+)', + 'value' => 'value' + } + ], + 'value' => {} + }, + { + 'pact:matcher:type' => 'each-value', + :rules => [ + { + 'pact:matcher:type' => 'type', + 'value' => '' + } + ], + 'value' => {} + } + ], + 'value' => { + 'some-key' => { + 'value1' => { + 'pact:matcher:type' => 'regex', + :regex => '(?-mix:.*)', + 'value' => '1' + } + } + } + }) + end + + it 'properly builds semver matcher' do + expect(test_class.match_semver.as_basic).to eq({ + 'pact:matcher:type' => 'semver' + }) + end + it 'properly builds content_type matcher' do + expect(test_class.match_content_type('application/xml').as_basic).to eq({ + 'pact:matcher:type' => 'contentType', + 'value' => 'application/xml' + }) + end + it 'properly builds not_empty matcher' do + expect(test_class.match_not_empty.as_basic).to eq({ + 'pact:matcher:type' => 'notEmpty' + }) + end + + it 'properly builds values matcher' do + expect(Pact::Matchers::V3::Values.new.as_basic).to eq({ + 'pact:matcher:type' => 'values' + }) + end + + it 'properly builds null matcher' do + expect(Pact::Matchers::V3::Null.new.as_basic).to eq({ + 'pact:matcher:type' => 'null' + }) + end + + it 'properly builds status_code matcher' do + expect(test_class.match_status_code(200).as_basic).to eq({ + 'pact:matcher:type' => 'statusCode', + 'status' => 200 + }) + expect(test_class.match_status_code('nonError').as_basic).to eq({ + 'pact:matcher:type' => 'statusCode', + 'status' => 'nonError' + }) + end + end + + context 'with plugin format serialization' do + it 'properly builds matcher for UUID' do + expect(test_class.match_uuid.as_plugin).to eq("matching(regex, '(?i-mx:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})', 'e1d01e04-3a2b-4eed-a4fb-54f5cd257338')") # rubocop:disable Layout/LineLength + end + + it 'properly builds matcher for regex' do + expect(test_class.match_regex(/(A-Z){1,3}/, + 'ABC').as_plugin).to eq("matching(regex, '(?-mix:(A-Z){1,3})', 'ABC')") + end + + it 'properly builds matcher for datetime' do + expect(test_class.match_datetime('yyyy-MM-dd HH:mm:ssZZZZZ', + '2020-05-21 16:44:32+10:00').as_plugin).to eq("matching(datetime, 'yyyy-MM-dd HH:mm:ssZZZZZ', '2020-05-21 16:44:32+10:00')") # rubocop:disable Layout/LineLength + end + + it 'properly builds matcher for iso8601' do + expect(test_class.match_iso8601('2020-05-21T16:44:32').as_plugin).to eq("matching(regex, '(?i-mx:\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)*(.\\d{2}:\\d{2})*)', '2020-05-21T16:44:32')") # rubocop:disable Layout/LineLength + end + + it 'properly builds matcher for date' do + expect(test_class.match_date('yyyy-MM-dd', + '2020-05-21').as_plugin).to eq("matching(date, 'yyyy-MM-dd', '2020-05-21')") + end + + it 'properly builds matcher for time' do + expect(test_class.match_time('HH:mm:ss', '16:44:32').as_plugin).to eq("matching(time, 'HH:mm:ss', '16:44:32')") + end + + it 'properly builds matcher for include' do + expect(test_class.match_include('some string').as_plugin).to eq("matching(include, 'some string')") + end + + it 'properly builds matcher for any string' do + expect(test_class.match_any_string.as_plugin).to eq("matching(regex, '(?-mix:.*)', 'any')") + expect(test_class.match_any_string('').as_plugin).to eq("matching(regex, '(?-mix:.*)', '')") + end + + it 'properly builds matcher for boolean values' do + expect(test_class.match_any_boolean.as_plugin).to eq('matching(boolean, true)') + end + + it 'properly builds matcher for integer values' do + expect(test_class.match_any_integer.as_plugin).to eq('matching(integer, 10)') + end + + it 'properly builds matcher for float values' do + expect(test_class.match_any_decimal.as_plugin).to eq('matching(decimal, 10.0)') + end + + it 'properly builds matcher for exact values' do + expect(test_class.match_exactly('some arg').as_plugin).to eq("matching(equalTo, 'some arg')") + expect(test_class.match_exactly(1).as_plugin).to eq('matching(equalTo, 1)') + expect(test_class.match_exactly(true).as_plugin).to eq('matching(equalTo, true)') + end + + it 'properly builds typed matcher' do + expect(test_class.match_type_of(1).as_plugin).to eq('matching(type, 1)') + expect { test_class.match_type_of(Object.new).as_plugin }.to raise_error(/is not a primitive/) + end + + it 'properly builds each matcher' do + expect(test_class.match_each(1).as_plugin).to eq('eachValue(matching(type, 1))') + expect(test_class.match_each(true).as_plugin).to eq('eachValue(matching(type, true))') + expect(test_class.match_each('some').as_plugin).to eq("eachValue(matching(type, 'some'))") + expect(test_class.match_each( + { + str: test_class.match_any_string('str'), + bool: test_class.match_any_boolean(true), + num: test_class.match_any_number(1), + nested: test_class.match_each( + { + a: 1, + b: '2' + } + ) + } + ).as_plugin).to eq({ + 'pact:match' => "eachValue(matching($'SAMPLE'))", + 'SAMPLE' => { + str: "matching(regex, '(?-mix:.*)', 'str')", + bool: 'matching(boolean, true)', + num: 'matching(number, 1)', + nested: { + 'pact:match' => "eachValue(matching($'SAMPLE'))", + 'SAMPLE' => { a: 1, b: '2' } + } + } + }) + end + + it 'properly builds each-key matcher' do + expect(test_class.match_each_key({ 'some-key' => 'value' }, + test_class.match_regex(/\w+-\w+/, + 'some-key')).as_plugin).to eq("eachKey(matching(regex, '(?-mix:\\w+-\\w+)', 'some-key'))") # rubocop:disable Layout/LineLength + expect(test_class.match_each_key({ 'some-key' => { 'value1' => 1, 'value2' => 2 } }, + test_class.match_regex(/\w+-\w+/, + 'some-key')).as_plugin).to eq("eachKey(matching(regex, '(?-mix:\\w+-\\w+)', 'some-key'))") # rubocop:disable Layout/LineLength + end + + it 'properly builds each-value matcher' do + expect(test_class.match_each_value( + { + str: test_class.match_any_string('str'), + bool: test_class.match_any_boolean(true), + num: test_class.match_any_number(1), + nested: test_class.match_each( + { + a: 1, + b: '2' + } + ) + } + ).as_plugin).to eq({ + 'pact:match' => "eachValue(matching($'SAMPLE'))", + 'SAMPLE' => { + str: "matching(regex, '(?-mix:.*)', 'str')", + bool: 'matching(boolean, true)', + num: 'matching(number, 1)', + nested: { + 'pact:match' => "eachValue(matching($'SAMPLE'))", + 'SAMPLE' => { a: 1, b: '2' } + } + } + }) + end + + it 'properly builds semver matcher' do + expect(test_class.match_semver('1.2.3').as_plugin).to eq("matching(semver, '1.2.3')") + end + + it 'properly builds content_type matcher' do + expect(test_class.match_content_type('application/xml', + '').as_plugin).to eq("matching(contentType, 'application/xml', '')") # rubocop:disable Layout/LineLength + end + + it 'properly builds not_empty matcher' do + expect(test_class.match_not_empty('some value').as_plugin).to eq("notEmpty('some value')") + end + end + + context 'with common regex' do + it 'has valid regex for iso8601' do + expect(described_class::ISO8601_REGEX).to match('2020-05-21T16:44:32') + expect(described_class::ISO8601_REGEX).to match('2020-05-21T16:44:32+10:00') + expect(described_class::ISO8601_REGEX).to match('2020-05-21T16:44:32.123+10:00') + expect(described_class::ISO8601_REGEX).to match('2020-05-21T16:44:32.123') + expect(described_class::ISO8601_REGEX).to match('2020-05-21T16:44:32.123456+10:00') + expect(described_class::ISO8601_REGEX).to match('2020-05-21T16:44:32.123456') + end + + it 'has valid regex for UUID' do + expect(described_class::UUID_REGEX).to match(SecureRandom.uuid) + end + end +end diff --git a/spec/lib/pact/cli/spec_criteria_spec.rb b/spec/lib/pact/cli/spec_criteria_spec.rb deleted file mode 100644 index 9ba242fe..00000000 --- a/spec/lib/pact/cli/spec_criteria_spec.rb +++ /dev/null @@ -1,58 +0,0 @@ -require 'pact/cli/spec_criteria' - -module Pact - module Cli - describe SpecCriteria do - describe "#spec_criteria" do - - let(:env_description) { "pact description set in ENV"} - let(:env_provider_state) { "provider state set in ENV"} - let(:env_pact_broker_interaction_id) { "interaction id set in ENV" } - let(:interaction_index) { 2 } - let(:env_criteria) do - { - :description=>/#{env_description}/, - :provider_state=>/#{env_provider_state}/, - :_id => env_pact_broker_interaction_id, - :index => interaction_index - } - end - - let(:defaults) { {:description => default_description, :provider_state => default_provider_state} } - - let(:subject) { Pact::App.new } - - context "when options are defined" do - let(:options) do - { - description: env_description, - provider_state: env_provider_state, - pact_broker_interaction_id: env_pact_broker_interaction_id, - interaction_index: interaction_index - } - end - - it "returns the env vars as regexes" do - expect(Pact::Cli::SpecCriteria.call(options)).to eq(env_criteria) - end - end - - context "when ENV variables are not defined" do - let(:options) { {} } - - it "returns an empty hash" do - expect(Pact::Cli::SpecCriteria.call(options)).to eq({}) - end - end - - context "when provider state is an empty string" do - let(:options) { { provider_state: '' } } - - it "returns a nil provider state so that it matches a nil provider state on the interaction" do - expect(Pact::Cli::SpecCriteria.call(options)[:provider_state]).to be_nil - end - end - end - end - end -end diff --git a/spec/lib/pact/cli_spec.rb b/spec/lib/pact/cli_spec.rb deleted file mode 100644 index a3227f72..00000000 --- a/spec/lib/pact/cli_spec.rb +++ /dev/null @@ -1,51 +0,0 @@ -require 'pact/cli' -require 'pact/cli/generate_pact_docs' -require 'pact/doc/generator' - -module Pact - describe CLI do - describe "docs" do - - let(:docs) { subject.invoke :docs } - - before do - allow(Pact::Doc::Generate).to receive(:call) - end - - it "generates Markdown documentation" do - expect(Pact::Doc::Generate).to receive(:call).with(anything, anything, [Pact::Doc::Markdown::Generator]) - docs - end - - context "with no arguments" do - - subject { CLI.new } - - it 'uses the default Pact configuration for pact_dir and doc_dir' do - expect(Pact::Doc::Generate).to receive(:call).with(Dir.pwd + '/spec/pacts', Dir.pwd + '/doc/pacts', anything) - docs - end - end - - context "with a pact_dir specified" do - - subject { CLI.new([], pact_dir: 'pacts') } - - it 'uses the specified pact_dir' do - expect(Pact::Doc::Generate).to receive(:call).with('pacts', anything, anything) - docs - end - end - - context "with a doc_dir specified" do - - subject { CLI.new([], doc_dir: 'docs') } - - it 'uses the specified doc_dir' do - expect(Pact::Doc::Generate).to receive(:call).with(anything, 'docs', anything) - docs - end - end - end - end -end diff --git a/spec/lib/pact/configuration_spec.rb b/spec/lib/pact/configuration_spec.rb deleted file mode 100644 index ebacf8fe..00000000 --- a/spec/lib/pact/configuration_spec.rb +++ /dev/null @@ -1,303 +0,0 @@ -require 'spec_helper' -require 'pact/configuration' - -describe Pact do - - before do - Pact.clear_configuration - end - - describe "configure" do - KEY_VALUE_PAIRS = {pact_dir: 'a path', log_dir: 'a dir', logger: 'a logger'} - - KEY_VALUE_PAIRS.each do | key, value | - it "should allow configuration of #{key}" do - Pact.configure do | config | - config.send("#{key}=".to_sym, value) - end - - expect(Pact.configuration.send(key)).to eql(value) - end - end - - end - - describe Pact::Configuration do - let(:configuration) { Pact::Configuration.new } - - describe "log_dir" do - it "sets the location of the logs" do - expect(Logger).to receive(:new).with("./tmp/logs/pact.log").and_call_original - Pact.configure do | config | - config.log_dir = "./tmp/logs" - end - Pact.configuration.logger - end - end - - describe "logger" do - it "sets the location of the logs to log_dir by default" do - expect(Logger).to receive(:new).with(File.expand_path("./log/pact.log")).and_call_original - Pact.configuration.logger - end - it "defaults to DEBUG" do - expect(Pact.configuration.logger.level).to eq Logger::DEBUG - end - end - - describe "doc_dir" do - it "defaults to ./doc/pacts" do - expect(Pact.configuration.doc_dir).to eq File.expand_path("./doc/pacts") - end - - it "can be changed" do - Pact.configuration.doc_dir = "newdir" - expect(Pact.configuration.doc_dir).to eq "newdir" - end - end - - describe "doc_generator" do - - it "allows the configuration of more than one generator" do - Pact.configuration.add_doc_generator :markdown - Pact.configuration.add_doc_generator :markdown - expect(Pact.configuration.doc_generators.size).to eq 2 - end - - context "with a symbol" do - it "allows configuration of a doc_generator" do - Pact.configuration.doc_generator = :markdown - expect(Pact.configuration.doc_generators).to eq [Pact::Doc::Markdown::Generator] - end - end - - context "with anything that responds to 'call'" do - - it "allows configuration of a doc_generator" do - callable = lambda { | pact_dir, doc_dir | "doc" } - Pact.configuration.doc_generator = callable - expect(Pact.configuration.doc_generators.first).to be callable - end - - end - - context "with something that does not respond to call and doesn't have a matching doc_generator" do - it "raises an error" do - expect { Pact.configuration.doc_generator = Object.new }.to raise_error "doc_generator needs to respond to call, or be in the preconfigured list: [:markdown]" - end - end - - end - - describe "#diff_formatter_for_content_type" do - - let(:subject) { Pact::Configuration.new } - - it "returns the Pact::Matchers::UnixDiffFormatter by default" do - expect(subject.diff_formatter_for_content_type 'anything').to eq(Pact::Matchers::UnixDiffFormatter) - end - - Pact::Configuration::DIFF_FORMATTERS.each_pair do | key, diff_formatter | - - context "when set to :#{key}" do - - before do - subject.diff_formatter = key - end - - it "sets the diff_formatter to #{diff_formatter}" do - expect(subject.diff_formatter_for_content_type nil).to be diff_formatter - end - end - - end - - context "when set to an object that responds to call" do - - let(:diff_formatter) { lambda{ | diff| } } - - before do - subject.diff_formatter = diff_formatter - end - - it "sets the diff_formatter to the object" do - expect(subject.diff_formatter_for_content_type nil).to be diff_formatter - end - end - - context "when set to an object that does not respond to call and isn't a known default option" do - it "raises an error" do - expect { subject.diff_formatter = Object.new }.to raise_error "Pact diff_formatter needs to respond to call, or be in the preconfigured list: [:embedded, :unix, :list]" - expect { subject.diff_formatter = Object.new }.to raise_error "Pact diff_formatter needs to respond to call, or be in the preconfigured list: [:embedded, :unix, :list]" - end - end - - end - - describe "diff_formatter_for_content_type" do - let(:diff_formatter) { lambda { |expected, actual| }} - context "with the default configuration" do - context "when the content type is nil" do - it "returns the UnixDiffFormatter" do - expect(Pact.configuration.diff_formatter_for_content_type nil).to eq Pact::Matchers::UnixDiffFormatter - end - end - context "when the content type is application/json" do - it "returns the UnixDiffFormatter" do - expect(Pact.configuration.diff_formatter_for_content_type nil).to eq Pact::Matchers::UnixDiffFormatter - end - end - context "when the content type is text/plain" do - it "returns the UnixDiffFormatter" do - expect(Pact.configuration.diff_formatter_for_content_type nil).to eq Pact::Matchers::UnixDiffFormatter - end - end - end - context "with a custom diff_formatter registered for nil content type" do - context "when the content_type is nil" do - it "returns the custom diff_formatter" do - Pact.configuration.register_diff_formatter nil, diff_formatter - expect(Pact.configuration.diff_formatter_for_content_type nil).to eq diff_formatter - end - end - end - context "with a custom diff_formatter registered for json content type" do - context "when the content_type is application/json" do - it "returns the custom diff_formatter" do - Pact.configuration.register_diff_formatter /json/, diff_formatter - expect(Pact.configuration.diff_formatter_for_content_type 'application/json').to eq diff_formatter - end - end - end - end - - describe "register_body_differ" do - - let(:differ) { lambda{ |expected, actual| } } - - context "with a string for a content type" do - it "configures the differ for the given content type" do - Pact.configure do | config | - config.register_body_differ 'application/xml', differ - end - - expect(Pact.configuration.body_differ_for_content_type 'application/xml').to be differ - end - end - - context "with a regexp for a content type" do - it "returns a matching differ" do - Pact.configuration.register_body_differ /application\/.*xml/, differ - expect(Pact.configuration.body_differ_for_content_type 'application/hal+xml').to be differ - end - end - - context "when a non string or regexp is used to register a differ" do - it "raises an error" do - expect { Pact.configuration.register_body_differ 1, differ }.to raise_error /Invalid/ - end - end - - context "when something that does not respond to call is sumbitted as a differ" do - it "raises an error" do - expect { Pact.configuration.register_body_differ 'thing', Object.new }.to raise_error /responds to call/ - end - end - - context "when a nil content type is registered for responses without a content type header" do - it "returns that differ if the differ for a nil content type is requested" do - Pact.configuration.register_body_differ nil, differ - expect(Pact.configuration.body_differ_for_content_type(nil)).to be differ - end - end - - end - - describe "body_differ_for_content_type" do - - let(:differ) { lambda { |expected, actual| }} - - context "when 2 potentially matching content types have a differ registered" do - let(:differ_1) { lambda{ |expected, actual| } } - let(:differ_2) { lambda{ |expected, actual| } } - - it "returns the differ that was configured first" do - Pact.configuration.register_body_differ /application\/.*xml/, differ_2 - Pact.configuration.register_body_differ /application\/hal\+xml/, differ_1 - expect(Pact.configuration.body_differ_for_content_type 'application/hal+xml').to be differ_2 - end - end - - context "when a nil content type is given" do - it "returns the text differ" do - expect(Pact.configuration.body_differ_for_content_type nil).to be Pact::TextDiffer - end - end - - context "when no matching content type is found" do - it "returns the text differ" do - expect(Pact.configuration.body_differ_for_content_type 'blah').to be Pact::TextDiffer - end - end - - context "when the nil content type has a custom differ configured" do - it "returns the custom differ" do - Pact.configuration.register_body_differ nil, differ - expect(Pact.configuration.body_differ_for_content_type(nil)).to be differ - end - end - - context "when a custom differ is registered for a content type that has a default differ" do - it "returns the custom differ" do - Pact.configuration.register_body_differ /application\/json/, differ - expect(Pact.configuration.body_differ_for_content_type 'application/json').to be differ - end - end - end - - describe "pactfile_write_mode" do - context "when @pactfile_write_mode is :overwrite" do - it 'returns :overwrite' do - configuration.pactfile_write_mode = :overwrite - expect(configuration.pactfile_write_mode).to eq :overwrite - end - end - context "when @pactfile_write_mode is :update" do - it 'returns :overwrite' do - configuration.pactfile_write_mode = :update - expect(configuration.pactfile_write_mode).to eq :update - end - end - context "when @pactfile_write_mode is :smart" do - before do - configuration.pactfile_write_mode = :smart - expect(configuration).to receive(:is_rake_running?).and_return(is_rake_running) - end - context "when rake is running" do - let(:is_rake_running) { true } - it "returns :overwrite" do - expect(configuration.pactfile_write_mode).to eq :overwrite - end - end - context "when rake is not running" do - let(:is_rake_running) { false } - it "returns :update" do - expect(configuration.pactfile_write_mode).to eq :update - end - end - end - end - end - describe "default_configuration" do - it "should have a default pact_dir" do - expect(Pact.configuration.pact_dir).to eql File.expand_path('./spec/pacts') - end - it "should have a default log_dir" do - expect(Pact.configuration.log_dir).to eql File.expand_path('./log') - end - it "should have a default logger configured" do - expect(Pact.configuration.logger).to be_instance_of Logger - end - end - -end \ No newline at end of file diff --git a/spec/lib/pact/consumer/configuration_spec.rb b/spec/lib/pact/consumer/configuration_spec.rb deleted file mode 100644 index eb3f1df0..00000000 --- a/spec/lib/pact/consumer/configuration_spec.rb +++ /dev/null @@ -1,87 +0,0 @@ -require 'spec_helper' -require 'pact/consumer/configuration' - -module Pact::Consumer::Configuration - - describe MockService do - - let(:world) { Pact::Consumer::World.new } - let(:port_number) { 8888 } - before do - Pact.clear_configuration - allow(Pact::MockService::AppManager.instance).to receive(:register_mock_service_for).and_return(port_number) - allow(Pact).to receive(:consumer_world).and_return(world) - end - - describe "configure_consumer_contract_builder" do - let(:consumer_name) {'consumer'} - subject { - MockService.build :mock_service, consumer_name, provider_name do - port port_number - standalone true - verify true - end - } - - let(:provider_name) { 'Mock Provider' } - let(:consumer_contract_builder) { instance_double('Pact::Consumer::ConsumerContractBuilder') } - let(:url) { "http://localhost:#{port_number}" } - - it "adds a verification to the Pact configuration" do - allow(Pact::Consumer::ConsumerContractBuilder).to receive(:new).and_return(consumer_contract_builder) - subject - expect(consumer_contract_builder).to receive(:verify) - Pact.configuration.provider_verifications.first.call - end - - context "when standalone" do - it "does not register the app with the AppManager" do - expect(Pact::MockService::AppManager.instance).to_not receive(:register_mock_service_for) - subject - end - end - context "when not standalone" do - subject { - MockService.build :mock_service, consumer_name, provider_name do - port port_number - standalone false - verify true - pact_specification_version '1' - end - } - let(:opts) { { pact_specification_version: '1', find_available_port: false } } - it "registers the app with the AppManager" do - expect(Pact::MockService::AppManager.instance).to receive(:register_mock_service_for). - with(provider_name, url, opts). - and_return(port_number) - subject - end - end - - context "without port specification" do - let(:url) { 'http://localhost' } - subject { MockService.build(:mock_service, consumer_name, provider_name) {} } - let(:opts) { { pact_specification_version: '2', find_available_port: true } } - it "registers the app with the AppManager with find_available_port option" do - expect(Pact::MockService::AppManager.instance).to receive(:register_mock_service_for). - with(provider_name, url, opts). - and_return(port_number) - subject - end - end - - context "without port specification and old pact-mock_service" do - let(:url) { 'http://localhost' } - subject { MockService.build(:mock_service, consumer_name, provider_name) {} } - let(:opts) { { pact_specification_version: '2', find_available_port: true } } - - it "checks and raises an error" do - expect(Pact::MockService::AppManager.instance).to receive(:register_mock_service_for). - with(provider_name, url, opts). - and_return(nil) - expect { subject }.to raise_error(/pact-mock_service.+does not support/) - end - end - end - end -end diff --git a/spec/lib/pact/consumer/consumer_contract_builder_spec.rb b/spec/lib/pact/consumer/consumer_contract_builder_spec.rb deleted file mode 100644 index 3f35a070..00000000 --- a/spec/lib/pact/consumer/consumer_contract_builder_spec.rb +++ /dev/null @@ -1,102 +0,0 @@ -require 'spec_helper' -require 'fileutils' -require 'pathname' - -module Pact - module Consumer - describe ConsumerContractBuilder do - - let(:consumer_name) { 'a consumer' } - let(:provider_name) { 'a provider' } - let(:pact_dir) { './spec/pacts' } - - subject do - Pact::Consumer::ConsumerContractBuilder.new( - consumer_name: consumer_name, - provider_name: provider_name, - pactfile_write_mode: :overwrite, - port: 2222, - host: 'localhost', - pact_dir: pact_dir) - end - - describe "#handle_interaction_fully_defined" do - - let(:interaction_hash) { - { - description: 'Test request', - request: { - method: 'post', - path: '/foo', - body: Term.new(generate: 'waffle', matcher: /ffl/), - headers: { 'Content-Type' => 'application/json' }, - query: "", - }, - response: { - baz: 'qux', - wiffle: 'wiffle' - } - } - } - - let(:interaction_json) { {} } - - let(:interaction) { Pact::Interaction.from_hash(JSON.load(interaction_hash.to_json)) } - - before do - stub_request(:post, 'localhost:2222/interactions') - end - - it "posts the interaction with generated response to the mock service" do - subject.handle_interaction_fully_defined interaction - expect(WebMock).to have_requested(:post, 'localhost:2222/interactions').with(body: interaction_json) - end - - it "resets the interaction_builder to nil" do - expect(subject).to receive(:interaction_builder=).with(nil) - subject.handle_interaction_fully_defined interaction - end - - it "validates the interaction" do - expect(interaction).to receive(:validate!) - subject.handle_interaction_fully_defined(interaction) - end - end - - describe "#mock_service_base_url" do - - subject do - ConsumerContractBuilder.new( - pactfile_write_mode: :overwrite, - pact_dir: './spec/pacts', - consumer_name: consumer_name, - provider_name: provider_name, - host: 'localhost', - port: 8888 - ) - end - - it "returns the mock service base URL" do - expect(subject.mock_service_base_url).to eq("http://localhost:8888") - end - end - - describe "#write_pact" do - - let(:body) do - { - consumer: { name: consumer_name }, - provider: { name: provider_name }, - pactfile_write_mode: "overwrite", - pact_dir: pact_dir - } - end - - it "posts the pact details to the mock service" do - allow_any_instance_of(Pact::MockService::Client).to receive(:write_pact).with(body) - subject.write_pact - end - end - end - end -end diff --git a/spec/lib/pact/consumer/interaction_builder_spec.rb b/spec/lib/pact/consumer/interaction_builder_spec.rb deleted file mode 100644 index 0927da9e..00000000 --- a/spec/lib/pact/consumer/interaction_builder_spec.rb +++ /dev/null @@ -1,108 +0,0 @@ -require 'spec_helper' -require 'pact/consumer/interaction_builder' - -module Pact - module Consumer - describe InteractionBuilder do - - subject { InteractionBuilder.new {|interaction|} } - let(:interaction) { double('Interaction').as_null_object} - - before do - expect(Interaction).to receive(:new).and_return(interaction) - end - - describe "given" do - context "with a string provider state" do - it "sets the provider_state on the interaction" do - expect(interaction).to receive(:provider_state=).with('blah') - subject.given('blah') - end - end - - context "with a symbol provider state" do - it "sets the provider_state on the interaction as a string" do - expect(interaction).to receive(:provider_state=).with('some_symbol') - subject.given(:some_symbol) - end - end - - it "returns itself" do - expect(subject.given(nil)).to be(subject) - end - end - - describe "upon_receiving" do - it "sets the description on the interaction" do - expect(interaction).to receive(:description=).with('blah') - subject.upon_receiving('blah') - end - - it "returns itself" do - expect(subject.given(nil)).to be(subject) - end - end - - describe "with" do - - let(:request) { {a: 'request'} } - let(:expected_request) { {an: 'expected_request'} } - - it "sets the request on the interaction as a instance of Request::Expected" do - expect(Pact::Request::Expected).to receive(:from_hash).with(request).and_return(expected_request) - expect(interaction).to receive(:request=).with(expected_request) - subject.with(request) - end - - it "returns itself" do - expect(subject.given(nil)).to be(subject) - end - end - - describe "will_respond_with" do - let(:response) { {a: 'response'} } - let(:provider) { double(callback: nil) } - let(:pact_response) { instance_double('Pact::Response') } - - before do - allow(Pact::Response).to receive(:new).and_return(pact_response) - end - - subject { InteractionBuilder.new {|interaction| provider.callback(interaction) } } - - it "creates a Pact::Response object from the given hash" do - expect(Pact::Response).to receive(:new).with(response) - subject.will_respond_with(response) - end - - it "sets the Pact::Response object on the interaction" do - expect(interaction).to receive(:response=).with(pact_response) - subject.will_respond_with(response) - end - - it "returns itself" do - expect(subject.given(nil)).to be(subject) - end - - it "invokes the 'on_interaction_fully_defined' callback" do - subject.will_respond_with response - end - end - - describe "without_writing_to_pact" do - it "sets the write_to_pact key to false on metadata" do - mock_metadata = {} - expect(interaction).to receive(:metadata).and_return(nil, mock_metadata) - - subject.without_writing_to_pact - - expect(mock_metadata).to eq({ write_to_pact: false }) - end - - it "returns itself" do - expect(subject.without_writing_to_pact).to be(subject) - end - end - end - end -end diff --git a/spec/lib/pact/consumer/service_consumer_spec.rb b/spec/lib/pact/consumer/service_consumer_spec.rb deleted file mode 100644 index d55f8447..00000000 --- a/spec/lib/pact/consumer/service_consumer_spec.rb +++ /dev/null @@ -1,11 +0,0 @@ -require 'spec_helper' - -module Pact - describe ServiceConsumer do - describe "as_json" do - it "returns a hash representation of the object" do - expect(ServiceConsumer.new(:name => "Bob").as_json).to eq :name => "Bob" - end - end - end -end diff --git a/spec/lib/pact/doc/generator_spec.rb b/spec/lib/pact/doc/generator_spec.rb deleted file mode 100644 index a9070222..00000000 --- a/spec/lib/pact/doc/generator_spec.rb +++ /dev/null @@ -1,84 +0,0 @@ -require 'spec_helper' -require 'pact/doc/generator' -require 'fileutils' - -module Pact - module Doc - describe Generator do - - let(:doc_dir) { './tmp/doc' } - let(:pact_dir) { './tmp/pacts' } - let(:file_name) { "Some Consumer - Some Provider#{file_extension}" } - let(:consumer_contract_renderer) { double("ConsumerContractRenderer", :call => doc_content) } - let(:doc_content) { "doc_content" } - let(:index_content) { "index_content" } - let(:expected_doc_path) { "#{doc_dir}/#{doc_type}/#{file_name}" } - let(:expected_index_path) { "#{doc_dir}/#{doc_type}/#{index_name}#{file_extension}" } - let(:doc_type) { 'markdown' } - let(:file_extension) { ".md" } - let(:actual_file_contents) { File.read(expected_doc_path) } - let(:actual_index_contents) { File.read(expected_index_path)} - let(:index_renderer) { double("IndexRenderer", :call => index_content)} - let(:index_name) { 'README' } - let(:after_hook) { double("hook", :call => nil)} - - before do - FileUtils.rm_rf doc_dir - FileUtils.rm_rf pact_dir - FileUtils.mkdir_p doc_dir - FileUtils.mkdir_p pact_dir - FileUtils.cp './spec/support/markdown_pact.json', pact_dir - end - - let(:options) { { consumer_contract_renderer: consumer_contract_renderer, doc_type: doc_type, file_extension: file_extension, index_renderer: index_renderer, index_name: index_name } } - - subject { Generator.new(pact_dir, doc_dir, options) } - - context "when there are existing files" do - let(:existing_doc_file_path) { File.join(doc_dir, doc_type, "leftover") } - - before do - FileUtils.mkdir_p File.dirname(existing_doc_file_path) - FileUtils.touch existing_doc_file_path - end - - it "clears the existing files" do - expect(File.exist?(existing_doc_file_path)).to be true - subject.call - expect(File.exist?(existing_doc_file_path)).to be false - end - end - - - it "creates an index" do - expect(index_renderer).to receive(:call).with("Some Consumer", {"Some Provider"=>"Some Consumer - Some Provider.md"}) - subject.call - expect(actual_index_contents).to eq(index_content) - end - - it "creates documentation" do - subject.call - expect(actual_file_contents).to eq(doc_content) - end - - context "with an after hook specified" do - - subject { Generator.new(pact_dir, doc_dir, options.merge(:after => after_hook)) } - - it "executes the hook" do - expect(after_hook).to receive(:call).with(pact_dir, "#{doc_dir}/#{doc_type}", instance_of(Array)) - subject.call - end - - it "passes in the consumer_contracts" do - expect(after_hook).to receive(:call) do | _, _, consumer_contracts | - expect(consumer_contracts.first).to be_instance_of(Pact::ConsumerContract) - end - subject.call - end - - end - - end - end -end \ No newline at end of file diff --git a/spec/lib/pact/doc/interaction_view_model_spec.rb b/spec/lib/pact/doc/interaction_view_model_spec.rb deleted file mode 100644 index 9fb3657a..00000000 --- a/spec/lib/pact/doc/interaction_view_model_spec.rb +++ /dev/null @@ -1,194 +0,0 @@ -require 'spec_helper' -require 'pact/doc/interaction_view_model' - -module Pact - module Doc - describe InteractionViewModel do - - let(:consumer_contract) { Pact::ConsumerContract.from_uri './spec/support/interaction_view_model.json' } - - let(:interaction_with_request_with_body_and_headers) { consumer_contract.find_interaction description: "a request with a body and headers" } - let(:interaction_with_request_without_body_and_headers) { consumer_contract.find_interaction description: "a request with an empty body and empty headers" } - let(:interaction_with_response_with_body_and_headers) { consumer_contract.find_interaction description: "a response with a body and headers" } - let(:interaction_with_response_without_body_and_headers) { consumer_contract.find_interaction description: "a response with an empty body and empty headers" } - - let(:interaction) { consumer_contract.interactions.first } - - subject { InteractionViewModel.new interaction, consumer_contract} - - describe "id" do - context "with HTML characters in the description" do - let(:interaction) { InteractionFactory.create description: "an alligator with > 100 legs exists" } - - it "escapes the HTML characters" do - expect(subject.id).to eq "an_alligator_with_>_100_legs_exists_given_a_thing_exists" - end - end - end - - describe "consumer_name" do - context "with markdown characters in the name" do - it "escapes the markdown characters" do - expect(subject.consumer_name).to eq "a\\*consumer" - end - end - end - - describe "provider_name" do - context "with markdown characters in the name" do - it "escapes the markdown characters" do - expect(subject.provider_name).to eq "a\\_provider" - end - end - end - - describe "request" do - - let(:interaction) { interaction_with_request_with_body_and_headers } - - it "includes the method" do - expect(subject.request).to include('"method"') - expect(subject.request).to include('"get"') - end - - it "includes the body" do - expect(subject.request).to include('"body"') - expect(subject.request).to include('"a body"') - end - - it "includes the headers" do - expect(subject.request).to include('"headers"') - expect(subject.request).to include('"a header"') - end - - it "includes the query" do - expect(subject.request).to include('"query"') - expect(subject.request).to include('"some=thing"') - end - - it "includes the path" do - expect(subject.request).to include('"path"') - expect(subject.request).to include('"/path"') - end - - it "renders the keys in a meaningful order" do - expect(subject.request).to match /"method".*"path".*"query".*"headers".*"body"/m - end - - context "when the body hash is empty" do - - let(:interaction) { interaction_with_request_without_body_and_headers } - - it "includes the body" do - expect(subject.request).to include("body") - end - end - context "when the headers hash is empty" do - - let(:interaction) { interaction_with_request_without_body_and_headers } - - it "does not include the headers" do - expect(subject.request).to_not include("headers") - end - end - - context "when a Pact::Term is present" do - let(:consumer_contract) { Pact::ConsumerContract.from_uri './spec/support/interaction_view_model_with_terms.json'} - let(:interaction) { consumer_contract.interactions.first } - - it "uses the generated value" do - expect(subject.request).to_not include("Term") - expect(subject.request).to include("sunny") - end - end - end - - describe "response" do - - let(:interaction) { interaction_with_response_with_body_and_headers } - - it "includes the status" do - expect(subject.response).to include('"status"') - end - - it "includes the body" do - expect(subject.response).to include('"body"') - expect(subject.response).to include('"a body"') - end - it "includes the headers" do - expect(subject.response).to include('"headers"') - expect(subject.response).to include('"a header"') - end - - it "renders the keys in a meaningful order" do - expect(subject.response).to match /"status".*"headers".*"body"/m - end - - context "when the body hash is empty" do - - let(:interaction) { interaction_with_response_without_body_and_headers } - - it "does not include the body" do - expect(subject.response).to_not include("body") - end - end - - context "when the headers hash is empty" do - - let(:interaction) { interaction_with_response_without_body_and_headers } - - it "does not include the headers" do - expect(subject.response).to_not include("headers") - end - end - - context "when a Pact::Term is present" do - let(:consumer_contract) { Pact::ConsumerContract.from_uri './spec/support/interaction_view_model_with_terms.json'} - let(:interaction) { consumer_contract.interactions.first } - - it "uses the generated value" do - expect(subject.response).to_not include("Term") - expect(subject.response).to include("rainy") - end - end - end - - describe "description" do - context "with a nil description" do - let(:interaction) do - interaction_with_request_with_body_and_headers.description = nil - interaction_with_request_with_body_and_headers - end - - it "does not blow up" do - expect(subject.description(true)).to eq '' - expect(subject.description(false)).to eq '' - end - end - - context "with markdown characters in the name" do - let(:interaction) do - interaction_with_request_with_body_and_headers.description = 'a *description' - interaction_with_request_with_body_and_headers - end - it "escapes the markdown characters" do - expect(subject.description).to eq "a \\*description" - end - end - end - - describe "provider_state" do - context "with markdown characters in the name" do - let(:interaction) do - interaction_with_request_with_body_and_headers.provider_state = 'a *provider state' - interaction_with_request_with_body_and_headers - end - it "escapes the markdown characters" do - expect(subject.provider_state).to eq "a \\*provider state" - end - end - end - - end - end -end \ No newline at end of file diff --git a/spec/lib/pact/doc/markdown/consumer_contract_renderer_spec.rb b/spec/lib/pact/doc/markdown/consumer_contract_renderer_spec.rb deleted file mode 100644 index b6e88e11..00000000 --- a/spec/lib/pact/doc/markdown/consumer_contract_renderer_spec.rb +++ /dev/null @@ -1,46 +0,0 @@ -require 'spec_helper' -require 'pact/doc/markdown/consumer_contract_renderer' - -module Pact - module Doc - module Markdown - describe ConsumerContractRenderer do - - subject { ConsumerContractRenderer.new(consumer_contract) } - let(:consumer_contract) { Pact::ConsumerContract.from_uri './spec/support/markdown_pact.json' } - - let(:expected_output) { File.read("./spec/support/generated_markdown.md", external_encoding: Encoding::UTF_8) } - - describe "#call" do - - context "with markdown characters in the pacticipant names" do - let(:consumer_contract) { Pact::ConsumerContract.from_uri './spec/support/markdown_pact_with_markdown_chars_in_names.json' } - - it "escapes the markdown characters" do - expect(subject.call).to include '### A pact between Some\*Consumer\*App and Some\_Provider\_App' - expect(subject.call).to include '#### Requests from Some\*Consumer\*App to Some\_Provider\_App' - end - end - - context "with ruby's default external encoding is not UTF-8" do - around do |example| - back = nil - WarningSilencer.enable { back, Encoding.default_external = Encoding.default_external, Encoding::ASCII_8BIT } - example.run - WarningSilencer.enable { Encoding.default_external = back } - end - - it "renders the interactions" do - expect(subject.call).to eq(expected_output) - end - end - - it "renders the interactions" do - expect(subject.call).to eq(expected_output) - end - end - - end - end - end -end diff --git a/spec/lib/pact/doc/markdown/index_renderer_spec.rb b/spec/lib/pact/doc/markdown/index_renderer_spec.rb deleted file mode 100644 index 277629de..00000000 --- a/spec/lib/pact/doc/markdown/index_renderer_spec.rb +++ /dev/null @@ -1,29 +0,0 @@ -require 'spec_helper' -require 'pact/doc/markdown/index_renderer' - -module Pact - module Doc - module Markdown - describe IndexRenderer do - - let(:consumer_name) { "Some Consumer" } - let(:docs) { {"Some Provider" => "Some Provider.md", "Some other provider" => "Some other provider.md"} } - let(:subject) { IndexRenderer.new(consumer_name, docs) } - let(:expected_content) { File.read('./spec/support/generated_index.md')} - - describe "#call" do - it "renders the index" do - expect(subject.call).to eq expected_content - end - end - - describe ".call" do - it "renders the index" do - expect(IndexRenderer.call(consumer_name, docs) ).to eq expected_content - end - end - - end - end - end -end \ No newline at end of file diff --git a/spec/lib/pact/hal/authorization_header_redactor_spec.rb b/spec/lib/pact/hal/authorization_header_redactor_spec.rb deleted file mode 100644 index 1b43eb70..00000000 --- a/spec/lib/pact/hal/authorization_header_redactor_spec.rb +++ /dev/null @@ -1,15 +0,0 @@ -require 'pact/hal/authorization_header_redactor' - -module Pact - module Hal - describe AuthorizationHeaderRedactor do - let(:stream) { StringIO.new } - let(:stream_redactor) { AuthorizationHeaderRedactor.new(stream) } - - it "redacts the authorizaton header" do - stream_redactor << "\\r\\nAuthorization: Bearer TOKEN\\r\\n" - expect(stream.string).to eq "\\r\\nAuthorization: [redacted]\\r\\n" - end - end - end -end diff --git a/spec/lib/pact/hal/entity_spec.rb b/spec/lib/pact/hal/entity_spec.rb deleted file mode 100644 index 9663565f..00000000 --- a/spec/lib/pact/hal/entity_spec.rb +++ /dev/null @@ -1,147 +0,0 @@ -require 'pact/hal/entity' -require 'pact/hal/http_client' - -module Pact - module Hal - describe Entity do - let(:http_client) do - instance_double('Pact::Hal::HttpClient', post: provider_response) - end - - let(:provider_response) do - double('response', body: provider_hash, success?: true, json?: true) - end - - let(:provider_hash) do - { - "name" => "Provider" - } - end - let(:pact_hash) do - { - "name" => "a name", - - "_links" => { - "pb:provider" => { - "href" => "http://provider" - }, - "pb:version-tag" => { - "href" => "http://provider/version/{version}/tag/{tag}" - } - } - } - end - - subject(:entity) { Entity.new("http://pact", pact_hash, http_client) } - - it "delegates to the properties in the data" do - expect(subject.name).to eq "a name" - end - - describe "post" do - let(:post_provider) { entity.post("pb:provider", {'some' => 'data'} ) } - - it "executes an http request" do - expect(http_client).to receive(:post).with("http://provider", '{"some":"data"}', {"Accept" => "application/hal+json", "Content-Type" => "application/json"}) - post_provider - end - - it "returns the entity for the relation" do - expect(post_provider).to be_a(Entity) - end - - context "with template params" do - let(:post_provider) { entity._link("pb:version-tag").expand(version: "1", tag: "prod").post({'some' => 'data'} ) } - - it "posts to the expanded URL" do - expect(http_client).to receive(:post).with("http://provider/version/1/tag/prod", '{"some":"data"}', {"Accept" => "application/hal+json", "Content-Type" => "application/json"}) - post_provider - end - end - end - - describe "assert_success!" do - context "when the response is successful" do - it "returns the entity" do - expect(entity.assert_success!).to be entity - end - end - - context "when the response is not successful and there is no response" do - subject(:entity) { ErrorEntity.new("http://pact", pact_hash, http_client) } - - it "raises an error" do - expect { entity.assert_success! }.to raise_error Pact::Hal::ErrorResponseReturned, "Error retrieving http://pact status= " - end - end - - context "when the response is not successful and there is a response" do - let(:response) { double('response', code: 200, raw_body: "body") } - - subject(:entity) { ErrorEntity.new("http://pact", pact_hash, http_client, response) } - - it "raises an error" do - expect { entity.assert_success! }.to raise_error Pact::Hal::ErrorResponseReturned, "Error retrieving http://pact status=200 body" - end - end - end - - describe "can?" do - context "when the relation exists" do - it "returns true" do - expect(subject.can?('pb:provider')).to be true - end - end - - context "when the relation does not exist" do - it "returns false" do - expect(subject.can?('pb:consumer')).to be false - end - end - end - - describe "_link!" do - context 'when the key exists' do - it 'returns a Link' do - expect(subject._link!('pb:provider')).to be_a(Link) - expect(subject._link!('pb:provider').href).to eq 'http://provider' - end - end - - context 'when the key does not exist' do - it 'raises an error' do - expect { subject._link!('foo') }.to raise_error RelationNotFoundError, "Could not find relation 'foo' in resource at http://pact" - end - end - end - - describe 'fetch' do - context 'when the key exists' do - it 'returns fetched value' do - expect(subject.fetch('pb:provider')).to eq("href" => 'http://provider') - end - end - - context "when the key doesn't not exist" do - it 'returns nil' do - expect(subject.fetch('i-dont-exist')).to be nil - end - end - - context "when a fallback key is provided" do - context "when the fallback value exists" do - it "returns the fallback value" do - expect(subject.fetch('i-dont-exist', 'pb:provider')).to eq("href" => 'http://provider') - end - end - - context "when the fallback value does not exist" do - it "returns nil" do - expect(subject.fetch('i-dont-exist', 'i-also-dont-exist')).to be nil - end - end - end - end - end - end -end diff --git a/spec/lib/pact/hal/http_client_spec.rb b/spec/lib/pact/hal/http_client_spec.rb deleted file mode 100644 index 85d6c355..00000000 --- a/spec/lib/pact/hal/http_client_spec.rb +++ /dev/null @@ -1,313 +0,0 @@ -require 'pact/hal/http_client' -require "faraday" -require "faraday/retry" - -module Pact - module Hal - describe HttpClient do - before do - allow(Retry).to receive(:until_true) { |&block| block.call } - end - - subject { HttpClient.new(username: 'foo', password: 'bar') } - - describe "get" do - let!(:request) do - stub_request(:get, "http://example.org/"). - with( headers: { - 'Accept'=>'*/*', - 'Authorization'=>'Basic Zm9vOmJhcg==' - }). - to_return(status: 200, body: response_body, headers: {'Content-Type' => 'application/json'}) - end - - let(:response_body) { {some: 'json'}.to_json } - let(:do_get) { subject.get('http://example.org') } - - it "performs a get request" do - do_get - expect(request).to have_been_made - end - - context "with get params" do - let!(:request) do - stub_request(:get, "http://example.org/?foo=hello+world&bar=wiffle"). - to_return(status: 200) - end - - let(:do_get) { subject.get('http://example.org', { 'foo' => 'hello world', 'bar' => 'wiffle' }) } - - it "correctly converts and encodes get params" do - do_get - expect(request).to have_been_made - end - - context "when there are existing params on the URL" do - let!(:request) do - stub_request(:get, "http://example.org/?foo=hello+world&bar=wiffle&a=b"). - to_return(status: 200) - end - - let(:do_get) { subject.get('http://example.org?foo=bar&a=b', { 'foo' => 'hello world', 'bar' => 'wiffle' }) } - - it "merges them in" do - do_get - expect(request).to have_been_made - end - end - end - - context "with broker token set" do - let!(:request) do - stub_request(:any, /.*/). - with( headers: { - 'Authorization'=>'Bearer mytoken123' - }). - to_return(status: 200, body: response_body, headers: {'Content-Type' => 'application/json'}) - end - - subject { HttpClient.new(token: 'mytoken123') } - - it "sets a bearer authorization header" do - do_get - expect(request).to have_been_made - end - end - - it "retries on failure" do - expect(Retry).to receive(:until_true) - do_get - end - - it "returns a response" do - expect(do_get.body).to eq({"some" => "json"}) - end - - context "when server returns 502 Bad Gateway" do - before do - allow(Retry).to receive(:until_true).and_call_original - allow($stderr).to receive(:puts) - end - - let!(:request) do - stub_request(:get, "http://example.org/"). - to_return(status: 502, body: "Bad Gateway").then. - to_return(status: 200, body: response_body, headers: {'Content-Type' => 'application/json'}) - end - - it "retries and succeeds on second attempt" do - expect(do_get.body).to eq({"some" => "json"}) - expect(request).to have_been_made.times(2) - end - end - - context "when server returns 503 Service Unavailable" do - before do - allow(Retry).to receive(:until_true).and_call_original - allow($stderr).to receive(:puts) - end - - let!(:request) do - stub_request(:get, "http://example.org/"). - to_return(status: 503, body: "Service Unavailable").then. - to_return(status: 200, body: response_body, headers: {'Content-Type' => 'application/json'}) - end - - it "retries and succeeds on second attempt" do - expect(do_get.body).to eq({"some" => "json"}) - expect(request).to have_been_made.times(2) - end - end - - context "when server returns 504 Gateway Timeout" do - before do - allow(Retry).to receive(:until_true).and_call_original - allow($stderr).to receive(:puts) - end - - let!(:request) do - stub_request(:get, "http://example.org/"). - to_return(status: 504, body: "Gateway Timeout").then. - to_return(status: 200, body: response_body, headers: {'Content-Type' => 'application/json'}) - end - - it "retries and succeeds on second attempt" do - expect(do_get.body).to eq({"some" => "json"}) - expect(request).to have_been_made.times(2) - end - end - - context "when server returns 500 Internal Server Error" do - before do - allow(Retry).to receive(:until_true).and_call_original - end - - let!(:request) do - stub_request(:get, "http://example.org/"). - to_return(status: 500, body: '{"error": "internal server error"}', headers: {'Content-Type' => 'application/json'}) - end - - it "does not retry on 500 status code" do - expect(do_get.body).to eq({"error" => "internal server error"}) - expect(request).to have_been_made.times(1) - end - end - - context "when server returns 404 Not Found" do - before do - allow(Retry).to receive(:until_true).and_call_original - end - - let!(:request) do - stub_request(:get, "http://example.org/"). - to_return(status: 404, body: '{"error": "not found"}', headers: {'Content-Type' => 'application/json'}) - end - - it "does not retry on 4xx status codes" do - expect(do_get.body).to eq({"error" => "not found"}) - expect(request).to have_been_made.times(1) - end - end - - context "when server returns 502 three times" do - before do - allow(Retry).to receive(:until_true).and_call_original - allow($stderr).to receive(:puts) - end - - let!(:request) do - stub_request(:get, "http://example.org/"). - to_return(status: 502, body: "Bad Gateway") - end - - it "raises RetriableHttpStatusError after max retries" do - expect { do_get }.to raise_error(Pact::Hal::RetriableHttpStatusError, /HTTP 502 error/) - expect(request).to have_been_made.times(3) - end - end - end - - describe "post" do - let!(:request) do - stub_request(:post, "http://example.org/"). - with( headers: { - 'Accept'=>'*/*', - 'Authorization'=>'Basic Zm9vOmJhcg==' - }, - body: request_body). - to_return(status: 200, body: response_body, headers: {'Content-Type' => 'application/json'}) - end - - let(:request_body) { {some: 'data'}.to_json } - let(:response_body) { {some: 'json'}.to_json } - - let(:do_post) { subject.post('http://example.org/', request_body) } - - it "performs a post request" do - do_post - expect(request).to have_been_made - end - - it "calls Retry.until_true" do - expect(Retry).to receive(:until_true) - do_post - end - - it "returns a response" do - expect(do_post.body).to eq({"some" => "json"}) - end - - context "with custom headers" do - let!(:request) do - stub_request(:post, "http://example.org/"). - with( headers: { - 'Accept'=>'foo' - }). - to_return(status: 200) - end - - let(:do_post) { subject.post('http://example.org/', request_body, {"Accept" => "foo"} ) } - - it "performs a post request with custom headers" do - do_post - expect(request).to have_been_made - end - end - end - - describe "x509 certificate" do - FAKE_SERVER_URL = 'https://localhost:4444' - X509_CERT_FILE_PATH = './spec/fixtures/certificates/client_cert.pem' - X509_KEY_FILE_PATH = './spec/fixtures/certificates/key.pem' - UNSIGNED_X509_CERT_FILE_PATH = './spec/fixtures/certificates/unsigned_cert.pem' - UNSIGNED_X509_KEY_FILE_PATH = './spec/fixtures/certificates/unsigned_key.pem' - - def wait_for_server_to_start - Faraday.new( - url: FAKE_SERVER_URL, - ssl: { - verify: false, - client_cert: OpenSSL::X509::Certificate.new(File.read(X509_CERT_FILE_PATH)), - client_key: OpenSSL::PKey::RSA.new(File.read(X509_KEY_FILE_PATH)) - } - ) do |builder| - builder.request :retry, max: 20, interval: 0.5, exceptions: [StandardError] - builder.adapter :net_http - end.get - end - - let(:do_get) { subject.get(FAKE_SERVER_URL) } - - before(:all) do - @pipe = IO.popen("bundle exec ruby ./spec/support/ssl_server.rb") - ENV['SSL_CERT_FILE'] = "./spec/fixtures/certificates/ca_cert.pem" - - wait_for_server_to_start() - end - - context "with valid x509 client certificates" do - before do - ENV['X509_CLIENT_CERT_FILE'] = X509_CERT_FILE_PATH - ENV['X509_CLIENT_KEY_FILE'] = X509_KEY_FILE_PATH - end - - it "succeeds" do - expect(do_get.status).to eq 200 - end - end - - context "when invalid x509 certificates are set" do - before do - ENV['X509_CLIENT_CERT_FILE'] = UNSIGNED_X509_CERT_FILE_PATH - ENV['X509_CLIENT_KEY_FILE'] = UNSIGNED_X509_KEY_FILE_PATH - end - - it "fails raising SSL error" do - expect { do_get } - .to raise_error { |error| - expect([OpenSSL::SSL::SSLError, Errno::ECONNRESET]).to include(error.class) - } - end - end - - context "when no x509 certificates are set" do - before do - ENV['X509_CLIENT_CERT_FILE'] = nil - ENV['X509_CLIENT_KEY_FILE'] = nil - end - - it "fails raising SSL error" do - expect { do_get } - .to raise_error { |error| - expect([OpenSSL::SSL::SSLError, Errno::ECONNRESET]).to include(error.class) - } - end - end - - after(:all) do - Process.kill "KILL", @pipe.pid - end - end - end - end -end diff --git a/spec/lib/pact/hal/link_spec.rb b/spec/lib/pact/hal/link_spec.rb deleted file mode 100644 index 441153f1..00000000 --- a/spec/lib/pact/hal/link_spec.rb +++ /dev/null @@ -1,121 +0,0 @@ -require 'pact/hal/link' -require 'pact/hal/entity' -require 'pact/hal/http_client' - -module Pact - module Hal - describe Link do - let(:http_client) do - instance_double('Pact::Hal::HttpClient', post: response) - end - - let(:response) do - instance_double('Pact::Hal::HttpClient::Response', success?: success, body: response_body, raw_body: response_body.to_json, json?: true) - end - - let(:success) { true } - - let(:entity) do - instance_double('Pact::Hal::Entity') - end - - let(:href) { 'http://foo/{bar}' } - let(:attrs) do - { - 'href' => href, - 'title' => 'title', - method: :post - } - end - - let(:response_body) do - { - 'some' => 'body' - } - end - - subject { Link.new(attrs, http_client) } - - before do - allow(Pact::Hal::Entity).to receive(:new).and_return(entity) - end - - describe "#run" do - let(:do_run) { subject.run('foo' => 'bar') } - - it "executes the configured http request" do - expect(http_client).to receive(:post) - do_run - end - - it "creates an Entity" do - expect(Pact::Hal::Entity).to receive(:new).with("http://foo/{bar}", response_body, http_client, response) - do_run - end - - it "returns an Entity" do - expect(do_run).to eq entity - end - - context "when an error response is returned" do - before do - allow(Pact::Hal::ErrorEntity).to receive(:new).and_return(entity) - end - - let(:success) { false } - - it "creates an ErrorEntity" do - expect(Pact::Hal::ErrorEntity).to receive(:new).with("http://foo/{bar}", response_body.to_json, http_client, response) - do_run - end - end - end - - describe "#get" do - before do - allow(http_client).to receive(:get).and_return(response) - end - - let(:do_get) { subject.get({ 'foo' => 'bar' }) } - - it "executes an HTTP Get request" do - expect(http_client).to receive(:get).with('http://foo/{bar}', { 'foo' => 'bar' }, { 'Accept' => 'application/hal+json' }) - do_get - end - end - - describe "#post" do - let(:do_post) { subject.post({ 'foo' => 'bar' }, { 'Accept' => 'foo' }) } - - context "with custom headers" do - it "executes an HTTP Post request with the custom headers" do - expect(http_client).to receive(:post).with('http://foo/{bar}', '{"foo":"bar"}', { 'Accept' => 'foo', 'Content-Type' => 'application/json' }) - do_post - end - end - end - - describe "#with_query" do - let(:href) { "http://example.org?a=1&b=2" } - - it "returns a link with the new query merged into the existing query" do - expect(subject.with_query("a" => "5", "c" => "3").href).to eq "http://example.org?a=5&b=2&c=3" - end - end - - describe "#expand" do - it "returns a duplicate Link with the expanded href" do - expect(subject.expand(bar: 'wiffle').href).to eq "http://foo/wiffle" - end - - it "returns a duplicate Link with the expanded href with URL escaping" do - expect(subject.expand(bar: 'wiffle meep').href).to eq "http://foo/wiffle%20meep" - end - - it "returns a duplicate Link with the expanded href with URL escaping for forward slashes" do - expect(subject.expand(bar: 'wiffle/meep').href).to eq "http://foo/wiffle%2Fmeep" - end - end - end - end -end diff --git a/spec/lib/pact/pact_broker/fetch_pact_uris_for_verification_spec.rb b/spec/lib/pact/pact_broker/fetch_pact_uris_for_verification_spec.rb deleted file mode 100644 index 3781ebf3..00000000 --- a/spec/lib/pact/pact_broker/fetch_pact_uris_for_verification_spec.rb +++ /dev/null @@ -1,71 +0,0 @@ -require 'pact/pact_broker/fetch_pact_uris_for_verification' - -module Pact - module PactBroker - describe FetchPactURIsForVerification do - describe "call" do - before do - allow(Pact.configuration).to receive(:output_stream).and_return(double('output stream').as_null_object) - end - - let(:provider) { "Foo"} - let(:broker_base_url) { "http://broker.org" } - let(:http_client_options) { {} } - let(:consumer_version_selectors) { [{ tag: "cmaster", latest: true, fallbackTag: 'blah' }] } - let(:provider_version_branch) { "pbranch" } - let(:provider_version_tags) { ["pmaster"] } - - subject { FetchPactURIsForVerification.call(provider, consumer_version_selectors, provider_version_branch, provider_version_tags, broker_base_url, http_client_options)} - - context "when there is an error retrieving the index resource" do - before do - stub_request(:get, "http://broker.org/").to_return(status: 500, body: "foo", headers: {}) - end - - let(:subject_with_rescue) do - begin - subject - rescue Pact::Error - # can't be bothered stubbing out everything to make the rest of the code execute nicely - # when all we care about is the message - end - end - - it "raises a Pact::Error" do - expect { subject }.to raise_error Pact::Error, /500.*foo/ - end - end - - context "when a single tag is provided instead of an array" do - - let(:provider_version_tags) { "pmaster" } - - subject { FetchPactURIsForVerification.new(provider, consumer_version_selectors, provider_version_branch, provider_version_tags, broker_base_url, http_client_options)} - - it "wraps an array around it" do - expect(subject.provider_version_tags).to eq ["pmaster"] - end - end - - context "when the beta:provider-pacts-for-verification relation does not exist" do - before do - allow(FetchPacts).to receive(:call).and_return([]) - stub_request(:get, "http://broker.org/").to_return(status: 200, body: response_body, headers: response_headers) - end - - let(:response_headers) { { "Content-Type" => "application/hal+json" } } - let(:response_body) do - { - _links: {} - }.to_json - end - - it "calls the old fetch pacts code" do - expect(FetchPacts).to receive(:call).with(provider, [{ name: "cmaster", all: false, fallback: "blah" }], broker_base_url, http_client_options) - expect { subject }.to raise_error( "No pacts found to verify" ) - end - end - end - end - end -end diff --git a/spec/lib/pact/pact_broker/fetch_pacts_spec.rb b/spec/lib/pact/pact_broker/fetch_pacts_spec.rb deleted file mode 100644 index 50d8fc31..00000000 --- a/spec/lib/pact/pact_broker/fetch_pacts_spec.rb +++ /dev/null @@ -1,71 +0,0 @@ -require 'pact/pact_broker/fetch_pacts' - -module Pact - module PactBroker - describe FetchPacts do - describe "call" do - before do - allow(Pact.configuration).to receive(:output_stream).and_return(double('output stream').as_null_object) - stub_request(:get, "http://broker.org/").to_return(status: 500, body: "foo", headers: {}) - end - - let(:provider) { "Foo"} - let(:tags) { ["master", "prod"] } - let(:broker_base_url) { "http://broker.org" } - let(:http_client_options) { {} } - - subject { FetchPacts.call(provider, tags, broker_base_url, http_client_options)} - - let(:subject_with_rescue) do - begin - subject - rescue Pact::Error - # can't be bothered stubbing out everything to make the rest of the code execute nicely - # when all we care about is the message - end - end - - context "when there is an error retrieving the index resource" do - it "raises a Pact::Error" do - expect { subject }.to raise_error Pact::Error, /500.*foo/ - end - end - - context "when there is a HAL relation missing" do - before do - stub_request(:get, "http://broker.org/").to_return(status: 200, body: {"_links" => {} }.to_json, headers: {"Content-Type" => "application/hal+json"}) - end - - it "raises a Pact::Error" do - expect { subject }.to raise_error Pact::Error, /Could not find relation/ - end - end - - context "for the latest tag" do - it "logs a message" do - expect(Pact.configuration.output_stream).to receive(:puts).with("INFO: Fetching pacts for Foo from http://broker.org for tags: latest master, latest prod") - subject_with_rescue - end - end - - context "with a fallback tag" do - let(:tags) { [{ name: "branch", fallback: "master" }] } - - it "logs a message" do - expect(Pact.configuration.output_stream).to receive(:puts).with("INFO: Fetching pacts for Foo from http://broker.org for tags: latest branch (or master if not found)") - subject_with_rescue - end - end - - context "when all: true" do - let(:tags) { [{ name: "prod", all: true }] } - - it "logs a message" do - expect(Pact.configuration.output_stream).to receive(:puts).with("INFO: Fetching pacts for Foo from http://broker.org for tags: all prod") - subject_with_rescue - end - end - end - end - end -end diff --git a/spec/lib/pact/pact_broker/notices_spec.rb b/spec/lib/pact/pact_broker/notices_spec.rb deleted file mode 100644 index 31a69e86..00000000 --- a/spec/lib/pact/pact_broker/notices_spec.rb +++ /dev/null @@ -1,58 +0,0 @@ -require 'pact/pact_broker/notices' - -module Pact - module PactBroker - describe Notices do - - let(:notice_hashes) do - [ - { text: "foo", when: "before_verification" } - ] - end - - subject(:notices) { Notices.new(notice_hashes) } - - it "behaves like an array" do - expect(subject.size).to eq notice_hashes.size - end - - describe "before_verification_notices" do - let(:notice_hashes) do - [ - { text: "foo", when: "before_verification" }, - { text: "bar", when: "blah" }, - ] - end - - its(:before_verification_notices_text) { is_expected.to eq [ "foo" ] } - end - - describe "after_verification_notices_text" do - let(:notice_hashes) do - [ - { text: "foo", when: "after_verification:success_false_published_true" }, - { text: "bar", when: "blah" }, - ] - end - - subject { notices.after_verification_notices_text(false, true) } - - it { is_expected.to eq [ "foo" ] } - end - - describe "after_verification_notices" do - let(:notice_hashes) do - [ - { text: "meep", when: "after_verification" }, - { text: "foo", when: "after_verification:success_false_published_true" }, - { text: "bar", when: "blah" }, - ] - end - - subject { notices.after_verification_notices(false, true) } - - it { is_expected.to eq [{ text: "meep", when: "after_verification" }, { text: "foo", when: "after_verification" }] } - end - end - end -end diff --git a/spec/lib/pact/pact_broker/pact_selection_description_spec.rb b/spec/lib/pact/pact_broker/pact_selection_description_spec.rb deleted file mode 100644 index cb93143c..00000000 --- a/spec/lib/pact/pact_broker/pact_selection_description_spec.rb +++ /dev/null @@ -1,91 +0,0 @@ -require 'pact/pact_broker/pact_selection_description' - -module Pact - module PactBroker - describe PactSelectionDescription do - include PactSelectionDescription - - describe "#pact_selection_description" do - let(:provider) { "Bar" } - let(:consumer_version_selectors) { [{ tag: "cmaster", latest: true, fallbackTag: "master" }, { tag: "prod" }] } - let(:options) do - { - include_wip_pacts_since: "2020-01-01" - } - end - let(:broker_base_url) { "http://broker" } - - subject { pact_selection_description(provider, consumer_version_selectors, options, broker_base_url) } - - it { is_expected.to eq "Fetching pacts for Bar from http://broker with the selection criteria: latest for tag cmaster (or master if not found), all for tag prod, work in progress pacts created after 2020-01-01" } - - describe "when consumer selector specifies a consumer name" do - let(:consumer_version_selectors) { [{ tag: "cmaster", latest: true, consumer: "Foo" }] } - - it { is_expected.to eq "Fetching pacts for Bar from http://broker with the selection criteria: latest for tag cmaster of Foo, work in progress pacts created after 2020-01-01" } - end - - describe "for branch" do - let(:consumer_version_selectors) { [{ branch: "feat/x", consumer: "Foo" }] } - - it { is_expected.to include "latest from branch feat/x of Foo" } - end - - describe "for main branch" do - let(:consumer_version_selectors) { [{ mainBranch: true, consumer: "Foo" }] } - - it { is_expected.to include "latest from main branch of Foo" } - end - - describe "for deployedOrReleased" do - let(:consumer_version_selectors) { [{ deployedOrReleased: true }] } - - it { is_expected.to include "currently deployed or released" } - end - - describe "for released in environment" do - let(:consumer_version_selectors) { [{ released: true, environment: "production" }] } - - it { is_expected.to include "currently released to production" } - end - - describe "for deployed in environment" do - let(:consumer_version_selectors) { [{ deployed: true, environment: "production" }] } - - it { is_expected.to include "currently deployed to production" } - end - - describe "for deployedOrReleased in environment" do - let(:consumer_version_selectors) { [{ deployedOrReleased: true, environment: "production" }] } - - it { is_expected.to include "currently deployed or released to production" } - end - - describe "in environment" do - let(:consumer_version_selectors) { [{ environment: "production" }] } - - it { is_expected.to include "in production" } - end - - describe "matching branch" do - let(:consumer_version_selectors) { [{ matchingBranch: true, consumer: "Foo" }] } - - it { is_expected.to include "matching current branch for Foo" } - end - - describe "matching tag" do - let(:consumer_version_selectors) { [{ matchingTag: true, consumer: "Foo" }] } - - it { is_expected.to include "matching tag for Foo" } - end - - describe "unknown" do - let(:consumer_version_selectors) { [{ branchPattern: "*foo" }] } - - it { is_expected.to include "branchPattern" } - it { is_expected.to include "*foo" } - end - end - end - end -end diff --git a/spec/lib/pact/pact_broker_spec.rb b/spec/lib/pact/pact_broker_spec.rb deleted file mode 100644 index f19f99db..00000000 --- a/spec/lib/pact/pact_broker_spec.rb +++ /dev/null @@ -1,25 +0,0 @@ -require 'pact/pact_broker' -require 'pact/provider/pact_uri' - -module Pact - module PactBroker - describe ".fetch_pact_uris_for_verification" do - before do - allow(Pact::PactBroker::FetchPactURIsForVerification).to receive(:call).and_return([pact_uri]) - end - - let(:pact_uri) { Pact::Provider::PactURI.new("http://pact") } - - subject { Pact::PactBroker.fetch_pact_uris_for_verification("foo") } - - it "calls Pact::PactBroker::FetchPendingPacts" do - expect(Pact::PactBroker::FetchPactURIsForVerification).to receive(:call).with("foo") - subject - end - - it "returns a list of pact uris" do - expect(subject).to eq [pact_uri] - end - end - end -end diff --git a/spec/lib/pact/provider/configuration/configuration_extension_spec.rb b/spec/lib/pact/provider/configuration/configuration_extension_spec.rb deleted file mode 100644 index 6855828f..00000000 --- a/spec/lib/pact/provider/configuration/configuration_extension_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'spec_helper' -require 'pact/provider/configuration/configuration_extension' - -module Pact - - module Provider - - module Configuration - - describe ConfigurationExtension do - - subject { Object.new.extend(ConfigurationExtension) } - - it 'replays interactions in the recorded order by default' do - expect(subject.interactions_replay_order).to eq :recorded - end - - end - end - end -end diff --git a/spec/lib/pact/provider/configuration/message_provider_dsl_spec.rb b/spec/lib/pact/provider/configuration/message_provider_dsl_spec.rb deleted file mode 100644 index 87862155..00000000 --- a/spec/lib/pact/provider/configuration/message_provider_dsl_spec.rb +++ /dev/null @@ -1,199 +0,0 @@ -require "spec_helper" -require "pact/provider/configuration/service_provider_dsl" -require "pact/provider/pact_uri" -require "pact/pact_broker/fetch_pacts" - -module Pact - module Provider - module Configuration - describe MessageProviderDSL do - describe "initialize" do - context "with an object instead of a block" do - subject do - described_class.build "name" do - app "blah" - end - end - - it "raises an error" do - expect { subject }.to raise_error /wrong number of arguments/ - end - end - - end - - describe "validate" do - context "when no name is provided" do - subject do - described_class.new " " do - app { Object.new } - end - end - - it "raises an error" do - expect { subject.send(:validate) }.to raise_error("Please provide a name for the Provider") - end - end - - context "when nil name is provided" do - subject do - described_class.new nil do - app { Object.new } - end - end - - it "raises an error" do - expect { subject.send(:validate) }.to raise_error(Pact::Provider::Configuration::Error, "Please provide a name for the Provider") - end - end - - context "when publish_verification_results is true" do - context "when no application version is provided" do - subject do - described_class.build "name" do - publish_verification_results true - end - end - - it "raises an error" do - expect { subject.send(:validate) }.to raise_error(Pact::Provider::Configuration::Error, "Please set the app_version when publish_verification_results is true") - end - end - - context "when an application version is provided" do - subject do - described_class.build "name" do - app_version "1.2.3" - publish_verification_results true - end - end - - it "does not raise an error" do - expect { subject.send(:validate) }.to_not raise_error - end - end - end - end - - describe "honours_pact_with" do - before do - Pact.clear_provider_world - end - let(:pact_url) { "blah" } - - context "with no optional params" do - subject do - described_class.build "some-provider" do - app {} - honours_pact_with "some-consumer" do - pact_uri pact_url - end - end - end - - it "adds a verification to the Pact.provider_world" do - subject - pact_uri = Pact::Provider::PactURI.new(pact_url) - expect(Pact.provider_world.pact_verifications.first) - .to eq(Pact::Provider::PactVerification.new("some-consumer", pact_uri, :head)) - end - end - - context "with all params specified" do - let(:pact_uri_options) do - { - username: "pact_user", - password: "pact_pw" - } - end - subject do - described_class.build "some-provider" do - app {} - honours_pact_with "some-consumer", ref: :prod do - pact_uri pact_url, pact_uri_options - end - end - end - - it "adds a verification to the Pact.provider_world" do - subject - pact_uri = Pact::Provider::PactURI.new(pact_url, pact_uri_options) - expect(Pact.provider_world.pact_verifications.first) - .to eq(Pact::Provider::PactVerification.new("some-consumer", pact_uri , :prod)) - end - end - end - - describe "honours_pacts_from_pact_broker" do - before do - Pact.clear_provider_world - end - let(:pact_url) { "blah" } - - context "with all params specified" do - let(:tag_1) { "master" } - - let(:tag_2) do - { - name: "tag-name", - all: false, - fallback: "master" - } - end - - let(:options) do - { - pact_broker_base_url: "some-url", - consumer_version_tags: [tag_1, tag_2] - } - end - - subject do - described_class.build "some-provider" do - app {} - app_version_branch 'main' - app_version_tags ["dev"] - honours_pacts_from_pact_broker do - end - end - end - - it "builds a PactVerificationFromBroker" do - expect(PactVerificationFromBroker).to receive(:build).with("some-provider", 'main', ["dev"]) - subject - end - end - end - - describe "builder" do - context "when builder is initialize with a object instead of a block" do - subject do - described_class.build "some-provider" do - builder "foo" - end - end - - it "raises an error" do - expect { subject }.to raise_error /wrong number of arguments/ - end - end - end - - describe "CONFIG_RU_APP" do - context "when a config.ru file does not exist" do - let(:path_that_does_not_exist) { "./tmp/this/path/does/not/exist/probably" } - - before do - allow(Pact.configuration).to receive(:config_ru_path).and_return(path_that_does_not_exist) - end - - it "raises an error with some helpful text" do - expect { described_class::CONFIG_RU_APP.call } - .to raise_error /Could not find config\.ru file.*#{Regexp.escape(path_that_does_not_exist)}/ - end - end - end - end - end - end -end diff --git a/spec/lib/pact/provider/configuration/pact_verification_from_broker_spec.rb b/spec/lib/pact/provider/configuration/pact_verification_from_broker_spec.rb deleted file mode 100644 index 2bb838f8..00000000 --- a/spec/lib/pact/provider/configuration/pact_verification_from_broker_spec.rb +++ /dev/null @@ -1,185 +0,0 @@ -require 'pact/provider/configuration/pact_verification' - -module Pact - module Provider - module Configuration - describe PactVerificationFromBroker do - describe 'build' do - let(:provider_name) {'provider-name'} - let(:provider_version_branch) { 'main' } - let(:provider_version_tags) { ['master'] } - let(:base_url) { "http://broker.org" } - let(:since) { "2020-01-01" } - let(:basic_auth_options) do - { - username: 'pact_broker_username', - password: 'pact_broker_password' - } - end - let(:tags) { ['master'] } - let(:fetch_pacts) { double('FetchPacts') } - - before do - allow(Pact::PactBroker::FetchPactURIsForVerification).to receive(:new).and_return(fetch_pacts) - allow(Pact.provider_world).to receive(:add_pact_uri_source) - end - - context "with valid values" do - subject do - PactVerificationFromBroker.build(provider_name, provider_version_branch, provider_version_tags) do - pact_broker_base_url base_url, basic_auth_options - consumer_version_tags tags - enable_pending true - include_wip_pacts_since since - verbose true - end - end - - let(:fetch_pacts) { double('FetchPacts') } - let(:basic_auth_opts) { basic_auth_options.merge(verbose: true) } - let(:options) { { fail_if_no_pacts_found: true, include_pending_status: true, include_wip_pacts_since: "2020-01-01" }} - let(:consumer_version_selectors) { [ { tag: 'master', latest: true }] } - - it "creates a instance of Pact::PactBroker::FetchPactURIsForVerification" do - expect(Pact::PactBroker::FetchPactURIsForVerification).to receive(:new).with( - provider_name, - consumer_version_selectors, - provider_version_branch, - provider_version_tags, - base_url, - basic_auth_opts, - options - ) - subject - end - - it "adds a pact_uri_source to the provider world" do - expect(Pact.provider_world).to receive(:add_pact_uri_source).with(fetch_pacts) - subject - end - - context "when since is a Date" do - let(:since) { Date.new(2020, 1, 1) } - - it "converts it to a string" do - expect(Pact::PactBroker::FetchPactURIsForVerification).to receive(:new).with( - anything, - anything, - anything, - anything, - anything, - anything, - { - fail_if_no_pacts_found: true, - include_pending_status: true, - include_wip_pacts_since: since.xmlschema - } - ) - subject - end - end - end - - context "with a missing base url" do - subject do - PactVerificationFromBroker.build(provider_name, provider_version_branch, provider_version_tags) do - - end - end - - it "raises an error" do - expect { subject }.to raise_error Pact::Error, /Please provide a pact_broker_base_url/ - end - end - - context "with a non array object for consumer_version_tags" do - subject do - PactVerificationFromBroker.build(provider_name, provider_version_branch, provider_version_tags) do - pact_broker_base_url base_url - consumer_version_tags "master" - end - end - - it "coerces the value into an array" do - expect(Pact::PactBroker::FetchPactURIsForVerification).to receive(:new).with(anything, [{ tag: "master", latest: true}], anything, anything, anything, anything, anything) - subject - end - end - - context "when no consumer_version_tags are provided" do - subject do - PactVerificationFromBroker.build(provider_name, provider_version_branch, provider_version_tags) do - pact_broker_base_url base_url - end - end - - it "creates an instance of FetchPacts with an empty array for the consumer_version_tags" do - expect(Pact::PactBroker::FetchPactURIsForVerification).to receive(:new).with(anything, [], anything, anything, anything, anything, anything) - subject - end - end - - context "when the old format of selector is supplied to the consumer_verison_tags" do - let(:tags) { [{ name: 'main', all: true, fallback: 'fallback' }] } - - subject do - PactVerificationFromBroker.build(provider_name, provider_version_branch, provider_version_tags) do - pact_broker_base_url base_url - consumer_version_tags tags - end - end - - it "converts them to selectors" do - expect(Pact::PactBroker::FetchPactURIsForVerification).to receive(:new).with(anything, [{ tag: "main", latest: false, fallbackTag: 'fallback'}], anything, anything, anything, anything, anything) - subject - end - end - - context "when an invalid class is used for the consumer_version_tags" do - let(:tags) { [true] } - - subject do - PactVerificationFromBroker.build(provider_name, provider_version_branch, provider_version_tags) do - pact_broker_base_url base_url - consumer_version_tags tags - end - end - - it "raises an error" do - expect { subject }.to raise_error Pact::Error, "The value supplied for consumer_version_tags must be a String or a Hash. Found TrueClass" - end - end - - context "when consumer_version_selectors are provided" do - let(:tags) { [{ tag: 'main', latest: true, fallback_tag: 'fallback' }] } - - subject do - PactVerificationFromBroker.build(provider_name, provider_version_branch, provider_version_tags) do - pact_broker_base_url base_url - consumer_version_selectors tags - end - end - - it "converts the casing of the key names" do - expect(Pact::PactBroker::FetchPactURIsForVerification).to receive(:new).with(anything, [{ tag: "main", latest: true, fallbackTag: 'fallback'}], anything, anything, anything, anything, anything) - subject - end - end - - context "when no verbose flag is provided" do - subject do - PactVerificationFromBroker.build(provider_name, provider_version_branch, provider_version_tags) do - pact_broker_base_url base_url - end - end - - it "creates an instance of FetchPactURIsForVerification with verbose: false" do - expect(Pact::PactBroker::FetchPactURIsForVerification).to receive(:new).with(anything, anything, anything, anything, anything, hash_including(verbose: false), anything) - subject - end - end - end - end - end - end -end diff --git a/spec/lib/pact/provider/configuration/pact_verification_spec.rb b/spec/lib/pact/provider/configuration/pact_verification_spec.rb deleted file mode 100644 index e8c02083..00000000 --- a/spec/lib/pact/provider/configuration/pact_verification_spec.rb +++ /dev/null @@ -1,51 +0,0 @@ -require 'spec_helper' -require 'pact/provider/configuration/pact_verification' - -module Pact - module Provider - module Configuration - describe PactVerification do - - describe 'create_verification' do - let(:url) { 'http://some/uri' } - let(:pact_repository_uri_options) do - { - username: 'pact_broker_username', - password: 'pact_broker_password' - } - end - let(:consumer_name) {'some consumer'} - let(:ref) { :prod } - let(:options) { { ref: :prod } } - - context "with valid values" do - subject do - uri = url - PactVerification.build(consumer_name, options) do - pact_uri uri, pact_repository_uri_options - end - end - - it "creates a Verification" do - pact_uri = Pact::Provider::PactURI.new(url, pact_repository_uri_options) - expect(Pact::Provider::PactVerification).to receive(:new).with(consumer_name, pact_uri, ref) - subject - end - end - - context "with a nil uri" do - subject do - PactVerification.build(consumer_name, options) do - pact_uri nil - end - end - - it "raises a validation error" do - expect { subject }.to raise_error /Please provide a pact_uri/ - end - end - end - end - end - end -end \ No newline at end of file diff --git a/spec/lib/pact/provider/configuration/service_provider_config_spec.rb b/spec/lib/pact/provider/configuration/service_provider_config_spec.rb deleted file mode 100644 index e37109dd..00000000 --- a/spec/lib/pact/provider/configuration/service_provider_config_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -require 'pact/provider/configuration/service_provider_config' - -module Pact - module Provider - module Configuration - describe ServiceProviderConfig do - describe "app" do - - let(:app_block) { ->{ Object.new } } - - subject { ServiceProviderConfig.new("1.2.3'", "main", [], true, 'http://ci/build/1', &app_block) } - - it "should execute the app_block each time" do - expect(subject.app.object_id).to_not equal(subject.app.object_id) - end - end - end - end - end -end diff --git a/spec/lib/pact/provider/configuration/service_provider_dsl_spec.rb b/spec/lib/pact/provider/configuration/service_provider_dsl_spec.rb deleted file mode 100644 index 7e75f9c8..00000000 --- a/spec/lib/pact/provider/configuration/service_provider_dsl_spec.rb +++ /dev/null @@ -1,203 +0,0 @@ -require 'spec_helper' -require 'pact/provider/configuration/service_provider_dsl' -require 'pact/provider/pact_uri' -require 'pact/pact_broker/fetch_pacts' - -module Pact - - module Provider - - module Configuration - - describe ServiceProviderDSL do - - describe "initialize" do - - context "with an object instead of a block" do - subject do - ServiceProviderDSL.build 'name' do - app 'blah' - end - end - - it "raises an error" do - expect { subject }.to raise_error /wrong number of arguments/ - end - end - - context 'with build_url' do - subject(:config_build_url) { Pact.configuration.provider.build_url } - let(:ci_build_url) { 'http://ci/build/1' } - - before do - ServiceProviderDSL.build 'name' do - build_url ci_build_url - end - end - - it { is_expected.to eq(ci_build_url) } - end - - end - - describe "validate" do - context "when no name is provided" do - subject do - ServiceProviderDSL.new ' ' do - app { Object.new } - end - end - - it "raises an error" do - expect { subject.send(:validate) }.to raise_error("Please provide a name for the Provider") - end - end - - context "when nil name is provided" do - subject do - ServiceProviderDSL.new nil do - app { Object.new } - end - end - - it "raises an error" do - expect { subject.send(:validate) }.to raise_error(Pact::Provider::Configuration::Error, "Please provide a name for the Provider") - end - end - - context "when publish_verification_results is true" do - context "when no application version is provided" do - subject do - ServiceProviderDSL.build "name" do - publish_verification_results true - end - end - - it "raises an error" do - expect { subject.send(:validate) }.to raise_error(Pact::Provider::Configuration::Error, "Please set the app_version when publish_verification_results is true") - end - end - - context "when an application version is provided" do - subject do - ServiceProviderDSL.build "name" do - app_version "1.2.3" - publish_verification_results true - end - end - - it "does not raise an error" do - expect { subject.send(:validate) }.to_not raise_error - end - end - end - end - - describe 'honours_pact_with' do - before do - Pact.clear_provider_world - end - let(:pact_url) { 'blah' } - - context "with no optional params" do - subject do - ServiceProviderDSL.build 'some-provider' do - app {} - honours_pact_with 'some-consumer' do - pact_uri pact_url - end - end - end - - it 'adds a verification to the Pact.provider_world' do - subject - pact_uri = Pact::Provider::PactURI.new(pact_url) - expect(Pact.provider_world.pact_verifications.first) - .to eq(Pact::Provider::PactVerification.new('some-consumer', pact_uri, :head)) - end - end - - context "with all params specified" do - let(:pact_uri_options) do - { - username: 'pact_user', - password: 'pact_pw' - } - end - subject do - ServiceProviderDSL.build 'some-provider' do - app {} - honours_pact_with 'some-consumer', ref: :prod do - pact_uri pact_url, pact_uri_options - end - end - end - - it 'adds a verification to the Pact.provider_world' do - subject - pact_uri = Pact::Provider::PactURI.new(pact_url, pact_uri_options) - expect(Pact.provider_world.pact_verifications.first) - .to eq(Pact::Provider::PactVerification.new('some-consumer', pact_uri , :prod)) - end - end - end - - describe 'honours_pacts_from_pact_broker' do - before do - Pact.clear_provider_world - end - let(:pact_url) { 'blah' } - - context 'with all params specified' do - let(:tag_1) { 'master' } - - let(:tag_2) do - { - name: 'tag-name', - all: false, - fallback: 'master' - } - end - - let(:options) do - { - pact_broker_base_url: 'some-url', - consumer_version_tags: [tag_1, tag_2] - } - end - - subject do - ServiceProviderDSL.build 'some-provider' do - app {} - app_version_branch 'main' - app_version_tags ['dev'] - honours_pacts_from_pact_broker do - end - end - end - - it 'builds a PactVerificationFromBroker' do - expect(PactVerificationFromBroker).to receive(:build).with('some-provider', 'main', ['dev']) - subject - end - end - end - - describe 'CONFIG_RU_APP' do - context 'when a config.ru file does not exist' do - let(:path_that_does_not_exist) { './tmp/this/path/does/not/exist/probably' } - - before do - allow(Pact.configuration).to receive(:config_ru_path).and_return(path_that_does_not_exist) - end - - it 'raises an error with some helpful text' do - expect { ServiceProviderDSL::CONFIG_RU_APP.call } - .to raise_error /Could not find config\.ru file.*#{Regexp.escape(path_that_does_not_exist)}/ - end - end - end - end - end - end -end diff --git a/spec/lib/pact/provider/configuration_spec.rb b/spec/lib/pact/provider/configuration_spec.rb deleted file mode 100644 index 8df845a1..00000000 --- a/spec/lib/pact/provider/configuration_spec.rb +++ /dev/null @@ -1,50 +0,0 @@ -require 'spec_helper' -require 'pact/provider/configuration' - -module Pact::Provider::Configuration - - describe ConfigurationExtension do - - before do - Pact.clear_configuration - end - - describe "service_provider" do - - context "when a provider is configured" do - - before do - Pact.service_provider "Fred" do - app { "An app" } - end - end - - it "should allow configuration of the test app" do - expect(Pact.configuration.provider.app).to eql "An app" - end - - end - - context "when a provider is not configured" do - - it "raises an error" do - expect{ Pact.configuration.provider }.to raise_error(/Please configure your provider/) - end - - end - - context "when a provider is configured without an app" do - - before do - Pact.service_provider "Fred" do - end - end - - it "uses the app from config.ru" do - expect( Pact.configuration.provider.app ).to be(AppForConfigRu) - end - - end - end - end -end diff --git a/spec/lib/pact/provider/generators_spec.rb b/spec/lib/pact/provider/generators_spec.rb deleted file mode 100644 index 8e4b4cc6..00000000 --- a/spec/lib/pact/provider/generators_spec.rb +++ /dev/null @@ -1,68 +0,0 @@ -require 'pact/generators' -require 'pact/provider/request' - -describe Pact::Generators do - it 'apply_generators for path' do - expected_request = Pact::Request::Expected.from_hash({ - method: 'GET', - path: '/path/1', - generators: { - 'path' => { - 'type' => 'ProviderState', - 'expression' => '/path/${itemID}' - } - } - }) - state_params = { - 'itemID' => 2 - } - request = Pact::Provider::Request::Replayable.new(expected_request, state_params) - expect(request.path).to eq('/path/2') - end - - it 'apply_generators for headers' do - expected_request = Pact::Request::Expected.from_hash({ - method: 'GET', - path: '/path/1', - headers: { - 'Authorization' => 'Bearer 123' - }, - generators: { - 'header' => { - '$.Authorization' => { - 'expression' => 'Bearer ${accessToken}', - 'type' => 'ProviderState' - } - } - } - }) - state_params = { - 'accessToken' => 'ABC' - } - request = Pact::Provider::Request::Replayable.new(expected_request, state_params) - expect(request.headers).to eq({ - 'HTTP_AUTHORIZATION' => 'Bearer ABC' - }) - end - - it 'apply_generators for body' do - expected_request = Pact::Request::Expected.from_hash({ - method: 'GET', - path: '/path/1', - body: { - 'result' => [ - '12345F' - ] - }, - generators: { - 'body' => { - '$.result[0]' => { - 'type' => 'RandomHexadecimal' - } - } - } - }) - request = Pact::Provider::Request::Replayable.new(expected_request) - expect(JSON.parse(request.body)['result'][0].length).to eq(8) - end -end diff --git a/spec/lib/pact/provider/help/console_text_spec.rb b/spec/lib/pact/provider/help/console_text_spec.rb deleted file mode 100644 index d8f661d0..00000000 --- a/spec/lib/pact/provider/help/console_text_spec.rb +++ /dev/null @@ -1,74 +0,0 @@ -require 'spec_helper' -require 'pact/provider/help/console_text' -require 'fileutils' - -module Pact - module Provider - module Help - describe ConsoleText do - - describe ".call" do - let(:help_path) { File.join(reports_dir, Write::HELP_FILE_NAME) } - let(:reports_dir) { "./tmp/reports/pacts" } - let(:color) { false } - - subject { ConsoleText.call(reports_dir, color: color) } - - context "when the help file is found" do - - let(:help_text) do -<<-EOS -# Heading -## Another heading -Text -EOS - end - - before do - FileUtils.mkdir_p reports_dir - File.open(help_path, "w") { |io| io << help_text } - end - - it "returns the help file text" do - expect(subject).to eq help_text - end - - context "when the reports_dir is nil" do - subject { ConsoleText.call(nil, color: false) } - - before do - allow(Pact.configuration).to receive(:reports_dir).and_return(reports_dir) - end - - it "uses the default reports_dir" do - expect(subject).to eq help_text - end - end - - context "with color: true" do - - let(:color) { true } - - it "colourises the headings" do - expect(subject).to_not include("# Heading") - expect(subject).to include("Heading") - end - end - - context "when the help file cannot be found" do - - before do - FileUtils.rm_rf reports_dir - end - - it "returns an apologetic error message" do - expect(subject).to include("Sorry") - expect(subject).to include("tmp/reports/pacts") - end - end - end - end - end - end - end -end diff --git a/spec/lib/pact/provider/help/content_spec.rb b/spec/lib/pact/provider/help/content_spec.rb deleted file mode 100644 index 928623ba..00000000 --- a/spec/lib/pact/provider/help/content_spec.rb +++ /dev/null @@ -1,34 +0,0 @@ -require 'pact/provider/help/content' - -module Pact - module Provider - module Help - describe Content do - describe "#text" do - before do - allow(PactDiff).to receive(:call).with(pact_source_1).and_return('diff 1') - allow(PactDiff).to receive(:call).with(pact_source_2).and_return(nil) - end - - let(:pact_source_1) { { some: 'json'}.to_json } - let(:pact_source_2) { { some: 'other json'}.to_json } - let(:pact_sources) { [pact_source_1, pact_source_2] } - - subject { Content.new(pact_sources) } - - it "displays the log path" do - expect(subject.text).to include Pact.configuration.log_path - end - - it "displays the tmp dir" do - expect(subject.text).to include Pact.configuration.tmp_dir - end - - it "displays the diff" do - expect(subject.text).to include 'diff 1' - end - end - end - end - end -end diff --git a/spec/lib/pact/provider/help/prompt_text_spec.rb b/spec/lib/pact/provider/help/prompt_text_spec.rb deleted file mode 100644 index 6ec9d86a..00000000 --- a/spec/lib/pact/provider/help/prompt_text_spec.rb +++ /dev/null @@ -1,36 +0,0 @@ -require 'spec_helper' -require 'pact/provider/help/prompt_text' - -module Pact - module Provider - module Help - describe PromptText do - - describe ".call" do - let(:reports_dir){ File.expand_path "./reports/pacts" } - let(:color) { false } - subject { PromptText.(reports_dir, color: color)} - - it "returns a prompt to tell the user how to get help" do - expect(subject).to eq "For assistance debugging failures, run `bundle exec rake pact:verify:help`\n" - end - - context "when color: true" do - let(:color) { true } - it "displays the message in color" do - expect(subject).to include "\e[" - end - end - - context "when the reports_dir is not in the standard location" do - let(:reports_dir) { File.expand_path "reportyporty/pacts" } - it "includes the report dir as the rake task arg so that the rake task knows where to find the help file" do - expect(subject).to include("[reportyporty/pacts]") - end - end - end - - end - end - end -end diff --git a/spec/lib/pact/provider/help/write_spec.rb b/spec/lib/pact/provider/help/write_spec.rb deleted file mode 100644 index 4a11a7b6..00000000 --- a/spec/lib/pact/provider/help/write_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -require 'pact/provider/help/write' - -module Pact - module Provider - module Help - describe Write do - - describe "#call" do - - let(:pact_sources) { double('pact jsons') } - let(:reports_dir) { "./tmp/reports" } - let(:text) { "help text" } - - before do - FileUtils.rm_rf reports_dir - allow_any_instance_of(Content).to receive(:text).and_return(text) - end - - subject { Write.call(pact_sources, reports_dir) } - - let(:actual_contents) { File.read(File.join(reports_dir, Write::HELP_FILE_NAME)) } - - it "passes the pact_sources into the Content" do - expect(Content).to receive(:new).with(pact_sources).and_return(double(text: '')) - subject - end - - it "writes the help content to a file" do - subject - expect(actual_contents).to eq(text) - end - - end - - end - end - end -end diff --git a/spec/lib/pact/provider/matchers/messages_spec.rb b/spec/lib/pact/provider/matchers/messages_spec.rb deleted file mode 100644 index 4a1150f4..00000000 --- a/spec/lib/pact/provider/matchers/messages_spec.rb +++ /dev/null @@ -1,148 +0,0 @@ -require 'spec_helper' -require 'pact/provider/matchers/messages' - -module Pact - module Matchers - describe Messages do - - include Messages - - describe "#match_term_failure_message" do - - let(:diff_formatter) { Pact::Matchers::UnixDiffFormatter } - let(:message) { "line1\nline2"} - let(:output_message) { "Actual: actual\n\n#{message}"} - let(:r) { "\e[0m" } - let(:output_message_with_resets) { "Actual: \e[37mactual#{r}\n\n#{r}line1\n#{r}line2"} - let(:diff) { double("diff") } - let(:actual) { "actual" } - let(:color_enabled) { true } - let(:message_line_count) { message.split("\n").size } - - before do - allow(diff_formatter).to receive(:call).and_return(message) - end - - subject { match_term_failure_message diff, actual, diff_formatter, color_enabled } - - it "creates a message using the diff_formatter" do - expect(diff_formatter).to receive(:call).with(diff) - subject - end - - context "when color_enabled is true" do - - it "returns the message with ANSI reset at the start of each line" do - expect(subject).to eq(output_message_with_resets) - end - - end - - context "when the actual is not a string" do - - let(:actual) { { the: "actual" } } - - it "includes the actual as json" do - expect(subject).to include(actual.to_json) - end - end - - context "when color_enabled is false" do - - let(:color_enabled) { false } - - it "returns the message unmodified" do - expect(subject).to eq(output_message) - end - - end - - end - - describe "#match_header_failure_message" do - - let(:header_name) { "Content-Type" } - let(:expected) { "application/json" } - let(:actual) { "text/plain" } - - subject { match_header_failure_message header_name, expected, actual } - let(:description_for_it) { expected_desc_for_it(expected) } - - context "when the expected value is a string" do - - let(:expected_message) { "Expected header \"Content-Type\" to equal \"application/json\", but was \"text/plain\"" } - - it "creates a message" do - expect(subject).to eq(expected_message) - end - - it "has a description for the it block" do - expect(description_for_it).to eq "equals \"application/json\"" - end - end - - context "when the actual is nil" do - let(:actual) { nil } - let(:expected_message) { "Expected header \"Content-Type\" to equal \"application/json\", but was nil" } - - it "creates a message" do - expect(subject).to eq(expected_message) - end - end - - context "when the expected is nil" do - let(:expected) { nil } - let(:expected_message) { "Expected header \"Content-Type\" to be nil, but was \"text/plain\"" } - - it "creates a message" do - expect(subject).to eq(expected_message) - end - - it "has a description for the it block" do - expect(description_for_it).to eq "is nil" - end - end - - context "when the expected is a regexp" do - let(:expected) { /hal/ } - let(:expected_message) { "Expected header \"Content-Type\" to match /hal/, but was \"text/plain\"" } - - it "creates a message with the regexp" do - expect(subject).to eq(expected_message) - end - - it "has a description for the it block" do - expect(description_for_it).to eq "matches /hal/" - end - end - - context "when the expected is a Term" do - let(:expected) { Pact.term("application/hal+json", /hal/) } - let(:expected_message) { "Expected header \"Content-Type\" to match /hal/, but was \"text/plain\"" } - - it "creates a message with the term's matcher" do - expect(subject).to eq(expected_message) - end - - it "has a description for the it block" do - expect(description_for_it).to eq "matches /hal/" - end - end - - context "when the expected is a SomethingLike" do - let(:actual) { nil } - let(:expected) { Pact.like("foo") } - let(:expected_message) { "Expected header \"Content-Type\" to be an instance of String, but was nil" } - - it "creates a message with the expected class" do - expect(subject).to eq(expected_message) - end - - it "has a description for the it block" do - expect(description_for_it).to eq "is an instance of String" - end - end - end - end - end -end diff --git a/spec/lib/pact/provider/pact_helper_locator_spec.rb b/spec/lib/pact/provider/pact_helper_locator_spec.rb deleted file mode 100644 index 72049a08..00000000 --- a/spec/lib/pact/provider/pact_helper_locator_spec.rb +++ /dev/null @@ -1,60 +0,0 @@ -require 'spec_helper' -require 'pact/provider/pact_helper_locator' - -module Pact::Provider - - describe PactHelperLocater do - describe "pact_helper_path", :fakefs => true, skip_jruby: true do - - subject { PactHelperLocater.pact_helper_path } - - def make_pactfile dir - FileUtils.mkdir_p ".#{dir}" - FileUtils.touch ".#{dir}/pact_helper.rb" - end - - PACT_HELPER_FILE_DIRS = [ - '/spec/blah/service-consumers', - '/spec/consumers', - '/spec/blah/service_consumers', - '/spec/serviceconsumers', - '/spec/consumer', - '/spec', - '/test/blah/service-consumers', - '/test/consumers', - '/test/blah/service_consumers', - '/test/serviceconsumers', - '/test/consumer', - '/test', - '/blah', - '/blah/consumer', - '' - ] - - PACT_HELPER_FILE_DIRS.each do |dir| - context "the pact_helper is stored in #{dir}" do - it "finds the pact_helper" do - make_pactfile dir - expect(subject).to eq File.join(Dir.pwd, dir, 'pact_helper.rb') - end - end - end - - context "when more than one pact_helper exists" do - it "returns the one that matches the most explict search pattern" do - make_pactfile '/spec/consumer' - FileUtils.touch 'pact_helper.rb' - expect(subject).to eq File.join(Dir.pwd, '/spec/consumer/pact_helper.rb') - end - end - - context "when a file exists ending in pact_helper.rb" do - it "is not identifed as a pact helper" do - FileUtils.mkdir_p './spec' - FileUtils.touch './spec/not_pact_helper.rb' - expect { subject }.to raise_error /Please create a pact_helper.rb file/ - end - end - end - end -end \ No newline at end of file diff --git a/spec/lib/pact/provider/pact_spec_runner_spec.rb b/spec/lib/pact/provider/pact_spec_runner_spec.rb deleted file mode 100644 index 9a8cbacb..00000000 --- a/spec/lib/pact/provider/pact_spec_runner_spec.rb +++ /dev/null @@ -1,94 +0,0 @@ -require 'pact/provider/pact_spec_runner' - -describe Pact::Provider::PactSpecRunner, skip_jruby: true do - let(:options) { {provider: double(:provider)} } - let(:pact_url) { double(:pact_url, uri: 'uri', options: {}) } - let(:pact_urls) { [pact_url] } - subject { described_class.new(pact_urls, options) } - - let(:interactions) do - [{ - "description" => "Description 1", - "provider_state" => "there is an alligator named Mary", - "request" => {}, - "response" => { - "status" => 200, - } - }, { - "description" => "Description 2", - "provider_state" => "there is not an alligator named Mary", - "request" => {}, - "response" => { - "status" => 200, - } - }, { - "description" => "Description 1", - "provider_state" => "an error occurs retrieving an alligator", - "request" => {}, - "response" => { - "status" => 500, - } - }] - end - - let(:pact_source) do - double(:pact_source, uri: 'uri', pact_json: {"interactions" => interactions}.to_json) - end - - describe '#run' do - - before do - Pact.configuration.interactions_replay_order = interactions_replay_order - Pact.service_provider "Fred" do - app { "An app" } - end - allow(subject).to receive(:configure_rspec) - allow(subject).to receive(:run_specs) - allow(Pact::Utils::Metrics).to receive(:report_metric) - - expect(Pact::Provider::PactSource).to receive(:new).with(pact_url).and_return(pact_source) - end - - context 'with multiple interactions' do - let(:interactions_replay_order) { :recorded } - - it 'matches the original consumer interactions' do - expect_any_instance_of(Array).to_not receive(:shuffle).and_call_original - - expect(subject).to receive(:honour_pactfile) do |_uri, pact_json, _options| - consumer_contract = JSON.parse(pact_json) - expect(consumer_contract["interactions"]).to eq(interactions) - end - - subject.run - end - - it 'reports pacts verified metric' do - allow(subject).to receive(:honour_pactfile).and_return([]) - - expect(Pact::Utils::Metrics).to receive(:report_metric).with("Pacts verified", "ProviderTest", "Completed") - subject.run - end - - context 'and interactions_replay_order option set to random' do - let(:interactions_replay_order) { :random } - - it 'randomised interactions within consumer contract' do - allow(subject).to receive(:honour_pactfile).and_return([]) - expect_any_instance_of(Array).to receive(:shuffle).and_call_original - - subject.run - end - - it 'does not change consumer interactions' do - expect(subject).to receive(:honour_pactfile) do |_uri, pact_json, _options| - consumer_contract = JSON.parse(pact_json) - expect(consumer_contract["interactions"]).to match_array(interactions) - end - - subject.run - end - end - end - end -end diff --git a/spec/lib/pact/provider/pact_uri_spec.rb b/spec/lib/pact/provider/pact_uri_spec.rb deleted file mode 100644 index f7b6df6c..00000000 --- a/spec/lib/pact/provider/pact_uri_spec.rb +++ /dev/null @@ -1,76 +0,0 @@ -describe Pact::Provider::PactURI do - let(:uri) { 'http://uri' } - let(:username) { 'pact' } - let(:options) { { username: username } } - let(:pact_uri) { Pact::Provider::PactURI.new(uri, options) } - - describe '#==' do - it 'should return false if object is not PactURI' do - expect(pact_uri == Object.new).to be false - end - - it 'should return false if uri is not equal' do - expect(pact_uri == Pact::Provider::PactURI.new('other_uri', options)).to be false - end - - it 'should return false if uri options are not equal' do - expect(pact_uri == Pact::Provider::PactURI.new(uri, username: 'wrong user')).to be false - end - - it 'should return true if uri and options are equal' do - expect(pact_uri == Pact::Provider::PactURI.new(uri, options)).to be true - end - end - - describe '#to_s' do - context 'with basic auth provided' do - let(:password) { 'my_password' } - let(:options) { { username: username, password: password } } - - it 'should include user name and and hide password' do - expect(pact_uri.to_s).to eq('http://pact:*****@uri') - end - - context 'when basic auth credentials have been set for a local file (eg. via environment variables, unintentionally)' do - let(:uri) { '/some/file thing.json' } - - it 'does not blow up' do - expect(pact_uri.to_s).to eq uri - end - end - - context "with a username that has an @ symbol" do - let(:username) { "foo@bar" } - - it 'does not blow up' do - expect(pact_uri.to_s).to eq "http://*****:*****@uri" - end - end - end - - context 'with personal access token provided' do - let(:pat) { 'should_be_secret' } - let(:options) { { username: pat } } - - it 'should hide the pat' do - expect(pact_uri.to_s).to eq('http://*****@uri') - end - - context 'when pat credentials have been set for a local file (eg. via environment variables, unintentionally)' do - let(:uri) { '/some/file thing.json' } - - it 'does not blow up' do - expect(pact_uri.to_s).to eq uri - end - end - end - - context 'without userinfo' do - let(:options) { {} } - - it 'should return original uri string' do - expect(pact_uri.to_s).to eq(uri) - end - end - end -end \ No newline at end of file diff --git a/spec/lib/pact/provider/print_missing_provider_states_spec.rb b/spec/lib/pact/provider/print_missing_provider_states_spec.rb deleted file mode 100644 index 67248946..00000000 --- a/spec/lib/pact/provider/print_missing_provider_states_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'spec_helper' -require 'pact/provider/print_missing_provider_states' - -module Pact - module Provider - describe PrintMissingProviderStates do - - describe "text" do - let(:missing_provider_states) { { 'Consumer 1' => ['state1', 'state2'], 'Consumer 2' => ['state3'] } } - let(:expected_output) { File.read("./spec/support/missing_provider_states_output.txt") } - - subject { PrintMissingProviderStates.text missing_provider_states } - - it "returns the text" do - expect(subject).to include expected_output - end - end - - end - end -end \ No newline at end of file diff --git a/spec/lib/pact/provider/request_spec.rb b/spec/lib/pact/provider/request_spec.rb deleted file mode 100644 index dcb07f70..00000000 --- a/spec/lib/pact/provider/request_spec.rb +++ /dev/null @@ -1,142 +0,0 @@ -require 'spec_helper' -require 'pact/provider/request' - -describe Pact::Provider::Request::Replayable do - - let(:path) { '/path?something' } - let(:body) { { a: 'body' } } - let(:headers) { {} } - let(:generators) { {} } - let(:expected_request) do - instance_double( - 'Pact::Request::Expected', - method: 'post', - full_path: path, - body: body, - headers: headers, - generators: generators, - ) - end - - subject { described_class.new(expected_request) } - - describe "method" do - it 'returns the method' do - expect(subject.method).to eq 'post' - end - end - - describe "path" do - it "returns the full path" do - expect(subject.path).to eq(path) - end - end - - describe "body" do - context "when body is a NullExpectation" do - let(:body) { Pact::NullExpectation.new } - - it "returns an empty string, not sure if it should do this or return nil???" do - expect(subject.body).to eq "" - end - end - - context "when body is an empty string" do - let(:body) { '' } - - it "returns an empty string" do - expect(subject.body).to eq "" - end - end - - context "when body is a string" do - let(:body) { 'a string' } - - it "returns the string" do - expect(subject.body).to eq body - end - end - - context "when body is a Term" do - let(:body) { Pact.term(generate: 'a', matcher: /a/) } - - it "returns the generated value" do - expect(subject.body).to eq "a" - end - end - - context "when body is not a string" do - let(:body) { { a: 'body' } } - - it "returns the object as a json string" do - expect(subject.body).to eq body.to_json - end - - context "and it uses generators" do - let(:body) { { a: 'body', b: '2025-04-08' } } - let(:generators) { {"body"=>{"b"=>{"type"=>"Date"}}} } - let(:expected_body) { { a: 'body', b: Date.today.to_s } } - - it "returns the object as a json string" do - expect(subject.body).to eq expected_body.to_json - end - end - end - end - - describe "" - - describe "headers" do - context "when headers are expected" do - let(:headers) do - { - "Content-Type" => "text/plain", - "Content-Length" => "123", - "X-Content-Type" => "special", - "Access-Control-Request-Method" => "POST" - } - end - - let(:expected_headers) do - { - "CONTENT_TYPE" => "text/plain", - "CONTENT_LENGTH" => "123", - "HTTP_ACCESS_CONTROL_REQUEST_METHOD" => "POST", - "HTTP_X_CONTENT_TYPE" => "special" - } - end - - it "transforms the headers into Rack format" do - expect(subject.headers).to eq(expected_headers) - end - end - - context "when headers are not specified" do - let(:headers) { Pact::NullExpectation.new } - - it "returns an empty hash" do - expect(subject.headers).to eq({}) - end - end - - context "when a Term is used"do - let(:headers) do - { "Authorization" => Pact.term("A", /A|B/) } - end - - it "reifies the headers" do - expect(subject.headers['HTTP_AUTHORIZATION']).to eq "A" - end - end - - context "when a header is nil"do - let(:headers) do - { "Authorization" => nil } - end - - it "reifies the headers" do - expect(subject.headers['HTTP_AUTHORIZATION']).to eq nil - end - end - end -end diff --git a/spec/lib/pact/provider/rspec/calculate_exit_code_spec.rb b/spec/lib/pact/provider/rspec/calculate_exit_code_spec.rb deleted file mode 100644 index 4e062442..00000000 --- a/spec/lib/pact/provider/rspec/calculate_exit_code_spec.rb +++ /dev/null @@ -1,56 +0,0 @@ -require 'pact/provider/rspec/calculate_exit_code' - -module Pact - module Provider - module RSpec - module CalculateExitCode - describe ".call" do - let(:pact_source_1) { double('pact_source_1', pending?: pending_1) } - let(:pending_1) { nil } - let(:pact_source_2) { double('pact_source_2', pending?: pending_2) } - let(:pending_2) { nil } - let(:pact_source_3) { double('pact_source_3', pending?: pending_3) } - let(:pending_3) { nil } - let(:pact_sources) { [pact_source_1, pact_source_2, pact_source_3]} - - let(:failed_examples) { [ example_1, example_2, example_3 ] } - let(:example_1) { double('example_1', metadata: { pact_source: pact_source_1 }) } - let(:example_2) { double('example_2', metadata: { pact_source: pact_source_1 }) } - let(:example_3) { double('example_3', metadata: { pact_source: pact_source_2 }) } - - subject { CalculateExitCode.call(pact_sources, failed_examples ) } - - context "when all pacts are pending" do - let(:pending_1) { true } - let(:pending_2) { true } - let(:pending_3) { true } - - it "returns 0" do - expect(subject).to eq 0 - end - end - - context "when a non pending pact has no failures" do - let(:pending_1) { true } - let(:pending_2) { true } - let(:pending_3) { false } - - it "returns 0" do - expect(subject).to eq 0 - end - end - - context "when a non pending pact no failures" do - let(:pending_1) { true } - let(:pending_2) { false } - let(:pending_3) { false } - - it "returns 1" do - expect(subject).to eq 1 - end - end - end - end - end - end -end diff --git a/spec/lib/pact/provider/rspec/formatter_rspec_2_spec.rb b/spec/lib/pact/provider/rspec/formatter_rspec_2_spec.rb deleted file mode 100644 index 8fd2268c..00000000 --- a/spec/lib/pact/provider/rspec/formatter_rspec_2_spec.rb +++ /dev/null @@ -1,104 +0,0 @@ -require 'spec_helper' -require 'pact/provider/rspec/formatter_rspec_2' -require './spec/support/factories' -require './spec/support/spec_support' -require 'pact/tasks/task_helper' - -module Pact - module Provider - module RSpec - describe Formatter2 do - - Pact::RSpec.with_rspec_3 do - # These methods don't exist in RSpec3 - class Formatter2 - def failure_color arg ; arg; end - def detail_color arg ; arg; end - end - end - - let(:interaction) { InteractionFactory.create 'provider_state' => 'a state', 'description' => 'a description'} - let(:pactfile_uri) { 'pact_file_uri' } - let(:description) { 'an interaction' } - let(:pact_json) { {some: 'pact json'}.to_json } - let(:metadata) do - { - pact_interaction: interaction, - pactfile_uri: pactfile_uri, - pact_interaction_example_description: description, - pact_json: pact_json - } - end - let(:example) { double("Example", metadata: metadata) } - let(:failed_examples) { [example, example] } - let(:output) { StringIO.new } - let(:rerun_command) { "bundle exec rake pact:verify:at[pact_file_uri] PACT_DESCRIPTION=\"a description\" PACT_PROVIDER_STATE=\"a state\" # an interaction" } - let(:missing_provider_states) { 'missing_provider_states'} - let(:pact_executing_language) { 'ruby' } - let(:pact_interaction_rerun_command) { Pact::TaskHelper::PACT_INTERACTION_RERUN_COMMAND } - - subject { Formatter2.new output } - - let(:output_result) { Pact::SpecSupport.remove_ansicolor output.string } - - before do - allow(ENV).to receive(:[]).with('PACT_INTERACTION_RERUN_COMMAND').and_return(pact_interaction_rerun_command) - allow(ENV).to receive(:[]).with('PACT_EXECUTING_LANGUAGE').and_return(pact_executing_language) - allow(PrintMissingProviderStates).to receive(:call) - allow(Pact::Provider::Help::PromptText).to receive(:call).and_return("some help") - allow(subject).to receive(:failed_examples).and_return(failed_examples) - allow(Pact.provider_world.provider_states).to receive(:missing_provider_states).and_return(missing_provider_states) - subject.dump_commands_to_rerun_failed_examples - end - - describe "#dump_commands_to_rerun_failed_examples" do - context "when PACT_INTERACTION_RERUN_COMMAND is set" do - it "prints a list of rerun commands" do - expect(output_result).to include(rerun_command) - end - - it "only prints unique commands" do - expect(output_result.scan(rerun_command).size).to eq 1 - end - end - - context "when PACT_INTERACTION_RERUN_COMMAND is not set" do - let(:pact_interaction_rerun_command) { nil } - - it "prints a list of failed interactions" do - expect(output_result).to include("* #{description}\n") - end - - it "only prints unique interactions" do - expect(output_result.scan("* #{description}\n").size).to eq 1 - end - end - - context "when PACT_EXECUTING_LANGUAGE is ruby" do - it "explains how get help debugging" do - expect(output_result).to include("some help") - end - - it "prints missing provider states" do - expect(PrintMissingProviderStates).to receive(:call).with(missing_provider_states, output) - subject.dump_commands_to_rerun_failed_examples - end - end - - context "when PACT_EXECUTING_LANGUAGE is not ruby" do - let(:pact_executing_language) { 'foo' } - - it "does not explain how get help debugging as the rake task is not exposed for other languages" do - expect(output_result).to_not include("some help") - end - - it "does not print missing provider states as these are set up dynamically" do - expect(PrintMissingProviderStates).to_not receive(:call) - subject.dump_commands_to_rerun_failed_examples - end - end - end - end - end - end -end diff --git a/spec/lib/pact/provider/rspec/formatter_rspec_3_spec.rb b/spec/lib/pact/provider/rspec/formatter_rspec_3_spec.rb deleted file mode 100644 index c2126190..00000000 --- a/spec/lib/pact/provider/rspec/formatter_rspec_3_spec.rb +++ /dev/null @@ -1,162 +0,0 @@ -require 'spec_helper' -require 'pact/provider/rspec/formatter_rspec_3' -require './spec/support/factories' -require './spec/support/spec_support' -require 'pact/tasks/task_helper' - -Pact::RSpec.with_rspec_3 do - module Pact - module Provider - module RSpec - describe Formatter do - - let(:interaction) { InteractionFactory.create 'provider_state' => 'a state', 'description' => 'a description', '_id' => id, 'index' => 2 } - let(:interaction_2) { InteractionFactory.create 'provider_state' => 'a state', 'description' => 'a description 2', '_id' => "#{id}2", 'index' => 3 } - let(:id) { nil } - let(:pactfile_uri) { Pact::Provider::PactURI.new('pact_file_uri') } - let(:description) { 'an interaction' } - let(:pact_json) { {some: 'pact json'}.to_json } - let(:metadata) do - { - pact_interaction: interaction, - pactfile_uri: pactfile_uri, - pact_interaction_example_description: description, - pact_json: pact_json, - pact_ignore_failures: ignore_failures, - } - end - let(:metadata_2) { metadata.merge(pact_interaction: interaction_2)} - let(:example) { double("Example", metadata: metadata) } - let(:example_2) { double("Example", metadata: metadata_2) } - let(:failed_examples) { [example, example] } - let(:examples) { [example, example, example_2]} - let(:output) { StringIO.new } - let(:rerun_command) { 'PACT_DESCRIPTION="a description" PACT_PROVIDER_STATE="a state" # an interaction' } - let(:broker_rerun_command) { "rake pact:verify:at[pact_file_uri] PACT_BROKER_INTERACTION_ID=\"8888\" # an interaction" } - let(:missing_provider_states) { 'missing_provider_states'} - let(:summary) { double("summary", failure_count: 1, failed_examples: failed_examples, examples: examples)} - let(:pact_executing_language) { 'ruby' } - let(:pact_interaction_rerun_command) { Pact::TaskHelper::PACT_INTERACTION_RERUN_COMMAND } - let(:pact_interaction_rerun_command_for_broker) { Pact::TaskHelper::PACT_INTERACTION_RERUN_COMMAND_FOR_BROKER } - let(:ignore_failures) { nil } - - subject { Formatter.new output } - - let(:output_result) { Pact::SpecSupport.remove_ansicolor output.string } - - before do - allow(ENV).to receive(:[]).with('PACT_INTERACTION_RERUN_COMMAND').and_return(pact_interaction_rerun_command) - allow(ENV).to receive(:[]).with('PACT_EXECUTING_LANGUAGE').and_return(pact_executing_language) - allow(ENV).to receive(:[]).with('PACT_INTERACTION_RERUN_COMMAND_FOR_BROKER').and_return(pact_interaction_rerun_command_for_broker) - allow(PrintMissingProviderStates).to receive(:call) - allow(Pact::Provider::Help::PromptText).to receive(:call).and_return("some help") - allow(subject).to receive(:failed_examples).and_return(failed_examples) - allow(Pact.provider_world.provider_states).to receive(:missing_provider_states).and_return(missing_provider_states) - allow(subject).to receive(:set_rspec_failure_color) - subject.dump_summary summary - end - - describe "#dump_summary" do - it "prints the number of interactions" do - expect(output_result).to include("2 interactions") - end - - it "prints the number of failures" do - expect(output_result).to include("1 failure") - end - - context "when PACT_INTERACTION_RERUN_COMMAND is set" do - it "prints a list of rerun commands" do - expect(output_result).to include(rerun_command) - end - - it "only prints unique commands" do - expect(output_result.scan(rerun_command).size).to eq 1 - end - end - - context "when PACT_INTERACTION_RERUN_COMMAND_FOR_BROKER is set" do - context "when the _id is populated" do - let(:id) { "8888" } - - it "prints a list of rerun commands" do - expect(output_result).to include(broker_rerun_command) - end - - it "only prints unique commands" do - expect(output_result.scan(broker_rerun_command).size).to eq 1 - end - end - - context "when the _id is not populated" do - it "prints a list of rerun commands using the provider state and description" do - expect(output_result).to include(rerun_command) - end - end - end - - context "when PACT_INTERACTION_RERUN_COMMAND and PACT_INTERACTION_RERUN_COMMAND_FOR_BROKER are not set" do - let(:pact_interaction_rerun_command) { nil } - let(:pact_interaction_rerun_command_for_broker) { nil } - - context "when the _id is populated" do - let(:id) { "8888" } - - it "prints a list of failed interactions" do - expect(output_result).to include('* an interaction (to re-run just this interaction, set environment variable PACT_BROKER_INTERACTION_ID="8888")') - end - end - - context "when the _id is not populated" do - it "prints a list of failed interactions" do - expect(output_result).to include('* an interaction (to re-run just this interaction, set environment variables PACT_DESCRIPTION="a description" PACT_PROVIDER_STATE="a state")') - end - end - - it "only prints unique commands" do - expect(output_result.scan("* #{description}").size).to eq 1 - end - end - - context "when PACT_EXECUTING_LANGUAGE is ruby" do - it "explains how get help debugging" do - expect(output_result).to include("some help") - end - - it "prints missing provider states" do - expect(PrintMissingProviderStates).to receive(:call).with(missing_provider_states, output) - subject.dump_summary summary - end - end - - context "when PACT_EXECUTING_LANGUAGE is not ruby" do - let(:pact_executing_language) { 'foo' } - - it "does not explain how get help debugging as the rake task is not exposed for other languages" do - expect(output_result).to_not include("some help") - end - - it "does not print missing provider states as these are set up dynamically" do - expect(PrintMissingProviderStates).to_not receive(:call) - subject.dump_summary summary - end - end - - context "when ignore_failures is true" do - let(:pactfile_uri) { Pact::Provider::PactURI.new('pact_file_uri', {}, { pending: true}) } - - it "reports failures as pending" do - expect(output_result).to include("1 pending") - expect(output_result).to_not include("1 failure") - end - - it "explains that failures will not affect the test results" do - expect(output_result).to include "Pending interactions: (Failures listed here are expected and do not affect your suite's status)" - end - end - end - end - end - end - end -end diff --git a/spec/lib/pact/provider/rspec_spec.rb b/spec/lib/pact/provider/rspec_spec.rb deleted file mode 100644 index b7846606..00000000 --- a/spec/lib/pact/provider/rspec_spec.rb +++ /dev/null @@ -1,60 +0,0 @@ -require 'pact/provider/rspec/matchers' -require 'pact/shared/json_differ' -require 'pact/matchers/unix_diff_formatter' - -describe "the match_term matcher" do - - include Pact::RSpec::Matchers - - let(:diff_formatter) { Pact::Matchers::UnixDiffFormatter } - - it 'does not match a hash to an array' do | example | - expect({}) - .to_not match_term([], { with: Pact::JsonDiffer, diff_formatter: diff_formatter }, example) - end - - it 'does not match an array to a hash' do | example | - expect([]) - .to_not match_term({}, { with: Pact::JsonDiffer, diff_formatter: diff_formatter }, example) - end - - it 'matches regular expressions' do | example | - expect('blah') - .to match_term(/[a-z]*/, { with: Pact::JsonDiffer, diff_formatter: diff_formatter }, example) - end - - it 'matches pact terms' do | example | - expect('wootle') - .to match_term Pact.term(generate: 'wootle', matcher: /woot../), { with: Pact::JsonDiffer, diff_formatter: diff_formatter }, example - end - - it 'matches all elements of arrays' do | example | - expect(['one', 'two', ['three']]) - .to match_term [/one/, 'two', [Pact.term(generate: 'three', matcher: /thr../)]], { with: Pact::JsonDiffer, diff_formatter: diff_formatter }, example - end - - it 'matches all values of hashes' do | example | - expect({ 1 => 'one', 2 => 2, 3 => 'three' }) - .to match_term({ 1 => /one/, 2 => 2, 3 => Pact.term(generate: 'three', matcher: /thr../) }, { with: Pact::JsonDiffer, diff_formatter: diff_formatter }, example) - end - - it 'matches all other objects using ==' do | example | - expect('wootle').to match_term 'wootle', { with: Pact::JsonDiffer, diff_formatter: diff_formatter }, example - end - - # Note: because a consumer specifies only the keys it cares about, the pact ignores keys that are returned - # by the provider, but not are not specified in the pact. This means that any hash will match an - # expected empty hash, because there is currently no way for a consumer to expect an absence of keys. - it 'is confused by an empty hash' do | example | - expect(hello: 'everyone').to match_term({}, { with: Pact::JsonDiffer, diff_formatter: diff_formatter }, example) - end - - it 'should not be confused by an empty array' do | example | - expect(['blah']).to_not match_term([], { with: Pact::JsonDiffer, diff_formatter: diff_formatter }, example) - end - - it "should allow matches on an array where each item in the array only contains a subset of the actual" do | example | - expect([{ name: 'Fred', age: 12 }, { name: 'John', age: 13 }]) - .to match_term([{ name: 'Fred' }, { name: 'John' }], { with: Pact::JsonDiffer, diff_formatter: diff_formatter }, example) - end -end diff --git a/spec/lib/pact/provider/state/provider_state_manager_spec.rb b/spec/lib/pact/provider/state/provider_state_manager_spec.rb deleted file mode 100644 index 25957deb..00000000 --- a/spec/lib/pact/provider/state/provider_state_manager_spec.rb +++ /dev/null @@ -1,86 +0,0 @@ -require 'spec_helper' -require 'pact/provider/state/provider_state_manager' - -module Pact::Provider::State - describe ProviderStateManager do - - PROVIDER_STATE_MESSAGES = [] - - before do - PROVIDER_STATE_MESSAGES.clear - Pact.clear_provider_world - - Pact.set_up do - PROVIDER_STATE_MESSAGES << :global_base_set_up - end - - Pact.tear_down do - PROVIDER_STATE_MESSAGES << :global_base_tear_down - end - - Pact.provider_states_for "a consumer with provider states" do - set_up do - PROVIDER_STATE_MESSAGES << :consumer_base_set_up - end - - tear_down do - PROVIDER_STATE_MESSAGES << :consumer_base_tear_down - end - - provider_state "a custom state" do - set_up do - PROVIDER_STATE_MESSAGES << :custom_consumer_state_set_up - end - - tear_down do - PROVIDER_STATE_MESSAGES << :custom_consumer_state_tear_down - end - end - - end - end - - let(:params) { { "foo" => "bar" } } - let(:provider_state_manager) { ProviderStateManager.new("a custom state", params, "a consumer with provider states") } - - describe "set_up_provider_state" do - - subject { provider_state_manager.set_up_provider_state } - - it "sets up the global base state" do - subject - expect(PROVIDER_STATE_MESSAGES[0]).to eq :global_base_set_up - end - - it "sets up the consumer base state" do - subject - expect(PROVIDER_STATE_MESSAGES[1]).to eq :consumer_base_set_up - end - - it "sets up the consumer custom state" do - subject - expect(PROVIDER_STATE_MESSAGES[2]).to eq :custom_consumer_state_set_up - end - end - - describe "tear_down_provider_state" do - - subject { provider_state_manager.tear_down_provider_state } - - it "tears down the consumer custom state" do - subject - expect(PROVIDER_STATE_MESSAGES[0]).to eq :custom_consumer_state_tear_down - end - - it "tears down the consumer base state" do - subject - expect(PROVIDER_STATE_MESSAGES[1]).to eq :consumer_base_tear_down - end - - it "tears down the global base state" do - subject - expect(PROVIDER_STATE_MESSAGES[2]).to eq :global_base_tear_down - end - end - end -end diff --git a/spec/lib/pact/provider/state/provider_state_proxy_spec.rb b/spec/lib/pact/provider/state/provider_state_proxy_spec.rb deleted file mode 100644 index 62fa93e0..00000000 --- a/spec/lib/pact/provider/state/provider_state_proxy_spec.rb +++ /dev/null @@ -1,78 +0,0 @@ -require 'spec_helper' -require 'pact/provider/state/provider_state_proxy' - -module Pact - module Provider::State - describe ProviderStateProxy do - - let(:provider_state_proxy) { ProviderStateProxy.new } - - let(:options) { { for: 'some consumer' } } - let(:provider_state) { double("provider_state") } - - describe "get" do - let(:name) { "some state" } - - subject { provider_state_proxy.get name, options } - - before do - allow(ProviderStates).to receive(:get).and_return(provider_state) - end - - context "when the provider state exists" do - - it "retrieves the provider state from ProviderState" do - expect(ProviderStates).to receive(:get).with(name, options).and_return(provider_state) - subject - end - - it "returns the state" do - expect(subject).to eq provider_state - end - - end - - context "when the state does not exist" do - - let(:provider_state) { nil } - let(:expected_missing_provider_states) { { "some consumer" => ["some state"] } } - - it "raises an error" do - expect { subject }.to raise_error /Could not find.*some state.*consumer.*/ - end - - it "records the provider state as missing" do - subject rescue nil - expect(provider_state_proxy.missing_provider_states).to eq expected_missing_provider_states - end - - context "when the same missing provider state is requested" do - it "ensures the list only contains unique entries" do - subject rescue nil - subject rescue nil - expect(provider_state_proxy.missing_provider_states['some consumer'].size).to eq 1 - end - end - end - end - - describe "get_base" do - - before do - allow(ProviderStates).to receive(:get_base).and_return(provider_state) - end - - subject { provider_state_proxy.get_base options } - - it "calls through to ProviderStates" do - expect(ProviderStates).to receive(:get_base).with(options) - subject - end - - it "returns the state" do - expect(subject).to eq provider_state - end - end - end - end -end \ No newline at end of file diff --git a/spec/lib/pact/provider/state/provider_state_spec.rb b/spec/lib/pact/provider/state/provider_state_spec.rb deleted file mode 100644 index 1c15cc67..00000000 --- a/spec/lib/pact/provider/state/provider_state_spec.rb +++ /dev/null @@ -1,303 +0,0 @@ -require 'spec_helper' -require 'pact/provider/state/provider_state' - -module Pact - module Provider::State - describe ProviderStates do - - module ProviderStatesHelper - def help_me_set_up - "set up helped" - end - - def help_me_tear_down - "tear down helped" - end - end - - MESSAGES = [] - - let(:params) { { "foo" => "bar" } } - - before do - MESSAGES.clear - Pact.configuration.include ProviderStatesHelper - end - - describe 'with an included module' do - Pact.provider_state :alligators_with_helper_module do - set_up do - MESSAGES << help_me_set_up - end - tear_down do - MESSAGES << help_me_tear_down - end - end - - subject { ProviderStates.get('alligators_with_helper_module') } - - it 'makes the helper module available to the set up block' do - subject.set_up - expect(MESSAGES).to eq ["set up helped"] - end - - it 'makes the helper module available to the tear down block' do - subject.tear_down - expect(MESSAGES).to eq ["tear down helped"] - end - end - - describe 'with params' do - Pact.provider_state :alligators_with_params do - set_up do | params | - MESSAGES << { set_up: params } - end - tear_down do | params | - MESSAGES << { tear_down: params } - end - end - - subject { ProviderStates.get('alligators_with_params') } - - it 'calls the block passed to set_up with the params as an argument' do - subject.set_up(params) - expect(MESSAGES).to eq [{ set_up: params }] - end - - it 'calls the block passed to tear_down with the params as an argument' do - subject.tear_down(params) - expect(MESSAGES).to eq [{ tear_down: params }] - end - end - - describe 'with a variable delcared outside the block' do - - variable_declared_outside = "foo" - - Pact.provider_state :alligators_with_variable_declared_out_of_scope do - set_up do | params | - MESSAGES << variable_declared_outside - end - end - - subject { ProviderStates.get('alligators_with_variable_declared_out_of_scope') } - - it 'calls the block passed to set_up with the params as an argument' do - subject.set_up(params) - expect(MESSAGES).to eq ["foo"] - end - end - - describe 'global ProviderState' do - - Pact.provider_state :no_alligators do - set_up do - MESSAGES << 'set_up' - end - tear_down do - MESSAGES << 'tear_down' - end - end - - Pact.provider_state 'some alligators' do - no_op - end - - subject { ProviderStates.get('no_alligators') } - - describe 'set_up' do - it 'should call the block passed to set_up' do - subject.set_up - expect(MESSAGES).to eq ['set_up'] - end - end - - describe 'tear_down' do - it 'should call the block passed to set_up' do - subject.tear_down - expect(MESSAGES).to eq ['tear_down'] - end - end - - describe '.get' do - context 'when the name is a matching symbol' do - it 'will return the ProviderState' do - expect(ProviderStates.get('no_alligators')).to_not be_nil - end - end - - context 'when the name is a matching string' do - it 'will return the ProviderState' do - expect(ProviderStates.get('some alligators')).to_not be_nil - end - end - end - end - - describe 'no_op' do - context "when a no_op is defined instead of a set_up or tear_down" do - it "treats set_up and tear_down as empty blocks" do - Pact.provider_state 'with_no_op' do - no_op - end - ProviderStates.get('with_no_op').set_up - ProviderStates.get('with_no_op').tear_down - end - end - - context "when a no_op is defined with a set_up" do - it "raises an error" do - expect do - Pact.provider_state 'with_no_op_and_set_up' do - no_op - set_up do - - end - end.to raise_error(/Provider state \"with_no_op_and_set_up\" has been defined as a no_op but it also has a set_up block. Please remove one or the other./) - end - end - end - - context "when a no_op is defined with a tear_down" do - it "raises an error" do - expect do - Pact.provider_state 'with_no_op_and_set_up' do - no_op - tear_down do - - end - end.to raise_error(/Provider state \"with_no_op_and_set_up\" has been defined as a no_op but it also has a tear_down block. Please remove one or the other./) - end - end - end - - end - - describe 'namespaced ProviderStates' do - - NAMESPACED_MESSAGES = [] - - Pact.provider_states_for 'a consumer' do - provider_state 'the weather is sunny' do - set_up do - NAMESPACED_MESSAGES << 'sunny!' - end - end - end - - Pact.provider_state 'the weather is cloudy' do - set_up do - NAMESPACED_MESSAGES << 'cloudy :(' - end - end - - before do - NAMESPACED_MESSAGES.clear - end - - describe '.get' do - context 'for a consumer' do - it 'has a namespaced name' do - expect(ProviderStates.get('the weather is sunny', for: 'a consumer')).to_not be_nil - end - - it 'falls back to a global state of the same name if one is not found for the specified consumer' do - expect(ProviderStates.get('the weather is cloudy', for: 'a consumer')).to_not be_nil - end - end - - end - - describe 'set_up' do - context 'for a consumer' do - it 'runs its own setup' do - ProviderStates.get('the weather is sunny', for: 'a consumer').set_up - expect(NAMESPACED_MESSAGES).to eq ['sunny!'] - end - end - end - end - - describe "base_provider_state" do - Pact.provider_states_for "a consumer with base state" do - set_up do - MESSAGES << "setting up base provider state" - end - end - - context "when the base state has been declared" do - it "creates a base state for the provider" do - ProviderStates.get_base(for: "a consumer with base state").set_up - expect(MESSAGES).to eq ["setting up base provider state"] - end - end - - context "when a base state has not been declared" do - it "returns a no op state" do - ProviderStates.get_base(for: "a consumer that does not exist").set_up - ProviderStates.get_base(for: "a consumer that does not exist").tear_down - end - end - - end - - describe "base_provider_state with a teardown and no setup" do - Pact.provider_states_for "a consumer with base state with only a tear down" do - tear_down do - MESSAGES << "tearing down without a set up" - end - end - - it "creates a base state for the provider" do - ProviderStates.get_base(for: "a consumer with base state with only a tear down").tear_down - expect(MESSAGES).to eq ["tearing down without a set up"] - end - end - - describe "global base_provider_state" do - - before(:all) do - Pact.set_up do - MESSAGES << "setting up global base provider state" - end - end - - context "when the base state has been declared" do - it "creates a base state for the provider" do - ProviderStates.get_base.set_up - expect(MESSAGES).to eq ["setting up global base provider state"] - end - - end - - context "when a base state has not been declared" do - it "returns a no op state" do - ProviderStates.get_base.set_up - ProviderStates.get_base.tear_down - end - end - end - - describe "invalid provider state" do - context "when no set_up or tear_down is provided" do - it "raises an error to prevent someone forgetting about the set_up and putting the set_up code directly in the provider_state block and wasting 20 minutes trying to work out why their provider states aren't working properly" do - expect do - Pact.provider_state 'invalid' do - end - end.to raise_error(/Please provide a set_up or tear_down block for provider state \"invalid\"/) - end - end - - context "when a no_op is defined" do - it "does not raise an error" do - expect do - Pact.provider_state 'valid' do - no_op - end - end.not_to raise_error - end - end - end - end - end -end diff --git a/spec/lib/pact/provider/verification_results/create_spec.rb b/spec/lib/pact/provider/verification_results/create_spec.rb deleted file mode 100644 index 0b4696ee..00000000 --- a/spec/lib/pact/provider/verification_results/create_spec.rb +++ /dev/null @@ -1,123 +0,0 @@ -require 'pact/provider/verification_results/create' - -module Pact - module Provider - module VerificationResults - describe Create do - before do - allow(Pact.configuration).to receive(:provider).and_return(provider_configuration) - allow(VerificationResult).to receive(:new).and_return(verification_result) - end - - let(:verification_result) { double('VerificationResult') } - let(:provider_configuration) do - double('provider_configuration', application_version: '1.2.3', build_url: ci_build) - end - let(:ci_build) { 'http://ci/build/1' } - let(:pact_source_1) do - instance_double('Pact::Provider::PactSource', uri: pact_uri_1, consumer_contract: consumer_contract) - end - let(:consumer_contract) { instance_double('Pact::ConsumerContract', interactions: interactions)} - let(:interactions) { [interaction_1] } - let(:interaction_1) { instance_double('Pact::Interaction', _id: "1") } - let(:interaction_2) { instance_double('Pact::Interaction', _id: "2") } - let(:pact_uri_1) { instance_double('Pact::Provider::PactURI', uri: URI('foo')) } - let(:pact_uri_2) { instance_double('Pact::Provider::PactURI', uri: URI('bar')) } - let(:example_1) do - { - pact_uri: pact_uri_1, - pact_interaction: interaction_1, - status: 'passed' - } - end - let(:example_2) do - { - pact_uri: pact_uri_2, - pact_interaction: interaction_2, - status: 'passed' - } - end - let(:test_results_hash) do - { - tests: [example_1, example_2] - } - end - - subject { Create.call(pact_source_1, test_results_hash) } - - it "returns a verification result" do - expect(subject).to eq verification_result - end - - it "creates a VerificationResult with the relevant test results" do - expected_test_results_hash = { - tests: [{ status: "passed" }], - summary: { testCount: 1, failureCount: 0}, - metadata: { - warning: "These test results use a beta format. Do not rely on it, as it will definitely change.", - pactVerificationResultsSpecification: { - version: "1.0.0-beta.1" - } - } - } - expect(VerificationResult).to receive(:new).with(anything, anything, anything, expected_test_results_hash, anything) - subject - end - - it "creates a VerificationResult with the provider application version" do - expect(provider_configuration).to receive(:application_version) - expect(VerificationResult).to receive(:new).with(anything, anything, '1.2.3', anything, anything) - subject - end - - it "creates a VerificationResult with the provider ci build url" do - expect(provider_configuration).to receive(:build_url) - expect(VerificationResult).to receive(:new).with(anything, anything, anything, anything, ci_build) - subject - end - - context "when every interaction has been executed" do - it "sets publishable to true" do - expect(VerificationResult).to receive(:new).with(true, anything, anything, anything, anything) - subject - end - end - - context "when not every interaction has been executed" do - let(:interaction_3) { instance_double('Pact::Interaction', _id: "3") } - let(:interactions) { [interaction_1, interaction_2]} - - it "sets publishable to false" do - expect(VerificationResult).to receive(:new).with(false, anything, anything, anything, anything) - subject - end - end - - context "when all the examples passed" do - it "sets the success to true" do - expect(VerificationResult).to receive(:new).with(anything, true, anything, anything, anything) - subject - end - end - - context "when not all the examples passed" do - before do - example_1[:status] = 'notpassed' - end - - it "sets the success to false" do - expect(VerificationResult).to receive(:new).with(anything, false, anything, anything, anything) - subject - end - - it "sets the failureCount" do - expect(VerificationResult).to receive(:new) do | _, _, _, test_results_hash| - expect(test_results_hash[:summary][:failureCount]).to eq 1 - end - subject - end - end - end - end - end -end diff --git a/spec/lib/pact/provider/verification_results/publish_spec.rb b/spec/lib/pact/provider/verification_results/publish_spec.rb deleted file mode 100644 index 1e5b67ba..00000000 --- a/spec/lib/pact/provider/verification_results/publish_spec.rb +++ /dev/null @@ -1,238 +0,0 @@ -require 'pact/provider/verification_results/publish' - -module Pact - module Provider - module VerificationResults - describe Publish do - describe "call" do - let(:publish_verification_url) { nil } - let(:stubbed_publish_verification_url) { 'http://broker/something/provider/Bar/verifications' } - let(:tag_version_url) { 'http://tag-me/{tag}' } - let(:pact_source) { instance_double("Pact::Provider::PactSource", pact_hash: pact_hash, uri: pact_url)} - let(:pact_url) { instance_double("Pact::Provider::PactURI", options: options) } - let(:options) { { username: 'username', password: 'password' } } - let(:provider_url) { 'http://provider' } - let(:basic_auth) { false } - let(:pact_hash) do - { - 'consumer' => { - 'name' => 'Foo' - }, - 'provider' => { - 'name' => 'Bar' - }, - '_links' => { - 'pb:publish-verification-results'=> { - 'href' => publish_verification_url - }, - 'pb:provider' => { - 'href' => provider_url - } - } - } - end - let(:created_verification_body) do - { - '_links' => { - 'self' => { - 'href' => 'http://broker/new-verification' - } - } - }.to_json - end - let(:provider_body) do - { - '_links' => { - 'self' => { - 'href' => provider_url - }, - 'pb:version-tag' => { - 'href' => 'http://provider/version/{version}/tag/{tag}' - }, - 'pb:branch-version' => { - 'href' => 'http://provider/branches/{branch}/versions/{version}' - } - } - }.to_json - end - let(:tag_body) do - { - '_links' => { - 'self' => { - 'href' => 'http://tag-url' - } - } - }.to_json - end - let(:app_version_set) { false } - let(:verification_json) { '{"foo": "bar"}' } - let(:publish_verification_results) { false } - let(:publishable) { true } - let(:branch) { nil } - let(:tags) { [] } - let(:verification) do - instance_double("Pact::Verifications::Verification", - to_json: verification_json, - provider_application_version_set?: app_version_set, - publishable?: publishable - ) - end - - let(:provider_configuration) do - double('provider config', publish_verification_results?: publish_verification_results, branch: branch, tags: tags, application_version: '1.2.3') - end - - before do - allow($stdout).to receive(:puts) - allow($stderr).to receive(:puts) - allow(Pact.configuration).to receive(:provider).and_return(provider_configuration) - stub_request(:post, stubbed_publish_verification_url).to_return(status: 200, headers: { 'Content-Type' => 'application/hal+json'}, body: created_verification_body) - stub_request(:put, 'http://provider/version/1.2.3/tag/foo').to_return(status: 200, headers: { 'Content-Type' => 'application/hal+json'}, body: tag_body) - stub_request(:put, "http://provider/branches/main/versions/1.2.3").to_return(status: 200, body: "{}", headers: { 'Content-Type' => 'application/hal+json' }) - stub_request(:get, provider_url).to_return(status: 200, headers: { 'Content-Type' => 'application/hal+json'}, body: provider_body) - allow(Retry).to receive(:until_true) { |&block| block.call } - end - - subject { Publish.call(pact_source, verification) } - - context "when publish_verification_results is false" do - it "does not publish the verification" do - subject - expect(WebMock).to_not have_requested(:post, 'http://broker/something/provider/Bar/verifications') - end - end - - context "when publish_verification_results is true" do - let(:publish_verification_results) { true } - - context "when the publish-verification link is present" do - let(:publish_verification_url) { stubbed_publish_verification_url } - - it "publishes the verification" do - subject - expect(WebMock).to have_requested(:post, publish_verification_url).with(body: verification_json, headers: {'Content-Type' => 'application/json', 'Accept' => 'application/hal+json, */*'} ) - end - - context "when the verification result is not publishable" do - let(:publishable) { false } - - it "does not publish the verification" do - subject - expect(WebMock).to_not have_requested(:post, stubbed_publish_verification_url) - end - end - - context "with a branch" do - let(:branch) { "main" } - - it "creates the branch version" do - subject - expect(WebMock).to have_requested(:put, 'http://provider/branches/main/versions/1.2.3').with(headers: {'Content-Type' => 'application/json'}) - end - - context "when there is an error creating the branch version" do - before do - stub_request(:put, "http://provider/branches/main/versions/1.2.3").to_return(status: 500, body: { some: "error" }.to_json, headers: { 'Content-Type' => 'application/hal+json' }) - end - - it "raises an error" do - expect { subject }.to raise_error PublicationError, /500.*some.*error/ - end - end - - context "when the broker does not support creating branch versions" do - let(:provider_body) do - {}.to_json - end - - it "raises an error" do - expect { subject }.to raise_error PublicationError, /does not support/ - end - end - end - - context "with tags" do - let(:tags) { ['foo'] } - - it "tags the provider version" do - subject - expect(WebMock).to have_requested(:put, 'http://provider/version/1.2.3/tag/foo').with(headers: {'Content-Type' => 'application/json'}) - end - - it "logs the tagging to stdout" do - expect($stdout).to receive(:puts).with("INFO: Tagging version 1.2.3 of Bar as \"foo\"") - subject - end - - context "when there is no pb:publish-verification-results link" do - before do - pact_hash['_links'].delete('pb:publish-verification-results') - end - - it "does not tag the version" do - subject - expect(WebMock).to_not have_requested(:put, /.*/) - end - end - end - - context "when there are no tags specified and there is no pb:tag-version link" do - before do - pact_hash['_links'].delete('pb:tag-version') - end - let(:tags) { [] } - - it "does not print a warning" do - expect($stderr).to_not receive(:puts).with /WARN: Cannot tag provider version/ - subject - end - end - - context "when basic auth is configured on the pact URL" do - it "sets the username and password for the publication URL" do - subject - expect(WebMock).to have_requested(:post, publish_verification_url).with(basic_auth: ['username', 'password']) - end - end - - context "when a token is configured on the pact URL" do - let(:options) { {token: 'token'} } - - it "sets the authorization header" do - subject - expect(WebMock).to have_requested(:post, publish_verification_url).with(headers: { 'Authorization' => 'Bearer token'}) - end - end - - context "when an HTTP error is returned" do - it "raises a PublicationError" do - stub_request(:post, stubbed_publish_verification_url).to_return(status: 500, body: '{}') - expect{ subject }.to raise_error(PublicationError, /Error returned/) - end - end - - context "when the connection can't be made" do - it "raises a PublicationError error" do - allow(Net::HTTP).to receive(:new).and_raise(SocketError) - expect{ subject }.to raise_error(PublicationError, /Failed to publish verification/) - end - end - - context "with https" do - before do - stub_request(:post, publish_verification_url).to_return(status: 200, headers: { 'Content-Type' => 'application/json' }, body: created_verification_body) - end - let(:publish_verification_url) { stubbed_publish_verification_url.gsub('http', 'https') } - - it "uses ssl" do - subject - expect(WebMock).to have_requested(:post, publish_verification_url) - end - end - end - end - end - end - end - end -end diff --git a/spec/lib/pact/provider/world_spec.rb b/spec/lib/pact/provider/world_spec.rb deleted file mode 100644 index f71a8cad..00000000 --- a/spec/lib/pact/provider/world_spec.rb +++ /dev/null @@ -1,76 +0,0 @@ -require 'spec_helper' -load 'pact/provider/world.rb' - -describe Pact do - describe ".provider_world" do - it "returns a world" do - expect(Pact.provider_world).to be_instance_of Pact::Provider::World - end - it "returns the same world each time" do - expect(Pact.provider_world).to be Pact.provider_world - end - end - - describe ".clear_provider_world" do - it "clears the world" do - original_world = Pact.provider_world - Pact.clear_provider_world - expect(original_world).to_not be Pact.provider_world - end - end - -end - -module Pact - module Provider - describe World do - - subject { World.new } - - describe "provider_states" do - it "returns a provider state proxy" do - expect(subject.provider_states).to be_instance_of State::ProviderStateProxy - end - it "returns the same object each time" do - expect(subject.provider_states).to be subject.provider_states - end - end - - describe "pact_urls" do - context "with pact_uri_sources" do - before do - subject.add_pact_uri_source(pact_uri_source_1) - subject.add_pact_uri_source(pact_uri_source_2) - end - - let(:pact_uri_source_1) { double('pact_uri_source_1', call: ["uri-1"]) } - let(:pact_uri_source_2) { double('pact_uri_source_2', call: ["uri-2"]) } - - let(:pact_urls) { subject.pact_urls } - - it "invokes call on the pact_uri_sources" do - expect(pact_uri_source_1).to receive(:call) - expect(pact_uri_source_2).to receive(:call) - pact_urls - end - - it "concatenates the results" do - expect(pact_urls).to eq ["uri-1", "uri-2"] - end - - context "with a pact_verification" do - before do - subject.add_pact_verification(pact_verification) - end - - let(:pact_verification) { double('PactVerification', uri: "uri-3") } - - it "concatenates the results with those of the pact_uri_sources" do - expect(pact_urls).to eq ["uri-3", "uri-1", "uri-2"] - end - end - end - end - end - end -end diff --git a/spec/lib/pact/tasks/task_helper_spec.rb b/spec/lib/pact/tasks/task_helper_spec.rb deleted file mode 100644 index 8ed2a10b..00000000 --- a/spec/lib/pact/tasks/task_helper_spec.rb +++ /dev/null @@ -1,230 +0,0 @@ -require 'spec_helper' -require 'pact/tasks/task_helper' -require 'rake/file_utils' - -module Pact - describe TaskHelper do - describe ".execute_pact_verify" do - let(:ruby_path) { "/path/to/ruby" } - let(:pact_uri) { "/pact/uri" } - let(:default_pact_helper_path) { "/pact/helper/path.rb" } - let(:verification_options) { { ignore_failures: ignore_failures } } - let(:ignore_failures) { nil } - - before do - stub_const("FileUtils::RUBY", ruby_path) - allow(Pact::Provider::PactHelperLocater).to receive(:pact_helper_path).and_return(default_pact_helper_path) - end - - it "returns an exit status based on the return value of the system command" do - allow(TaskHelper).to receive(:system).and_return(true) - expect(TaskHelper.execute_pact_verify(pact_uri, "pact_helper")).to eq 0 - end - - context "when PACT_EXECUTING_LANGUAGE is not set" do - it "sets PACT_EXECUTING_LANGUAGE to ruby" do - expect(TaskHelper).to receive(:system) do | command | - expect(ENV['PACT_EXECUTING_LANGUAGE']).to eq 'ruby' - end - TaskHelper.execute_pact_verify(pact_uri) - end - end - - context "when PACT_EXECUTING_LANGUAGE is set" do - it "keeps the value of PACT_EXECUTING_LANGUAGE" do - ENV['PACT_EXECUTING_LANGUAGE'] = 'foo' - expect(TaskHelper).to receive(:system) do | command | - expect(ENV['PACT_EXECUTING_LANGUAGE']).to eq 'foo' - end - TaskHelper.execute_pact_verify(pact_uri) - end - - after do - ENV['PACT_EXECUTING_LANGUAGE'] = nil - end - end - - context "when PACT_INTERACTION_RERUN_COMMAND is not set" do - it "sets PACT_INTERACTION_RERUN_COMMAND to TaskHelper::PACT_INTERACTION_RERUN_COMMAND" do - expect(TaskHelper).to receive(:system) do | command | - expect(ENV['PACT_INTERACTION_RERUN_COMMAND']).to eq TaskHelper::PACT_INTERACTION_RERUN_COMMAND - end - TaskHelper.execute_pact_verify(pact_uri) - end - end - - context "when PACT_INTERACTION_RERUN_COMMAND is set" do - it "keeps the value of PACT_INTERACTION_RERUN_COMMAND" do - ENV['PACT_INTERACTION_RERUN_COMMAND'] = 'foo' - expect(TaskHelper).to receive(:system) do | command | - expect(ENV['PACT_INTERACTION_RERUN_COMMAND']).to eq 'foo' - end - TaskHelper.execute_pact_verify(pact_uri) - end - - after do - ENV['PACT_INTERACTION_RERUN_COMMAND'] = nil - end - end - - context "with no pact_helper or pact URI" do - let(:command) { "SPEC_OPTS='' #{ruby_path} -S pact verify --pact-helper #{default_pact_helper_path}" } - it "executes the command" do - expect(TaskHelper).to receive(:execute_cmd).with(command) - TaskHelper.execute_pact_verify - end - end - - context "with a pact URI" do - let(:command) { "SPEC_OPTS='' #{ruby_path} -S pact verify --pact-helper #{default_pact_helper_path} --pact-uri #{pact_uri}" } - it "executes the command" do - expect(TaskHelper).to receive(:execute_cmd).with(command) - TaskHelper.execute_pact_verify(pact_uri) - end - end - - context "with a pact URI and a pact_helper" do - let(:custom_pact_helper_path) { '/custom/pact_helper.rb' } - let(:command) { "SPEC_OPTS='' #{ruby_path} -S pact verify --pact-helper #{custom_pact_helper_path} --pact-uri #{pact_uri}" } - it "executes the command" do - expect(TaskHelper).to receive(:execute_cmd).with(command) - TaskHelper.execute_pact_verify(pact_uri, custom_pact_helper_path) - end - end - - context "with a pact_helper with whitespace in its path" do - let(:custom_pact_helper_path) { '/path/to the/pact_helper.rb' } - let(:escaped_custom_pact_helper_path) { '/path/to\\ the/pact_helper.rb' } - let(:command) { "SPEC_OPTS='' #{ruby_path} -S pact verify --pact-helper #{escaped_custom_pact_helper_path} --pact-uri #{pact_uri}" } - it "executes the command" do - expect(TaskHelper).to receive(:execute_cmd).with(command) - TaskHelper.execute_pact_verify(pact_uri, custom_pact_helper_path) - end - end - - context "with a pact_helper with no .rb on the end" do - let(:custom_pact_helper_path) { '/custom/pact_helper' } - let(:command) { "SPEC_OPTS='' #{ruby_path} -S pact verify --pact-helper #{custom_pact_helper_path}.rb --pact-uri #{pact_uri}" } - it "executes the command" do - expect(TaskHelper).to receive(:execute_cmd).with(command) - TaskHelper.execute_pact_verify(pact_uri, custom_pact_helper_path) - end - end - - context "with a pact URI and a nil pact_helper" do - let(:command) { "SPEC_OPTS='' #{ruby_path} -S pact verify --pact-helper #{default_pact_helper_path} --pact-uri #{pact_uri}" } - it "executes the command" do - expect(TaskHelper).to receive(:execute_cmd).with(command) - TaskHelper.execute_pact_verify(pact_uri, nil) - end - end - - context "with ignore_failures: true" do - let(:ignore_failures) { true } - it "executes the command with --ignore-failures" do - expect(TaskHelper).to receive(:execute_cmd).with(/ --ignore-failures\b/) - TaskHelper.execute_pact_verify(pact_uri, nil, nil, verification_options) - end - end - - context "with PACT_BROKER_USERNAME set" do - before do - ENV['PACT_BROKER_USERNAME'] = 'pact_username' - end - - it "includes the -u option in the command" do - expect(TaskHelper).to receive(:execute_cmd).with(/--pact-broker-username pact_username/) - TaskHelper.execute_pact_verify(pact_uri, nil, nil) - end - - after do - ENV.delete('PACT_BROKER_USERNAME') - end - end - - context "with PACT_BROKER_PASSWORD set" do - before do - ENV['PACT_BROKER_PASSWORD'] = 'pact_password' - end - - it "includes the -w option in the command" do - expect(TaskHelper).to receive(:execute_cmd).with(/--pact-broker-password pact_password/) - TaskHelper.execute_pact_verify(pact_uri, nil, nil) - end - - after do - ENV.delete('PACT_BROKER_PASSWORD') - end - end - - context "with rspec_opts" do - it "includes the rspec_opts as SPEC_OPTS in the command" do - expect(TaskHelper).to receive(:execute_cmd) do | command | - expect(command).to start_with("SPEC_OPTS=--reporter\\ SomeReporter #{ruby_path}") - end - TaskHelper.execute_pact_verify(pact_uri, nil, "--reporter SomeReporter") - end - end - - context "with $BACKTRACE=true" do - before do - ENV['BACKTRACE'] = 'true' - end - - it "includes the -b option in the command" do - expect(TaskHelper).to receive(:execute_cmd).with(/\s\--backtrace\b/) - TaskHelper.execute_pact_verify(pact_uri, nil, nil) - end - - after do - ENV.delete('BACKTRACE') - end - end - - context "with PACT_DESCRIPTION set" do - before do - ENV['PACT_DESCRIPTION'] = 'some description' - end - - it "includes the -b option in the command" do - expect(TaskHelper).to receive(:execute_cmd).with(/--description some\\ description/) - TaskHelper.execute_pact_verify(pact_uri, nil, nil) - end - - after do - ENV.delete('PACT_DESCRIPTION') - end - end - - context "with PACT_PROVIDER_STATE set" do - before do - ENV['PACT_PROVIDER_STATE'] = 'some state' - end - - it "includes the -b option in the command" do - expect(TaskHelper).to receive(:execute_cmd).with(/--provider-state some\\ state/) - TaskHelper.execute_pact_verify(pact_uri, nil, nil) - end - - after do - ENV.delete('PACT_PROVIDER_STATE') - end - end - - context "with PACT_PROVIDER_STATE set as an emtpy string" do - before do - ENV['PACT_PROVIDER_STATE'] = '' - end - - it "includes the -b option in the command" do - expect(TaskHelper).to receive(:execute_cmd).with(/--provider-state ''/) - TaskHelper.execute_pact_verify(pact_uri, nil, nil) - end - - after do - ENV.delete('PACT_PROVIDER_STATE') - end - end - end - end -end diff --git a/spec/lib/pact/tasks/verification_task_spec.rb b/spec/lib/pact/tasks/verification_task_spec.rb deleted file mode 100644 index 5ec6a2ec..00000000 --- a/spec/lib/pact/tasks/verification_task_spec.rb +++ /dev/null @@ -1,83 +0,0 @@ -require 'spec_helper' -require 'pact/tasks/verification_task' -require 'pact/tasks/task_helper' - -module Pact - describe VerificationTask do - before :all do - @pact_helper = '/custom/path/pact_helper .rb' - @pact_uri = 'http://example.org/pact.json' - @task_name = 'pact:verify:pact_rake_spec' - @task_name_with_explict_pact_helper = 'pact:verify:pact_rake_spec_with_explict_pact_helper' - @task_name_ignore_failures = 'pact:verify:pact_rake_spec_ignore_failures' - @consumer = 'some-consumer' - @criteria = {:description => /wiffle/} - - VerificationTask.new(:pact_rake_spec_with_explict_pact_helper) do | pact | - pact.uri @pact_uri, pact_helper: @pact_helper - end - - VerificationTask.new(:pact_rake_spec) do | pact | - pact.uri @pact_uri - end - - VerificationTask.new(:pact_rake_spec_ignore_failures) do | pact | - pact.uri @pact_uri - pact.ignore_failures = true - end - end - - before do - allow(Pact::TaskHelper).to receive(:execute_pact_verify).and_return(0) - end - - describe '.initialize' do - context 'with an explict pact_helper' do - it 'creates the tasks' do - expect(Rake::Task.tasks).to include_task @task_name - end - end - context 'with no explict pact_helper' do - it 'creates the tasks' do - expect(Rake::Task.tasks).to include_task @task_name_with_explict_pact_helper - end - end - end - - describe 'execute' do - context "with no explicit pact_helper" do - it 'verifies the pacts using the TaskHelper' do - expect(Pact::TaskHelper).to receive(:execute_pact_verify).with(@pact_uri, nil, nil, { ignore_failures: false }) - Rake::Task[@task_name].execute - end - end - - context "with an explict pact_helper" do - let(:verification_config) { [ uri: @pact_uri, pact_helper: @pact_helper] } - it 'verifies the pacts using specified pact_helper' do - expect(Pact::TaskHelper).to receive(:execute_pact_verify).with(@pact_uri, @pact_helper, nil, { ignore_failures: false }) - Rake::Task[@task_name_with_explict_pact_helper].execute - end - end - - context "with ignore_failures: true" do - it 'verifies the pacts with ignore_failures: true' do - expect(Pact::TaskHelper).to receive(:execute_pact_verify).with(@pact_uri, anything, anything, { ignore_failures: true }) - Rake::Task[@task_name_ignore_failures].execute - end - end - - context 'when all specs pass' do - it 'does not raise an exception' do - Rake::Task[@task_name].execute - end - end - end - end -end - -RSpec::Matchers.define :include_task do |expected| - match do |actual| - actual.any? { |task| task.name == expected } - end -end diff --git a/spec/lib/pact/utils/metrics_spec.rb b/spec/lib/pact/utils/metrics_spec.rb deleted file mode 100644 index 488409d2..00000000 --- a/spec/lib/pact/utils/metrics_spec.rb +++ /dev/null @@ -1,58 +0,0 @@ -require "pact/utils/metrics" - -describe Pact::Utils::Metrics do - describe ".report_metric" do - before do - ENV["COMPUTERNAME"] = "test" - ENV["HOSTNAME"] = "test" - stub_request(:post, "https://www.google-analytics.com/collect").to_return(status: 200, body: "", headers: {}) - stub_const("RUBY_PLATFORM", "x86_64-darwin20") - allow(Pact::Utils::Metrics).to receive(:in_thread) { |&block| block.call } - allow(Pact.configuration).to receive(:output_stream).and_return(output_stream) - end - - let(:output_stream) { double("stream").as_null_object } - - subject { Pact::Utils::Metrics.report_metric("Event", "Category", "Action", "Value") } - - context "when do not track is not set" do - let(:expected_event) { { - "v" => 1, - "t" => "event", - "tid" => "UA-117778936-1", - "cid" => "098f6bcd4621d373cade4e832627b4f6", - "an" => "Pact Ruby", - "av" => Pact::VERSION, - "aid" => "pact-ruby", - "aip" => 1, - "ds" => ENV["PACT_EXECUTING_LANGUAGE"] ? "client" : "cli", - "cd2" => ENV["CI"] == "true" ? "CI" : "unknown", - "cd3" => RUBY_PLATFORM, - "cd6" => ENV["PACT_EXECUTING_LANGUAGE"] || "unknown", - "cd7" => ENV["PACT_EXECUTING_LANGUAGE_VERSION"], - "el" => "Event", - "ec" => "Category", - "ea" => "Action", - "ev" => "Value" - } } - - it "sends metrics" do - subject - - expect(WebMock).to have_requested(:post, "https://www.google-analytics.com/collect"). - with(body: Rack::Utils.build_query(expected_event)) - end - end - - context "when do not track is set to true" do - before do - ENV["PACT_DO_NOT_TRACK"] = "true" - end - - it "does not send metrics" do - subject - expect(WebMock).to_not have_requested(:post, "https://www.google-analytics.com/collect") - end - end - end -end diff --git a/spec/lib/provider/base_verifier_spec.rb b/spec/lib/provider/base_verifier_spec.rb new file mode 100644 index 00000000..1c88fb87 --- /dev/null +++ b/spec/lib/provider/base_verifier_spec.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +describe Pact::Provider::BaseVerifier do + subject { described_class.new(Pact::Provider::PactConfig::Base.new(provider_name: 'provider')) } + + let(:build_selectors) { subject.send(:build_consumer_selectors, verify_only, consumer_name, consumer_branch) } + + context 'when verify_only is defined' do + let(:verify_only) { %w[consumer-1 consumer-2] } + + context 'when consumer / branch are defined and matched' do + let(:consumer_name) { 'consumer-1' } + let(:consumer_branch) { '32b53c01' } + + it 'builds proper selectors' do + expect(build_selectors).to eq([{ 'branch' => '32b53c01', 'consumer' => 'consumer-1' }]) + end + end + + context 'when consumer / branch are defined and not matched' do + let(:consumer_name) { 'consumer-3' } + let(:consumer_branch) { 'feature-branch' } + + it 'builds proper selectors' do + expect(build_selectors).to be_empty + end + end + + context 'when consumer is not defined' do + let(:consumer_name) { nil } + let(:consumer_branch) { nil } + + it 'builds proper selectors' do + expect(build_selectors) + .to eq([ + { 'consumer' => 'consumer-1' }, + { 'consumer' => 'consumer-2' } + ]) + end + end + end + + context 'when verify_only is not defined' do + let(:verify_only) { [] } + + context 'when consumer / branch are defined' do + let(:consumer_name) { 'consumer-1' } + let(:consumer_branch) { '32b53c01' } + + it 'builds proper selectors' do + expect(build_selectors).to eq([{ 'branch' => '32b53c01', 'consumer' => 'consumer-1' }]) + end + end + + context 'when only consumer is defined' do + let(:consumer_name) { 'consumer-3' } + let(:consumer_branch) { nil } + + it 'builds proper selectors' do + expect(build_selectors).to eq([{ 'consumer' => 'consumer-3' }]) + end + end + + context 'when consumer is not defined' do + let(:consumer_name) { nil } + let(:consumer_branch) { nil } + + it 'builds proper selectors' do + expect(build_selectors).to eq([{}]) + end + end + end +end diff --git a/spec/lib/provider/gruf_server_spec.rb b/spec/lib/provider/gruf_server_spec.rb new file mode 100644 index 00000000..d7256f4f --- /dev/null +++ b/spec/lib/provider/gruf_server_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +describe Pact::Provider::GrufServer do + let(:api) { ::PetStore::Grpc::PetStore::V1::Pets::Stub.new('localhost:3009', :this_channel_is_insecure) } + let(:call_rpc) do + subject.run { api.pet_by_id(PetStore::Grpc::PetStore::V1::PetByIdRequest.new(id: 1)) } + end + + context 'when success' do + it 'succeeds' do + resp = call_rpc + + expect(resp.pet.id).to eq 1 + expect(resp.pet.name).to eq 'Jack' + end + end +end diff --git a/spec/v2/pact/provider/pact_broker_proxy_runner_spec.rb b/spec/lib/provider/pact_broker_proxy_runner_spec.rb similarity index 52% rename from spec/v2/pact/provider/pact_broker_proxy_runner_spec.rb rename to spec/lib/provider/pact_broker_proxy_runner_spec.rb index ad649514..2715f0bb 100644 --- a/spec/v2/pact/provider/pact_broker_proxy_runner_spec.rb +++ b/spec/lib/provider/pact_broker_proxy_runner_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe Pact::V2::Provider::PactBrokerProxyRunner do +describe Pact::Provider::PactBrokerProxyRunner do let(:http_client) do Faraday.new do |conn| conn.response :json @@ -8,42 +8,43 @@ end end - let(:broker_host) { "https://example.org" } + let(:broker_host) { 'https://example.org' } let(:proxy_host) { server.proxy_url } let(:make_request) { server.run { http_client.get(request_url) } } - context "with pact data request" do - let(:request_url) { "#{proxy_host}/pacts/provider/paas-stand-seeker/consumer/paas-stand-placer/pact-version/2967a9343bd8fdd28a286c4b8322380020618892/metadata/c1tdW2VdPXByb2R1Y3Rpb24mc1tdW2N2XT03MzIy" } + context 'with pact data request' do + let(:request_url) do + "#{proxy_host}/pacts/provider/paas-stand-seeker/consumer/paas-stand-placer/pact-version/2967a9343bd8fdd28a286c4b8322380020618892/metadata/c1tdW2VdPXByb2R1Y3Rpb24mc1tdW2N2XT03MzIy" # rubocop:disable Layout/LineLength + end let(:server) { described_class.new(pact_broker_host: broker_host) } around do |example| - VCR.use_cassette("pact-broker/pact_data") { example.run } + VCR.use_cassette('pact-broker/pact_data') { example.run } end - end - context "with other broker request" do + context 'with other broker request' do let(:server) { described_class.new(pact_broker_host: broker_host) } let(:request_url) { "#{proxy_host}/pacts/provider/paas-stand-seeker/for-verification" } - it "proxies without modification" do - VCR.use_cassette "pact-broker/for_verification" do + it 'proxies without modification' do + VCR.use_cassette 'pact-broker/for_verification' do response = make_request expect(response.status).to eq(200) - expect(response.headers["content-length"]).to eq("2817") + expect(response.headers['content-length']).to eq('2817') end end end - context "with broker error" do + context 'with broker error' do let(:server) { described_class.new(pact_broker_host: broker_host) } let(:request_url) { "#{proxy_host}/pacts/provider/non-existent-provider/for-verification" } - it "proxies without modification" do - VCR.use_cassette "pact-broker/not_found" do + it 'proxies without modification' do + VCR.use_cassette 'pact-broker/not_found' do response = make_request expect(response.status).to eq(404) - expect(response.body).to eq("error" => "No provider with name 'non-existent-provider' found") + expect(response.body).to eq('error' => "No provider with name 'non-existent-provider' found") end end end diff --git a/spec/lib/provider/provider_server_runner_spec.rb b/spec/lib/provider/provider_server_runner_spec.rb new file mode 100644 index 00000000..827f3656 --- /dev/null +++ b/spec/lib/provider/provider_server_runner_spec.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +describe Pact::Provider::ProviderServerRunner do + let(:http_client) do + Faraday.new do |conn| + conn.response :json + conn.request :json + end + end + + let(:make_request) do + server.run { http_client.post('http://localhost:9001/setup-provider', request_body) } + end + + let(:server) do + subject.tap do |s| + s.add_setup_state('state1') {} + s.add_teardown_state('state1') {} + end + end + + context 'with setup callback' do + let(:request_body) do + { 'action' => 'setup', 'params' => { 'param1' => 'value1' }, 'state' => 'state1' } + end + + it 'succeeds' do + expect_any_instance_of(Pact::Provider::ProviderStateServlet).to receive(:call_setup).and_call_original + + response = make_request + expect(response.status).to eq(200) + end + end + + context 'with teardown callback' do + let(:request_body) do + { 'action' => 'teardown', 'params' => { 'param1' => 'value1' }, 'state' => 'state1' } + end + + it 'succeeds' do + expect_any_instance_of(Pact::Provider::ProviderStateServlet).to receive(:call_teardown).and_call_original + + response = make_request + expect(response.status).to eq(200) + end + end + + context 'with unknown state callback' do + let(:request_body) do + { 'action' => 'unknown', 'params' => { 'param1' => 'value1' }, 'state' => 'state1' } + end + + it 'succeeds' do + expect_any_instance_of(Pact::Provider::ProviderStateServlet).not_to receive(:call_setup) + expect_any_instance_of(Pact::Provider::ProviderStateServlet).not_to receive(:call_teardown) + + response = make_request + expect(response.status).to eq(200) + end + end + + context 'with unknown data' do + let(:request_body) { 'non-json data' } + + it 'fails' do + response = make_request + expect(response.status).to eq(500) + end + end +end diff --git a/spec/pact/consumers/kafka_spec.rb b/spec/pact/consumers/kafka_spec.rb index 55c879b9..6fc78639 100644 --- a/spec/pact/consumers/kafka_spec.rb +++ b/spec/pact/consumers/kafka_spec.rb @@ -1,21 +1,21 @@ # frozen_string_literal: true -require "pact/v2/rspec" +require 'pact/rspec' -RSpec.describe "Pact::V2::Consumers::Kafka", :pact_v2, skip_windows: true do - message_pact_provider "pact-v2-test-app-kafka", opts: { - pact_dir: File.expand_path('../../pacts', __dir__), - message_handlers: { - "pet message as json" => proc do |provider_state| - pet_id = provider_state.dig("params", "pet_id") - with_pact_producer { |client| PetJsonProducer.new(client: client).call(pet_id) } - end, - "pet message as proto" => proc do |provider_state| - pet_id = provider_state.dig("params", "pet_id") - with_pact_producer { |client| PetProtoProducer.new(client: client).call(pet_id) } - end - } +RSpec.describe 'PactConsumers::Kafka', :pact, skip_windows: true do + message_pact_provider 'pact-test-app-kafka', opts: { + pact_dir: File.expand_path('../../pacts', __dir__), + message_handlers: { + 'pet message as json' => proc do |provider_state| + pet_id = provider_state.dig('params', 'pet_id') + with_pact_producer { |client| PetJsonProducer.new(client: client).call(pet_id) } + end, + 'pet message as proto' => proc do |provider_state| + pet_id = provider_state.dig('params', 'pet_id') + with_pact_producer { |client| PetProtoProducer.new(client: client).call(pet_id) } + end } + } # handle_message "pet message as json" do |provider_state| # pet_id = provider_state.dig("params", "pet_id") @@ -26,5 +26,4 @@ # pet_id = provider_state.dig("params", "pet_id") # with_pact_producer { |client| PetProtoProducer.new(client: client).call(pet_id) } # end - end diff --git a/spec/pact/consumers/message_spec.rb b/spec/pact/consumers/message_spec.rb index 9ac10c47..7dd98b45 100644 --- a/spec/pact/consumers/message_spec.rb +++ b/spec/pact/consumers/message_spec.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true -require 'pact/v2' -require 'pact/v2/rspec' +require 'pact' +require 'pact/rspec' require_relative '../../internal/app/producers/test_message_producer' -RSpec.describe 'Test Message Provider', :pact_v2 do +RSpec.describe 'Test Message Provider', :pact do message_pact_provider 'Test Message Producer', opts: { - pact_dir: File.expand_path('../../pacts', __dir__), + pact_dir: File.expand_path('../../pacts', __dir__) } handle_message 'a customer created message' do |provider_state| diff --git a/spec/pact/consumers/multi_spec.rb b/spec/pact/consumers/multi_spec.rb index 17848b8a..bf792653 100644 --- a/spec/pact/consumers/multi_spec.rb +++ b/spec/pact/consumers/multi_spec.rb @@ -1,17 +1,16 @@ # frozen_string_literal: true -require "pact/v2/rspec" +require 'pact/rspec' -RSpec.describe "Pact::V2::Consumers::Http", :pact_v2 do - mixed_pact_provider "pact-v2-test-app", opts: { +RSpec.describe 'PactConsumers::Http', :pact do + mixed_pact_provider 'pact-test-app', opts: { http: { http_port: 3000, log_level: :info, - pact_dir: File.expand_path('../../pacts', __dir__), + pact_dir: File.expand_path('../../pacts', __dir__) }, grpc: { grpc_port: 3009 } } - end diff --git a/spec/pact/providers/pact-ruby-v2-test-app/grpc_client_spec.rb b/spec/pact/providers/pact-ruby-v2-test-app/grpc_client_spec.rb index d2529e3b..031f7c46 100644 --- a/spec/pact/providers/pact-ruby-v2-test-app/grpc_client_spec.rb +++ b/spec/pact/providers/pact-ruby-v2-test-app/grpc_client_spec.rb @@ -1,26 +1,25 @@ # frozen_string_literal: true -require "pact/v2/rspec" +require 'pact/rspec' - -RSpec.describe "Pact::V2::Providers::Test::GrpcClient", :pact_v2 do - has_grpc_pact_between "pact-ruby-v2-test-app", "pact-ruby-v2-test-app" +RSpec.describe 'PactProviders::Test::GrpcClient', :pact do + has_grpc_pact_between 'pact-ruby-test-app', 'pact-ruby-test-app' let(:pet_id) { 123 } - let(:api) { ::PetStore::Grpc::PetStore::V1::Pets::Stub.new("localhost:3009", :this_channel_is_insecure) } + let(:api) { ::PetStore::Grpc::PetStore::V1::Pets::Stub.new('localhost:3009', :this_channel_is_insecure) } let(:make_request) { api.pet_by_id(PetStore::Grpc::PetStore::V1::PetByIdRequest.new(id: pet_id)) } let(:interaction) do new_interaction - .with_service("spec/internal/deps/services/pet_store/grpc/pet_store.proto", "Pets/PetById") + .with_service('spec/internal/deps/services/pet_store/grpc/pet_store.proto', 'Pets/PetById') end - context "with Pets/PetById" do - context "with successful interaction" do + context 'with Pets/PetById' do + context 'with successful interaction' do let(:interaction) do super() - .given("pet exists", pet_id: pet_id) + .given('pet exists', pet_id: pet_id) .with_request(id: match_any_integer(pet_id)) .will_respond_with( pet: { @@ -29,7 +28,7 @@ ) end - it "executes the pact test without errors" do + it 'executes the pact test without errors' do interaction.execute do expect { make_request }.not_to raise_error end diff --git a/spec/pact/providers/pact-ruby-v2-test-app/http_client_spec.rb b/spec/pact/providers/pact-ruby-v2-test-app/http_client_spec.rb index 966e0c5c..6a348e3b 100644 --- a/spec/pact/providers/pact-ruby-v2-test-app/http_client_spec.rb +++ b/spec/pact/providers/pact-ruby-v2-test-app/http_client_spec.rb @@ -1,14 +1,14 @@ # frozen_string_literal: true -require "pact/v2/rspec" +require 'pact/rspec' -RSpec.describe "Pact::V2::Providers::Test::HttpClient", :pact_v2 do - has_http_pact_between "pact-ruby-v2-test-app", "pact-ruby-v2-test-app", opts: { +RSpec.describe 'PactProviders::Test::HttpClient', :pact do + has_http_pact_between 'pact-ruby-test-app', 'pact-ruby-test-app', opts: { mock_port: 3000 } let(:pet_id) { 123 } - let(:host) { "localhost:3000" } + let(:host) { 'localhost:3000' } let(:interaction) { new_interaction } let(:http_client) do Faraday.new do |conn| @@ -17,27 +17,27 @@ end end - context "with GET /pets/:id" do + context 'with GET /pets/:id' do let(:make_request) do http_client.get("http://#{host}/pets/#{pet_id}") end - context "with successful interaction" do + context 'with successful interaction' do let(:interaction) do super() - .given("pet exists", pet_id: pet_id) - .upon_receiving("getting a pet") + .given('pet exists', pet_id: pet_id) + .upon_receiving('getting a pet') .with_request(method: :get, path: "/pets/#{pet_id}") .will_respond_with(status: 200, body: { - pet: { - id: match_any_integer(pet_id), - bark: match_any_boolean(true), - breed: match_any_string("Husky") - } - }) + pet: { + id: match_any_integer(pet_id), + bark: match_any_boolean(true), + breed: match_any_string('Husky') + } + }) end - it "executes the pact test without errors" do + it 'executes the pact test without errors' do interaction.execute do expect(make_request).to be_success end @@ -45,33 +45,33 @@ end end - context "with PATCH /pets" do + context 'with PATCH /pets' do let(:make_request) do http_client.patch("http://#{host}/pets/#{pet_id}", pet_data.to_json, - {"Authorization" => "some-token"}) + { 'Authorization' => 'some-token' }) end - let(:pet_data) { {breed: "Shepherd"} } + let(:pet_data) { { breed: 'Shepherd' } } - context "with successful interaction" do + context 'with successful interaction' do let(:interaction) do super() - .given("pet exists", pet_id: pet_id) - .upon_receiving("updating a pet") + .given('pet exists', pet_id: pet_id) + .upon_receiving('updating a pet') .with_request(method: :patch, path: "/pets/#{pet_id}", - headers: {Authorization: match_any_string("some-token")}, - body: pet_data) + headers: { Authorization: match_any_string('some-token') }, + body: pet_data) .will_respond_with(status: 200, - headers: {TRACE_ID: match_any_string("xxx-xxx")}, - body: { - pet: { - id: match_any_integer(pet_id), - bark: match_any_boolean(true), - breed: match_any_string("Shepherd") - } - }) + headers: { TRACE_ID: match_any_string('xxx-xxx') }, + body: { + pet: { + id: match_any_integer(pet_id), + bark: match_any_boolean(true), + breed: match_any_string('Shepherd') + } + }) end - it "executes the pact test without errors" do + it 'executes the pact test without errors' do interaction.execute do expect(make_request).to be_success end diff --git a/spec/pact/providers/pact-ruby-v2-test-app/kafka_spec.rb b/spec/pact/providers/pact-ruby-v2-test-app/kafka_spec.rb index e13af3fc..5703b1f4 100644 --- a/spec/pact/providers/pact-ruby-v2-test-app/kafka_spec.rb +++ b/spec/pact/providers/pact-ruby-v2-test-app/kafka_spec.rb @@ -1,59 +1,59 @@ # frozen_string_literal: true -require "pact/v2/rspec" +require 'pact/rspec' -RSpec.describe "Pact::V2::Providers::Test::Kafka", :pact_v2, skip_windows: true do - has_message_pact_between "pact-ruby-v2-test-app", "pact-ruby-v2-test-app-kafka" +RSpec.describe 'PactProviders::Test::Kafka', :pact, skip_windows: true do + has_message_pact_between 'pact-ruby-test-app', 'pact-ruby-test-app-kafka' let(:karafka_message) { Struct.new(:payload, keyword_init: true) } let(:interaction) do new_interaction - .given("pet exists", pet_id: 1) + .given('pet exists', pet_id: 1) .with_headers( - "identity-key" => match_any_string("some-key") + 'identity-key' => match_any_string('some-key') ) .with_metadata( - topic: match_regex(/.+/, "some-topic"), - key: match_any_string("key") + topic: match_regex(/.+/, 'some-topic'), + key: match_any_string('key') ) end - context "with json message payload" do + context 'with json message payload' do let(:consumer) { PetJsonConsumer.consumer_klass } let(:interaction) do super() - .upon_receiving("pet message as json") + .upon_receiving('pet message as json') .with_json_contents( id: match_any_integer(1), tags: match_each_regex(/\w+/, %w[tagX tagY]), colors: match_each_kv( { - "red" => { - description: match_any_string("description"), - link: match_any_string("http://some-site.ru"), + 'red' => { + description: match_any_string('description'), + link: match_any_string('http://some-site.ru'), relatesTo: match_each_regex(/(red|green|blue)/, %w[blue]), - title: match_any_string("title") + title: match_any_string('title') } }, - match_regex(/(red|green|blue)/, "red") + match_regex(/(red|green|blue)/, 'red') ) ) end - it "executes the pact test without errors" do + it 'executes the pact test without errors' do interaction.execute do |json_payload, meta| message = karafka_message.new(payload: json_payload) expect(Rails.logger).to receive(:info) expect(meta).to eq( { - "contentType" => "application/json", - "headers" => { - "identity-key" => "some-key" + 'contentType' => 'application/json', + 'headers' => { + 'identity-key' => 'some-key' }, - "key" => "key", - "topic" => "some-topic" + 'key' => 'key', + 'topic' => 'some-topic' } ) @@ -62,28 +62,28 @@ end end - context "with proto message payload" do + context 'with proto message payload' do let(:consumer) { PetProtoConsumer.consumer_klass } let(:interaction) do super() - .upon_receiving("pet message as proto") - .with_proto_class("spec/internal/deps/services/pet_store/grpc/pet_store.proto", "Pet") + .upon_receiving('pet message as proto') + .with_proto_class('spec/internal/deps/services/pet_store/grpc/pet_store.proto', 'Pet') .with_proto_contents( id: match_any_integer(1), - name: match_any_string("some pet"), - tags: match_each_regex(/\w+/, "tagX"), + name: match_any_string('some pet'), + tags: match_each_regex(/\w+/, 'tagX'), colors: match_each( { - description: match_any_string("description"), - link: match_any_string("http://some-site.ru"), - relates_to: match_each_regex(/(red|green|blue)/, "blue"), - color: match_regex(/(RED|GREEN|BLUE)/, "RED") + description: match_any_string('description'), + link: match_any_string('http://some-site.ru'), + relates_to: match_each_regex(/(red|green|blue)/, 'blue'), + color: match_regex(/(RED|GREEN|BLUE)/, 'RED') } ) ) end - it "executes the pact test without errors" do + it 'executes the pact test without errors' do interaction.execute do |proto_payload, meta| deserialized = PetStore::Grpc::PetStore::V1::Pet.decode(proto_payload) message = karafka_message.new(payload: deserialized) @@ -91,12 +91,12 @@ expect(Rails.logger).to receive(:info) expect(meta).to eq( { - "contentType" => "application/protobuf;message=.pet_store.v1.Pet", - "headers" => { - "identity-key" => "some-key" + 'contentType' => 'application/protobuf;message=.pet_store.v1.Pet', + 'headers' => { + 'identity-key' => 'some-key' }, - "key" => "key", - "topic" => "some-topic" + 'key' => 'key', + 'topic' => 'some-topic' } ) diff --git a/spec/pact/providers/pact-ruby-v2-test-app/message_spec.rb b/spec/pact/providers/pact-ruby-v2-test-app/message_spec.rb index 450b163b..a1d93de7 100644 --- a/spec/pact/providers/pact-ruby-v2-test-app/message_spec.rb +++ b/spec/pact/providers/pact-ruby-v2-test-app/message_spec.rb @@ -1,8 +1,8 @@ -require 'pact/v2' -require 'pact/v2/rspec' +require 'pact' +require 'pact/rspec' require_relative '../../../internal/app/consumers/test_message_consumer' -describe TestMessageConsumer, :pact_v2 do +describe TestMessageConsumer, :pact do has_message_pact_between 'Test Message Consumer', 'Test Message Provider' subject(:consumer) { TestMessageConsumer.new } diff --git a/spec/pact/providers/pact-ruby-v2-test-app/plugin_grpc_sync_message_spec.rb b/spec/pact/providers/pact-ruby-v2-test-app/plugin_grpc_sync_message_spec.rb index 32b65f20..74e7b539 100644 --- a/spec/pact/providers/pact-ruby-v2-test-app/plugin_grpc_sync_message_spec.rb +++ b/spec/pact/providers/pact-ruby-v2-test-app/plugin_grpc_sync_message_spec.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true -require 'pact/v2/rspec' +require 'pact/rspec' -RSpec.describe 'Test grpc sync message plugin loading', :pact_v2 do - has_plugin_sync_message_pact_between 'pact-ruby-v2-test-app', 'pact-ruby-v2-test-app', opts: { mock_port: 3009 } +RSpec.describe 'Test grpc sync message plugin loading', :pact do + has_plugin_sync_message_pact_between 'pact-ruby-test-app', 'pact-ruby-test-app', opts: { mock_port: 3009 } let(:pet_id) { 123 } @@ -23,7 +23,10 @@ .with_content_type('application/grpc') .with_transport('grpc') .with_plugin_metadata({ - 'pact:proto' => File.expand_path('spec/internal/deps/services/pet_store/grpc/pet_store.proto'), + 'pact:proto' => + File.expand_path( + 'spec/internal/deps/services/pet_store/grpc/pet_store.proto' + ), 'pact:proto-service' => 'Pets/PetById', 'pact:content-type' => 'application/protobuf' }) diff --git a/spec/pact/providers/pact-ruby-v2-test-app/plugin_matt_async_message_spec.rb b/spec/pact/providers/pact-ruby-v2-test-app/plugin_matt_async_message_spec.rb index 54940fd4..63e2b70a 100644 --- a/spec/pact/providers/pact-ruby-v2-test-app/plugin_matt_async_message_spec.rb +++ b/spec/pact/providers/pact-ruby-v2-test-app/plugin_matt_async_message_spec.rb @@ -1,32 +1,32 @@ # frozen_string_literal: true -require "pact/v2/rspec" +require 'pact/rspec' -RSpec.describe 'Test matt plugin sync message loading', :pact_v2 do - has_plugin_async_message_pact_between "matttcpconsumer", "matttcpprovider" +RSpec.describe 'Test matt plugin sync message loading', :pact do + has_plugin_async_message_pact_between 'matttcpconsumer', 'matttcpprovider' let(:matt_message) do { - "response" => { "body" => "tcpworld" } + 'response' => { 'body' => 'tcpworld' } } end let(:interaction) do new_interaction - .given("the world exists") - .with_plugin("matt", "0.1.1") - .with_content_type("application/matt") - .with_transport("matt") + .given('the world exists') + .with_plugin('matt', '0.1.1') + .with_content_type('application/matt') + .with_transport('matt') .with_contents(matt_message) end - it "executes the matt plugin pact test without errors" do + it 'executes the matt plugin pact test without errors' do interaction.execute do |transport| # Here you would call your matt TCP service using the transport info. # For demonstration, we'll just check the response body. # Replace the following with actual TCP call if needed. - response = matt_message["response"]["body"] - expect(response).to eq("tcpworld") + response = matt_message['response']['body'] + expect(response).to eq('tcpworld') end end end diff --git a/spec/pact/providers/pact-ruby-v2-test-app/plugin_matt_http_spec.rb b/spec/pact/providers/pact-ruby-v2-test-app/plugin_matt_http_spec.rb index 4afd691c..1a523ba5 100644 --- a/spec/pact/providers/pact-ruby-v2-test-app/plugin_matt_http_spec.rb +++ b/spec/pact/providers/pact-ruby-v2-test-app/plugin_matt_http_spec.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true -require 'pact/v2/rspec' +require 'pact/rspec' require 'net/http' require 'json' require 'faraday' -RSpec.describe 'HTTP transport', :pact_v2 do +RSpec.describe 'HTTP transport', :pact do has_plugin_http_pact_between 'myconsumer', 'myprovider' let(:matt_request) { { 'request' => { 'body' => 'hello' } } } @@ -15,8 +15,17 @@ .given('the Matt protocol is up') .upon_receiving('an HTTP request to /matt') .with_plugin('matt', '0.1.1') - .with_request(method: 'POST', path: '/matt', body: matt_request, headers: { 'content-type' => 'application/matt' }) - .will_respond_with(status: 200, body: matt_response, headers: { 'content-type' => 'application/matt' }) + .with_request( + method: 'POST', + path: '/matt', + body: matt_request, + headers: { 'content-type' => 'application/matt' } + ) + .will_respond_with( + status: 200, + body: matt_response, + headers: { 'content-type' => 'application/matt' } + ) end it 'returns a valid MATT message' do diff --git a/spec/pact/providers/pact-ruby-v2-test-app/plugin_matt_sync_message_spec.rb b/spec/pact/providers/pact-ruby-v2-test-app/plugin_matt_sync_message_spec.rb index 2ac55319..892909a5 100644 --- a/spec/pact/providers/pact-ruby-v2-test-app/plugin_matt_sync_message_spec.rb +++ b/spec/pact/providers/pact-ruby-v2-test-app/plugin_matt_sync_message_spec.rb @@ -1,33 +1,33 @@ # frozen_string_literal: true -require "pact/v2/rspec" +require 'pact/rspec' -RSpec.describe 'Test matt plugin sync message loading', :pact_v2 do - has_plugin_sync_message_pact_between "myconsumer", "myprovider" +RSpec.describe 'Test matt plugin sync message loading', :pact do + has_plugin_sync_message_pact_between 'myconsumer', 'myprovider' let(:matt_message) do { - "request" => { "body" => "hellotcp" }, - "response" => { "body" => "tcpworld" } + 'request' => { 'body' => 'hellotcp' }, + 'response' => { 'body' => 'tcpworld' } } end let(:interaction) do - new_interaction("a MATT message") - .given("the world exists") - .with_plugin("matt", "0.1.1") - .with_content_type("application/matt") - .with_transport("matt") - .with_request(matt_message["request"]) - .will_respond_with(matt_message["response"]) + new_interaction('a MATT message') + .given('the world exists') + .with_plugin('matt', '0.1.1') + .with_content_type('application/matt') + .with_transport('matt') + .with_request(matt_message['request']) + .will_respond_with(matt_message['response']) end - it "returns a valid MATT message" do + it 'returns a valid MATT message' do interaction.execute do |transport| # Replace this with your actual TCP call if needed # For demonstration, we'll just check the response body. - response = matt_message["response"]["body"] - expect(response).to eq("tcpworld") + response = matt_message['response']['body'] + expect(response).to eq('tcpworld') end end end diff --git a/spec/pact_specification/compliance-1.0.0.rb b/spec/pact_specification/compliance-1.0.0.rb deleted file mode 100644 index f7af4f6c..00000000 --- a/spec/pact_specification/compliance-1.0.0.rb +++ /dev/null @@ -1,47 +0,0 @@ -require 'spec_helper' -require 'pact/consumer/request' -require 'pact/consumer_contract/request' - -PACT_SPEC_DIR = "../pact-specification/testcases" -REQUEST_TEST_CASE_FOLDERS = Dir.glob("#{PACT_SPEC_DIR}/request/**") -REQUEST_TEST_CASE_FILES = Dir.glob("#{PACT_SPEC_DIR}/request/**/*.json") - -TEST_DESCRIPTIONS = {true => "matches", false => "does not match"} - -describe "Pact gem complicance with Pact Specification 1.0.0" do - - directories = Dir.glob("#{PACT_SPEC_DIR}/*") - - directories.each do | dir_name | - - describe File.basename(dir_name) do - - sub_directories = Dir.glob("#{dir_name}/*") - - sub_directories.each do | sub_dir_name | - - context File.basename(sub_dir_name) do - testcases = Dir.glob("#{sub_dir_name}/**/*.json") - - testcases.each do | file_name | - - context File.basename(file_name).chomp(".json") do - - file_content = JSON.parse(File.read(file_name)) - expected = Pact::Request::Expected.from_hash(file_content["expected"]) - actual = Pact::Consumer::Request::Actual.from_hash(file_content["actual"]) - expected_result = file_content.fetch("match") - comment = file_content["comment"] - - it "#{TEST_DESCRIPTIONS[expected_result]} - #{comment}" do - expect(expected.matches?(actual)).to eq expected_result - end - - end - - end - end - end - end - end -end diff --git a/spec/rails_helper_v2.rb b/spec/rails_helper.rb similarity index 69% rename from spec/rails_helper_v2.rb rename to spec/rails_helper.rb index 4d5b3a46..bf81073c 100644 --- a/spec/rails_helper_v2.rb +++ b/spec/rails_helper.rb @@ -1,34 +1,34 @@ # frozen_string_literal: true -ENV["RAILS_ENV"] = "test" +ENV['RAILS_ENV'] = 'test' # Engine root is used by rails_configuration to correctly # load fixtures and support files -require "pathname" -ENGINE_ROOT = Pathname.new(File.expand_path("..", __dir__)) +require 'pathname' +ENGINE_ROOT = Pathname.new(File.expand_path(__dir__)) puts "Loading Rails environment for tests from #{ENGINE_ROOT}" -require "webmock" -require "vcr" -require "faraday" -require "gruf" -require "gruf/rspec" +require 'webmock' +require 'vcr' +require 'faraday' +require 'gruf' +require 'gruf/rspec' # require "yabeda" # we have to require it becase of this https://github.com/yabeda-rb/yabeda/pull/38 -require "combustion" +require 'combustion' puts "Rails root: #{Rails.root}" begin Combustion.initialize! :action_controller do - config.log_level = :fatal if ENV["LOG"].to_s.empty? + config.log_level = :fatal if ENV['LOG'].to_s.empty? end -rescue => e +rescue StandardError => e # Fail fast if application couldn't be loaded warn "💥 Failed to load the app: #{e.message}\n#{e.backtrace.join("\n")}" exit(1) end -require "rspec/rails" +require 'rspec/rails' puts "Rails root: #{Rails.root}" # Add additional requires below this line. Rails is not loaded until this point! @@ -37,13 +37,13 @@ # Optional dependencies unless RUBY_PLATFORM =~ /win32|x64-mingw32|x64-mingw-ucrt/ - require "sbmt/kafka_consumer" - require "sbmt/kafka_producer" + require 'sbmt/kafka_consumer' + require 'sbmt/kafka_producer' end # Monkey patch Gruf::Server to remove QUIT from KILL_SIGNALS for windows compatibility if Gem.win_platform? - warn "[⚠️] Windows platform detected, monkey patching Gruf::Server to remove QUIT from KILL_SIGNALS" + warn '[⚠️] Windows platform detected, monkey patching Gruf::Server to remove QUIT from KILL_SIGNALS' module Gruf class Server remove_const(:KILL_SIGNALS) if const_defined?(:KILL_SIGNALS) diff --git a/spec/service_providers/helper.rb b/spec/service_providers/helper.rb deleted file mode 100644 index 7511bf57..00000000 --- a/spec/service_providers/helper.rb +++ /dev/null @@ -1,10 +0,0 @@ -require 'pact/consumer/rspec' - -Pact.service_consumer 'Pact Ruby' do - has_pact_with 'Pact Broker' do - mock_service :pact_broker do - port 8888 - pact_specification_version '2.0.0' - end - end -end diff --git a/spec/service_providers/pact_ruby_fetch_pacts_for_verification_test.rb b/spec/service_providers/pact_ruby_fetch_pacts_for_verification_test.rb deleted file mode 100644 index 6260dce9..00000000 --- a/spec/service_providers/pact_ruby_fetch_pacts_for_verification_test.rb +++ /dev/null @@ -1,114 +0,0 @@ -require_relative 'helper' -require 'pact/pact_broker/fetch_pact_uris_for_verification' - -describe Pact::PactBroker::FetchPactURIsForVerification, pact: true do - before do - allow($stdout).to receive(:puts) - end - - let(:get_headers) { { "Accept" => 'application/hal+json' } } - let(:post_headers) do - { - "Accept" => 'application/hal+json', - "Content-Type" => "application/json" - } - end - let(:pacts_for_verification_relation) { Pact::PactBroker::FetchPactURIsForVerification::PACTS_FOR_VERIFICATION_RELATION } - let(:body) do - { - "providerVersionBranch" => "main", - "providerVersionTags" => ["pdev"], - "consumerVersionSelectors" => [{ "tag" => "cdev", "latest" => true}], - "includePendingStatus" => true - } - end - let(:provider_version_branch) { "main" } - let(:provider_version_tags) { %w[pdev] } - let(:consumer_version_selectors) { [ { tag: "cdev", latest: true }] } - let(:options) { { include_pending_status: true }} - - subject { Pact::PactBroker::FetchPactURIsForVerification.call(provider, consumer_version_selectors, provider_version_branch, provider_version_tags, broker_base_url, basic_auth_options, options) } - - describe 'fetch pacts' do - let(:provider) { 'Bar' } - let(:broker_base_url) { pact_broker.mock_service_base_url} - let(:basic_auth_options) { { username: 'username', password: 'password' } } - - before do - pact_broker - .given('the relation for retrieving pacts for verifications exists in the index resource') - .upon_receiving('a request for the index resource') - .with( - method: :get, - path: '/', - headers: get_headers - ) - .will_respond_with( - status: 200, - headers: { "Content-Type" => Pact.term("application/hal+json", /hal/) }, - body: { - _links: { - pacts_for_verification_relation => { - href: Pact.term( - generate: broker_base_url + '/pacts/provider/{provider}/for-verification', - matcher: %r{/pacts/provider/{provider}/for-verification$} - ) - } - } - } - ) - end - - context 'retrieving pacts for verification by provider' do - before do - pact_broker - .given('Foo has a pact tagged cdev with provider Bar') - .upon_receiving('a request to retrieve the pacts for verification for a provider') - .with( - method: :post, - path: '/pacts/provider/Bar/for-verification', - body: body, - headers: post_headers - ) - .will_respond_with( - status: 200, - headers: { "Content-Type" => Pact.term("application/hal+json", /hal/) }, - body: { - "_embedded" => { - "pacts" => [{ - "shortDescription" => "a description", - "verificationProperties" => { - "pending" => Pact.like(true), - "notices" => Pact.each_like("text" => "some text") - }, - '_links' => { - "self" => { - "href" => Pact.term('http://pact-broker-url-for-foo', %r{http://.*}) - } - } - }] - } - } - ) - end - - let(:expected_metadata) do - { - pending: true, - notices: [ - text: "some text" - ], - short_description: "a description" - } - end - - it 'returns the array of pact urls' do - expect(subject).to eq( - [ - Pact::Provider::PactURI.new('http://pact-broker-url-for-foo', basic_auth_options, expected_metadata) - ] - ) - end - end - end -end diff --git a/spec/service_providers/pact_ruby_fetch_pacts_test.rb b/spec/service_providers/pact_ruby_fetch_pacts_test.rb deleted file mode 100644 index 9379eecb..00000000 --- a/spec/service_providers/pact_ruby_fetch_pacts_test.rb +++ /dev/null @@ -1,383 +0,0 @@ -require_relative 'helper' -require 'pact/pact_broker/fetch_pacts' - - -describe Pact::PactBroker::FetchPacts, pact: true do - - before do - allow($stdout).to receive(:puts) - end - - let(:get_headers) { { Accept: 'application/hal+json' } } - - describe 'fetch pacts' do - let(:provider) { 'provider-1' } - let(:broker_base_url) { pact_broker.mock_service_base_url + '/' } - let(:basic_auth_options) { { username: 'foo', password: 'bar' } } - - before do - pact_broker - .given('the relations for retrieving pacts exist in the index resource') - .upon_receiving('a request for the index resource') - .with( - method: :get, - path: '/', - headers: get_headers - ) - .will_respond_with( - status: 200, - headers: { - 'Content-Type' => Pact.term('application/hal+json', /json/) - }, - body: { - _links: { - 'pb:latest-provider-pacts' => { - href: Pact.term( - generate: broker_base_url + 'pacts/provider/{provider}/latest', - matcher: %r{/pacts/provider/{provider}/latest$} - ) - }, - 'pb:latest-provider-pacts-with-tag' => { - href: Pact.term( - generate: broker_base_url + 'pacts/provider/{provider}/latest/{tag}', - matcher: %r{/pacts/provider/{provider}/latest/{tag}$} - ) - }, - :'pb:provider-pacts-with-tag' => { - href: Pact.term( - generate: broker_base_url + 'pacts/provider/{provider}/tag/{tag}', - matcher: %r{/pacts/provider/{provider}/tag/{tag}$} - ) - } - } - } - ) - end - - context 'retrieving latest pacts by provider' do - let(:tags) { nil } - - before do - pact_broker - .given('consumer-1 and consumer-2 have pacts with provider provider-1') - .upon_receiving('a request to retrieve the latest pacts for provider') - .with( - method: :get, - path: '/pacts/provider/provider-1/latest', - headers: get_headers - ) - .will_respond_with( - status: 200, - headers: { - 'Content-Type' => Pact.term('application/hal+json', /json/) - }, - body: { - _links: { - 'pb:pacts' => [ - { - href: Pact.term('http://pact-broker-url-for-consumer-1', %r{http://.*}) - }, - { - href: Pact.term('http://pact-broker-url-for-consumer-2', %r{http://.*}) - } - ] - } - } - ) - end - - it 'returns the array of pact urls' do - pacts = Pact::PactBroker::FetchPacts.call(provider, tags, broker_base_url, basic_auth_options) - expect(pacts).to eq( - [ - Pact::Provider::PactURI.new('http://pact-broker-url-for-consumer-1', basic_auth_options), - Pact::Provider::PactURI.new('http://pact-broker-url-for-consumer-2', basic_auth_options) - ] - ) - end - end - - context 'retrieving latest pacts by provider with the specified tag' do - let(:tags) { ['tag-1', { name: 'tag-2', all: false }] } - - before do - pact_broker - .given('consumer-1 and consumer-2 have pacts with provider provider-1 tagged with tag-1') - .upon_receiving('a request to retrieve the latest tagged (tag-1) pacts for provider') - .with( - method: :get, - path: '/pacts/provider/provider-1/latest/tag-1', - headers: get_headers - ) - .will_respond_with( - status: 200, - headers: { - 'Content-Type' => Pact.term('application/hal+json', /json/) - }, - body: { - _links: { - 'pb:pacts' => [ - { - href: Pact.term('http://pact-broker-url-for-consumer-1-tag-1', %r{http://.*}) - }, - { - href: Pact.term('http://pact-broker-url-for-consumer-2-tag-1', %r{http://.*}) - } - ] - } - } - ) - pact_broker - .given('consumer-1 and consumer-2 have pacts with provider provider-1 tagged with tag-2') - .upon_receiving('a request to retrieve the latest tagged (tag-2) pacts for provider') - .with( - method: :get, - path: '/pacts/provider/provider-1/latest/tag-2', - headers: get_headers - ) - .will_respond_with( - status: 200, - headers: { - 'Content-Type' => Pact.term('application/hal+json', /json/) - }, - body: { - _links: { - 'pb:pacts' => [ - { - href: Pact.term('http://pact-broker-url-for-consumer-1-tag-2', %r{http://.*}) - }, - { - href: Pact.term('http://pact-broker-url-for-consumer-2-tag-2', %r{http://.*}) - } - ] - } - } - ) - end - - it 'returns the array of pact urls' do - pacts = Pact::PactBroker::FetchPacts.call(provider, tags, broker_base_url, basic_auth_options) - - expect(pacts).to eq( - [ - Pact::Provider::PactURI.new('http://pact-broker-url-for-consumer-1-tag-1', basic_auth_options), - Pact::Provider::PactURI.new('http://pact-broker-url-for-consumer-2-tag-1', basic_auth_options), - Pact::Provider::PactURI.new('http://pact-broker-url-for-consumer-1-tag-2', basic_auth_options), - Pact::Provider::PactURI.new('http://pact-broker-url-for-consumer-2-tag-2', basic_auth_options) - ] - ) - end - end - - context 'retrieving latest pacts by provider with the fallback tag' do - let(:tags) { [{ name: 'tag-1', all: false, fallback: 'master' }] } - - before do - pact_broker - .given('consumer-1 and consumer-2 have no pacts with provider provider-1 tagged with tag-1') - .upon_receiving('a request to retrieve the latest tagged (tag-1) pacts for provider') - .with( - method: :get, - path: '/pacts/provider/provider-1/latest/tag-1', - headers: get_headers - ) - .will_respond_with( - status: 200, - headers: { - 'Content-Type' => Pact.term('application/hal+json', /json/) - }, - body: { - _links: { - 'pb:pacts' => [] - } - } - ) - pact_broker - .given('consumer-1 and consumer-2 have pacts with provider provider-1 tagged with master') - .upon_receiving('a request to retrieve the latest tagged (master) pacts for provider') - .with( - method: :get, - path: '/pacts/provider/provider-1/latest/master', - headers: get_headers - ) - .will_respond_with( - status: 200, - headers: { - 'Content-Type' => Pact.term('application/hal+json', /json/) - }, - body: { - _links: { - 'pb:pacts' => [ - { - href: Pact.term('http://pact-broker-url-for-consumer-1-master', %r{http://.*}) - }, - { - href: Pact.term('http://pact-broker-url-for-consumer-2-master', %r{http://.*}) - } - ] - } - } - ) - end - - it 'returns the array of pact urls' do - pacts = Pact::PactBroker::FetchPacts.call(provider, tags, broker_base_url, basic_auth_options) - expect(pacts).to eq( - [ - Pact::Provider::PactURI.new('http://pact-broker-url-for-consumer-1-master', basic_auth_options), - Pact::Provider::PactURI.new('http://pact-broker-url-for-consumer-2-master', basic_auth_options) - ] - ) - end - end - - context 'when neither pacts are available for a tag nor fallback tag is available' do - let(:tags) { ['tag-1'] } - - before do - pact_broker - .given('consumer-1 has no pacts with provider provider-1 tagged with tag-1') - .upon_receiving('a request to retrieve the latest tagged (tag-1) pacts for provider') - .with( - method: :get, - path: '/pacts/provider/provider-1/latest/tag-1', - headers: get_headers - ) - .will_respond_with( - status: 200, - headers: { - 'Content-Type' => Pact.term('application/hal+json', /json/) - }, - body: { - _links: { - 'pb:pacts' => [] - } - } - ) - end - - it 'returns empty array' do - pacts = Pact::PactBroker::FetchPacts.call(provider, tags, broker_base_url, basic_auth_options) - - expect(pacts).to eq([]) - end - end - - context 'retrieving all pact versions for tag-2 and latest pact versions for tag-1 for the provider with the specified consumer version tag' do - let(:tags) { ['tag-1', { name: 'tag-2', all: true }] } - - before do - pact_broker - .given('consumer-1 and consumer-2 have 2 pacts with provider provider-1 tagged with tag-1') - .upon_receiving('a request to retrieve latest pact versions for the provider with the specified consumer version tag (tag-1)') - .with( - method: :get, - path: '/pacts/provider/provider-1/latest/tag-1', - headers: get_headers - ) - .will_respond_with( - status: 200, - headers: { - 'Content-Type' => Pact.term('application/hal+json', /json/) - }, - body: { - _links: { - 'pb:pacts' => [ - { - href: Pact.term('http://pact-broker-url-for-consumer-1-tag-1', %r{http://.*}) - }, - { - href: Pact.term('http://pact-broker-url-for-consumer-2-tag-1', %r{http://.*}) - } - ] - } - } - ) - - pact_broker - .given('consumer-1 and consumer-2 have 2 pacts with provider provider-1 tagged with tag-2') - .upon_receiving('a request to retrieve all pact versions for the provider with the specified consumer version tag (tag-2)') - .with( - method: :get, - path: '/pacts/provider/provider-1/tag/tag-2', - headers: get_headers - ) - .will_respond_with( - status: 200, - headers: { - 'Content-Type' => Pact.term('application/hal+json', /json/) - }, - body: { - _links: { - 'pb:pacts' => [ - { - href: Pact.term('http://pact-broker-url-for-consumer-1-tag-2-all', %r{http://.*}) - }, - { - href: Pact.term('http://pact-broker-url-for-consumer-2-tag-2-all', %r{http://.*}) - } - ] - } - } - ) - end - - it 'returns the array of pact urls' do - pacts = Pact::PactBroker::FetchPacts.call(provider, tags, broker_base_url, basic_auth_options) - - expect(pacts).to eq( - [ - Pact::Provider::PactURI.new('http://pact-broker-url-for-consumer-1-tag-1', basic_auth_options), - Pact::Provider::PactURI.new('http://pact-broker-url-for-consumer-2-tag-1', basic_auth_options), - Pact::Provider::PactURI.new('http://pact-broker-url-for-consumer-1-tag-2-all', basic_auth_options), - Pact::Provider::PactURI.new('http://pact-broker-url-for-consumer-2-tag-2-all', basic_auth_options) - ] - ) - end - end - - context 'retrieving all the latest pact versions for the specified provider' do - let(:tags) { nil } - - before do - pact_broker - .given('consumer-1 and consumer-2 have 2 pacts with provider provider-1') - .upon_receiving('a request to retrieve latest pacts for the specified provider') - .with( - method: :get, - path: '/pacts/provider/provider-1/latest', - headers: get_headers - ) - .will_respond_with( - status: 200, - headers: { - 'Content-Type' => Pact.term('application/hal+json', /json/) - }, - body: { - _links: { - 'pb:pacts' => [ - { - href: Pact.term('http://pact-broker-url-for-consumer-1-all', %r{http://.*}) - }, - { - href: Pact.term('http://pact-broker-url-for-consumer-2-all', %r{http://.*}) - } - ] - } - } - ) - end - - it 'returns the array of pact urls' do - pacts = Pact::PactBroker::FetchPacts.call(provider, tags, broker_base_url, basic_auth_options) - - expect(pacts).to eq( - [ - Pact::Provider::PactURI.new('http://pact-broker-url-for-consumer-1-all', basic_auth_options), - Pact::Provider::PactURI.new('http://pact-broker-url-for-consumer-2-all', basic_auth_options) - ] - ) - end - end - end -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 6ee324fc..02ca750c 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,30 +1,33 @@ -require 'rspec' -require 'rspec/its' -require 'fakefs/spec_helpers' -require 'pact' -require 'webmock/rspec' -require 'support/factories' -require 'support/spec_support' -require 'pact/provider/rspec' +# frozen_string_literal: true -WebMock.disable_net_connect!(allow_localhost: true, allow: "https://www.google-analytics.com") +ENV['RAILS_ENV'] = 'test' -require './spec/support/active_support_if_configured' -require './spec/support/warning_silencer' +require 'bundler/setup' +require 'rspec' +require 'rspec_junit_formatter' -is_jruby = defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby' is_windows = Gem.win_platform? -RSpec.configure do | config | - config.include(FakeFS::SpecHelpers, fakefs: true) - - config.extend Pact::Provider::RSpec::ClassMethods - config.include Pact::Provider::RSpec::InstanceMethods - config.include Pact::Provider::TestMethods - config.include Pact::SpecSupport - if config.respond_to?(:example_status_persistence_file_path=) - config.example_status_persistence_file_path = "./spec/examples.txt" +RSpec.configure do |config| + config.expect_with :rspec do |expectations| + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + config.mock_with :rspec do |mocks| + mocks.verify_partial_doubles = true end - config.filter_run_excluding skip_jruby: is_jruby + + config.filter_run_when_matching :focus config.filter_run_excluding skip_windows: is_windows + config.example_status_persistence_file_path = 'tmp/rspec_examples.txt' + config.run_all_when_everything_filtered = true + + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = 'doc' + end + + config.order = :random + Kernel.srand config.seed end diff --git a/spec/spec_helper_v2.rb b/spec/spec_helper_v2.rb deleted file mode 100644 index 0215ee27..00000000 --- a/spec/spec_helper_v2.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -ENV["RAILS_ENV"] = "test" - -require "bundler/setup" -require "rspec" -require "rspec_junit_formatter" - -is_windows = Gem.win_platform? - -RSpec.configure do |config| - config.expect_with :rspec do |expectations| - expectations.include_chain_clauses_in_custom_matcher_descriptions = true - end - config.mock_with :rspec do |mocks| - mocks.verify_partial_doubles = true - end - - config.filter_run_when_matching :focus - config.filter_run_excluding skip_windows: is_windows - config.example_status_persistence_file_path = "tmp/rspec_examples.txt" - config.run_all_when_everything_filtered = true - - if config.files_to_run.one? - # Use the documentation formatter for detailed output, - # unless a formatter has already been configured - # (e.g. via a command-line flag). - config.default_formatter = "doc" - end - - config.order = :random - Kernel.srand config.seed -end diff --git a/spec/standalone/consumer_fail_test.rb b/spec/standalone/consumer_fail_test.rb deleted file mode 100644 index ec5d0bd7..00000000 --- a/spec/standalone/consumer_fail_test.rb +++ /dev/null @@ -1,55 +0,0 @@ -require 'pact/consumer/rspec' -require './spec/support/active_support_if_configured' - -Pact.service_consumer "Standalone Consumer" do - has_pact_with "Standalone Provider" do - mock_service :standalone_service do - port 1238 - end - end -end - -class StandaloneClient - - def initialize base_url - @base_url = base_url - end - - def call - uri = URI("#{@base_url}/something") - post_req = Net::HTTP::Post.new(uri.path) - post_req['Content-Type'] = "application/json" - post_req.body = {a: "not matching body"}.to_json - response = Net::HTTP.start(uri.hostname, uri.port) do |http| - http.request post_req - end - JSON.parse(response.body) - end - -end - -describe StandaloneClient, pact: true do - - subject { StandaloneClient.new("http://localhost:1238") } - - describe "call" do - - let(:expected_body) { {a: "body"} } - let(:expected_headers) { {'Content-Type' => "application/hal+json"} } - - before do - standalone_service. - upon_receiving("a request to create something").with(method: 'post', path: '/something', headers: expected_headers, body: expected_body). - will_respond_with(status: 200, headers: {}, body: {a: 'response body'}) - - standalone_service. - upon_receiving("a request to create something else").with(method: 'post', path: '/something-else', headers: expected_headers, body: expected_body). - will_respond_with(status: 200, headers: {}, body: {a: 'response body'}) - end - - it "will fail and display a helpful message" do - subject.call - end - end - -end \ No newline at end of file diff --git a/spec/standalone/consumer_pass_test.rb b/spec/standalone/consumer_pass_test.rb deleted file mode 100644 index 18f6c6d8..00000000 --- a/spec/standalone/consumer_pass_test.rb +++ /dev/null @@ -1,51 +0,0 @@ -require 'pact/consumer/rspec' -require './spec/support/active_support_if_configured' - -Pact.service_consumer "Standalone Consumer" do - has_pact_with "Standalone Provider" do - mock_service :standalone_service do - port 1237 - end - end -end - -class StandaloneClient - - def initialize base_url - @base_url = base_url - end - - def call - uri = URI("#{@base_url}/something") - post_req = Net::HTTP::Post.new(uri.path) - post_req['Content-Type'] = "application/json" - post_req.body = {a: "body"}.to_json - response = Net::HTTP.start(uri.hostname, uri.port) do |http| - http.request post_req - end - response.body - end - -end - -describe StandaloneClient, pact: true do - - subject { StandaloneClient.new("http://localhost:1237") } - - describe "call" do - - let(:expected_body) { {a: "body"} } - let(:response_body) { {a: 'response body'} } - - before do - standalone_service. - upon_receiving("a request to create something").with(method: 'post', path: '/something', body: expected_body). - will_respond_with(status: 200, headers: {}, body: response_body) - end - - it "will pass" do - expect(subject.call).to eq response_body.to_json - end - end - -end \ No newline at end of file diff --git a/spec/support/a_consumer-a_producer.json b/spec/support/a_consumer-a_producer.json deleted file mode 100644 index 399cfa06..00000000 --- a/spec/support/a_consumer-a_producer.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "producer": { - "name": "an old producer" - }, - "consumer": { - "name": "a consumer" - }, - "interactions": [ - { - "description": "request one", - "request": { - "method": "get", - "path": "/path_one" - }, - "response": { - }, - "producer_state": "state one" - }, - { - "description": "request two", - "request": { - "method": "get", - "path": "/path_two" - }, - "response": { - } - } - ], - "metadata": { - "pactSpecificationVersion": "1.0" - } -} \ No newline at end of file diff --git a/spec/support/a_consumer-a_provider.json b/spec/support/a_consumer-a_provider.json deleted file mode 100644 index e3093d03..00000000 --- a/spec/support/a_consumer-a_provider.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "provider": { - "name": "a provider" - }, - "consumer": { - "name": "a consumer" - }, - "interactions": [ - { - "description": "request one", - "request": { - "method": "get", - "path": "/path_one" - }, - "response": { - }, - "provider_state": "state one" - }, - { - "description": "request two", - "request": { - "method": "get", - "path": "/path_two" - }, - "response": { - } - } - ], - "metadata": { - "pactSpecificationVersion": "1.0" - } -} \ No newline at end of file diff --git a/spec/support/active_support_if_configured.rb b/spec/support/active_support_if_configured.rb deleted file mode 100644 index c7bb7d74..00000000 --- a/spec/support/active_support_if_configured.rb +++ /dev/null @@ -1,6 +0,0 @@ -if ENV['LOAD_ACTIVE_SUPPORT'] - $stderr.puts 'LOADING ACTIVE SUPPORT!!!! Hopefully it all still works' - require 'active_support/all' - require 'active_support' - require 'active_support/json' -end \ No newline at end of file diff --git a/spec/support/app_for_config_ru.rb b/spec/support/app_for_config_ru.rb deleted file mode 100644 index fb3abec8..00000000 --- a/spec/support/app_for_config_ru.rb +++ /dev/null @@ -1,4 +0,0 @@ -class AppForConfigRu - def call env - end -end \ No newline at end of file diff --git a/spec/support/bar_fail_pact_helper.rb b/spec/support/bar_fail_pact_helper.rb deleted file mode 100644 index 29dd1df9..00000000 --- a/spec/support/bar_fail_pact_helper.rb +++ /dev/null @@ -1,27 +0,0 @@ -require 'json' -require 'pact/provider/rspec' - -module Pact - module Test - class BarApp - def call env - [200, {'Content-Type' => 'application/hal+json'}, [{name: "Thing 2"}.to_json]] - end - end - - Pact.configure do | config | - config.logger.level = Logger::DEBUG - end - - Pact.service_provider "Bar" do - app { BarApp.new } - app_version '1.2.3' - app_version_tags ['master'] - publish_verification_results true - - honours_pact_with 'Foo' do - pact_uri './spec/support/foo-bar.json' - end - end - end -end diff --git a/spec/support/bar_pact_helper.rb b/spec/support/bar_pact_helper.rb deleted file mode 100644 index 8f1752f9..00000000 --- a/spec/support/bar_pact_helper.rb +++ /dev/null @@ -1,33 +0,0 @@ -require 'json' -require 'pact/provider/rspec' - -module Pact - module Test - class BarApp - def call env - [200, {'Content-Type' => 'application/json'}, [{name: "Thing 1"}.to_json]] - end - end - - Pact.configure do | config | - config.logger.level = Logger::DEBUG - end - - Pact.service_provider "Bar" do - app { BarApp.new } - app_version '1.2.3' - app_version_branch 'master' - app_version_tags ['master'] - publish_verification_results true - - honours_pacts_from_pact_broker do - pact_broker_base_url "http://localhost:9292" - consumer_version_tags ["prod"] - end - - honours_pact_with 'Foo' do - pact_uri './spec/pacts/foo-bar.json' - end - end - end -end diff --git a/spec/support/case-insensitive-response-header-matching.json b/spec/support/case-insensitive-response-header-matching.json deleted file mode 100644 index a75608b3..00000000 --- a/spec/support/case-insensitive-response-header-matching.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "consumer": { - "name": "an easy consumer" - }, - "provider": { - "name": "a provider which returns headers that don't match the expected case" - }, - "interactions": [ - { - "description": "a test request", - "request": { - "method": "get", - "path": "/" - }, - "response": { - "status": 200, - "headers": {"Content-Type": "application/hippo"} - } - } - ] -} \ No newline at end of file diff --git a/spec/support/case-insensitive-response-header-matching.rb b/spec/support/case-insensitive-response-header-matching.rb deleted file mode 100644 index ca19651e..00000000 --- a/spec/support/case-insensitive-response-header-matching.rb +++ /dev/null @@ -1,15 +0,0 @@ -module Pact - module Test - class CaseInsensitiveResponseHeadersApp - - def call env - [200, {'cOnTent-tYpe' => 'application/hippo'},[]] - end - - end - end -end - -Pact.service_provider "Provider" do - app { Pact::Test::CaseInsensitiveResponseHeadersApp.new } -end \ No newline at end of file diff --git a/spec/support/cli.rb b/spec/support/cli.rb deleted file mode 100644 index 62f7b702..00000000 --- a/spec/support/cli.rb +++ /dev/null @@ -1,27 +0,0 @@ -module Pact - module Support - module CLI - - def execute_command command, options = {} - output = `#{command}` - ensure_patterns_present(command, options, output) if options[:with] - ensure_patterns_not_present(command, options, output) if options[:without] - end - - def ensure_patterns_present command, options, output - require 'rainbow' - options[:with].each do | pattern | - raise ("Could not find #{pattern.inspect} in output of #{command}" + "\n\n#{output}") unless output =~ pattern - end - end - - def ensure_patterns_not_present command, options, output - require 'rainbow' - options[:without].each do | pattern | - raise ("Expected not to find #{pattern.inspect} in output of #{command}" + "\n\n#{output}") if output =~ pattern - end - end - - end - end -end diff --git a/spec/support/consumer_contract_template.json b/spec/support/consumer_contract_template.json deleted file mode 100644 index 42f0e7e1..00000000 --- a/spec/support/consumer_contract_template.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "provider": { - "name": "a provider" - }, - "consumer": { - "name": "a consumer" - }, - "interactions": [ - { - "description": "request one", - "request": { - "method": "get", - "path": "/path_one" - }, - "response": { - "status" : 200 - }, - "provider_state": "state one" - } - ], - "metadata": { - "pactSpecificationVersion": "1.0" - } -} \ No newline at end of file diff --git a/spec/support/docs/a_consumer-a_provider.json b/spec/support/docs/a_consumer-a_provider.json deleted file mode 100644 index e3093d03..00000000 --- a/spec/support/docs/a_consumer-a_provider.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "provider": { - "name": "a provider" - }, - "consumer": { - "name": "a consumer" - }, - "interactions": [ - { - "description": "request one", - "request": { - "method": "get", - "path": "/path_one" - }, - "response": { - }, - "provider_state": "state one" - }, - { - "description": "request two", - "request": { - "method": "get", - "path": "/path_two" - }, - "response": { - } - } - ], - "metadata": { - "pactSpecificationVersion": "1.0" - } -} \ No newline at end of file diff --git a/spec/support/factories.rb b/spec/support/factories.rb deleted file mode 100644 index 2c9b325b..00000000 --- a/spec/support/factories.rb +++ /dev/null @@ -1,82 +0,0 @@ -require 'hashie' -require 'hashie/extensions/key_conversion' - -module Pact - module HashUtils - - class Converter < Hash - include Hashie::Extensions::KeyConversion - include Hashie::Extensions::DeepMerge - end - - def symbolize_keys hash - Hash[Converter[hash].symbolize_keys] - end - - def stringify_keys hash - Hash[Converter[hash].stringify_keys] - end - - def deep_merge hash1, hash2 - Converter[hash1].deep_merge(Converter[hash2]) - end - end -end - -class InteractionFactory - - extend Pact::HashUtils - - def self.create hash = {} - defaults = { - 'description' => 'a description', - 'provider_state' => 'a thing exists', - 'request' => { - 'path' => '/path', - 'method' => 'get', - }, - 'response' => { - 'status' => 200, - 'body' => {a: 'response body'} - } - } - Pact::Interaction.from_hash(stringify_keys(deep_merge(defaults, stringify_keys(hash)))) - end -end - - -class ConsumerContractFactory - extend Pact::HashUtils - DEFAULTS = {:consumer_name => 'consumer', - :provider_name => 'provider', - :interactions => [InteractionFactory.create]} - - def self.create overrides = {} - options = deep_merge(symbolize_keys(DEFAULTS), symbolize_keys(overrides)) - Pact::ConsumerContract.new({:consumer => Pact::ServiceConsumer.new(name: options[:consumer_name]), - :provider => Pact::ServiceProvider.new(name: options[:provider_name]), - :interactions => options[:interactions]}) - end -end - - - -class ResponseFactory - extend Pact::HashUtils - DEFAULTS = {:status => 200, :body => {a: 'body'}}.freeze - def self.create_hash overrides = {} - deep_merge(DEFAULTS, overrides) - end -end - -class RequestFactory - extend Pact::HashUtils - DEFAULTS = {:path => '/path', :method => 'get', :query => 'query', :headers => {}}.freeze - def self.create_hash overrides = {} - deep_merge(DEFAULTS, overrides) - end - - def self.create_actual overrides = {} - Pact::Consumer::Request::Actual.from_hash(create_hash(overrides)) - end -end \ No newline at end of file diff --git a/spec/support/foo-bar-message.json b/spec/support/foo-bar-message.json deleted file mode 100644 index cca3395e..00000000 --- a/spec/support/foo-bar-message.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "consumer": { - "name": "Foo" - }, - "provider": { - "name": "Bar" - }, - "messages": [ - { - "description": "a message", - "providerStates": [ - { - "name": "a world exists" - } - ], - "contents": { - "text": "Hello world" - } - } - ], - "metadata": { - "pactSpecification": { - "version": "2.0.0" - } - } -} diff --git a/spec/support/generated_index.md b/spec/support/generated_index.md deleted file mode 100644 index befffaeb..00000000 --- a/spec/support/generated_index.md +++ /dev/null @@ -1,4 +0,0 @@ -### Pacts for Some Consumer - -* [Some Provider](Some%20Provider.md) -* [Some other provider](Some%20other%20provider.md) diff --git a/spec/support/generated_markdown.md b/spec/support/generated_markdown.md deleted file mode 100644 index 280d4d4a..00000000 --- a/spec/support/generated_markdown.md +++ /dev/null @@ -1,55 +0,0 @@ -### A pact between Some Consumer and Some Provider - -#### Requests from Some Consumer to Some Provider - -* [A request for alligators in Brüssel](#a_request_for_alligators_in_Brüssel_given_alligators_exist) given alligators exist - -* [A request for polar bears](#a_request_for_polar_bears) - -#### Interactions - - -Given **alligators exist**, upon receiving **a request for alligators in Brüssel** from Some Consumer, with -```json -{ - "method": "get", - "path": "/alligators" -} -``` -Some Provider will respond with: -```json -{ - "status": 200, - "headers": { - "Content-Type": "application/json" - }, - "body": { - "alligators": [ - { - "name": "Bob", - "phoneNumber": "12345678" - } - ] - } -} -``` - -Upon receiving **a request for polar bears** from Some Consumer, with -```json -{ - "method": "get", - "path": "/polar-bears" -} -``` -Some Provider will respond with: -```json -{ - "status": 404, - "headers": { - "Content-Type": "application/json" - }, - "body": { - "message": "Sorry, due to climate change, the polar bears are currently unavailable." - } -} -``` diff --git a/spec/support/interaction_view_model.json b/spec/support/interaction_view_model.json deleted file mode 100644 index 3c14c0a5..00000000 --- a/spec/support/interaction_view_model.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "provider": { - "name": "a_provider" - }, - "consumer": { - "name": "a*consumer" - }, - "interactions": [ - { - "description": "a request with a body and headers", - "request": { - "method": "get", - "path": "/path", - "query": "some=thing", - "headers": { - "key": "a header" - }, - "body": { - "key": "a body" - } - }, - "response": {} - }, - { - "description": "a request with an empty body and empty headers", - "request": { - "method": "get", - "path": "/", - "headers": {}, - "body": {} - }, - "response": {} - }, - { - "description": "a response with a body and headers", - "request": { - "method": "get", - "path": "/" - }, - "response": { - "headers": { - "key": "a header" - }, - "body": { - "key": "a body" - }, - "status": 200 - } - }, - { - "description": "a response with an empty body and empty headers", - "request": { - "method": "get", - "path": "/" - }, - "response": { - "status": 200, - "headers": {}, - "body": {} - } - } - ] -} \ No newline at end of file diff --git a/spec/support/interaction_view_model_with_terms.json b/spec/support/interaction_view_model_with_terms.json deleted file mode 100644 index a669bbc1..00000000 --- a/spec/support/interaction_view_model_with_terms.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "provider": { - "name": "a provider" - }, - "consumer": { - "name": "a consumer" - }, - "interactions": [ - { - "description": "an interaction with terms", - "request": { - "method": "post", - "path": "/path", - "query": "some=thing", - "headers": { - "key": "a header" - }, - "body": { - "term": { - "json_class": "Pact::Term", - "data": { - "generate": "sunny", - "matcher": { - "json_class": "Regexp", - "o": 0, - "s": "sun" - } - } - } - } - }, - "response": { - "status": 200, - "body": { - "term": { - "json_class": "Pact::Term", - "data": { - "generate": "rainy", - "matcher": { - "json_class": "Regexp", - "o": 0, - "s": "rain" - } - } - } - } - } - } - ] -} \ No newline at end of file diff --git a/spec/support/markdown_pact.json b/spec/support/markdown_pact.json deleted file mode 100644 index 357a0e9f..00000000 --- a/spec/support/markdown_pact.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "provider": { - "name": "Some Provider" - }, - "consumer": { - "name": "Some Consumer" - }, - "interactions": [ - { - "description": "a request for alligators in Brüssel", - "provider_state": "alligators exist", - "request": { - "method": "get", - "path": "/alligators" - }, - "response": { - "headers" : {"Content-Type": "application/json"}, - "status" : 200, - "body" : { - "alligators": [{ - "name": "Bob", - "phoneNumber" : { - "json_class": "Pact::Term", - "data": { - "generate": "12345678", - "matcher": {"json_class":"Regexp","o":0,"s":"\\d+"} - } - } - }] - } - } - },{ - "description": "a request for polar bears", - "provider_state": null, - "request": { - "method": "get", - "path": "/polar-bears" - }, - "response": { - "headers" : {"Content-Type": "application/json"}, - "status" : 404, - "body" : { - "message": "Sorry, due to climate change, the polar bears are currently unavailable." - } - } - } - ] -} diff --git a/spec/support/markdown_pact_with_markdown_chars_in_names.json b/spec/support/markdown_pact_with_markdown_chars_in_names.json deleted file mode 100644 index a8a7b251..00000000 --- a/spec/support/markdown_pact_with_markdown_chars_in_names.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "provider": { - "name": "Some_Provider_App" - }, - "consumer": { - "name": "Some*Consumer*App" - }, - "interactions": [ - - ] -} diff --git a/spec/support/message_spec_helper.rb b/spec/support/message_spec_helper.rb deleted file mode 100644 index 13a0f046..00000000 --- a/spec/support/message_spec_helper.rb +++ /dev/null @@ -1,41 +0,0 @@ -require 'pact/message' - -# Example data store - -class DataStore - def self.greeting_recipient= greeting_recipient - @greeting_recipient = greeting_recipient - end - - def self.greeting_recipient - @greeting_recipient - end -end - -# Example message producer - -class BarProvider - def create_message - { - text: "Hello #{DataStore.greeting_recipient}" - } - end -end - -# Provider states - -Pact.provider_states_for "Foo" do - provider_state "a world exists" do - set_up do - DataStore.greeting_recipient = "world" - end - end -end - -CONFIG = { - "a message" => lambda { BarProvider.new.create_message } -} - -Pact.message_provider "Bar" do - builder { |description| CONFIG[description].call } -end diff --git a/spec/support/missing_provider_states_output.txt b/spec/support/missing_provider_states_output.txt deleted file mode 100644 index dfba00bf..00000000 --- a/spec/support/missing_provider_states_output.txt +++ /dev/null @@ -1,25 +0,0 @@ -Pact.provider_states_for "Consumer 1" do - - provider_state "state1" do - set_up do - # Your set up code goes here - end - end - - provider_state "state2" do - set_up do - # Your set up code goes here - end - end - -end - -Pact.provider_states_for "Consumer 2" do - - provider_state "state3" do - set_up do - # Your set up code goes here - end - end - -end \ No newline at end of file diff --git a/spec/support/options.json b/spec/support/options.json deleted file mode 100644 index 38477147..00000000 --- a/spec/support/options.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "consumer": { - "name": "Consumer" - }, - "provider": { - "name": "Provider" - }, - "interactions": [ - { - "description": "an OPTIONS request", - "request": { - "method": "options", - "path": "/" - }, - "response": { - "status": 200 - }, - "provider_state": null - } - ] -} \ No newline at end of file diff --git a/spec/support/options_app.rb b/spec/support/options_app.rb deleted file mode 100644 index d4659af8..00000000 --- a/spec/support/options_app.rb +++ /dev/null @@ -1,15 +0,0 @@ -require 'pact/provider/rspec' - -class App - def self.call env - if env['REQUEST_METHOD'] == 'OPTIONS' - [200, {}, []] - else - [500, {}, ["Expected an options request"]] - end - end -end - -Pact.service_provider 'Provider' do - app { App } -end \ No newline at end of file diff --git a/spec/support/pact_helper.rb b/spec/support/pact_helper.rb deleted file mode 100644 index a994556d..00000000 --- a/spec/support/pact_helper.rb +++ /dev/null @@ -1,59 +0,0 @@ -# This is the pact_helper for rake pact:tests -require 'json' -require 'pact/provider/rspec' -require './spec/support/active_support_if_configured' - -module Pact - module Test - class TestApp - def call env - if env['PATH_INFO'] == '/weather' - [200, {'Content-Type' => 'application/json'}, [{message: WEATHER[:current_state], :array => [{"foo"=> "blah"}]}.to_json]] - elsif env['PATH_INFO'] == '/sometext' - [200, {'Content-Type' => 'text/plain'}, ['some text']] - elsif env['PATH_INFO'] == '/content_type_is_important' - [200, {'Content-Type' => 'application/json'}, [{message: "A message", note: "This will cause verify to fail if it using the wrong content type differ."}.to_json]] - else - raise "unexpected path #{env['PATH_INFO']}!!!" - end - end - end - - Pact.configure do | config | - config.logger.level = Logger::DEBUG - config.diff_formatter = :unix - config.reports_dir = 'tmp/spec_reports' - end - - Pact.service_provider "Some Provider" do - app { TestApp.new } - app_version '1.2.3' - - honours_pact_with 'some-test-consumer' do - pact_uri './spec/support/test_app_pass.json' - end - end - - Pact.set_up do - WEATHER ||= {} - end - - #one with a top level consumer - Pact.provider_states_for 'some-test-consumer' do - - provider_state "the weather is sunny" do - set_up do - - WEATHER[:current_state] = 'sunny' - end - end - end - - #one without a top level consumer - Pact.provider_state "the weather is cloudy" do - set_up do - WEATHER[:current_state] = 'cloudy' - end - end - end -end diff --git a/spec/support/pact_helper_for_provider_state_params_test.rb b/spec/support/pact_helper_for_provider_state_params_test.rb deleted file mode 100644 index 8e04e52a..00000000 --- a/spec/support/pact_helper_for_provider_state_params_test.rb +++ /dev/null @@ -1,34 +0,0 @@ -# This is the pact_helper for rake pact:tests -require 'json' -require 'pact/provider/rspec' -require 'ostruct' - -module Pact - module Test - class ParamsTestApp - - ALLIGATORS = [] - - def call env - [200, {'Content-Type' => 'application/json'}, [ALLIGATORS.first.to_h.to_json]] - end - end - - Pact.configure do | config | - config.reports_dir = 'tmp/spec_reports' - end - - Pact.service_provider "some-test-provider" do - app { ParamsTestApp.new } - app_version '1.2.3' - end - - Pact.provider_states_for 'some-test-consumer' do - provider_state "the first alligator exists" do - set_up do | params | - ParamsTestApp::ALLIGATORS << OpenStruct.new(name: params.fetch('name')) - end - end - end - end -end diff --git a/spec/support/provider_states_params_test.json b/spec/support/provider_states_params_test.json deleted file mode 100644 index cbf7ef45..00000000 --- a/spec/support/provider_states_params_test.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "consumer" : { "name" : "some-test-consumer" }, - "provider" : { "name" : "some-test-provider" }, - "interactions": [ - { - "description": "a request for the first alligator", - "providerStates": [ - { - "name": "the first alligator exists", - "params": { - "name": "Mary" - } - } - ], - "request": { - "method": "GET", - "path": "/alligators/first" - }, - "response": { - "status": 200, - "body": { - "name": "Mary" - } - } - } - ], - "metadata": { - "pactSpecification": { - "version": "3" - } - } -} diff --git a/spec/support/response_body_term.json b/spec/support/response_body_term.json deleted file mode 100644 index 3f3b00b7..00000000 --- a/spec/support/response_body_term.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "consumer": { - "name": "Foo" - }, - "provider": { - "name": "Bar" - }, - "interactions": [ - { - "description": "a retrieve thing request", - "request": { - "method": "get", - "path": "/thing", - "headers": { - "Accept": "application/json" - } - }, - "response": { - "status": 200, - "headers": { - "Content-Type": "application/json" - }, - "body": { - "action_history": [ - { - "at": "2016-02-11T12:00:00Z" - } - ] - }, - "matchingRules": { - "$.body.action_history[0].at": { - "match": "regex", - "regex": "^\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d([+-][0-2]\\d:[0-5]\\d|Z)$" - } - } - } - } - ], - "metadata": { - "pactSpecification": { - "version": "2.0.0" - } - } -} diff --git a/spec/support/response_body_term_app.rb b/spec/support/response_body_term_app.rb deleted file mode 100644 index d057b401..00000000 --- a/spec/support/response_body_term_app.rb +++ /dev/null @@ -1,11 +0,0 @@ -require 'pact/provider/rspec' - -class App - def self.call env - [200, {'Content-Type' => 'application/json'}, []] - end -end - -Pact.service_provider 'Provider' do - app { App } -end diff --git a/spec/support/shared_examples_for_request.rb b/spec/support/shared_examples_for_request.rb deleted file mode 100644 index 3ff39e94..00000000 --- a/spec/support/shared_examples_for_request.rb +++ /dev/null @@ -1,103 +0,0 @@ -shared_examples "a request" do - - describe 'matching' do - let(:expected) do - Pact::Request::Expected.from_hash( - 'method' => 'get', 'path' => 'path', 'query' => /b/ - ) - end - - let(:actual) do - Pact::Consumer::Request::Actual.from_hash( - 'method' => 'get', 'path' => 'path', 'query' => 'blah', 'headers' => {}, 'body' => '' - ) - end - - it "should match" do - expect(expected.difference(actual)).to eq({}) - end - end - - describe 'full_path' do - context "with empty path" do - subject { described_class.from_hash(path: '', method: 'get', query: '', headers: {}) } - - it "returns the full path" do - expect(subject.full_path).to eq "/" - end - end - - context "with a path" do - subject { described_class.from_hash(path: '/path', method: 'get', query: '', headers: {}) } - - it "returns the full path" do - expect(subject.full_path).to eq "/path" - end - end - - context "with a path and query" do - subject { described_class.from_hash(path: '/path', method: 'get', query: "something", headers: {}) } - - it "returns the full path" do - expect(subject.full_path).to eq "/path?something" - end - end - - context "with a path and a query that is a Term" do - subject { described_class.from_hash(path: '/path', method: 'get', headers: {}, query: Pact.term(generate: 'a', matcher: /a/)) } - - it "returns the full path with reified path" do - expect(subject.full_path).to eq "/path?a" - end - end - end - - describe "building from a hash" do - - let(:raw_request) do - { - 'method' => 'get', - 'path' => '/mallory', - 'query' => 'query', - 'headers' => { - 'Content-Type' => 'application/json' - }, - 'body' => 'hello mallory' - } - end - - subject { described_class.from_hash(raw_request) } - - it "extracts the method" do - expect(subject.method).to eq 'get' - end - - it "extracts the path" do - expect(subject.path).to eq '/mallory' - end - - it "extracts the body" do - expect(subject.body).to eq 'hello mallory' - end - - it "extracts the query" do - expect(subject.query).to eq 'query' - end - - it "blows up if method is absent" do - raw_request.delete 'method' - expect { described_class.from_hash(raw_request) }.to raise_error - end - - it "blows up if path is absent" do - raw_request.delete 'path' - expect { described_class.from_hash(raw_request) }.to raise_error - end - - it "does not blow up if body is missing" do - raw_request.delete 'body' - expect { described_class.from_hash(raw_request) }.to_not raise_error - end - - end -end \ No newline at end of file diff --git a/spec/support/spec_support.rb b/spec/support/spec_support.rb deleted file mode 100644 index 78756b25..00000000 --- a/spec/support/spec_support.rb +++ /dev/null @@ -1,20 +0,0 @@ -require 'pact/rspec' - -module Pact - module SpecSupport - - extend self - - def remove_ansicolor string - string.gsub(/\e\[(\d+)m/, '') - end - - Pact::RSpec.with_rspec_2 do - - def instance_double *args - double(*args) - end - - end - end -end \ No newline at end of file diff --git a/spec/support/ssl_server.rb b/spec/support/ssl_server.rb deleted file mode 100644 index 3009183e..00000000 --- a/spec/support/ssl_server.rb +++ /dev/null @@ -1,48 +0,0 @@ -if __FILE__ == $0 - - SSL_KEY = "spec/fixtures/certificates/key.pem" - SSL_CERT = "spec/fixtures/certificates/client_cert.pem" - SSL_CA_CERT = "spec/fixtures/certificates/ca_cert.pem" - - trap(:INT) do - @server.shutdown - exit - end - - def webrick_opts port - certificate = OpenSSL::X509::Certificate.new(File.read(SSL_CERT)) - cert_name = certificate.subject.to_a.collect{|a| a[0..1] } - logger_stream = ENV["DEBUG"] ? $stderr : StringIO.new - { - Port: port, - Host: "0.0.0.0", - AccessLog: [], - Logger: WEBrick::Log.new(logger_stream,WEBrick::Log::INFO), - SSLVerifyClient: OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT | OpenSSL::SSL::VERIFY_PEER, - SSLCACertificateFile: SSL_CA_CERT, - SSLCertificate: certificate, - SSLPrivateKey: OpenSSL::PKey::RSA.new(File.read(SSL_KEY)), - SSLEnable: true, - SSLCertName: cert_name, - } - end - - app = ->(_env) { puts "hello"; [200, {}, ["Hello world" + "\n"]] } - - require "webrick" - require "webrick/https" - require "rack" - # Rack 2/3 compatibility - begin - require 'rack/handler/webrick' - handler = Rack::Handler::WEBrick - rescue LoadError - require 'rackup/handler/webrick' - handler = Class.new(Rackup::Handler::WEBrick) - end - opts = webrick_opts(4444) - - handler.run(app, **opts) do |server| - @server = server - end -end diff --git a/spec/support/stubbing.json b/spec/support/stubbing.json deleted file mode 100644 index 85c99eb7..00000000 --- a/spec/support/stubbing.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "consumer": { - "name": "Consumer" - }, - "provider": { - "name": "Provider" - }, - "interactions": [ - { - "description": "a test request", - "request": { - "method": "get", - "path": "/" - }, - "response": { - "status": 200, - "body": "stubbing works" - }, - "provider_state": "something is stubbed" - } - ] -} \ No newline at end of file diff --git a/spec/support/stubbing_using_allow.rb b/spec/support/stubbing_using_allow.rb deleted file mode 100644 index 172bfdf7..00000000 --- a/spec/support/stubbing_using_allow.rb +++ /dev/null @@ -1,29 +0,0 @@ -require 'pact/provider/rspec' -require 'rspec/mocks' -require './spec/support/active_support_if_configured' - -class StubbedThing - def self.stub_me - end -end - -class App - def self.call env - [200, {}, [StubbedThing.stub_me]] - end -end - -Pact.provider_states_for 'Consumer' do - provider_state 'something is stubbed' do - set_up do - allow(StubbedThing).to receive(:stub_me).and_return("stubbing works") - end - end -end - -# Include the ExampleMethods module after the provider states are declared -# to ensure the ordering doesn't matter - -Pact.service_provider 'Provider' do - app { App } -end \ No newline at end of file diff --git a/spec/support/term-v2.json b/spec/support/term-v2.json deleted file mode 100644 index 60bdd571..00000000 --- a/spec/support/term-v2.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "consumer": { - "name": "some-test-consumer" - }, - "provider": { - "name": "an unknown provider" - }, - "interactions": [ - { - "description": "a test request", - "request": { - "method": "get", - "path": "/weather", - "query": "" - }, - "response": { - "matchingRules": { - "$.headers.Content-Type" : { - "match": "regex", "regex": "json" - }, - "$.body.message" : { - "match": "regex", "regex": "sun" - } - }, - "status": 200, - "headers" : { - "Content-Type": "foo/json" - }, - "body": { - "message" : "sunful" - } - }, - "provider_state": "the weather is sunny" - } - ] -} \ No newline at end of file diff --git a/spec/support/term.json b/spec/support/term.json deleted file mode 100644 index 19256f2e..00000000 --- a/spec/support/term.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "consumer": { - "name": "some-test-consumer" - }, - "provider": { - "name": "an unknown provider" - }, - "interactions": [ - { - "description": "a test request", - "request": { - "method": "get", - "path": "/weather", - "query": "" - }, - "response": { - "status": 200, - "headers" : { - "Content-type": { - "json_class": "Pact::Term", - "data": { - "generate": "text/plain", - "matcher": { - "json_class": "Regexp", - "o": 0, - "s": "text" - } - } - } - }, - "body": { - "message" : { - "json_class": "Pact::Term", - "data": { - "generate": "rainy", - "matcher": { - "json_class": "Regexp", - "o": 0, - "s": "rain" - } - } - } - } - }, - "provider_state": "the weather is sunny" - } - ] -} \ No newline at end of file diff --git a/spec/support/test_app_fail.json b/spec/support/test_app_fail.json deleted file mode 100644 index 46d1ba6b..00000000 --- a/spec/support/test_app_fail.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "consumer": { - "name": "an unknown consumer" - }, - "provider": { - "name": "an unknown provider" - }, - "interactions": [ - { - "description": "a test request", - "request": { - "method": "get", - "path": "/weather", - "query": "" - }, - "response": { - "status": 200, - "headers" : {"Content-type": "application/json"}, - "body": {"message" : "this is not the weather you are looking for", "array": [{"foo": "bar"}], "somethingElse" : {"blah" : {"nested" : "that is missing"} }} - - }, - "provider_state": "the weather is cloudy" - },{ - "description": "another test request", - "request": { - "method": "get", - "path": "/weather", - "query": "" - }, - "response": { - "status": 200, - "headers" : { - "Content-type": { - "json_class": "Pact::Term", - "data": { - "generate": "application/hal+json", - "matcher": {"json_class":"Regexp","o":0,"s":"hal"} - } - }, - "X-Special-Header": "something" - }, - "body": {"message" : "this is not the weather you are looking for"} - - } - },{ - "description": "another test request", - "provider_state": "a missing provider state", - "request": { - "method": "get", - "path": "/weather", - "query": "" - }, - "response": { - "status": 200, - "headers" : {"Content-type": "application/json"}, - "body": {"message" : "this is not the weather you are looking for"} - - } - } - ] -} \ No newline at end of file diff --git a/spec/support/test_app_pass.json b/spec/support/test_app_pass.json deleted file mode 100644 index 386b19ca..00000000 --- a/spec/support/test_app_pass.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "consumer": { - "name": "some-test-consumer" - }, - "provider": { - "name": "an unknown provider" - }, - "interactions": [ - { - "description": "a test request", - "request": { - "method": "get", - "path": "/weather", - "query": "" - }, - "response": { - "status": 200, - "headers" : {"Content-type": "application/json"}, - "body": {"message" : "sunny"} - }, - "provider_state": "the weather is sunny" - }, - { - "description": "a test request for text", - "request": { - "method": "get", - "path": "/sometext", - "query": "", - "body" : "some request text" - }, - "response": { - "status": 200, - "headers" : {"Content-type": "text/plain"}, - "body": "some text" - } - } - ] -} \ No newline at end of file diff --git a/spec/support/test_app_with_right_content_type_differ.json b/spec/support/test_app_with_right_content_type_differ.json deleted file mode 100644 index df43c78a..00000000 --- a/spec/support/test_app_with_right_content_type_differ.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "consumer": { - "name": "some-test-consumer" - }, - "provider": { - "name": "the test app in the pact_helper file" - }, - "interactions": [ - { - "description": "a test request expecting an application/json response", - "request": { - "method": "get", - "path": "/content_type_is_important", - "query": "" - }, - "response": { - "status": 200, - "headers" : {"Content-type": "application/json"}, - "body": {"message" : "A message"} - } - } - ] -} \ No newline at end of file diff --git a/spec/support/text.txt b/spec/support/text.txt deleted file mode 100644 index 91aaf469..00000000 --- a/spec/support/text.txt +++ /dev/null @@ -1 +0,0 @@ -This is a file \ No newline at end of file diff --git a/spec/support/warning_silencer.rb b/spec/support/warning_silencer.rb deleted file mode 100644 index 0f2c978d..00000000 --- a/spec/support/warning_silencer.rb +++ /dev/null @@ -1,10 +0,0 @@ -module WarningSilencer - extend self - - def enable - old, $VERBOSE = $VERBOSE, nil - yield - ensure - $VERBOSE = old - end -end diff --git a/spec/v2/pact/consumer/grpc_interaction_builder_spec.rb b/spec/v2/pact/consumer/grpc_interaction_builder_spec.rb deleted file mode 100644 index 0a6d70a7..00000000 --- a/spec/v2/pact/consumer/grpc_interaction_builder_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe Pact::V2::Consumer::GrpcInteractionBuilder do - subject { described_class.new(nil) } - - let(:proto_path) { Rails.root.join("deps/services/pet_store/grpc/pet_store.proto").to_s } - let(:builder) do - subject - .with_service(proto_path, "Pets/PetById") - .with_request(param: "some data") - .will_respond_with(result: "some data") - end - - it "builds proper json" do - result = JSON.parse(builder.interaction_json) - expect(result).to eq( - "pact:content-type" => "application/protobuf", - "pact:proto" => File.expand_path(proto_path).to_s, - "pact:proto-service" => "Pets/PetById", - "request" => { - "param" => "some data" - }, - "response" => { - "result" => "some data" - } - ) - end -end diff --git a/spec/v2/pact/consumer/interaction_contents_spec.rb b/spec/v2/pact/consumer/interaction_contents_spec.rb deleted file mode 100644 index b31e6c72..00000000 --- a/spec/v2/pact/consumer/interaction_contents_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe Pact::V2::Consumer::InteractionContents do - include Pact::V2::Matchers - - let(:contents) do - { - str: match_any_string("str"), - bool: match_any_boolean(true), - num: match_any_number(1), - nested: match_each( - { - a: 1, - b: "2" - } - ) - } - end - - context "with plugin interaction" do - it "serializes properly to json" do - expect(described_class.plugin(contents).to_json) - .to eq("{\"str\":\"matching(regex, '(?-mix:.*)', 'str')\",\"bool\":\"matching(boolean, true)\",\"num\":\"matching(number, 1)\",\"nested\":{\"pact:match\":\"eachValue(matching($'SAMPLE'))\",\"SAMPLE\":{\"a\":1,\"b\":\"2\"}}}") - end - end - - context "with basic interaction" do - it "serializes properly to json" do - expect(described_class.basic(contents).to_json) - .to eq("{\"str\":{\"pact:matcher:type\":\"regex\",\"value\":\"str\",\"regex\":\"(?-mix:.*)\"},\"bool\":{\"pact:matcher:type\":\"boolean\",\"value\":true},\"num\":{\"pact:matcher:type\":\"number\",\"value\":1},\"nested\":{\"pact:matcher:type\":\"type\",\"value\":[{\"a\":1,\"b\":\"2\"}],\"min\":1}}") - end - end -end diff --git a/spec/v2/pact/consumer/message_interaction_builder_spec.rb b/spec/v2/pact/consumer/message_interaction_builder_spec.rb deleted file mode 100644 index e86022e9..00000000 --- a/spec/v2/pact/consumer/message_interaction_builder_spec.rb +++ /dev/null @@ -1,39 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe Pact::V2::Consumer::MessageInteractionBuilder do - subject { described_class.new(nil) } - - context "when proto message is used" do - let(:proto_path) { "spec/internal/deps/services/pet_store/grpc/pet_store.proto" } - let(:builder) do - subject - .upon_receiving("message as proto") - .with_proto_class(proto_path, "Pet") - .with_proto_contents(id: 1) - end - - it "builds proper json" do - result = JSON.parse(builder.build_interaction_json) - expect(result).to eq( - "pact:content-type" => "application/protobuf", - "pact:message-type" => "Pet", - "pact:proto" => File.expand_path(proto_path).to_s, - "id" => 1 - ) - end - end - - context "when json message is used" do - let(:proto_path) { "spec/internal/deps/services/pet_store/grpc/pet_store.proto" } - let(:builder) do - subject - .upon_receiving("message as proto") - .with_json_contents(id: 1) - end - - it "builds proper json" do - result = JSON.parse(builder.build_interaction_json) - expect(result).to eq("id" => 1) - end - end -end diff --git a/spec/v2/pact/generators_spec.rb b/spec/v2/pact/generators_spec.rb deleted file mode 100644 index a670497e..00000000 --- a/spec/v2/pact/generators_spec.rb +++ /dev/null @@ -1,215 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -require 'pact/v2/generators' - -module Pact - module V2 - module Generators - RSpec.describe RandomIntGenerator do - subject { described_class.new(min: 1, max: 10) } - - describe '#as_basic' do - it 'returns the correct hash' do - expect(subject.as_basic).to match({ - 'pact:matcher:type' => 'integer', - 'pact:generator:type' => 'RandomInt', - 'min' => 1, - 'max' => 10, - 'value' => a_value_between(1, 10) - }) - end - end - end - - RSpec.describe RandomDecimalGenerator do - subject { described_class.new(digits: 5) } - - describe '#as_basic' do - it 'returns the correct hash' do - expect(subject.as_basic).to match({ - 'pact:matcher:type' => 'decimal', - 'pact:generator:type' => 'RandomDecimal', - 'digits' => 5, - 'value' => a_value_between(0.00001, 0.99999) - }) - end - end - end - - RSpec.describe RandomHexadecimalGenerator do - subject { described_class.new(digits: 8) } - - describe '#as_basic' do - it 'returns the correct hash' do - expect(subject.as_basic).to match({ - 'pact:matcher:type' => 'decimal', - 'pact:generator:type' => 'RandomHexadecimal', - 'digits' => 8, - 'value' => match(/[0-9a-f]{8}/) - }) - end - end - end - - RSpec.describe RandomStringGenerator do - subject { described_class.new(size: 12) } - - describe '#as_basic' do - it 'returns the correct hash' do - expect(subject.as_basic).to match({ - 'pact:matcher:type' => 'type', - 'pact:generator:type' => 'RandomString', - 'size' => 12, - 'value' => match(/[a-zA-Z0-9]{12}/) - }) - end - end - end - - RSpec.describe UuidGenerator do - subject { described_class.new } - - describe '#as_basic' do - it 'returns the correct hash' do - match({ - 'pact:generator:type' => 'Uuid', - 'pact:matcher:type' => 'regex', - 'regex' => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}', - 'value' => match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/) - }) - end - end - end - - RSpec.describe DateGenerator do - context 'with format' do - subject { described_class.new(format: 'yyyy-MM-dd') } - - it 'returns the correct hash' do - expect(subject.as_basic).to match({ - 'pact:matcher:type' => 'date', - 'pact:generator:type' => 'Date', - 'format' => 'yyyy-MM-dd', - 'value' => match(/\d{4}-\d{2}-\d{2}/) - }) - end - end - - context 'without format' do - subject { described_class.new } - - it 'returns the correct hash' do - match({ - 'format' => 'yyyy-MM-dd', - 'pact:generator:type' => 'Date', - 'pact:matcher:type' => 'date', - 'value' => match(/\d{4}-\d{2}-\d{2}/) - }) - end - end - end - - RSpec.describe TimeGenerator do - context 'with format' do - subject { described_class.new(format: 'HH:mm:ss') } - - it 'returns the correct hash' do - match({ - 'pact:generator:type' => 'Time', - 'pact:matcher:type' => 'time', - 'format' => 'HH:mm:ss', - 'value' => match(/\d{2}:\d{2}:\d{2}/) - }) - end - end - - context 'without format' do - subject { described_class.new } - - it 'returns the correct hash' do - match({ - 'format' => 'HH:mm', - 'pact:generator:type' => 'Time', - 'pact:matcher:type' => 'time', - 'value' => match(/\d{2}:\d{2}/) - }) - end - end - end - - RSpec.describe DateTimeGenerator do - context 'with format' do - subject { described_class.new(format: "yyyy-MM-dd'T'HH:mm:ssZ") } - - it 'returns the correct hash' do - match({ - 'pact:generator:type' => 'DateTime', - 'pact:matcher:type' => 'datetime', - 'format' => "yyyy-MM-dd'T'HH:mm:ssZ", - 'value' => match(/\d{4}-\d{2}-\d{2}'T'\d{2}:\d{2}:\d{2}\+\d{4}/) - }) - end - end - - context 'without format' do - subject { described_class.new } - - it 'returns the correct hash' do - match({ - 'format' => 'yyyy-MM-dd HH:mm', - 'pact:generator:type' => 'DateTime', - 'pact:matcher:type' => 'datetime', - 'value' => match(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}/) - }) - end - end - end - - RSpec.describe RandomBooleanGenerator do - subject { described_class.new } - - describe '#as_basic' do - it 'returns the correct hash' do - eq({ - 'pact:generator:type' => 'RandomBoolean', - 'pact:matcher:type' => 'boolean', - 'value' => true - }) - end - end - end - - RSpec.describe ProviderStateGenerator do - subject { described_class.new(expression: '/alligators/${alligator_name}', example: '/alligators/Mary') } - - describe '#as_basic' do - it 'returns the correct hash' do - eq({ - 'pact:generator:type' => 'ProviderState', - 'pact:matcher:type' => 'type', - 'expression' => '/alligators/${alligator_name}', - 'value' => '/alligators/Mary' - }) - end - end - end - - RSpec.describe MockServerURLGenerator do - subject { described_class.new(regex: 'http://localhost:\\d+', example: 'http://localhost:1234') } - - describe '#as_basic' do - it 'returns the correct hash' do - expect(subject.as_basic).to eq({ - 'pact:generator:type' => 'MockServerURL', - 'pact:matcher:type' => 'regex', - 'regex' => 'http://localhost:\\d+', - 'example' => 'http://localhost:1234', - 'value' => 'http://localhost:1234' - }) - end - end - end - end - end -end diff --git a/spec/v2/pact/matchers_spec.rb b/spec/v2/pact/matchers_spec.rb deleted file mode 100644 index 55009f0b..00000000 --- a/spec/v2/pact/matchers_spec.rb +++ /dev/null @@ -1,480 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe Pact::V2::Matchers do - subject(:test_class) { Class.new { extend Pact::V2::Matchers } } - - context "with basic format serialization" do - it "properly builds matcher for UUID" do - expect(test_class.match_uuid.as_basic).to eq({ - "pact:matcher:type" => "regex", - "value" => "e1d01e04-3a2b-4eed-a4fb-54f5cd257338", - :regex => "(?i-mx:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})" - }) - end - - it "properly builds matcher for regex" do - expect(test_class.match_regex(/(A-Z){1,3}/, "ABC").as_basic).to eq({ - "pact:matcher:type" => "regex", - "value" => "ABC", - :regex => "(?-mix:(A-Z){1,3})" - }) - end - - it "properly builds matcher for datetime" do - expect(test_class.match_datetime("yyyy-MM-dd HH:mm:ssZZZZZ", "2020-05-21 16:44:32+10:00").as_basic).to eq({ - "pact:matcher:type" => "datetime", - "value" => "2020-05-21 16:44:32+10:00", - :format => "yyyy-MM-dd HH:mm:ssZZZZZ" - }) - end - - it "properly builds matcher for iso8601" do - expect(test_class.match_iso8601("2020-05-21T16:44:32").as_basic).to eq({ - "pact:matcher:type" => "regex", - "value" => "2020-05-21T16:44:32", - :regex => "(?i-mx:\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)*(.\\d{2}:\\d{2})*)" - }) - end - - it "properly builds matcher for date" do - expect(test_class.match_date("yyyy-MM-dd", "2020-05-21").as_basic).to eq({ - "pact:matcher:type" => "date", - "value" => "2020-05-21", - :format => "yyyy-MM-dd" - }) - end - - it "properly builds matcher for time" do - expect(test_class.match_time("HH:mm:ss", "16:44:32").as_basic).to eq({ - "pact:matcher:type" => "time", - "value" => "16:44:32", - :format => "HH:mm:ss" - }) - end - - it "properly builds matcher for include" do - expect(test_class.match_include("some string").as_basic).to eq({ - "pact:matcher:type" => "include", - "value" => "some string" - }) - end - - it "properly builds matcher for any string" do - expect(test_class.match_any_string.as_basic).to eq({ - "pact:matcher:type" => "regex", - "value" => "any", - :regex => "(?-mix:.*)" - }) - expect(test_class.match_any_string("").as_basic).to eq({ - "pact:matcher:type" => "regex", - "value" => "", - :regex => "(?-mix:.*)" - }) - end - - it "properly builds matcher for boolean values" do - expect(test_class.match_any_boolean.as_basic).to eq({ - "pact:matcher:type" => "boolean", - "value" => true - }) - end - - it "properly builds matcher for integer values" do - expect(test_class.match_any_integer.as_basic).to eq({ - "pact:matcher:type" => "integer", - "value" => 10 - }) - end - - it "properly builds matcher for float values" do - expect(test_class.match_any_decimal.as_basic).to eq({ - "pact:matcher:type" => "decimal", - "value" => 10.0 - }) - end - - it "properly builds matcher for exact values" do - expect(test_class.match_exactly("some arg").as_basic).to eq({ - "pact:matcher:type" => "equality", - "value" => "some arg" - }) - expect(test_class.match_exactly(1).as_basic).to eq({ - "pact:matcher:type" => "equality", - "value" => 1 - }) - expect(test_class.match_exactly(true).as_basic).to eq({ - "pact:matcher:type" => "equality", - "value" => true - }) - end - - it "properly builds typed matcher" do - expect(test_class.match_type_of(1).as_basic).to eq({ - "pact:matcher:type" => "type", - "value" => 1 - }) - expect { test_class.match_type_of(Object.new).as_basic }.to raise_error(/is not a primitive/) - end - - it "properly builds each matcher" do - expect(test_class.match_each(1).as_basic).to eq({ - "pact:matcher:type" => "type", - "value" => [1], - :min => 1 - }) - expect(test_class.match_each(true).as_basic).to eq({ - "pact:matcher:type" => "type", - "value" => [true], - :min => 1 - }) - expect(test_class.match_each("some").as_basic).to eq({ - "pact:matcher:type" => "type", - "value" => ["some"], - :min => 1 - }) - expect(test_class.match_each( - { - str: test_class.match_any_string("str"), - bool: test_class.match_any_boolean(true), - num: test_class.match_any_number(1), - nested: test_class.match_each( - { - a: 1, - b: "2" - } - ) - } - ).as_basic).to eq({ - "pact:matcher:type" => "type", - "value" => [ - { - str: { - "pact:matcher:type" => "regex", - :regex => "(?-mix:.*)", - "value" => "str" - }, - bool: { - "pact:matcher:type" => "boolean", - "value" => true - }, - num: { - "pact:matcher:type" => "number", - "value" => 1 - }, - nested: { - "pact:matcher:type" => "type", - "value" => [ - {a: 1, b: "2"} - ], - :min => 1 - } - } - ], - :min => 1 - }) - end - - it "properly builds each-key matcher" do - expect(test_class.match_each_key({"some-key" => "value"}, test_class.match_regex(/\w+-\w+/, "some-key")).as_basic).to eq( - { - "pact:matcher:type" => "each-key", - :rules => [ - { - "pact:matcher:type" => "regex", - :regex => "(?-mix:\\w+-\\w+)", - "value" => "some-key" - } - ], - "value" => {"some-key" => "value"} - } - ) - expect(test_class.match_each_key({"some-key" => {"value1" => 1, "value2" => 2}}, test_class.match_regex(/\w+-\w+/, "some-key")).as_basic).to eq( - { - "pact:matcher:type" => "each-key", - :rules => [ - { - "pact:matcher:type" => "regex", - :regex => "(?-mix:\\w+-\\w+)", - "value" => "some-key" - } - ], - "value" => {"some-key" => {"value1" => 1, "value2" => 2}} - } - ) - end - - it "properly builds each-value matcher" do - expect(test_class.match_each_value({"some-key" => "value"}, test_class.match_regex(/\w+/, "value")).as_basic).to eq( - { - "pact:matcher:type" => "each-value", - :rules => [ - { - "pact:matcher:type" => "regex", - :regex => "(?-mix:\\w+)", - "value" => "value" - } - ], - "value" => {"some-key" => "value"} - } - ) - expect(test_class.match_each_value( - {"some-key" => {"value1" => test_class.match_any_string("1"), "value2" => test_class.match_any_number(2)}}, - test_class.match_regex(/\w+-\w+/, "some-key") - ).as_basic).to eq( - { - "pact:matcher:type" => "each-value", - :rules => [ - { - "pact:matcher:type" => "regex", - :regex => "(?-mix:\\w+-\\w+)", - "value" => "some-key" - } - ], - "value" => { - "some-key" => { - "value1" => { - "pact:matcher:type" => "regex", - :regex => "(?-mix:.*)", - "value" => "1" - }, - "value2" => { - "pact:matcher:type" => "number", - "value" => 2 - } - } - } - } - ) - end - - it "properly builds each-key-value matcher" do - expect(test_class.match_each_kv( - { - "some-key" => { - "value1" => test_class.match_any_string("1") - } - }, test_class.match_regex(/\w+/, "value") - ).as_basic).to eq({ - "pact:matcher:type" => [ - { - "pact:matcher:type" => "each-key", - :rules => [ - { - "pact:matcher:type" => "regex", - :regex => "(?-mix:\\w+)", - "value" => "value" - } - ], - "value" => {} - }, - { - "pact:matcher:type" => "each-value", - :rules => [ - { - "pact:matcher:type" => "type", - "value" => "" - } - ], - "value" => {} - } - ], - "value" => { - "some-key" => { - "value1" => { - "pact:matcher:type" => "regex", - :regex => "(?-mix:.*)", - "value" => "1" - } - } - } - }) - end - - it "properly builds semver matcher" do - expect(test_class.match_semver.as_basic).to eq({ - "pact:matcher:type" => "semver", - }) - end - it "properly builds content_type matcher" do - expect(test_class.match_content_type("application/xml").as_basic).to eq({ - "pact:matcher:type" => "contentType", - "value" => "application/xml" - }) - end - it "properly builds not_empty matcher" do - expect(test_class.match_not_empty.as_basic).to eq({ - "pact:matcher:type" => "notEmpty" - }) - end - - it "properly builds values matcher" do - expect(Pact::V2::Matchers::V3::Values.new.as_basic).to eq({ - "pact:matcher:type" => "values" - }) - end - - it "properly builds null matcher" do - expect(Pact::V2::Matchers::V3::Null.new.as_basic).to eq({ - "pact:matcher:type" => "null" - }) - end - - it "properly builds status_code matcher" do - expect(test_class.match_status_code(200).as_basic).to eq({ - "pact:matcher:type" => "statusCode", - "status" => 200 - }) - expect(test_class.match_status_code('nonError').as_basic).to eq({ - "pact:matcher:type" => "statusCode", - "status" => 'nonError' - }) - end - end - - context "with plugin format serialization" do - it "properly builds matcher for UUID" do - expect(test_class.match_uuid.as_plugin).to eq("matching(regex, '(?i-mx:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})', 'e1d01e04-3a2b-4eed-a4fb-54f5cd257338')") - end - - it "properly builds matcher for regex" do - expect(test_class.match_regex(/(A-Z){1,3}/, "ABC").as_plugin).to eq("matching(regex, '(?-mix:(A-Z){1,3})', 'ABC')") - end - - it "properly builds matcher for datetime" do - expect(test_class.match_datetime("yyyy-MM-dd HH:mm:ssZZZZZ", "2020-05-21 16:44:32+10:00").as_plugin).to eq("matching(datetime, 'yyyy-MM-dd HH:mm:ssZZZZZ', '2020-05-21 16:44:32+10:00')") - end - - it "properly builds matcher for iso8601" do - expect(test_class.match_iso8601("2020-05-21T16:44:32").as_plugin).to eq("matching(regex, '(?i-mx:\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)*(.\\d{2}:\\d{2})*)', '2020-05-21T16:44:32')") - end - - it "properly builds matcher for date" do - expect(test_class.match_date("yyyy-MM-dd", "2020-05-21").as_plugin).to eq("matching(date, 'yyyy-MM-dd', '2020-05-21')") - end - - it "properly builds matcher for time" do - expect(test_class.match_time("HH:mm:ss", "16:44:32").as_plugin).to eq("matching(time, 'HH:mm:ss', '16:44:32')") - end - - it "properly builds matcher for include" do - expect(test_class.match_include("some string").as_plugin).to eq("matching(include, 'some string')") - end - - it "properly builds matcher for any string" do - expect(test_class.match_any_string.as_plugin).to eq("matching(regex, '(?-mix:.*)', 'any')") - expect(test_class.match_any_string("").as_plugin).to eq("matching(regex, '(?-mix:.*)', '')") - end - - it "properly builds matcher for boolean values" do - expect(test_class.match_any_boolean.as_plugin).to eq("matching(boolean, true)") - end - - it "properly builds matcher for integer values" do - expect(test_class.match_any_integer.as_plugin).to eq("matching(integer, 10)") - end - - it "properly builds matcher for float values" do - expect(test_class.match_any_decimal.as_plugin).to eq("matching(decimal, 10.0)") - end - - it "properly builds matcher for exact values" do - expect(test_class.match_exactly("some arg").as_plugin).to eq("matching(equalTo, 'some arg')") - expect(test_class.match_exactly(1).as_plugin).to eq("matching(equalTo, 1)") - expect(test_class.match_exactly(true).as_plugin).to eq("matching(equalTo, true)") - end - - it "properly builds typed matcher" do - expect(test_class.match_type_of(1).as_plugin).to eq("matching(type, 1)") - expect { test_class.match_type_of(Object.new).as_plugin }.to raise_error(/is not a primitive/) - end - - it "properly builds each matcher" do - expect(test_class.match_each(1).as_plugin).to eq("eachValue(matching(type, 1))") - expect(test_class.match_each(true).as_plugin).to eq("eachValue(matching(type, true))") - expect(test_class.match_each("some").as_plugin).to eq("eachValue(matching(type, 'some'))") - expect(test_class.match_each( - { - str: test_class.match_any_string("str"), - bool: test_class.match_any_boolean(true), - num: test_class.match_any_number(1), - nested: test_class.match_each( - { - a: 1, - b: "2" - } - ) - } - ).as_plugin).to eq({ - "pact:match" => "eachValue(matching($'SAMPLE'))", - "SAMPLE" => { - str: "matching(regex, '(?-mix:.*)', 'str')", - bool: "matching(boolean, true)", - num: "matching(number, 1)", - nested: { - "pact:match" => "eachValue(matching($'SAMPLE'))", - "SAMPLE" => {a: 1, b: "2"} - } - } - }) - end - - it "properly builds each-key matcher" do - expect(test_class.match_each_key({"some-key" => "value"}, test_class.match_regex(/\w+-\w+/, "some-key")).as_plugin).to eq("eachKey(matching(regex, '(?-mix:\\w+-\\w+)', 'some-key'))") - expect(test_class.match_each_key({"some-key" => {"value1" => 1, "value2" => 2}}, test_class.match_regex(/\w+-\w+/, "some-key")).as_plugin).to eq("eachKey(matching(regex, '(?-mix:\\w+-\\w+)', 'some-key'))") - end - - it "properly builds each-value matcher" do - expect(test_class.match_each_value( - { - str: test_class.match_any_string("str"), - bool: test_class.match_any_boolean(true), - num: test_class.match_any_number(1), - nested: test_class.match_each( - { - a: 1, - b: "2" - } - ) - } - ).as_plugin).to eq({ - "pact:match" => "eachValue(matching($'SAMPLE'))", - "SAMPLE" => { - str: "matching(regex, '(?-mix:.*)', 'str')", - bool: "matching(boolean, true)", - num: "matching(number, 1)", - nested: { - "pact:match" => "eachValue(matching($'SAMPLE'))", - "SAMPLE" => {a: 1, b: "2"} - } - } - }) - end - - it "properly builds semver matcher" do - expect(test_class.match_semver("1.2.3").as_plugin).to eq("matching(semver, '1.2.3')") - end - - it "properly builds content_type matcher" do - expect(test_class.match_content_type("application/xml", '').as_plugin).to eq("matching(contentType, 'application/xml', '')") - end - - it "properly builds not_empty matcher" do - expect(test_class.match_not_empty("some value").as_plugin).to eq("notEmpty('some value')") - end - end - - context "with common regex" do - it "has valid regex for iso8601" do - expect(described_class::ISO8601_REGEX).to match("2020-05-21T16:44:32") - expect(described_class::ISO8601_REGEX).to match("2020-05-21T16:44:32+10:00") - expect(described_class::ISO8601_REGEX).to match("2020-05-21T16:44:32.123+10:00") - expect(described_class::ISO8601_REGEX).to match("2020-05-21T16:44:32.123") - expect(described_class::ISO8601_REGEX).to match("2020-05-21T16:44:32.123456+10:00") - expect(described_class::ISO8601_REGEX).to match("2020-05-21T16:44:32.123456") - end - - it "has valid regex for UUID" do - expect(described_class::UUID_REGEX).to match(SecureRandom.uuid) - end - end -end diff --git a/spec/v2/pact/provider/base_verifier_spec.rb b/spec/v2/pact/provider/base_verifier_spec.rb deleted file mode 100644 index cb969409..00000000 --- a/spec/v2/pact/provider/base_verifier_spec.rb +++ /dev/null @@ -1,73 +0,0 @@ -# frozen_string_literal: true - -describe Pact::V2::Provider::BaseVerifier do - subject { described_class.new(Pact::V2::Provider::PactConfig::Base.new(provider_name: "provider")) } - - let(:build_selectors) { subject.send(:build_consumer_selectors, verify_only, consumer_name, consumer_branch) } - - context "when verify_only is defined" do - let(:verify_only) { ["consumer-1", "consumer-2"] } - - context "when consumer / branch are defined and matched" do - let(:consumer_name) { "consumer-1" } - let(:consumer_branch) { "32b53c01" } - - it "builds proper selectors" do - expect(build_selectors).to eq([{"branch" => "32b53c01", "consumer" => "consumer-1"}]) - end - end - - context "when consumer / branch are defined and not matched" do - let(:consumer_name) { "consumer-3" } - let(:consumer_branch) { "feature-branch" } - - it "builds proper selectors" do - expect(build_selectors).to be_empty - end - end - - context "when consumer is not defined" do - let(:consumer_name) { nil } - let(:consumer_branch) { nil } - - it "builds proper selectors" do - expect(build_selectors) - .to eq([ - {"consumer" => "consumer-1"}, - {"consumer" => "consumer-2"} - ]) - end - end - end - - context "when verify_only is not defined" do - let(:verify_only) { [] } - - context "when consumer / branch are defined" do - let(:consumer_name) { "consumer-1" } - let(:consumer_branch) { "32b53c01" } - - it "builds proper selectors" do - expect(build_selectors).to eq([{"branch" => "32b53c01", "consumer" => "consumer-1"}]) - end - end - - context "when only consumer is defined" do - let(:consumer_name) { "consumer-3" } - let(:consumer_branch) { nil } - - it "builds proper selectors" do - expect(build_selectors).to eq([{"consumer" => "consumer-3"}]) - end - end - - context "when consumer is not defined" do - let(:consumer_name) { nil } - let(:consumer_branch) { nil } - - it "builds proper selectors" do - expect(build_selectors).to eq([{}]) - end - end - end -end diff --git a/spec/v2/pact/provider/gruf_server_spec.rb b/spec/v2/pact/provider/gruf_server_spec.rb deleted file mode 100644 index c475f8cc..00000000 --- a/spec/v2/pact/provider/gruf_server_spec.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -describe Pact::V2::Provider::GrufServer do - let(:api) { ::PetStore::Grpc::PetStore::V1::Pets::Stub.new("localhost:3009", :this_channel_is_insecure) } - let(:call_rpc) do - subject.run { api.pet_by_id(PetStore::Grpc::PetStore::V1::PetByIdRequest.new(id: 1)) } - end - - context "when success" do - it "succeeds" do - resp = call_rpc - - expect(resp.pet.id).to eq 1 - expect(resp.pet.name).to eq "Jack" - end - end -end diff --git a/spec/v2/pact/provider/provider_server_runner_spec.rb b/spec/v2/pact/provider/provider_server_runner_spec.rb deleted file mode 100644 index fbf546d4..00000000 --- a/spec/v2/pact/provider/provider_server_runner_spec.rb +++ /dev/null @@ -1,70 +0,0 @@ -# frozen_string_literal: true - -describe Pact::V2::Provider::ProviderServerRunner do - let(:http_client) do - Faraday.new do |conn| - conn.response :json - conn.request :json - end - end - - let(:make_request) do - server.run { http_client.post("http://localhost:9001/setup-provider", request_body) } - end - - let(:server) do - subject.tap do |s| - s.add_setup_state("state1") {} - s.add_teardown_state("state1") {} - end - end - - context "with setup callback" do - let(:request_body) do - {"action" => "setup", "params" => {"param1" => "value1"}, "state" => "state1"} - end - - it "succeeds" do - expect_any_instance_of(Pact::V2::Provider::ProviderStateServlet).to receive(:call_setup).and_call_original - - response = make_request - expect(response.status).to eq(200) - end - end - - context "with teardown callback" do - let(:request_body) do - {"action" => "teardown", "params" => {"param1" => "value1"}, "state" => "state1"} - end - - it "succeeds" do - expect_any_instance_of(Pact::V2::Provider::ProviderStateServlet).to receive(:call_teardown).and_call_original - - response = make_request - expect(response.status).to eq(200) - end - end - - context "with unknown state callback" do - let(:request_body) do - {"action" => "unknown", "params" => {"param1" => "value1"}, "state" => "state1"} - end - - it "succeeds" do - expect_any_instance_of(Pact::V2::Provider::ProviderStateServlet).not_to receive(:call_setup) - expect_any_instance_of(Pact::V2::Provider::ProviderStateServlet).not_to receive(:call_teardown) - - response = make_request - expect(response.status).to eq(200) - end - end - - context "with unknown data" do - let(:request_body) { "non-json data" } - - it "fails" do - response = make_request - expect(response.status).to eq(500) - end - end -end diff --git a/tasks/foo-bar.rake b/tasks/foo-bar.rake deleted file mode 100644 index f2ab505d..00000000 --- a/tasks/foo-bar.rake +++ /dev/null @@ -1,56 +0,0 @@ -require 'pact/tasks/verification_task' -require 'faraday' -# Use for end to end manual debugging of issues. - -BROKER_BASE_URL = ENV.fetch('PACT_BROKER_BASE_URL', 'http://localhost:9292') -BROKER_USERNAME = ENV['PACT_BROKER_USERNAME'] -BROKER_PASSWORD = ENV['PACT_BROKER_PASSWORD'] -BROKER_TOKEN = ENV['PACT_BROKER_TOKEN'] - -RSpec::Core::RakeTask.new('pact:foobar:create') do | task | - task.pattern = "spec/features/foo_bar_spec.rb" -end - -task 'pact:foobar:publish' do - # Can't require pact_broker-client because it requires pact - circular dependency - require 'net/http' - uri = URI("#{BROKER_BASE_URL}/pacts/provider/Bar/consumer/Foo/version/1.0.0") - put_request = Net::HTTP::Put.new(uri.path) - put_request['Content-Type'] = "application/json" - put_request.body = File.read("spec/pacts/foo-bar.json") - put_request.basic_auth(BROKER_USERNAME, BROKER_PASSWORD) if BROKER_USERNAME - put_request['Authorization'] = "Bearer #{BROKER_TOKEN}" if BROKER_TOKEN - response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: BROKER_BASE_URL.start_with?('https')) do |http| - http.request put_request - end - puts response.code unless response.code == '200' - - # TODO put auth in these requests - tag_response = Faraday.put("#{BROKER_BASE_URL}/pacticipants/Foo/versions/1.0.0/tags/dev", nil, { 'Content-Type' => 'application/json' }) - puts tag_response.status unless tag_response.status == 200 - tag_response = Faraday.put("#{BROKER_BASE_URL}/pacticipants/Foo/versions/1.0.0/tags/prod", nil, { 'Content-Type' => 'application/json' }) - puts tag_response.status unless tag_response.status == 200 -end - -#'./spec/pacts/foo-bar.json' -Pact::VerificationTask.new('foobar') do | pact | - pact.uri nil, pact_helper: './spec/support/bar_pact_helper.rb' -end - -Pact::VerificationTask.new('foobar:wip') do | pact | - pact.uri './spec/pacts/foo-bar-wip.json', pact_helper: './spec/support/bar_pact_helper.rb' - pact.ignore_failures = true -end - -Pact::VerificationTask.new(:foobar_using_broker) do | pact | - pact.uri nil, :pact_helper => './spec/support/bar_pact_helper.rb', username: BROKER_USERNAME, password: BROKER_PASSWORD, token: BROKER_TOKEN -end - -Pact::VerificationTask.new('foobar_using_broker:fail') do | pact | - pact.uri "#{BROKER_BASE_URL}/pacts/provider/Bar/consumer/Foo/version/1.0.0", :pact_helper => './spec/support/bar_fail_pact_helper.rb', username: BROKER_USERNAME, password: BROKER_PASSWORD, token: BROKER_TOKEN -end - -task 'pact:verify:foobar' => ['pact:foobar:create'] -task 'pact:verify:foobar_using_broker' => ['pact:foobar:create', 'pact:foobar:publish'] -task 'pact:verify:foobar_using_broker:fail' => ['pact:foobar:create', 'pact:foobar:publish'] - diff --git a/tasks/message-test.rake b/tasks/message-test.rake deleted file mode 100644 index 680dbc0b..00000000 --- a/tasks/message-test.rake +++ /dev/null @@ -1,5 +0,0 @@ -require 'pact/tasks' - -Pact::VerificationTask.new(:message) do | pact | - pact.uri 'spec/support/foo-bar-message.json', pact_helper: 'spec/support/message_spec_helper.rb' -end diff --git a/tasks/pact-test.rake b/tasks/pact-test.rake deleted file mode 100644 index 1dae68b3..00000000 --- a/tasks/pact-test.rake +++ /dev/null @@ -1,135 +0,0 @@ -require 'pact/tasks/verification_task' -require 'open3' - -Pact::VerificationTask.new(:stubbing) do | pact | - pact.uri './spec/support/stubbing.json', :pact_helper => './spec/support/stubbing_using_allow.rb' -end - -Pact::VerificationTask.new(:options) do | pact | - pact.uri './spec/support/options.json', :pact_helper => './spec/support/options_app.rb' -end - -Pact::VerificationTask.new(:pass) do | pact | - pact.uri './spec/support/test_app_pass.json' -end - -Pact::VerificationTask.new(:fail) do | pact | - pact.uri './spec/support/test_app_fail.json' -end - -Pact::VerificationTask.new(:term) do | pact | - pact.uri './spec/support/term.json' -end - -Pact::VerificationTask.new(:response_body_term) do | pact | - pact.uri './spec/support/response_body_term.json', :pact_helper => './spec/support/response_body_term_app.rb' -end - -Pact::VerificationTask.new(:term_v2) do | pact | - pact.uri './spec/support/term-v2.json' -end - -Pact::VerificationTask.new(:case_insensitive_response_header_matching) do | pact | - pact.uri './spec/support/case-insensitive-response-header-matching.json', :pact_helper => './spec/support/case-insensitive-response-header-matching.rb' -end - -RSpec::Core::RakeTask.new('spec:standalone:fail') do | task | - task.pattern = FileList["spec/standalone/**/*_fail_test.rb"] -end - -RSpec::Core::RakeTask.new('spec:standalone:pass') do | task | - task.pattern = FileList["spec/standalone/**/*_pass_test.rb"] -end - -Pact::VerificationTask.new('test_app:pass') do | pact | - pact.uri './spec/support/test_app_pass.json' -end - -Pact::VerificationTask.new('test_app:content_type') do | pact | - pact.uri './spec/support/test_app_with_right_content_type_differ.json' -end - -Pact::VerificationTask.new('test_app:fail') do | pact | - pact.uri './spec/support/test_app_fail.json', pact_helper: './spec/support/pact_helper.rb' -end - -Pact::VerificationTask.new('test_app_with_provider_state_params') do | pact | - pact.uri './spec/support/provider_states_params_test.json', pact_helper: './spec/support/pact_helper_for_provider_state_params_test.rb' -end - -Pact::VerificationTask.new('test_app:wip') do | pact | - pact.uri './spec/support/test_app_fail.json', pact_helper: './spec/support/pact_helper.rb' - pact.ignore_failures = true -end - - -task :bethtest => ['pact:tests:all','pact:tests:all:with_active_support'] - -namespace :pact do - - desc "All the verification tests" - task "tests:all" do - next if Gem.win_platform? - - Rake::Task['pact:verify:stubbing'].execute - Rake::Task['spec:standalone:pass'].execute - Rake::Task['pact:verify'].execute - Rake::Task['pact:verify:test_app:pass'].execute - Rake::Task['pact:test:fail'].execute - Rake::Task['pact:test:pactfile'].execute - Rake::Task['pact:verify:test_app:content_type'].execute - Rake::Task['pact:verify:case_insensitive_response_header_matching'].execute - Rake::Task['pact:verify:term_v2'].execute - Rake::Task['pact:verify:test_app_with_provider_state_params'].execute - Rake::Task['pact:verify:test_app:wip'].execute - Rake::Task['pact:verify:message'].execute - end - - desc "All the verification tests with active support loaded" - task 'tests:all:with_active_support' => :set_active_support_on do - Rake::Task['pact:tests:all'].execute - end - - desc "Ensure pact file is written" - task 'test:pactfile' do - pact_path = './spec/pacts/standalone_consumer-standalone_provider.json' - FileUtils.rm_rf pact_path - Rake::Task['spec:standalone:pass'].execute - fail "Did not find expected pact file at #{pact_path}" unless File.exist?(pact_path) - end - - desc 'Runs pact tests against a sample application, testing failure and success.' - task 'test:fail' do - require 'open3' - silent = true - # Run these specs silently, otherwise expected failures will be written to stdout and look like unexpected failures. - #Pact.configuration.output_stream = StringIO.new if silent - - expect_to_fail "bundle exec rake pact:verify:test_app:fail", with: [/Could not find one or more provider states/] - expect_to_fail "bundle exec rake spec:standalone:fail", with: [/Actual interactions do not match expected interactions/] - expect_to_fail "bundle exec rake pact:verify:term", with: [%r{"Content-type" which matches /text/}] - expect_to_fail "bundle exec rake pact:verify:response_body_term", with: [%r{- "at": "2016-02-11T12:00:00Z"}] - end - - def expect_to_fail command, options = {} - success = execute_command command, options - fail "Expected '#{command}' to fail" if success - end - - def execute_command command, options - result = nil - Open3.popen3(command) {|stdin, stdout, stderr, wait_thr| - result = wait_thr.value - ensure_patterns_present(command, options, stdout, stderr) if options[:with] - } - result.success? - end - - def ensure_patterns_present command, options, stdout, stderr - require 'rainbow' - output = stdout.read + stderr.read - options[:with].each do | pattern | - raise (Rainbow("Could not find #{pattern.inspect} in output of #{command}").red + "\n\n#{output}") unless output =~ pattern - end - end -end diff --git a/tasks/release.rake b/tasks/release.rake index 422e4d21..ccc714e8 100644 --- a/tasks/release.rake +++ b/tasks/release.rake @@ -9,7 +9,7 @@ task :generate_changelog do end desc 'Tag for release' -task :tag_for_release do | t, args | +task :tag_for_release do |t, args| command = "git tag -a v#{Pact::VERSION} -m \"chore(release): version #{Pact::VERSION}\" && git push origin v#{Pact::VERSION}" puts command puts `#{command}` diff --git a/tasks/spec.rake b/tasks/spec.rake index c1218bad..0539422b 100644 --- a/tasks/spec.rake +++ b/tasks/spec.rake @@ -1,19 +1,17 @@ -RSpec::Core::RakeTask.new(:spec) do |t| +RSpec::Core::RakeTask.new('spec') do |t| t.pattern = 'spec/**/*_spec.rb' - t.exclude_pattern = 'spec/pact/**/*_spec.rb,spec/v2/**/*_spec.rb' -end -# Need to run this in separate process because left over state from -# testing the actual pact framework messes up the tests that actually -# use pact. -RSpec::Core::RakeTask.new('spec:provider') do |task| - task.pattern = 'spec/service_providers/**/*_test.rb' + t.rspec_opts = '--require spec_helper --require rails_helper' end -task :set_active_support_on do - ENV['LOAD_ACTIVE_SUPPORT'] = 'true' +RSpec::Core::RakeTask.new('pact:spec') do |task| + task.pattern = 'spec/pact/providers/**/*_spec.rb' + task.rspec_opts = ['-t pact', '--require spec_helper --require rails_helper'] end -desc 'This is to ensure that the gem still works even when active support JSON is loaded.' -task spec_with_active_support: [:set_active_support_on] do - Rake::Task['spec'].execute +RSpec::Core::RakeTask.new('pact:verify') do |task| + task.pattern = 'spec/pact/consumers/*_spec.rb' + task.rspec_opts = ['-t pact', '--require spec_helper --require rails_helper'] end + +desc 'Run all spec tasks' +task 'spec:all' => ['spec', 'pact:spec', 'pact:verify'] diff --git a/tasks/spec_v2.rake b/tasks/spec_v2.rake deleted file mode 100644 index c4ccbb74..00000000 --- a/tasks/spec_v2.rake +++ /dev/null @@ -1,34 +0,0 @@ -RSpec::Core::RakeTask.new('spec:v2') do |t| - t.pattern = 'spec/v2/**/*_spec.rb' - t.rspec_opts = '--require spec_helper_v2 --require rails_helper_v2' -end - -RSpec::Core::RakeTask.new('pact:v2:spec') do |task| - task.pattern = 'spec/pact/providers/**/*_spec.rb' - task.rspec_opts = ['-t pact_v2', '--require spec_helper_v2 --require rails_helper_v2'] -end - -RSpec::Core::RakeTask.new('pact:v2:verify') do |task| - task.pattern = 'spec/pact/consumers/*_spec.rb' - task.rspec_opts = ['-t pact_v2', '--require spec_helper_v2 --require rails_helper_v2'] -end - -# Need to run this in separate process because left over state from -# testing the actual pact framework messes up the tests that actually -# use pact. -# RSpec::Core::RakeTask.new('spec:provider') do |task| -# task.pattern = 'spec/service_providers/**/*_test.rb' -# end - -# task :set_active_support_on do -# ENV['LOAD_ACTIVE_SUPPORT'] = 'true' -# end - -# desc 'This is to ensure that the gem still works even when active support JSON is loaded.' -# task : [:set_active_support_on] do -# Rake::Task['pact:v2'].execute -# end - - -desc 'Run all v2 spec tasks' -task 'spec:v2:all' => ['spec:v2', 'pact:v2:spec', 'pact:v2:verify'] \ No newline at end of file