diff --git a/.rspec b/.rspec index d632cbc..58e8efe 100644 --- a/.rspec +++ b/.rspec @@ -1,4 +1,2 @@ --colour --require spec_helper ---require integration/spec_helper ---require unit/spec_helper diff --git a/.rspec_integration b/.rspec_integration deleted file mode 100644 index 58bc43e..0000000 --- a/.rspec_integration +++ /dev/null @@ -1,4 +0,0 @@ ---colour ---require spec_helper ---require integration/spec_helper ---pattern spec/integration/**/*_spec.rb diff --git a/.rspec_unit b/.rspec_unit deleted file mode 100644 index 5443833..0000000 --- a/.rspec_unit +++ /dev/null @@ -1,4 +0,0 @@ ---colour ---require spec_helper ---require unit/spec_helper ---pattern spec/unit/**/*_spec.rb diff --git a/README.md b/README.md index 730e35e..5bb7b17 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,9 @@ contract = Pacto.build_from_file('/path/to/contract.json', 'http://dummyprovider response = Net::HTTP.get_response(URI.parse('http://dummyprovider.com')).body contract.validate response, body_only: true ``` + +Pacto also has the ability to match a request signature to a contract that is currently in used, via ```Pacto.contract_for request_signature``` + ## Auto-Generated Stubs Pacto provides an API to be used in the consumer's acceptance tests. It uses a custom JSON Schema parser and generator diff --git a/Rakefile b/Rakefile index c375d74..9142001 100644 --- a/Rakefile +++ b/Rakefile @@ -18,16 +18,12 @@ Cucumber::Rake::Task.new(:journeys) do |t| t.cucumber_opts = 'features --format pretty' end -if defined?(RSpec) - desc 'Run unit tests' - task :unit do - abort unless system('rspec --option .rspec_unit') - end - - desc 'Run integration tests' - task :integration do - abort unless system('rspec --option .rspec_integration') - end +RSpec::Core::RakeTask.new(:unit) do |t| + t.pattern = 'spec/unit/**/*_spec.rb' +end - task :default => [:unit, :integration, :journeys, :rubocop, 'coveralls:push'] +RSpec::Core::RakeTask.new(:integration) do |t| + t.pattern = 'spec/integration/**/*_spec.rb' end + +task :default => [:unit, :integration, :journeys, :rubocop, 'coveralls:push'] diff --git a/lib/pacto.rb b/lib/pacto.rb index 0255535..3bfa01d 100644 --- a/lib/pacto.rb +++ b/lib/pacto.rb @@ -12,6 +12,7 @@ require 'pacto/core/contract_repository' require 'pacto/core/configuration' +require 'pacto/core/callback' require 'pacto/logger' require 'pacto/exceptions/invalid_contract.rb' require 'pacto/extensions' @@ -25,6 +26,7 @@ require 'pacto/hash_merge_processor' require 'pacto/stubs/built_in' require 'pacto/meta_schema' +require 'pacto/hooks/erb_hook' module Pacto class << self @@ -34,6 +36,7 @@ def configuration end def clear! + Pacto.configuration.provider.reset! @configuration = nil unregister_all! end diff --git a/lib/pacto/contract.rb b/lib/pacto/contract.rb index e1934a5..f54e722 100644 --- a/lib/pacto/contract.rb +++ b/lib/pacto/contract.rb @@ -1,18 +1,26 @@ module Pacto class Contract - def initialize(request, response) + attr_reader :values + + def initialize(request, response, file = nil) @request = request @response = response + @file = file end - def stub! - Pacto.configuration.provider.stub!(@request, stub_response) unless @request.nil? + def stub_contract! values = {} + @values = values + @stub = Pacto.configuration.provider.stub_request!(@request, stub_response) unless @request.nil? end def validate(response_gotten = provider_response, opt = {}) @response.validate(response_gotten, opt) end + def matches? request_signature + @stub.matches? request_signature unless @stub.nil? + end + private def provider_response @@ -22,5 +30,6 @@ def provider_response def stub_response @response.instantiate end + end end diff --git a/lib/pacto/contract_factory.rb b/lib/pacto/contract_factory.rb index ee825d6..53c9ea2 100644 --- a/lib/pacto/contract_factory.rb +++ b/lib/pacto/contract_factory.rb @@ -9,7 +9,7 @@ def self.build_from_file(contract_path, host, preprocessor) schema.validate definition request = Request.new(host, definition['request']) response = Response.new(definition['response']) - Contract.new(request, response) + Contract.new(request, response, contract_path) end def self.schema diff --git a/lib/pacto/core/callback.rb b/lib/pacto/core/callback.rb new file mode 100644 index 0000000..8e6ef30 --- /dev/null +++ b/lib/pacto/core/callback.rb @@ -0,0 +1,11 @@ +module Pacto + class Callback + def initialize(&block) + @callback = block + end + + def process(contracts, request_signature, response) + @callback.call contracts, request_signature, response + end + end +end diff --git a/lib/pacto/core/configuration.rb b/lib/pacto/core/configuration.rb index 7f16cc4..137e12c 100644 --- a/lib/pacto/core/configuration.rb +++ b/lib/pacto/core/configuration.rb @@ -1,6 +1,7 @@ module Pacto class Configuration attr_accessor :preprocessor, :postprocessor, :provider, :strict_matchers, :contracts_path, :logger + attr_reader :callback def initialize @preprocessor = ERBProcessor.new @@ -10,10 +11,20 @@ def initialize @contracts_path = nil @logger = Logger.instance @logger.level = :debug if ENV['PACTO_DEBUG'] + @callback = Pacto::Hooks::ERBHook.new end def register_contract(contract = nil, *tags) Pacto.register_contract(contract, *tags) end + + def register_callback(callback = nil, &block) + if block_given? + @callback = Pacto::Callback.new(&block) + else + raise 'Expected a Pacto::Callback' unless callback.is_a? Pacto::Callback + @callback = callback + end + end end end diff --git a/lib/pacto/core/contract_repository.rb b/lib/pacto/core/contract_repository.rb index c5a843e..65d08f7 100644 --- a/lib/pacto/core/contract_repository.rb +++ b/lib/pacto/core/contract_repository.rb @@ -10,15 +10,13 @@ def register_contract(contract = nil, *tags) registered.count - start_count end - def use(tag, values = nil) - merged_contracts = registered[:default].merge registered[tag] + def use(tag, values = {}) + merged_contracts = registered[:default] + registered[tag] raise ArgumentError, "contract \"#{tag}\" not found" if merged_contracts.empty? - configuration.provider.values = values - merged_contracts.each do |contract| - contract.stub! + contract.stub_contract! values end merged_contracts.count end @@ -30,5 +28,17 @@ def registered def unregister_all! registered.clear end + + def contract_for(request_signature) + matches = Set.new + registered.values.each do |contract_set| + contract_set.each do |contract| + if contract.matches? request_signature + matches.add contract + end + end + end + matches + end end end diff --git a/lib/pacto/hooks/erb_hook.rb b/lib/pacto/hooks/erb_hook.rb new file mode 100644 index 0000000..282dc9c --- /dev/null +++ b/lib/pacto/hooks/erb_hook.rb @@ -0,0 +1,17 @@ +module Pacto + module Hooks + class ERBHook < Pacto::Callback + def initialize + @processor = ERBProcessor.new + end + + def process(contracts, request_signature, response) + bound_values = contracts.empty? ? {} : contracts.first.values + bound_values.merge!({:req => { 'HEADERS' => request_signature.headers}}) + response.body = @processor.process response.body, bound_values + response.body + end + + end + end +end diff --git a/lib/pacto/stubs/built_in.rb b/lib/pacto/stubs/built_in.rb index 54272f6..ecc18af 100644 --- a/lib/pacto/stubs/built_in.rb +++ b/lib/pacto/stubs/built_in.rb @@ -1,13 +1,12 @@ module Pacto module Stubs class BuiltIn - attr_accessor :values def initialize register_callbacks end - def stub! request, response + def stub_request! request, response stub = WebMock.stub_request(request.method, "#{request.host}#{request.path}") stub = stub.with(request_details(request)) if Pacto.configuration.strict_matchers stub.to_return({ @@ -17,28 +16,20 @@ def stub! request, response }) end - def process(request_signature, response) - unless processor.nil? - bound_values = {} - bound_values.merge!({:req => {'HEADERS' => request_signature.headers}}) if processor.class == ERBProcessor - bound_values.merge! @values unless @values.nil? - response.body = processor.process response.body, bound_values - end - response.body + def reset! + WebMock.reset! + WebMock.reset_callbacks end private def register_callbacks WebMock.after_request do |request_signature, response| - process request_signature, response + contracts = Pacto.contract_for request_signature + Pacto.configuration.callback.process contracts, request_signature, response end end - def processor - Pacto.configuration.postprocessor - end - def format_body(body) if body.is_a?(Hash) || body.is_a?(Array) body.to_json diff --git a/spec/integration/e2e_spec.rb b/spec/integration/e2e_spec.rb index 2d591e3..27cff10 100644 --- a/spec/integration/e2e_spec.rb +++ b/spec/integration/e2e_spec.rb @@ -39,19 +39,21 @@ context 'Journey' do it 'stubs multiple services with a single use' do - Pacto.configure do |c| + c.strict_matchers = false c.postprocessor = Pacto::ERBProcessor.new c.preprocessor = nil + c.register_callback Pacto::Hooks::ERBHook.new end + # Preprocessor must be off before building! login_contract = Pacto.build_from_file(contract_path, 'http://dummyprovider.com') contract = Pacto.build_from_file(strict_contract_path, 'http://dummyprovider.com') - Pacto.configure do |c| c.register_contract login_contract, :default c.register_contract contract, :devices end + Pacto.use(:devices, {:device_id => 42}) raw_response = HTTParty.get('http://dummyprovider.com/hello', headers: {'Accept' => 'application/json' }) diff --git a/spec/integration/spec_helper.rb b/spec/integration/spec_helper.rb deleted file mode 100644 index e69de29..0000000 diff --git a/spec/integration/templating_spec.rb b/spec/integration/templating_spec.rb index fd5afa7..5e0cf70 100644 --- a/spec/integration/templating_spec.rb +++ b/spec/integration/templating_spec.rb @@ -30,6 +30,9 @@ c.preprocessor = nil c.postprocessor = nil c.strict_matchers = false + c.register_callback do |contracts, req, res| + res + end end expect(response.keys).to eq ['message'] @@ -41,6 +44,7 @@ it 'processes erb on each request' do Pacto.configure do |c| c.preprocessor = nil + c.strict_matchers = false c.postprocessor = Pacto::ERBProcessor.new end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 12c0a35..fe58891 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -8,4 +8,11 @@ config.expect_with :rspec do |c| c.syntax = :expect end + config.before(:each) do + provider = Pacto.configuration.provider + unless provider.respond_to? :reset! + provider.stub(:reset!) + end + Pacto.clear! + end end diff --git a/spec/unit/hooks/erb_hook_spec.rb b/spec/unit/hooks/erb_hook_spec.rb new file mode 100644 index 0000000..d23cc6d --- /dev/null +++ b/spec/unit/hooks/erb_hook_spec.rb @@ -0,0 +1,51 @@ +describe Pacto::Hooks::ERBHook do + describe '.process' do + let(:req) { + OpenStruct.new({:headers => {'User-Agent' => 'abcd'}}) + } + let(:converted_req) { + {'HEADERS' => {'User-Agent' => 'abcd'}} + } + let(:res) { + OpenStruct.new({:body => 'before'}) + } + + before do + end + + context 'no matching contracts' do + it 'binds the request' do + contracts = Set.new + mock_erb({ :req => converted_req }) + described_class.new.process contracts, req, res + expect(res.body).to eq('after') + end + end + + context 'one matching contract' do + it 'binds the request and the contract\'s values' do + contract = OpenStruct.new({:values => {:max => 'test'}}) + contracts = Set.new([contract]) + mock_erb({ :req => converted_req, :max => 'test'}) + described_class.new.process contracts, req, res + expect(res.body).to eq('after') + end + end + + context 'multiple matching contracts' do + it 'binds the request and the first contract\'s values' do + contract1 = OpenStruct.new({:values => {:max => 'test'}}) + contract2 = OpenStruct.new({:values => {:mob => 'team'}}) + res = OpenStruct.new({:body => 'before'}) + mock_erb({ :req => converted_req, :max => 'test'}) + contracts = Set.new([contract1, contract2]) + described_class.new.process contracts, req, res + expect(res.body).to eq('after') + end + end + end + + def mock_erb(hash) + Pacto::ERBProcessor.any_instance.should_receive(:process).with('before', hash).and_return('after') + end +end diff --git a/spec/unit/pacto/contract_spec.rb b/spec/unit/pacto/contract_spec.rb index 8c23921..0a2c0ab 100644 --- a/spec/unit/pacto/contract_spec.rb +++ b/spec/unit/pacto/contract_spec.rb @@ -1,23 +1,23 @@ module Pacto describe Contract do let(:request) { double 'request' } + let(:request_signature) { double 'request_signature' } let(:response) { double 'response' } - - let(:contract) { described_class.new request, response } let(:provider) { double 'provider' } + let(:instantiated_response) { double 'instantiated response' } - describe '#stub!' do - before do - response.stub(:instantiate => instantiated_response) - Pacto.configuration.provider = provider - end + subject(:contract) { described_class.new request, response } - let(:instantiated_response) { double 'instantiated response' } + before do + response.stub(:instantiate => instantiated_response) + Pacto.configuration.provider = provider + end + describe '#stub_contract!' do it 'instantiates the response and registers a stub' do response.should_receive :instantiate - provider.should_receive(:stub!).with request, instantiated_response - contract.stub! + provider.should_receive(:stub_request!).with request, instantiated_response + contract.stub_contract! end end @@ -51,5 +51,28 @@ module Pacto end end end + + describe '#matches?' do + let(:request_matcher) do + double('fake request matcher').tap do |matcher| + matcher.stub(:matches?) { |r| r == request_signature } + end + end + + context 'when the contract is not stubbed' do + it 'returns false' do + expect(contract.matches? request_signature).to be_false + end + end + + context 'when the contract is stubbed' do + it 'returns true if it matches the request' do + provider.should_receive(:stub_request!).with(request, instantiated_response).and_return(request_matcher) + contract.stub_contract! + expect(contract.matches? request_signature).to be_true + expect(contract.matches? :anything).to be_false + end + end + end end end diff --git a/spec/unit/pacto/core/configuration_spec.rb b/spec/unit/pacto/core/configuration_spec.rb index 79d4faa..6860597 100644 --- a/spec/unit/pacto/core/configuration_spec.rb +++ b/spec/unit/pacto/core/configuration_spec.rb @@ -16,5 +16,13 @@ end expect(Pacto.configuration.contracts_path).to eq(contracts_path) end + + it 'register a Pacto Callback' do + callback_block = Pacto::Callback.new { } + Pacto.configure do |c| + c.register_callback(callback_block) + end + expect(Pacto.configuration.callback).to eq(callback_block) + end end end diff --git a/spec/unit/pacto/core/contract_repository_spec.rb b/spec/unit/pacto/core/contract_repository_spec.rb index c048679..099069d 100644 --- a/spec/unit/pacto/core/contract_repository_spec.rb +++ b/spec/unit/pacto/core/contract_repository_spec.rb @@ -70,13 +70,13 @@ let(:response_body) { double('response_body') } it 'stubs a contract with default values' do - contract.should_receive(:stub!) - another_contract.should_receive(:stub!) + contract.should_receive(:stub_contract!) + another_contract.should_receive(:stub_contract!) expect(described_class.use(tag)).to eq 2 end it 'stubs default contract if unused tag' do - another_contract.should_receive(:stub!) + another_contract.should_receive(:stub_contract!) expect(described_class.use(another_tag)).to eq 1 end end @@ -96,4 +96,38 @@ expect(described_class.registered).to be_empty end end + + describe '.contract_for' do + let(:request_signature) { double('request signature') } + + context 'when no contracts are found for a request' do + it 'returns an empty list' do + expect(described_class.contract_for request_signature).to be_empty + end + end + + context 'when contracts are found for a request' do + let(:contracts_that_match) { create_contracts 2, true } + let(:contracts_that_dont_match) { create_contracts 3, false } + let(:all_contracts) { contracts_that_match + contracts_that_dont_match } + + it 'returns the matching contracts' do + register_and_use all_contracts + expect(described_class.contract_for request_signature).to eq(contracts_that_match) + end + end + end + + def create_contracts(total, matches) + total.times.map do + double('contract', + :stub_contract! => double('request matcher'), + :matches? => matches) + end.to_set + end + + def register_and_use contracts + contracts.each { |contract| described_class.register_contract contract } + Pacto.use :default + end end diff --git a/spec/unit/pacto/stubs/built_in_spec.rb b/spec/unit/pacto/stubs/built_in_spec.rb index 34e239a..eb4994f 100644 --- a/spec/unit/pacto/stubs/built_in_spec.rb +++ b/spec/unit/pacto/stubs/built_in_spec.rb @@ -38,7 +38,7 @@ module Stubs end end - describe '#stub!' do + describe '#stub_request!' do before do WebMock.should_receive(:stub_request). with(request.method, "#{request.host}#{request.path}"). @@ -65,7 +65,7 @@ module Stubs stubbed_request.stub(:with).and_return(stubbed_request) - described_class.new.stub! request, response + described_class.new.stub_request! request, response end end @@ -83,7 +83,7 @@ module Stubs stubbed_request.stub(:with).and_return(stubbed_request) - described_class.new.stub! request, response + described_class.new.stub_request! request, response end end @@ -99,7 +99,7 @@ module Stubs stubbed_request.stub(:with).and_return(stubbed_request) - described_class.new.stub! request, response + described_class.new.stub_request! request, response end end @@ -110,7 +110,7 @@ module Stubs stubbed_request.should_receive(:with). with({:headers => request.headers, :query => request.params}). and_return(stubbed_request) - described_class.new.stub! request, response + described_class.new.stub_request! request, response end end @@ -121,7 +121,7 @@ module Stubs stubbed_request.should_receive(:with). with({:headers => request.headers, :body => request.params}). and_return(stubbed_request) - described_class.new.stub! request, response + described_class.new.stub_request! request, response end end @@ -140,7 +140,7 @@ module Stubs stubbed_request.should_receive(:with). with({:query => request.params}). and_return(stubbed_request) - described_class.new.stub! request, response + described_class.new.stub_request! request, response end end @@ -159,7 +159,7 @@ module Stubs stubbed_request.should_receive(:with). with({}). and_return(stubbed_request) - described_class.new.stub! request, response + described_class.new.stub_request! request, response end end end diff --git a/spec/unit/spec_helper.rb b/spec/unit/spec_helper.rb deleted file mode 100644 index 12e752a..0000000 --- a/spec/unit/spec_helper.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'coveralls_helper' - -RSpec.configure do |config| - config.before(:each) do - Pacto.clear! - end -end