This repository has been archived by the owner on Dec 5, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 58
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #134 from thoughtworks/forensics
New RSpec matcher (have_investigated)
- Loading branch information
Showing
15 changed files
with
375 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,26 @@ | ||
module Pacto | ||
class PactoResponse | ||
# FIXME: Need case insensitive header lookup, but case-sensitive storage | ||
attr_accessor :headers, :body, :status | ||
attr_accessor :headers, :body, :status, :parsed_body | ||
attr_reader :parsed_body | ||
|
||
def initialize(data) | ||
mash = Hashie::Mash.new data | ||
@headers = mash.headers.nil? ? {} : mash.headers | ||
@body = mash.body | ||
@body = mash.body | ||
@status = mash.status.to_i | ||
end | ||
|
||
def parsed_body | ||
if body.is_a?(String) && content_type == 'application/json' | ||
JSON.parse(body) | ||
else | ||
body | ||
end | ||
end | ||
|
||
def content_type | ||
headers['Content-Type'] | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
module Pacto | ||
module Forensics | ||
class FilterExhaustedError < StandardError | ||
attr_reader :suspects | ||
|
||
def initialize(msg, filter, suspects = []) | ||
@suspects = suspects | ||
if filter.respond_to? :description | ||
msg = "#{msg} #{filter.description}" | ||
else | ||
msg = "#{msg} #{filter}" | ||
end | ||
super(msg) | ||
end | ||
end | ||
|
||
class InvestigationFilter | ||
# CaseEquality makes sense for some of the rspec matchers and compound matching behavior | ||
# rubocop:disable Style/CaseEquality | ||
attr_reader :investigations, :filtered_investigations | ||
|
||
def initialize(investigations, track_suspects = true) | ||
investigations ||= [] | ||
@investigations = investigations.dup | ||
@filtered_investigations = @investigations.dup | ||
@track_suspects = track_suspects | ||
end | ||
|
||
def with_name(contract_name) | ||
@filtered_investigations.keep_if do |investigation| | ||
return false if investigation.contract.nil? | ||
|
||
contract_name === investigation.contract.name | ||
end | ||
self | ||
end | ||
|
||
def with_request(request_constraints) | ||
return self if request_constraints.nil? | ||
[:headers, :body].each do |section| | ||
filter_request_section(section, request_constraints[section]) | ||
end | ||
self | ||
end | ||
|
||
def with_response(response_constraints) | ||
return self if response_constraints.nil? | ||
[:headers, :body].each do |section| | ||
filter_response_section(section, response_constraints[section]) | ||
end | ||
self | ||
end | ||
|
||
def successful_investigations | ||
@filtered_investigations.select { |i| i.successful? } | ||
end | ||
|
||
def unsuccessful_investigations | ||
@filtered_investigations - successful_investigations | ||
end | ||
|
||
protected | ||
|
||
def filter_request_section(section, filter) | ||
suspects = [] | ||
section = :parsed_body if section == :body | ||
@filtered_investigations.keep_if do |investigation| | ||
candidate = investigation.request.send(section) | ||
suspects << candidate if @track_suspects | ||
filter === candidate | ||
end if filter | ||
fail FilterExhaustedError.new("no requests matched #{section}", filter, suspects) if @filtered_investigations.empty? | ||
end | ||
|
||
def filter_response_section(section, filter) | ||
section = :parsed_body if section == :body | ||
suspects = [] | ||
@filtered_investigations.keep_if do |investigation| | ||
candidate = investigation.response.send(section) | ||
suspects << candidate if @track_suspects | ||
filter === candidate | ||
end if filter | ||
fail FilterExhaustedError.new("no responses matched #{section}", filter, suspects) if @filtered_investigations.empty? | ||
end | ||
|
||
# rubocop:enable Style/CaseEquality | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
RSpec::Matchers.define :have_investigated do |service_name| | ||
match do | ||
investigations = Pacto::InvestigationRegistry.instance.investigations | ||
@service_name = service_name | ||
|
||
begin | ||
@investigation_filter = Pacto::Forensics::InvestigationFilter.new(investigations) | ||
@investigation_filter.with_name(@service_name) | ||
.with_request(@request_constraints) | ||
.with_response(@response_constraints) | ||
|
||
@matched_investigations = @investigation_filter.filtered_investigations | ||
@unsuccessful_investigations = @investigation_filter.unsuccessful_investigations | ||
|
||
!@matched_investigations.empty? && (@allow_citations || @unsuccessful_investigations.empty?) | ||
rescue Pacto::Forensics::FilterExhaustedError => e | ||
@filter_error = e | ||
false | ||
end | ||
end | ||
|
||
def describe(obj) | ||
obj.respond_to?(:description) ? obj.description : obj.to_s | ||
end | ||
|
||
description do | ||
buffer = StringIO.new | ||
buffer.puts "to have investigated #{@service_name}" | ||
if @request_constraints | ||
buffer.puts ' with request matching' | ||
@request_constraints.each do |k, v| | ||
buffer.puts " #{k}: #{describe(v)}" | ||
end | ||
end | ||
buffer.puts ' and' if @request_constraints && @response_constraints | ||
if @response_constraint | ||
buffer.puts ' with response matching' | ||
@request_constraints.each do |k, v| | ||
buffer.puts " #{k}: #{describe(v)}" | ||
end | ||
end | ||
buffer.string | ||
end | ||
|
||
chain :with_request do |request_constraints| | ||
@request_constraints = request_constraints | ||
end | ||
|
||
chain :with_response do |response_constraints| | ||
@response_constraints = response_constraints | ||
end | ||
|
||
chain :allow_citations do | ||
@allow_citations = true | ||
end | ||
|
||
failure_message_for_should do | group | | ||
buffer = StringIO.new | ||
buffer.puts "expected #{group} " + description | ||
if @filter_error | ||
buffer.puts "but #{@filter_error.message}" | ||
unless @filter_error.suspects.empty? | ||
buffer.puts ' suspects:' | ||
@filter_error.suspects.each do |suspect| | ||
buffer.puts " #{suspect}" | ||
end | ||
end | ||
elsif @matched_investigations.empty? | ||
investigated_services = @investigation_filter.investigations.map(&:contract).compact.map(&:name).uniq | ||
buffer.puts "but it was not among the services investigated: #{investigated_services}" | ||
elsif @unsuccessful_investigations | ||
buffer.puts 'but investigation errors were found:' | ||
@unsuccessful_investigations.each do |investigation| | ||
buffer.puts " #{investigation}" | ||
end | ||
end | ||
buffer.string | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
# Pacto has a few RSpec matchers to help you ensure a **consumer** and **producer** are | ||
# interacting properly. First, let's setup the rspec suite. | ||
require 'rspec/autorun' # Not generally needed | ||
require 'pacto/rspec' | ||
WebMock.allow_net_connect! | ||
Pacto.validate! | ||
Pacto.load_contracts('contracts', 'http://localhost:5000').stub_providers | ||
|
||
# It's usually a good idea to reset Pacto between each scenario. `Pacto.reset` just clears the | ||
# data and metrics about which services were called. `Pacto.clear!` also resets all configuration | ||
# and plugins. | ||
RSpec.configure do |c| | ||
c.after(:each) { Pacto.reset } | ||
end | ||
|
||
# Pacto provides some RSpec matchers related to contract testing, like making sure | ||
# Pacto didn't received any unrecognized requests (`have_unmatched_requests`) and that | ||
# the HTTP requests matched up with the terms of the contract (`have_failed_investigations`). | ||
describe Faraday do | ||
let(:connection) { described_class.new(url: 'http://localhost:5000') } | ||
|
||
it 'passes contract tests' do | ||
connection.get '/api/ping' | ||
expect(Pacto).to_not have_failed_investigations | ||
expect(Pacto).to_not have_unmatched_requests | ||
end | ||
end | ||
|
||
# There are also some matchers for collaboration testing, so you can make sure each scenario is | ||
# calling the expected services and sending the right type of data. | ||
describe Faraday do | ||
let(:connection) { described_class.new(url: 'http://localhost:5000') } | ||
before(:each) do | ||
connection.get '/api/ping' | ||
|
||
connection.post do |req| | ||
req.url '/api/echo' | ||
req.headers['Content-Type'] = 'application/json' | ||
req.body = '{"foo": "bar"}' | ||
end | ||
end | ||
|
||
it 'calls the ping service' do | ||
expect(Pacto).to have_validated(:get, 'http://localhost:5000/api/ping').against_contract('Ping') | ||
end | ||
|
||
it 'sends data to the echo service' do | ||
expect(Pacto).to have_investigated('Ping').with_response(body: hash_including('ping' => 'pong - from the example!')) | ||
expect(Pacto).to have_investigated('Echo').with_request(body: hash_including('foo' => 'bar')) | ||
expect(Pacto).to have_investigated('Echo').with_response(body: /foo.*bar/) | ||
end | ||
end |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
{ | ||
"name": "Strict Contract", | ||
"request": { | ||
"http_method": "GET", | ||
"path": "/strict", | ||
|
Oops, something went wrong.