pact 1.1.0.rc2 → 1.1.0.rc3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (136) hide show
  1. data/.gitignore +0 -1
  2. data/CHANGELOG.md +46 -1
  3. data/Gemfile.lock +6 -4
  4. data/README.md +40 -186
  5. data/Rakefile +1 -1
  6. data/documentation/README.md +10 -0
  7. data/documentation/best-practices.md +33 -0
  8. data/documentation/configuration.md +166 -0
  9. data/documentation/diff_formatter_embedded.png +0 -0
  10. data/documentation/diff_formatter_list.png +0 -0
  11. data/documentation/diff_formatter_unix.png +0 -0
  12. data/documentation/faq.md +36 -6
  13. data/documentation/provider-states.md +173 -0
  14. data/documentation/raq.md +4 -4
  15. data/documentation/terminology.md +2 -2
  16. data/example/animal-service/Gemfile.lock +6 -9
  17. data/example/animal-service/Rakefile +2 -0
  18. data/example/animal-service/db/animal_db.sqlite3 +0 -0
  19. data/example/animal-service/lib/animal_service/animal_repository.rb +1 -5
  20. data/example/animal-service/lib/animal_service/api.rb +1 -1
  21. data/example/animal-service/spec/service_consumers/pact_helper.rb +7 -10
  22. data/example/animal-service/spec/service_consumers/provider_states_for_zoo_app.rb +5 -1
  23. data/example/zoo-app/Gemfile.lock +6 -9
  24. data/example/zoo-app/Rakefile +5 -0
  25. data/example/zoo-app/doc/markdown/README.md +3 -0
  26. data/example/zoo-app/doc/markdown/Zoo App - Animal Service.md +75 -0
  27. data/example/zoo-app/lib/zoo_app/animal_service_client.rb +2 -2
  28. data/example/zoo-app/spec/pacts/zoo_app-animal_service.json +1 -1
  29. data/example/zoo-app/spec/service_providers/animal_service_client_spec.rb +29 -34
  30. data/example/zoo-app/spec/service_providers/pact_helper.rb +4 -0
  31. data/lib/pact/configuration.rb +49 -1
  32. data/lib/pact/consumer/configuration.rb +4 -172
  33. data/lib/pact/consumer/configuration/configuration_extensions.rb +15 -0
  34. data/lib/pact/consumer/configuration/dsl.rb +12 -0
  35. data/lib/pact/consumer/configuration/mock_service.rb +89 -0
  36. data/lib/pact/consumer/configuration/service_consumer.rb +51 -0
  37. data/lib/pact/consumer/configuration/service_provider.rb +40 -0
  38. data/lib/pact/consumer/mock_service/interaction_mismatch.rb +3 -3
  39. data/lib/pact/consumer/mock_service/interaction_post.rb +2 -2
  40. data/lib/pact/consumer/mock_service/interaction_replay.rb +3 -4
  41. data/lib/pact/consumer/mock_service/verification_get.rb +32 -13
  42. data/lib/pact/consumer/rspec.rb +2 -4
  43. data/lib/pact/consumer/spec_hooks.rb +3 -1
  44. data/lib/pact/consumer_contract/consumer_contract.rb +1 -1
  45. data/lib/pact/consumer_contract/interaction.rb +1 -1
  46. data/lib/pact/doc/doc_file.rb +40 -0
  47. data/lib/pact/doc/generate.rb +11 -0
  48. data/lib/pact/doc/generator.rb +81 -0
  49. data/lib/pact/doc/interaction_view_model.rb +113 -0
  50. data/lib/pact/doc/markdown/generator.rb +26 -0
  51. data/lib/pact/doc/markdown/index_renderer.rb +41 -0
  52. data/lib/pact/doc/markdown/interaction.erb +14 -0
  53. data/lib/pact/doc/markdown/interaction_renderer.rb +38 -0
  54. data/lib/pact/doc/markdown/interactions_renderer.rb +56 -0
  55. data/lib/pact/doc/sort_interactions.rb +17 -0
  56. data/lib/pact/matchers/actual_type.rb +16 -0
  57. data/lib/pact/matchers/base_difference.rb +37 -0
  58. data/lib/pact/matchers/differ.rb +150 -0
  59. data/lib/pact/matchers/difference.rb +5 -30
  60. data/lib/pact/matchers/difference_indicator.rb +26 -0
  61. data/lib/pact/matchers/embedded_diff_formatter.rb +62 -0
  62. data/lib/pact/matchers/expected_type.rb +35 -0
  63. data/lib/pact/matchers/index_not_found.rb +3 -12
  64. data/lib/pact/matchers/{diff_decorator.rb → list_diff_formatter.rb} +28 -11
  65. data/lib/pact/matchers/matchers.rb +35 -39
  66. data/lib/pact/matchers/no_diff_indicator.rb +18 -0
  67. data/lib/pact/matchers/regexp_difference.rb +13 -0
  68. data/lib/pact/matchers/type_difference.rb +16 -0
  69. data/lib/pact/matchers/unexpected_index.rb +3 -13
  70. data/lib/pact/matchers/unexpected_key.rb +3 -12
  71. data/lib/pact/matchers/{plus_minus_diff_decorator.rb → unix_diff_formatter.rb} +22 -7
  72. data/lib/pact/provider/configuration.rb +5 -178
  73. data/lib/pact/provider/configuration/configuration_extension.rb +58 -0
  74. data/lib/pact/provider/configuration/dsl.rb +13 -0
  75. data/lib/pact/provider/configuration/pact_verification.rb +46 -0
  76. data/lib/pact/provider/configuration/service_provider_config.rb +16 -0
  77. data/lib/pact/provider/configuration/service_provider_dsl.rb +54 -0
  78. data/lib/pact/provider/matchers.rb +21 -13
  79. data/lib/pact/provider/matchers/messages.rb +43 -0
  80. data/lib/pact/provider/pact_spec_runner.rb +8 -0
  81. data/lib/pact/provider/rspec.rb +1 -1
  82. data/lib/pact/provider/rspec/formatter.rb +9 -7
  83. data/lib/pact/{consumer_contract → shared}/active_support_support.rb +4 -0
  84. data/lib/pact/shared/jruby_support.rb +18 -0
  85. data/lib/pact/shared/key_not_found.rb +3 -16
  86. data/lib/pact/shared/request.rb +5 -5
  87. data/lib/pact/term.rb +2 -2
  88. data/lib/pact/version.rb +1 -1
  89. data/pact.gemspec +2 -2
  90. data/spec/integration/pact/provider_configuration_spec.rb +2 -1
  91. data/spec/lib/pact/configuration_spec.rb +73 -0
  92. data/spec/lib/pact/consumer/mock_service/interaction_mismatch_spec.rb +21 -3
  93. data/spec/lib/pact/consumer/mock_service/verification_get_spec.rb +134 -0
  94. data/spec/lib/pact/consumer/request_spec.rb +1 -1
  95. data/spec/lib/pact/consumer_contract/active_support_support_spec.rb +1 -1
  96. data/spec/lib/pact/doc/generator_spec.rb +69 -0
  97. data/spec/lib/pact/doc/interaction_view_model_spec.rb +112 -0
  98. data/spec/lib/pact/doc/markdown/index_renderer_spec.rb +29 -0
  99. data/spec/lib/pact/doc/markdown/interactions_renderer_spec.rb +29 -0
  100. data/spec/lib/pact/matchers/differ_spec.rb +214 -0
  101. data/spec/lib/pact/matchers/difference_spec.rb +2 -12
  102. data/spec/lib/pact/matchers/embedded_diff_formatter_spec.rb +77 -0
  103. data/spec/lib/pact/matchers/index_not_found_spec.rb +21 -0
  104. data/spec/lib/pact/matchers/list_diff_formatter_spec.rb +114 -0
  105. data/spec/lib/pact/matchers/matchers_spec.rb +38 -22
  106. data/spec/lib/pact/matchers/regexp_difference_spec.rb +20 -0
  107. data/spec/lib/pact/matchers/type_difference_spec.rb +34 -0
  108. data/spec/lib/pact/matchers/unexpected_index_spec.rb +20 -0
  109. data/spec/lib/pact/matchers/unexpected_key_spec.rb +20 -0
  110. data/spec/lib/pact/matchers/{plus_minus_diff_decorator_spec.rb → unix_diff_formatter_spec.rb} +35 -6
  111. data/spec/lib/pact/provider/configuration/configuration_extension_spec.rb +30 -0
  112. data/spec/lib/pact/provider/configuration/pact_verification_spec.rb +43 -0
  113. data/spec/lib/pact/provider/configuration/service_provider_config_spec.rb +21 -0
  114. data/spec/lib/pact/provider/configuration/service_provider_dsl_spec.rb +92 -0
  115. data/spec/lib/pact/provider/configuration_spec.rb +7 -150
  116. data/spec/lib/pact/provider/matchers/messages_spec.rb +104 -0
  117. data/spec/lib/pact/provider/rspec/formatter_spec.rb +56 -0
  118. data/spec/lib/pact/shared/key_not_found_spec.rb +20 -0
  119. data/spec/lib/pact/shared/request_spec.rb +28 -0
  120. data/spec/spec_helper.rb +3 -0
  121. data/spec/standalone/consumer_fail_test.rb +54 -0
  122. data/spec/standalone/consumer_pass_test.rb +50 -0
  123. data/spec/support/generated_index.md +4 -0
  124. data/spec/support/generated_markdown.md +55 -0
  125. data/spec/support/interaction_view_model.json +63 -0
  126. data/spec/support/markdown_pact.json +48 -0
  127. data/spec/support/pact_helper.rb +2 -1
  128. data/spec/support/spec_support.rb +7 -0
  129. data/spec/support/test_app_fail.json +11 -2
  130. data/tasks/pact-test.rake +9 -0
  131. metadata +113 -20
  132. data/example/animal-service/db/animals_db.sqlite3 +0 -0
  133. data/lib/pact/consumer/rspec/full_example_description.rb +0 -28
  134. data/lib/pact/matchers/nested_json_diff_decorator.rb +0 -53
  135. data/spec/lib/pact/matchers/diff_decorator_spec.rb +0 -80
  136. data/spec/lib/pact/matchers/nested_json_diff_decorator_spec.rb +0 -48
@@ -24,27 +24,46 @@ module Pact
24
24
  def respond env
25
25
  if interaction_list.all_matched?
26
26
  logger.info "Verifying - interactions matched for example \"#{example_description(env)}\""
27
- [200, {}, ['Interactions matched']]
27
+ [200, {'Content-Type' => 'text/plain'}, ['Interactions matched']]
28
28
  else
29
29
 
30
- missing_interactions_summaries = interaction_list.missing_interactions_summaries
31
- interaction_mismatches_summaries = interaction_list.interaction_mismatches_summaries
32
- unexpected_requests_summaries = interaction_list.unexpected_requests_summaries
33
- error_message = "Missing requests:
34
- #{missing_interactions_summaries.join("\n ")}
35
- Incorrect requests:
36
- #{interaction_mismatches_summaries.join("\n ")}
37
- Unexpected requests:
38
- #{unexpected_requests_summaries.join("\n ")}"
39
- logger.warn "Verifying - actual interactions do not match expected interactions for example \"#{example_description(env)}\". \n#{error_message}"
40
- logger.warn error_message
41
- [500, {}, ["Actual interactions do not match expected interactions for mock #{name}.\n#{error_message}\nSee #{log_description} for details."]]
30
+ error_message = FailureMessage.new(interaction_list).to_s
31
+ logger.warn "Verifying - actual interactions do not match expected interactions for example \"#{example_description(env)}\". \n#{error_message}"
32
+ logger.warn error_message
33
+ [500, {'Content-Type' => 'text/plain'}, ["Actual interactions do not match expected interactions for mock #{name}.\n\n#{error_message}See #{log_description} for details."]]
42
34
  end
43
35
  end
44
36
 
45
37
  def example_description env
46
38
  params_hash(env)['example_description']
47
39
  end
40
+
41
+ class FailureMessage
42
+
43
+ def initialize interaction_list
44
+ @interaction_list = interaction_list
45
+ end
46
+
47
+ def to_s
48
+ titles_and_summaries.collect do | title, summaries |
49
+ "#{title}:\n\t#{summaries.join("\n\t")}\n\n" if summaries.any?
50
+ end.compact.join
51
+
52
+ end
53
+
54
+ private
55
+
56
+ attr_reader :interaction_list
57
+
58
+ def titles_and_summaries
59
+ {
60
+ "Incorrect requests" => interaction_list.interaction_mismatches_summaries,
61
+ "Missing requests" => interaction_list.missing_interactions_summaries,
62
+ "Unexpected requests" => interaction_list.unexpected_requests_summaries,
63
+ }
64
+ end
65
+
66
+ end
48
67
  end
49
68
  end
50
69
  end
@@ -1,6 +1,5 @@
1
1
  require 'pact/consumer'
2
2
  require 'pact/consumer/spec_hooks'
3
- require 'pact/consumer/rspec/full_example_description'
4
3
 
5
4
  module Pact
6
5
  module Consumer
@@ -12,7 +11,6 @@ end
12
11
 
13
12
  hooks = Pact::Consumer::SpecHooks.new
14
13
 
15
-
16
14
  RSpec.configure do |config|
17
15
  config.include Pact::Consumer::RSpec, :pact => true
18
16
 
@@ -21,11 +19,11 @@ RSpec.configure do |config|
21
19
  end
22
20
 
23
21
  config.before :each, :pact => true do | example |
24
- hooks.before_each Pact::Consumer::RSpec::FullExampleDescription.new(example).to_s
22
+ hooks.before_each example.example.full_description
25
23
  end
26
24
 
27
25
  config.after :each, :pact => true do | example |
28
- hooks.after_each Pact::Consumer::RSpec::FullExampleDescription.new(example).to_s
26
+ hooks.after_each example.example.full_description
29
27
  end
30
28
 
31
29
  config.after :suite do
@@ -1,3 +1,5 @@
1
+ require 'pact/doc/generate'
2
+
1
3
  module Pact
2
4
  module Consumer
3
5
  class SpecHooks
@@ -22,7 +24,7 @@ module Pact
22
24
  end
23
25
 
24
26
  def after_suite
25
- Pact.configuration.logger.info "After suite"
27
+ Pact::Doc::Generate.call
26
28
  Pact::Consumer::AppManager.instance.kill_all
27
29
  Pact::Consumer::AppManager.instance.clear_all
28
30
  end
@@ -3,6 +3,7 @@ require 'pact/something_like'
3
3
  require 'pact/symbolize_keys'
4
4
  require 'pact/term'
5
5
  require 'pact/version'
6
+ require 'pact/shared/active_support_support'
6
7
  require 'date'
7
8
  require 'json/add/regexp'
8
9
  require 'open-uri'
@@ -10,7 +11,6 @@ require_relative 'service_consumer'
10
11
  require_relative 'service_provider'
11
12
  require_relative 'interaction'
12
13
  require_relative 'request'
13
- require_relative 'active_support_support'
14
14
  require_relative 'pact_file'
15
15
  require_relative 'file_name'
16
16
 
@@ -1,6 +1,6 @@
1
1
  require 'pact/consumer_contract/request'
2
2
  require 'pact/symbolize_keys'
3
- require 'pact/consumer_contract/active_support_support'
3
+ require 'pact/shared/active_support_support'
4
4
 
5
5
  module Pact
6
6
  class Interaction
@@ -0,0 +1,40 @@
1
+ module Pact
2
+ module Doc
3
+
4
+ class DocFile
5
+
6
+ def initialize consumer_contract, dir, interactions_renderer, file_extension
7
+ @dir = dir
8
+ @consumer_contract = consumer_contract
9
+ @interactions_renderer = interactions_renderer
10
+ @file_extension = file_extension
11
+ end
12
+
13
+ def write
14
+ File.open(path, "w") { |io| io << doc_file_contents }
15
+ end
16
+
17
+ def title
18
+ consumer_contract.provider.name
19
+ end
20
+
21
+ def name
22
+ "#{consumer_contract.consumer.name} - #{consumer_contract.provider.name}#{file_extension}"
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :dir, :consumer_contract, :interactions_renderer, :file_extension
28
+
29
+
30
+ def path
31
+ File.join(dir, name)
32
+ end
33
+
34
+ def doc_file_contents
35
+ interactions_renderer.call(consumer_contract)
36
+ end
37
+
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,11 @@
1
+ module Pact
2
+ module Doc
3
+ class Generate
4
+
5
+ def self.call pact_dir = Pact.configuration.pact_dir, doc_dir = Pact.configuration.doc_dir, doc_generators = Pact.configuration.doc_generators
6
+ doc_generators.each{| doc_generator| doc_generator.call pact_dir, doc_dir }
7
+ end
8
+
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,81 @@
1
+ require 'pact/doc/doc_file'
2
+ require 'fileutils'
3
+
4
+ module Pact
5
+ module Doc
6
+
7
+ class Generator
8
+
9
+ def initialize pact_dir, doc_dir, options
10
+ @doc_dir = doc_dir
11
+ @pact_dir = pact_dir
12
+ @interactions_renderer = options[:interactions_renderer]
13
+ @doc_type = options[:doc_type]
14
+ @file_extension = options[:file_extension]
15
+ @index_renderer = options[:index_renderer]
16
+ @index_name = options[:index_name]
17
+ @after = options.fetch(:after, lambda{|pact_dir, target_dir, consumer_contracts| })
18
+ end
19
+
20
+ def call
21
+ ensure_target_dir_exists
22
+ write_index if consumer_contracts.any?
23
+ write_doc_files
24
+ perform_after_hook
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :doc_dir, :pact_dir, :interactions_renderer, :doc_type, :file_extension, :index_renderer, :after
30
+
31
+ def write_index
32
+ File.open(index_file_path, "w") { |io| io << index_file_contents }
33
+ end
34
+
35
+ def index_file_path
36
+ File.join(target_dir, "#{@index_name}#{file_extension}")
37
+ end
38
+
39
+ def index_file_contents
40
+ index_renderer.call(consumer_contracts.first.consumer.name, index_data)
41
+ end
42
+
43
+ def index_data
44
+ doc_files.each_with_object({}) do | doc_file, data |
45
+ data[doc_file.title] = doc_file.name
46
+ end
47
+ end
48
+
49
+ def write_doc_files
50
+ doc_files.each(&:write)
51
+ end
52
+
53
+ def doc_files
54
+ consumer_contracts.collect do | consumer_contract |
55
+ DocFile.new(consumer_contract, target_dir, interactions_renderer, file_extension)
56
+ end
57
+ end
58
+
59
+ def consumer_contracts
60
+ @consumer_contracts ||= begin
61
+ Dir.glob("#{pact_dir}/**").collect do |file|
62
+ Pact::ConsumerContract.from_uri file
63
+ end
64
+ end
65
+ end
66
+
67
+ def perform_after_hook
68
+ after.call(pact_dir, target_dir, consumer_contracts)
69
+ end
70
+
71
+ def ensure_target_dir_exists
72
+ FileUtils.mkdir_p target_dir
73
+ end
74
+
75
+ def target_dir
76
+ File.join(doc_dir, doc_type)
77
+ end
78
+
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,113 @@
1
+ require 'pact/shared/active_support_support'
2
+
3
+ module Pact
4
+ module Doc
5
+ class InteractionViewModel
6
+
7
+ include Pact::ActiveSupportSupport
8
+
9
+ def initialize interaction, consumer_contract
10
+ @interaction = interaction
11
+ @consumer_contract = consumer_contract
12
+ end
13
+
14
+ def id
15
+ @id ||= begin
16
+ if has_provider_state?
17
+ "#{description} given #{interaction.provider_state}"
18
+ else
19
+ interaction.description
20
+ end.gsub(/\s+/,'_')
21
+ end
22
+ end
23
+
24
+ def request_method
25
+ interaction.request.method.upcase
26
+ end
27
+
28
+ def request_path
29
+ interaction.request.path
30
+ end
31
+
32
+ def response_status
33
+ interaction.response['status']
34
+ end
35
+
36
+ def consumer_name
37
+ @consumer_contract.consumer.name
38
+ end
39
+
40
+ def provider_name
41
+ @consumer_contract.provider.name
42
+ end
43
+
44
+ def has_provider_state?
45
+ @interaction.provider_state && !@interaction.provider_state.empty?
46
+ end
47
+
48
+ def provider_state start_of_sentence = false
49
+ apply_capitals(@interaction.provider_state.strip, start_of_sentence)
50
+ end
51
+
52
+ def description start_of_sentence = false
53
+ apply_capitals(@interaction.description.strip, start_of_sentence)
54
+ end
55
+
56
+ def request
57
+ fix_json_formatting JSON.pretty_generate(clean_request)
58
+ end
59
+
60
+ def response
61
+ fix_json_formatting JSON.pretty_generate(clean_response)
62
+ end
63
+
64
+ private
65
+
66
+ attr_reader :interaction, :consumer_contract
67
+
68
+ def clean_request
69
+ ordered_clean_hash Reification.from_term(interaction.request).to_hash
70
+ end
71
+
72
+ def clean_response
73
+ ordered_clean_hash Reification.from_term(interaction.response)
74
+ end
75
+
76
+ # Remove empty body and headers hashes from response, and empty headers from request,
77
+ # as an empty hash means "allow anything" - it's more intuitive and cleaner to just
78
+ # remove the empty hashes from display.
79
+ def ordered_clean_hash source
80
+ ordered_keys.each_with_object({}) do |key, target|
81
+ if source.key? key
82
+ target[key] = source[key] unless value_is_an_empty_hash_that_is_not_request_body(source[key], key)
83
+ end
84
+ end
85
+ end
86
+
87
+ def value_is_an_empty_hash_that_is_not_request_body value, key
88
+ value.is_a?(Hash) && value.empty? && key != :body
89
+ end
90
+
91
+ def ordered_keys
92
+ [:method, :path, :query, :headers, :body, "status", "headers","body"]
93
+ end
94
+
95
+ def remove_key_if_empty key, hash
96
+ hash.delete(key) if hash[key].is_a?(Hash) && hash[key].empty?
97
+ end
98
+
99
+ def apply_capitals string, start_of_sentence = false
100
+ start_of_sentence ? capitalize_first_letter(string) : lowercase_first_letter(string)
101
+ end
102
+
103
+ def capitalize_first_letter string
104
+ string[0].upcase + string[1..-1]
105
+ end
106
+
107
+ def lowercase_first_letter string
108
+ string[0].downcase + string[1..-1]
109
+ end
110
+
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,26 @@
1
+ require 'pact/doc/generator'
2
+ require 'pact/doc/markdown/interactions_renderer'
3
+ require 'pact/doc/markdown/index_renderer'
4
+
5
+ module Pact
6
+ module Doc
7
+ module Markdown
8
+ class Generator < Pact::Doc::Generator
9
+
10
+ def initialize pact_dir, doc_dir
11
+ super(pact_dir, doc_dir,
12
+ interactions_renderer: InteractionsRenderer,
13
+ doc_type: 'markdown',
14
+ file_extension: '.md',
15
+ index_renderer: IndexRenderer,
16
+ index_name: 'README')
17
+ end
18
+
19
+ def self.call pact_dir, doc_dir
20
+ new(pact_dir, doc_dir).call
21
+ end
22
+
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,41 @@
1
+ module Pact
2
+ module Doc
3
+ module Markdown
4
+ class IndexRenderer
5
+
6
+ attr_reader :consumer_name
7
+ attr_reader :docs # Hash of pact title => file_name
8
+
9
+ def initialize consumer_name, docs
10
+ @consumer_name = consumer_name
11
+ @docs = docs
12
+ end
13
+
14
+ def self.call consumer_name, docs
15
+ new(consumer_name, docs).call
16
+ end
17
+
18
+ def call
19
+ title + "\n\n" + table_of_contents + "\n"
20
+ end
21
+
22
+ private
23
+
24
+ def table_of_contents
25
+ docs.collect do | title, file_name |
26
+ item title, file_name
27
+ end.join("\n")
28
+ end
29
+
30
+ def title
31
+ "### Pacts for #{consumer_name}"
32
+ end
33
+
34
+ def item title, file_name
35
+ "* [#{title}](#{file_name})"
36
+ end
37
+
38
+ end
39
+ end
40
+ end
41
+ end