From bae60638f93db3e48476c3bd17e87f879d1d95e0 Mon Sep 17 00:00:00 2001 From: Tucker McKnight Date: Wed, 6 Jun 2018 16:31:25 -0600 Subject: [PATCH 1/5] feat(provider params): add support for provider state parameters Allow provider parameters to change the request content before the request is replayed. The provider state returns a hash of parameters, which is passed in when the interaction is replayed. --- lib/pact/consumer.rb | 1 + lib/pact/provider/matchers/messages.rb | 1 + lib/pact/provider/request.rb | 7 +- lib/pact/provider/rspec.rb | 5 +- lib/pact/provider/state/provider_state.rb | 6 ++ .../provider/state/provider_state_manager.rb | 5 +- lib/pact/provider/test_methods.rb | 4 +- spec/features/foo_bar_spec.rb | 64 +++++++++++++------ spec/features/production_spec.rb | 36 +++++++++++ spec/features/provider_states/zebras.rb | 15 +++++ spec/support/pact_helper.rb | 11 +++- spec/support/provider_param.json | 46 +++++++++++++ tasks/pact-test.rake | 5 ++ 13 files changed, 179 insertions(+), 27 deletions(-) create mode 100644 spec/support/provider_param.json diff --git a/lib/pact/consumer.rb b/lib/pact/consumer.rb index 37bd7934..e478c919 100644 --- a/lib/pact/consumer.rb +++ b/lib/pact/consumer.rb @@ -4,4 +4,5 @@ require 'pact/consumer/consumer_contract_builders' require 'pact/consumer/interaction_builder' require 'pact/term' +require 'pact/provider_param' require 'pact/something_like' diff --git a/lib/pact/provider/matchers/messages.rb b/lib/pact/provider/matchers/messages.rb index c5ad9fb4..1d01aaa3 100644 --- a/lib/pact/provider/matchers/messages.rb +++ b/lib/pact/provider/matchers/messages.rb @@ -1,5 +1,6 @@ require 'term/ansicolor' require 'pact/term' +require 'pact/provider_param' module Pact module Matchers diff --git a/lib/pact/provider/request.rb b/lib/pact/provider/request.rb index 3aaf0662..fef1d611 100644 --- a/lib/pact/provider/request.rb +++ b/lib/pact/provider/request.rb @@ -10,8 +10,9 @@ class Replayable # See https://github.com/rack/rack/blob/e7d741c6282ca4cf4e01506f5681e6e6b14c0b32/SPEC#L87-89 NO_HTTP_PREFIX = ["CONTENT-TYPE", "CONTENT-LENGTH"] - def initialize expected_request + def initialize expected_request, provider_params={} @expected_request = expected_request + @provider_params = provider_params end def method @@ -19,7 +20,7 @@ def method end def path - expected_request.full_path + expected_request.full_path(@provider_params) end def body @@ -35,7 +36,7 @@ def headers request_headers = {} return request_headers if expected_request.headers.is_a?(Pact::NullExpectation) expected_request.headers.each do |key, value| - request_headers[rack_request_header_for(key)] = Pact::Reification.from_term(value) + request_headers[rack_request_header_for(key)] = Pact::Reification.from_term(value, @provider_params) end request_headers end diff --git a/lib/pact/provider/rspec.rb b/lib/pact/provider/rspec.rb index e6ed3c2d..55b1f943 100644 --- a/lib/pact/provider/rspec.rb +++ b/lib/pact/provider/rspec.rb @@ -82,8 +82,9 @@ def describe_interaction interaction, options before do | example | interaction_context.run_once :before do Pact.configuration.logger.info "Running example '#{Pact::RSpec.full_description(example)}'" - set_up_provider_state interaction.provider_state, options[:consumer] - replay_interaction interaction + call_params = set_up_provider_state interaction.provider_state, options[:consumer] + interaction_context.call_params = call_params + replay_interaction interaction, call_params interaction_context.last_response = last_response end end diff --git a/lib/pact/provider/state/provider_state.rb b/lib/pact/provider/state/provider_state.rb index 435510b0..b7fd46b8 100644 --- a/lib/pact/provider/state/provider_state.rb +++ b/lib/pact/provider/state/provider_state.rb @@ -68,6 +68,7 @@ class ProviderState attr_accessor :name attr_accessor :namespace + attr_accessor :provider_params extend Pact::DSL @@ -77,6 +78,11 @@ def initialize name, namespace, &block @set_up_defined = false @tear_down_defined = false @no_op_defined = false + @provider_params = {} + end + + def provider_param var_name, value + @provider_params[var_name] = value end dsl do diff --git a/lib/pact/provider/state/provider_state_manager.rb b/lib/pact/provider/state/provider_state_manager.rb index 72457caf..a5b0290c 100644 --- a/lib/pact/provider/state/provider_state_manager.rb +++ b/lib/pact/provider/state/provider_state_manager.rb @@ -13,8 +13,11 @@ def set_up_provider_state get_global_base_provider_state.set_up get_consumer_base_provider_state.set_up if provider_state_name - get_provider_state.set_up + provider_state = get_provider_state + provider_state.set_up + return provider_state.provider_params end + return {} end def tear_down_provider_state diff --git a/lib/pact/provider/test_methods.rb b/lib/pact/provider/test_methods.rb index 76486685..b8a405c9 100644 --- a/lib/pact/provider/test_methods.rb +++ b/lib/pact/provider/test_methods.rb @@ -14,8 +14,8 @@ module TestMethods include Pact::Logging include Rack::Test::Methods - def replay_interaction interaction - request = Request::Replayable.new(interaction.request) + def replay_interaction interaction, provider_params={} + request = Request::Replayable.new(interaction.request, provider_params) 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" diff --git a/spec/features/foo_bar_spec.rb b/spec/features/foo_bar_spec.rb index 9a37bbe5..791c08e3 100644 --- a/spec/features/foo_bar_spec.rb +++ b/spec/features/foo_bar_spec.rb @@ -8,37 +8,65 @@ describe "Bar", :pact => true do - it "can retrieve a thing" do - - Pact.clear_configuration - Pact.clear_consumer_world + before :all 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" - port 4638 - end + Pact.service_consumer "Foo" do + has_pact_with "Bar" do + mock_service :bar_service do + pact_specification_version "2" + port 4638 end end + end + end - bar_service. - upon_receiving("a retrieve thing request").with({ + it "can retrieve a thing" do + bar_service. + upon_receiving("a retrieve thing request").with({ method: :get, path: '/thing' }). - will_respond_with({ + will_respond_with({ status: 200, headers: { 'Content-Type' => 'application/json' }, body: Pact.each_like({status: Pact.term(/\d+/, "4")}) }) - bar_response = Net::HTTP.get_response(URI('http://localhost:4638/thing')) + 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 [{"status" => "4"}] + expect(bar_response.code).to eql '200' + expect(JSON.parse(bar_response.body)).to eq [{"status" => "4"}] - puts bar_service.write_pact + puts bar_service.write_pact end -end \ No newline at end of file + it 'can retrieve a specific thing with a provider param' do + bar_service. + upon_receiving("a retrieve specific thing request").with({ + method: :get, + path: Pact.provider_param('/thing/:{id}', {id: '99'}), + headers: { + Authorization: Pact.provider_param('Bearer :{auth_token}', {auth_token: 'faketoken'}) + } + }). + will_respond_with({ + status: 200, + headers: { 'Content-Type' => 'application/json' }, + body: Pact.each_like({status: Pact.term(/\d+/, "4")}) + }) + + uri = URI('http://localhost:4638/thing/99') + req = Net::HTTP::Get.new(uri) + req['Authorization'] = 'Bearer faketoken' + bar_response = Net::HTTP.start(uri.hostname, uri.port) { |http| + http.request(req) + } + + expect(bar_response.code).to eql '200' + expect(JSON.parse(bar_response.body)).to eq [{"status" => "4"}] + + puts bar_service.write_pact + end +end diff --git a/spec/features/production_spec.rb b/spec/features/production_spec.rb index ca4ccef8..903e8a69 100644 --- a/spec/features/production_spec.rb +++ b/spec/features/production_spec.rb @@ -30,6 +30,11 @@ def find_zebra_names end def call(env) + if (env['HTTP_AUTHORIZATION']) + if env['HTTP_AUTHORIZATION'] != 'password' + return [401, {'Content-Type' => 'application/json'}, {error: "The password is 'password'"}.to_json] + end + end case env['PATH_INFO'] when "/zebra_names" [200, {'Content-Type' => 'application/json'}, { names: find_zebra_names }.to_json] @@ -151,5 +156,36 @@ def call(env) honour_consumer_contract consumer_contract end + context "with a header being added by call_params" 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 + + honour_consumer_contract consumer_contract +end + end end diff --git a/spec/features/provider_states/zebras.rb b/spec/features/provider_states/zebras.rb index 916bcf6d..a7e6d47c 100644 --- a/spec/features/provider_states/zebras.rb +++ b/spec/features/provider_states/zebras.rb @@ -26,3 +26,18 @@ FileUtils.rm_rf("tmp/a_mock_database.json") end end + +Pact.provider_state "zebra with provider param" do + set_up do + some_data = { + id: '99', + name: 'Zee' + } + File.open("tmp/a_mock_database.json", "w") { |file| file << some_data.to_json } + provider_param :id, '99' + end + + tear_down do + FileUtils.rm_rf("tmp/a_mock_database.json") + end +end diff --git a/spec/support/pact_helper.rb b/spec/support/pact_helper.rb index a994556d..30026622 100644 --- a/spec/support/pact_helper.rb +++ b/spec/support/pact_helper.rb @@ -13,7 +13,9 @@ def call env [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 + elsif env['PATH_INFO'] == '/food_item/7' + [200, {'Content-Type' => 'application/json'}, [{message: 'One apple'}.to_json]] + else raise "unexpected path #{env['PATH_INFO']}!!!" end end @@ -46,6 +48,13 @@ def call env WEATHER[:current_state] = 'sunny' end + + provider_state 'create one apple' do + set_up do + provider_param :item, 'apple' + provider_param :id, '7' + end + end end end diff --git a/spec/support/provider_param.json b/spec/support/provider_param.json new file mode 100644 index 00000000..112b6f42 --- /dev/null +++ b/spec/support/provider_param.json @@ -0,0 +1,46 @@ +{ + "consumer": { + "name": "some-test-consumer" + }, + "provider": { + "name": "an unknown provider" + }, + "interactions": [ + { + "description": "a test request", + "request": { + "method": "get", + "path": "/food_item/5", + "query": "", + "headers": { + "Authorization": "Bearer faketoken" + }, + "matchingRules": { + "$.path": { + "match": "provider_param", + "fill_string": "/food_item/:{id}" + }, + "$.headers.Authorization": { + "match": "provider_param", + "fill_string": "Bearer :{auth_token}" + } + } + }, + "response": { + "matchingRules": { + "$.headers.Content-Type" : { + "match": "regex", "regex": "json" + } + }, + "status": 200, + "headers" : { + "Content-Type": "foo/json" + }, + "body": { + "message" : "One apple" + } + }, + "provider_state": "create one apple" + } + ] +} diff --git a/tasks/pact-test.rake b/tasks/pact-test.rake index 9d49fd2a..e6e834ef 100644 --- a/tasks/pact-test.rake +++ b/tasks/pact-test.rake @@ -29,6 +29,10 @@ Pact::VerificationTask.new(:term_v2) do | pact | pact.uri './spec/support/term-v2.json' end +Pact::VerificationTask.new(:provider_param) do |pact| + pact.uri './spec/support/provider_param.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 @@ -68,6 +72,7 @@ namespace :pact do 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:provider_param'].execute end desc "All the verification tests with active support loaded" From 3bda3624f3cc05bcef8ed62ad2e7ee306d2b2395 Mon Sep 17 00:00:00 2001 From: Tucker McKnight Date: Wed, 13 Jun 2018 15:47:50 -0600 Subject: [PATCH 2/5] Remove a duplicate test --- spec/features/production_spec.rb | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/spec/features/production_spec.rb b/spec/features/production_spec.rb index 903e8a69..662d7854 100644 --- a/spec/features/production_spec.rb +++ b/spec/features/production_spec.rb @@ -156,36 +156,5 @@ def call(env) honour_consumer_contract consumer_contract end - context "with a header being added by call_params" 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 - - honour_consumer_contract consumer_contract -end - end end From 225d23945785e2919b7de632b06a589c86f89d63 Mon Sep 17 00:00:00 2001 From: Tucker McKnight Date: Wed, 13 Jun 2018 16:05:30 -0600 Subject: [PATCH 3/5] Remove unused assignment --- lib/pact/provider/rspec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/pact/provider/rspec.rb b/lib/pact/provider/rspec.rb index 55b1f943..b440ecc5 100644 --- a/lib/pact/provider/rspec.rb +++ b/lib/pact/provider/rspec.rb @@ -83,7 +83,6 @@ def describe_interaction interaction, options interaction_context.run_once :before do Pact.configuration.logger.info "Running example '#{Pact::RSpec.full_description(example)}'" call_params = set_up_provider_state interaction.provider_state, options[:consumer] - interaction_context.call_params = call_params replay_interaction interaction, call_params interaction_context.last_response = last_response end From b1fe36f904ae458aaeaaaecafec3c5da66351197 Mon Sep 17 00:00:00 2001 From: Tucker McKnight Date: Wed, 13 Jun 2018 17:08:29 -0600 Subject: [PATCH 4/5] style(provider params): whitespace consistency --- spec/support/pact_helper.rb | 12 ++++++------ tasks/pact-test.rake | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/spec/support/pact_helper.rb b/spec/support/pact_helper.rb index 30026622..407ccecc 100644 --- a/spec/support/pact_helper.rb +++ b/spec/support/pact_helper.rb @@ -49,12 +49,12 @@ def call env WEATHER[:current_state] = 'sunny' end - provider_state 'create one apple' do - set_up do - provider_param :item, 'apple' - provider_param :id, '7' - end - end + provider_state 'create one apple' do + set_up do + provider_param :item, 'apple' + provider_param :id, '7' + end + end end end diff --git a/tasks/pact-test.rake b/tasks/pact-test.rake index e6e834ef..7a174e5d 100644 --- a/tasks/pact-test.rake +++ b/tasks/pact-test.rake @@ -30,7 +30,7 @@ Pact::VerificationTask.new(:term_v2) do | pact | end Pact::VerificationTask.new(:provider_param) do |pact| - pact.uri './spec/support/provider_param.json' + pact.uri './spec/support/provider_param.json' end Pact::VerificationTask.new(:case_insensitive_response_header_matching) do | pact | @@ -72,7 +72,7 @@ namespace :pact do 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:provider_param'].execute + Rake::Task['pact:verify:provider_param'].execute end desc "All the verification tests with active support loaded" From d64b69782f98cf773233082230707d288d47fe48 Mon Sep 17 00:00:00 2001 From: Tucker McKnight Date: Wed, 13 Jun 2018 17:10:22 -0600 Subject: [PATCH 5/5] style(provider params): whitespace consistency --- spec/support/pact_helper.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/support/pact_helper.rb b/spec/support/pact_helper.rb index 407ccecc..4e77aa1d 100644 --- a/spec/support/pact_helper.rb +++ b/spec/support/pact_helper.rb @@ -13,9 +13,9 @@ def call env [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]] - elsif env['PATH_INFO'] == '/food_item/7' - [200, {'Content-Type' => 'application/json'}, [{message: 'One apple'}.to_json]] - else + elsif env['PATH_INFO'] == '/food_item/7' + [200, {'Content-Type' => 'application/json'}, [{message: 'One apple'}.to_json]] + else raise "unexpected path #{env['PATH_INFO']}!!!" end end