pacto 0.3.1 → 0.4.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (204) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.rubocop.yml +29 -7
  4. data/.travis.yml +8 -1
  5. data/CONTRIBUTING.md +3 -6
  6. data/Gemfile +13 -2
  7. data/Guardfile +4 -4
  8. data/Procfile +1 -0
  9. data/README.md +47 -13
  10. data/Rakefile +66 -19
  11. data/TODO.md +33 -10
  12. data/bin/pacto +4 -0
  13. data/changelog.md +30 -0
  14. data/docs/configuration.md +69 -0
  15. data/docs/consumer.md +18 -0
  16. data/docs/cops.md +39 -0
  17. data/docs/forensics.md +66 -0
  18. data/docs/generation.md +65 -0
  19. data/docs/rake_tasks.md +10 -0
  20. data/docs/rspec.md +0 -0
  21. data/docs/samples.md +133 -0
  22. data/docs/server.md +34 -0
  23. data/docs/server_cli.md +18 -0
  24. data/docs/stenographer.md +20 -0
  25. data/features/configuration/strict_matchers.feature +10 -10
  26. data/features/evolve/existing_services.feature +12 -10
  27. data/features/generate/generation.feature +11 -11
  28. data/features/steps/pacto_steps.rb +17 -12
  29. data/features/stub/templates.feature +4 -4
  30. data/features/support/env.rb +21 -9
  31. data/features/validate/meta_validation.feature +9 -17
  32. data/features/validate/validation.feature +5 -6
  33. data/lib/pacto.rb +41 -33
  34. data/lib/pacto/actor.rb +5 -0
  35. data/lib/pacto/actors/from_examples.rb +67 -0
  36. data/lib/pacto/actors/json_generator.rb +20 -0
  37. data/lib/pacto/cli.rb +75 -0
  38. data/lib/pacto/cli/helpers.rb +20 -0
  39. data/lib/pacto/consumer.rb +80 -0
  40. data/lib/pacto/consumer/faraday_driver.rb +34 -0
  41. data/lib/pacto/contract.rb +48 -20
  42. data/lib/pacto/contract_builder.rb +125 -0
  43. data/lib/pacto/contract_factory.rb +31 -12
  44. data/lib/pacto/contract_files.rb +1 -0
  45. data/lib/pacto/contract_set.rb +12 -0
  46. data/lib/pacto/cops.rb +46 -0
  47. data/lib/pacto/cops/body_cop.rb +23 -0
  48. data/lib/pacto/cops/request_body_cop.rb +10 -0
  49. data/lib/pacto/cops/response_body_cop.rb +10 -0
  50. data/lib/pacto/{validators/response_header_validator.rb → cops/response_header_cop.rb} +9 -15
  51. data/lib/pacto/cops/response_status_cop.rb +18 -0
  52. data/lib/pacto/core/configuration.rb +16 -5
  53. data/lib/pacto/core/contract_registry.rb +13 -32
  54. data/lib/pacto/core/hook.rb +1 -0
  55. data/lib/pacto/core/http_middleware.rb +23 -0
  56. data/lib/pacto/core/investigation_registry.rb +60 -0
  57. data/lib/pacto/core/modes.rb +1 -0
  58. data/lib/pacto/core/pacto_request.rb +59 -0
  59. data/lib/pacto/core/pacto_response.rb +41 -0
  60. data/lib/pacto/dash.rb +9 -0
  61. data/lib/pacto/erb_processor.rb +1 -0
  62. data/lib/pacto/exceptions/invalid_contract.rb +1 -0
  63. data/lib/pacto/extensions.rb +3 -16
  64. data/lib/pacto/forensics/investigation_filter.rb +90 -0
  65. data/lib/pacto/forensics/investigation_matcher.rb +80 -0
  66. data/lib/pacto/generator.rb +31 -53
  67. data/lib/pacto/generator/filters.rb +8 -7
  68. data/lib/pacto/generator/hint.rb +26 -0
  69. data/lib/pacto/generator/native_contract_generator.rb +74 -0
  70. data/lib/pacto/hooks/erb_hook.rb +2 -1
  71. data/lib/pacto/investigation.rb +49 -0
  72. data/lib/pacto/logger.rb +1 -0
  73. data/lib/pacto/meta_schema.rb +12 -6
  74. data/lib/pacto/native_contract_factory.rb +60 -0
  75. data/lib/pacto/observers/stenographer.rb +42 -0
  76. data/lib/pacto/provider.rb +27 -0
  77. data/lib/pacto/rake_task.rb +25 -70
  78. data/lib/pacto/request_clause.rb +31 -29
  79. data/lib/pacto/request_pattern.rb +20 -3
  80. data/lib/pacto/resettable.rb +22 -0
  81. data/lib/pacto/response_clause.rb +5 -12
  82. data/lib/pacto/rspec.rb +38 -31
  83. data/lib/pacto/server.rb +4 -0
  84. data/lib/pacto/stubs/uri_pattern.rb +21 -11
  85. data/lib/pacto/stubs/webmock_adapter.rb +69 -34
  86. data/lib/pacto/swagger_contract_factory.rb +90 -0
  87. data/lib/pacto/test_helper.rb +37 -0
  88. data/lib/pacto/ui.rb +32 -2
  89. data/lib/pacto/uri.rb +2 -1
  90. data/lib/pacto/version.rb +2 -1
  91. data/pacto-server.gemspec +24 -0
  92. data/pacto.gemspec +13 -9
  93. data/resources/contract_schema.json +46 -18
  94. data/resources/draft-04.json +150 -0
  95. data/sample_apis/album/cover_api.rb +12 -0
  96. data/sample_apis/config.ru +25 -0
  97. data/sample_apis/echo_api.rb +26 -0
  98. data/sample_apis/files_api.rb +50 -0
  99. data/sample_apis/hello_api.rb +14 -0
  100. data/sample_apis/ping_api.rb +11 -0
  101. data/sample_apis/reverse_api.rb +20 -0
  102. data/samples/README.md +11 -0
  103. data/samples/Rakefile +2 -0
  104. data/samples/configuration.rb +33 -0
  105. data/samples/consumer.rb +15 -0
  106. data/samples/contracts/README.md +1 -0
  107. data/samples/contracts/contract.js +93 -0
  108. data/samples/contracts/get_album_cover.json +48 -0
  109. data/samples/contracts/localhost/api/echo.json +37 -0
  110. data/samples/contracts/localhost/api/ping.json +38 -0
  111. data/samples/cops.rb +30 -0
  112. data/samples/forensics.rb +54 -0
  113. data/samples/generation.rb +48 -0
  114. data/samples/rake_tasks.sh +7 -0
  115. data/samples/rspec.rb +1 -0
  116. data/samples/samples.rb +92 -0
  117. data/samples/scripts/bootstrap +2 -0
  118. data/samples/scripts/wrapper +11 -0
  119. data/samples/server.rb +24 -0
  120. data/samples/server_cli.sh +12 -0
  121. data/samples/stenographer.rb +17 -0
  122. data/spec/coveralls_helper.rb +1 -0
  123. data/spec/fabricators/contract_fabricator.rb +94 -0
  124. data/spec/fabricators/http_fabricator.rb +48 -0
  125. data/spec/fabricators/webmock_fabricator.rb +24 -0
  126. data/spec/{unit/data → fixtures/contracts}/contract.json +2 -2
  127. data/spec/fixtures/contracts/contract_with_examples.json +58 -0
  128. data/spec/{unit/data → fixtures/contracts}/simple_contract.json +5 -3
  129. data/spec/{integration/data → fixtures/contracts}/strict_contract.json +5 -3
  130. data/spec/{integration/data → fixtures/contracts}/templating_contract.json +3 -2
  131. data/spec/{integration/data/simple_contract.json → fixtures/deprecated_contracts/deprecated_contract.json} +2 -1
  132. data/spec/fixtures/swagger/petstore.yaml +101 -0
  133. data/spec/integration/e2e_spec.rb +19 -20
  134. data/spec/integration/forensics/integration_matcher_spec.rb +90 -0
  135. data/spec/integration/rspec_spec.rb +22 -25
  136. data/spec/integration/templating_spec.rb +7 -6
  137. data/spec/pacto/dummy_server.rb +4 -0
  138. data/spec/pacto/{server → dummy_server}/dummy.rb +7 -6
  139. data/spec/pacto/dummy_server/jruby_workaround_helper.rb +23 -0
  140. data/spec/pacto/{server → dummy_server}/playback_servlet.rb +3 -2
  141. data/spec/spec_helper.rb +16 -7
  142. data/spec/unit/actors/from_examples_spec.rb +70 -0
  143. data/spec/unit/actors/json_generator_spec.rb +105 -0
  144. data/spec/unit/pacto/actor_spec.rb +23 -0
  145. data/spec/unit/pacto/configuration_spec.rb +7 -6
  146. data/spec/unit/pacto/consumer/faraday_driver_spec.rb +40 -0
  147. data/spec/unit/pacto/contract_builder_spec.rb +89 -0
  148. data/spec/unit/pacto/contract_factory_spec.rb +62 -11
  149. data/spec/unit/pacto/contract_files_spec.rb +1 -0
  150. data/spec/unit/pacto/contract_set_spec.rb +36 -0
  151. data/spec/unit/pacto/contract_spec.rb +51 -39
  152. data/spec/unit/pacto/cops/body_cop_spec.rb +107 -0
  153. data/spec/unit/pacto/{validators/response_header_validator_spec.rb → cops/response_header_cop_spec.rb} +30 -19
  154. data/spec/unit/pacto/cops/response_status_cop_spec.rb +26 -0
  155. data/spec/unit/pacto/cops_spec.rb +75 -0
  156. data/spec/unit/pacto/core/configuration_spec.rb +6 -5
  157. data/spec/unit/pacto/core/contract_registry_spec.rb +16 -83
  158. data/spec/unit/pacto/core/http_middleware_spec.rb +36 -0
  159. data/spec/unit/pacto/core/investigation_spec.rb +62 -0
  160. data/spec/unit/pacto/core/modes_spec.rb +5 -4
  161. data/spec/unit/pacto/erb_processor_spec.rb +3 -2
  162. data/spec/unit/pacto/extensions_spec.rb +10 -20
  163. data/spec/unit/pacto/generator/filters_spec.rb +11 -10
  164. data/spec/unit/pacto/generator/native_contract_generator_spec.rb +171 -0
  165. data/spec/unit/{hooks → pacto/hooks}/erb_hook_spec.rb +18 -11
  166. data/spec/unit/pacto/investigation_registry_spec.rb +77 -0
  167. data/spec/unit/pacto/logger_spec.rb +6 -5
  168. data/spec/unit/pacto/meta_schema_spec.rb +5 -4
  169. data/spec/unit/pacto/native_contract_factory_spec.rb +26 -0
  170. data/spec/unit/pacto/pacto_spec.rb +13 -28
  171. data/spec/unit/pacto/request_clause_spec.rb +16 -51
  172. data/spec/unit/pacto/request_pattern_spec.rb +6 -5
  173. data/spec/unit/pacto/response_clause_spec.rb +6 -19
  174. data/spec/unit/pacto/server/playback_servlet_spec.rb +21 -18
  175. data/spec/unit/pacto/stubs/observers/stenographer_spec.rb +33 -0
  176. data/spec/unit/pacto/stubs/uri_pattern_spec.rb +39 -11
  177. data/spec/unit/pacto/stubs/webmock_adapter_spec.rb +67 -117
  178. data/spec/unit/pacto/swagger_contract_factory_spec.rb +56 -0
  179. data/spec/unit/pacto/uri_spec.rb +1 -0
  180. data/tasks/release.rake +57 -0
  181. metadata +247 -76
  182. data/.rubocop-todo.yml +0 -24
  183. data/.ruby-gemset +0 -1
  184. data/.ruby-version +0 -1
  185. data/CHANGELOG +0 -12
  186. data/features/validate/body_only.feature +0 -85
  187. data/lib/pacto/contract_list.rb +0 -17
  188. data/lib/pacto/contract_validator.rb +0 -29
  189. data/lib/pacto/core/validation_registry.rb +0 -40
  190. data/lib/pacto/stubs/webmock_helper.rb +0 -69
  191. data/lib/pacto/validation.rb +0 -54
  192. data/lib/pacto/validators/body_validator.rb +0 -49
  193. data/lib/pacto/validators/request_body_validator.rb +0 -26
  194. data/lib/pacto/validators/response_body_validator.rb +0 -26
  195. data/lib/pacto/validators/response_status_validator.rb +0 -24
  196. data/spec/pacto/server.rb +0 -2
  197. data/spec/unit/pacto/contract_list_spec.rb +0 -35
  198. data/spec/unit/pacto/contract_validator_spec.rb +0 -85
  199. data/spec/unit/pacto/core/validation_registry_spec.rb +0 -76
  200. data/spec/unit/pacto/core/validation_spec.rb +0 -60
  201. data/spec/unit/pacto/generator_spec.rb +0 -132
  202. data/spec/unit/pacto/stubs/webmock_helper_spec.rb +0 -20
  203. data/spec/unit/pacto/validators/body_validator_spec.rb +0 -118
  204. data/spec/unit/pacto/validators/response_status_validator_spec.rb +0 -20
@@ -1,43 +1,24 @@
1
+ # -*- encoding : utf-8 -*-
1
2
  module Pacto
2
- class ContractRegistry
3
- def initialize
4
- @registry = Hash.new { |hash, key| hash[key] = Set.new }
5
- end
6
-
7
- def [](tag)
8
- @registry[tag]
9
- end
3
+ class ContractNotFound < StandardError; end
10
4
 
11
- def register(contract, *tags)
12
- tags << :default if tags.empty?
5
+ class ContractRegistry < Set
6
+ include Logger
13
7
 
14
- tags.each do |tag|
15
- @registry[tag] << contract
16
- end
17
-
18
- self
8
+ def register(contract)
9
+ fail ArgumentError, 'expected a Pacto::Contract' unless contract.is_a? Contract
10
+ logger.debug "Registering #{contract.request_pattern} as #{contract.name}"
11
+ add contract
19
12
  end
20
13
 
21
- def use(tag, values = {})
22
- merged_contracts = @registry[:default] + @registry[tag]
23
-
24
- fail ArgumentError, "contract \"#{tag}\" not found" if merged_contracts.empty?
25
-
26
- merged_contracts.each do |contract|
27
- contract.stub_contract! values
28
- end
29
-
30
- self
14
+ def find_by_name(name)
15
+ contract = select { |c| c.name == name }.first
16
+ fail ContractNotFound, "No contract found for #{name}" unless contract
17
+ contract
31
18
  end
32
19
 
33
20
  def contracts_for(request_signature)
34
- all_contracts.select { |c| c.matches? request_signature }
35
- end
36
-
37
- private
38
-
39
- def all_contracts
40
- @registry.values.inject(Set.new, :+)
21
+ select { |c| c.matches? request_signature }
41
22
  end
42
23
  end
43
24
  end
@@ -1,3 +1,4 @@
1
+ # -*- encoding : utf-8 -*-
1
2
  module Pacto
2
3
  class Hook
3
4
  def initialize(&block)
@@ -0,0 +1,23 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'observer'
3
+
4
+ module Pacto
5
+ module Core
6
+ class HTTPMiddleware
7
+ include Logger
8
+ include Observable
9
+
10
+ def process(request, response)
11
+ contracts = Pacto.contracts_for request
12
+ Pacto.configuration.hook.process contracts, request, response
13
+
14
+ changed
15
+ begin
16
+ notify_observers request, response
17
+ rescue StandardError => e
18
+ logger.error(e)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,60 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module Pacto
3
+ class InvestigationRegistry
4
+ include Singleton
5
+ include Logger
6
+ include Resettable
7
+ attr_reader :investigations
8
+
9
+ def initialize
10
+ @investigations = []
11
+ end
12
+
13
+ def self.reset!
14
+ instance.reset!
15
+ end
16
+
17
+ def reset!
18
+ @investigations.clear
19
+ @stenographer = nil
20
+ end
21
+
22
+ def validated?(request_pattern)
23
+ matched_investigations = @investigations.select do |investigation|
24
+ request_pattern.matches? investigation.request
25
+ end
26
+ matched_investigations unless matched_investigations.empty?
27
+ end
28
+
29
+ def register_investigation(investigation)
30
+ @investigations << investigation
31
+ stenographer.log_investigation investigation
32
+ logger.info "Detected #{investigation.summary}"
33
+ logger.debug(investigation.to_s) unless investigation.successful?
34
+ investigation
35
+ end
36
+
37
+ def unmatched_investigations
38
+ @investigations.select do |investigation|
39
+ investigation.contract.nil?
40
+ end
41
+ end
42
+
43
+ def failed_investigations
44
+ @investigations.select do |investigation|
45
+ !investigation.successful?
46
+ end
47
+ end
48
+
49
+ protected
50
+
51
+ def stenographer
52
+ @stenographer ||= create_stenographer
53
+ end
54
+
55
+ def create_stenographer
56
+ stenographer_log = File.open(Pacto.configuration.stenographer_log_file, 'a+')
57
+ Pacto::Observers::Stenographer.new stenographer_log
58
+ end
59
+ end
60
+ end
@@ -1,3 +1,4 @@
1
+ # -*- encoding : utf-8 -*-
1
2
  module Pacto
2
3
  class << self
3
4
  def generate!
@@ -0,0 +1,59 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'hashie/mash'
3
+
4
+ module Pacto
5
+ class PactoRequest
6
+ # FIXME: Need case insensitive header lookup, but case-sensitive storage
7
+ attr_accessor :headers, :body, :method, :uri
8
+
9
+ def initialize(data)
10
+ mash = Hashie::Mash.new data
11
+ @headers = mash.headers.nil? ? {} : mash.headers
12
+ @body = mash.body
13
+ @method = mash[:method]
14
+ @uri = mash.uri
15
+ normalize
16
+ end
17
+
18
+ def to_hash
19
+ {
20
+ method: method,
21
+ uri: uri,
22
+ headers: headers,
23
+ body: body
24
+ }
25
+ end
26
+
27
+ def to_s
28
+ string = Pacto::UI.colorize_method(method)
29
+ string << " #{relative_uri}"
30
+ string << " with body (#{body.bytesize} bytes)" if body
31
+ string
32
+ end
33
+
34
+ def relative_uri
35
+ uri.to_s.tap do |s|
36
+ s.slice!(uri.normalized_site)
37
+ end
38
+ end
39
+
40
+ def parsed_body
41
+ if body.is_a?(String) && content_type == 'application/json'
42
+ JSON.parse(body)
43
+ else
44
+ body
45
+ end
46
+ rescue
47
+ body
48
+ end
49
+
50
+ def content_type
51
+ headers['Content-Type']
52
+ end
53
+
54
+ def normalize
55
+ @method = @method.to_s.downcase.to_sym
56
+ @uri = @uri.normalize if @uri
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,41 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module Pacto
3
+ class PactoResponse
4
+ # FIXME: Need case insensitive header lookup, but case-sensitive storage
5
+ attr_accessor :headers, :body, :status, :parsed_body
6
+ attr_reader :parsed_body
7
+
8
+ def initialize(data)
9
+ mash = Hashie::Mash.new data
10
+ @headers = mash.headers.nil? ? {} : mash.headers
11
+ @body = mash.body
12
+ @status = mash.status.to_i
13
+ end
14
+
15
+ def to_hash
16
+ {
17
+ status: status,
18
+ headers: headers,
19
+ body: body
20
+ }
21
+ end
22
+
23
+ def to_s
24
+ string = "STATUS: #{status}"
25
+ string << " with body (#{body.bytesize} bytes)" if body
26
+ string
27
+ end
28
+
29
+ def parsed_body
30
+ if body.is_a?(String) && content_type == 'application/json'
31
+ JSON.parse(body)
32
+ else
33
+ body
34
+ end
35
+ end
36
+
37
+ def content_type
38
+ headers['Content-Type']
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,9 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'hashie'
3
+
4
+ module Pacto
5
+ class Dash < Hashie::Dash
6
+ include Hashie::Extensions::Coercion
7
+ include Hashie::Extensions::Dash::IndifferentAccess
8
+ end
9
+ end
@@ -1,3 +1,4 @@
1
+ # -*- encoding : utf-8 -*-
1
2
  module Pacto
2
3
  class ERBProcessor
3
4
  include Logger
@@ -1,3 +1,4 @@
1
+ # -*- encoding : utf-8 -*-
1
2
  class InvalidContract < ArgumentError
2
3
  attr_reader :errors
3
4
 
@@ -1,32 +1,19 @@
1
+ # -*- encoding : utf-8 -*-
1
2
  module Pacto
2
3
  module Extensions
3
4
  # Adapted from Faraday
4
5
  HeaderKeyMap = Hash.new do |map, key|
5
6
  split_char = key.to_s.include?('-') ? '-' : '_'
6
7
  map[key] = key.to_s.split(split_char). # :user_agent => %w(user agent)
7
- each { |w| w.capitalize! }. # => %w(User Agent)
8
+ each(&:capitalize!). # => %w(User Agent)
8
9
  join('-') # => "User-Agent"
9
10
  end
10
11
  HeaderKeyMap[:etag] = 'ETag'
11
12
 
12
13
  def self.normalize_header_keys(headers)
13
- headers.reduce({}) do |normalized, (key, value)|
14
+ headers.each_with_object({}) do |(key, value), normalized|
14
15
  normalized[HeaderKeyMap[key]] = value
15
- normalized
16
- end
17
- end
18
-
19
- module HashSubsetOf
20
- # FIXME: Only used by HashMergeProcessor, which I'd like to deprecate
21
- def normalize_keys
22
- reduce({}) do |normalized, (key, value)|
23
- normalized[key.to_s.downcase] = value
24
- normalized
25
- end
26
16
  end
27
17
  end
28
18
  end
29
19
  end
30
-
31
- # FIXME: Let's not extend Hash...
32
- Hash.send(:include, Pacto::Extensions::HashSubsetOf)
@@ -0,0 +1,90 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module Pacto
3
+ module Forensics
4
+ class FilterExhaustedError < StandardError
5
+ attr_reader :suspects
6
+
7
+ def initialize(msg, filter, suspects = [])
8
+ @suspects = suspects
9
+ if filter.respond_to? :description
10
+ msg = "#{msg} #{filter.description}"
11
+ else
12
+ msg = "#{msg} #{filter}"
13
+ end
14
+ super(msg)
15
+ end
16
+ end
17
+
18
+ class InvestigationFilter
19
+ # CaseEquality makes sense for some of the rspec matchers and compound matching behavior
20
+ # rubocop:disable Style/CaseEquality
21
+ attr_reader :investigations, :filtered_investigations
22
+
23
+ def initialize(investigations, track_suspects = true)
24
+ investigations ||= []
25
+ @investigations = investigations.dup
26
+ @filtered_investigations = @investigations.dup
27
+ @track_suspects = track_suspects
28
+ end
29
+
30
+ def with_name(contract_name)
31
+ @filtered_investigations.keep_if do |investigation|
32
+ return false if investigation.contract.nil?
33
+
34
+ contract_name === investigation.contract.name
35
+ end
36
+ self
37
+ end
38
+
39
+ def with_request(request_constraints)
40
+ return self if request_constraints.nil?
41
+ [:headers, :body].each do |section|
42
+ filter_request_section(section, request_constraints[section])
43
+ end
44
+ self
45
+ end
46
+
47
+ def with_response(response_constraints)
48
+ return self if response_constraints.nil?
49
+ [:headers, :body].each do |section|
50
+ filter_response_section(section, response_constraints[section])
51
+ end
52
+ self
53
+ end
54
+
55
+ def successful_investigations
56
+ @filtered_investigations.select(&:successful?)
57
+ end
58
+
59
+ def unsuccessful_investigations
60
+ @filtered_investigations - successful_investigations
61
+ end
62
+
63
+ protected
64
+
65
+ def filter_request_section(section, filter)
66
+ suspects = []
67
+ section = :parsed_body if section == :body
68
+ @filtered_investigations.keep_if do |investigation|
69
+ candidate = investigation.request.send(section)
70
+ suspects << candidate if @track_suspects
71
+ filter === candidate
72
+ end if filter
73
+ fail FilterExhaustedError.new("no requests matched #{section}", filter, suspects) if @filtered_investigations.empty?
74
+ end
75
+
76
+ def filter_response_section(section, filter)
77
+ section = :parsed_body if section == :body
78
+ suspects = []
79
+ @filtered_investigations.keep_if do |investigation|
80
+ candidate = investigation.response.send(section)
81
+ suspects << candidate if @track_suspects
82
+ filter === candidate
83
+ end if filter
84
+ fail FilterExhaustedError.new("no responses matched #{section}", filter, suspects) if @filtered_investigations.empty?
85
+ end
86
+
87
+ # rubocop:enable Style/CaseEquality
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,80 @@
1
+ # -*- encoding : utf-8 -*-
2
+ RSpec::Matchers.define :have_investigated do |service_name|
3
+ match do
4
+ investigations = Pacto::InvestigationRegistry.instance.investigations
5
+ @service_name = service_name
6
+
7
+ begin
8
+ @investigation_filter = Pacto::Forensics::InvestigationFilter.new(investigations)
9
+ @investigation_filter.with_name(@service_name)
10
+ .with_request(@request_constraints)
11
+ .with_response(@response_constraints)
12
+
13
+ @matched_investigations = @investigation_filter.filtered_investigations
14
+ @unsuccessful_investigations = @investigation_filter.unsuccessful_investigations
15
+
16
+ !@matched_investigations.empty? && (@allow_citations || @unsuccessful_investigations.empty?)
17
+ rescue Pacto::Forensics::FilterExhaustedError => e
18
+ @filter_error = e
19
+ false
20
+ end
21
+ end
22
+
23
+ def describe(obj)
24
+ obj.respond_to?(:description) ? obj.description : obj.to_s
25
+ end
26
+
27
+ description do
28
+ buffer = StringIO.new
29
+ buffer.puts "to have investigated #{@service_name}"
30
+ if @request_constraints
31
+ buffer.puts ' with request matching'
32
+ @request_constraints.each do |k, v|
33
+ buffer.puts " #{k}: #{describe(v)}"
34
+ end
35
+ end
36
+ buffer.puts ' and' if @request_constraints && @response_constraints
37
+ if @response_constraint
38
+ buffer.puts ' with response matching'
39
+ @request_constraints.each do |k, v|
40
+ buffer.puts " #{k}: #{describe(v)}"
41
+ end
42
+ end
43
+ buffer.string
44
+ end
45
+
46
+ chain :with_request do |request_constraints|
47
+ @request_constraints = request_constraints
48
+ end
49
+
50
+ chain :with_response do |response_constraints|
51
+ @response_constraints = response_constraints
52
+ end
53
+
54
+ chain :allow_citations do
55
+ @allow_citations = true
56
+ end
57
+
58
+ failure_message do | group |
59
+ buffer = StringIO.new
60
+ buffer.puts "expected #{group} " + description
61
+ if @filter_error
62
+ buffer.puts "but #{@filter_error.message}"
63
+ unless @filter_error.suspects.empty?
64
+ buffer.puts ' suspects:'
65
+ @filter_error.suspects.each do |suspect|
66
+ buffer.puts " #{suspect}"
67
+ end
68
+ end
69
+ elsif @matched_investigations.empty?
70
+ investigated_services = @investigation_filter.investigations.map(&:contract).compact.map(&:name).uniq
71
+ buffer.puts "but it was not among the services investigated: #{investigated_services}"
72
+ elsif @unsuccessful_investigations
73
+ buffer.puts 'but investigation errors were found:'
74
+ @unsuccessful_investigations.each do |investigation|
75
+ buffer.puts " #{investigation}"
76
+ end
77
+ end
78
+ buffer.string
79
+ end
80
+ end