pact 0.1.28 → 1.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (285) hide show
  1. data/.gitignore +2 -1
  2. data/.ruby-version +1 -0
  3. data/.travis.yml +8 -0
  4. data/CHANGELOG.md +374 -0
  5. data/Gemfile +0 -1
  6. data/Gemfile.lock +58 -55
  7. data/README.md +214 -139
  8. data/Rakefile +4 -28
  9. data/bin/pact +2 -2
  10. data/config.ru +3 -0
  11. data/documentation/README.md +13 -0
  12. data/documentation/configuration.md +166 -0
  13. data/documentation/diff_formatter_embedded.png +0 -0
  14. data/documentation/diff_formatter_list.png +0 -0
  15. data/documentation/diff_formatter_unix.png +0 -0
  16. data/example/animal-service/Gemfile +14 -0
  17. data/example/animal-service/Gemfile.lock +69 -0
  18. data/example/animal-service/Rakefile +5 -0
  19. data/example/animal-service/config.ru +3 -0
  20. data/example/animal-service/db/animal_db.sqlite3 +0 -0
  21. data/example/animal-service/lib/animal_service/animal_repository.rb +12 -0
  22. data/example/animal-service/lib/animal_service/api.rb +28 -0
  23. data/example/animal-service/lib/animal_service/db.rb +5 -0
  24. data/example/animal-service/spec/service_consumers/pact_helper.rb +11 -0
  25. data/example/animal-service/spec/service_consumers/provider_states_for_zoo_app.rb +26 -0
  26. data/example/zoo-app/Gemfile +12 -0
  27. data/example/zoo-app/Gemfile.lock +63 -0
  28. data/example/zoo-app/Rakefile +5 -0
  29. data/example/zoo-app/doc/pacts/markdown/README.md +3 -0
  30. data/example/zoo-app/doc/pacts/markdown/Zoo App - Animal Service.md +75 -0
  31. data/example/zoo-app/lib/zoo_app/animal_service_client.rb +40 -0
  32. data/example/zoo-app/lib/zoo_app/models/alligator.rb +15 -0
  33. data/example/zoo-app/spec/pacts/zoo_app-animal_service.json +67 -0
  34. data/example/zoo-app/spec/service_providers/animal_service_client_spec.rb +71 -0
  35. data/example/zoo-app/spec/service_providers/pact_helper.rb +15 -0
  36. data/example/zoo-app/spec/spec_helper.rb +6 -0
  37. data/lib/pact/cli.rb +138 -0
  38. data/lib/pact/configuration.rb +169 -28
  39. data/lib/pact/consumer/app_manager.rb +10 -29
  40. data/lib/pact/consumer/configuration/configuration_extensions.rb +15 -0
  41. data/lib/pact/consumer/configuration/dsl.rb +12 -0
  42. data/lib/pact/consumer/configuration/mock_service.rb +91 -0
  43. data/lib/pact/consumer/configuration/service_consumer.rb +51 -0
  44. data/lib/pact/consumer/configuration/service_provider.rb +40 -0
  45. data/lib/pact/consumer/configuration.rb +11 -0
  46. data/lib/pact/consumer/consumer_contract_builder.rb +44 -47
  47. data/lib/pact/consumer/interaction_builder.rb +41 -0
  48. data/lib/pact/consumer/interactions_filter.rb +48 -0
  49. data/lib/pact/consumer/mock_service/app.rb +82 -0
  50. data/lib/pact/consumer/mock_service/interaction_delete.rb +33 -0
  51. data/lib/pact/consumer/mock_service/interaction_list.rb +76 -0
  52. data/lib/pact/consumer/mock_service/interaction_mismatch.rb +73 -0
  53. data/lib/pact/consumer/mock_service/interaction_post.rb +31 -0
  54. data/lib/pact/consumer/mock_service/interaction_replay.rb +139 -0
  55. data/lib/pact/consumer/mock_service/log_get.rb +28 -0
  56. data/lib/pact/consumer/mock_service/missing_interactions_get.rb +30 -0
  57. data/lib/pact/consumer/mock_service/mock_service_administration_endpoint.rb +31 -0
  58. data/lib/pact/consumer/mock_service/pact_post.rb +33 -0
  59. data/lib/pact/consumer/mock_service/rack_request_helper.rb +51 -0
  60. data/lib/pact/consumer/mock_service/verification_get.rb +68 -0
  61. data/lib/pact/consumer/mock_service.rb +1 -339
  62. data/lib/pact/consumer/mock_service_client.rb +65 -0
  63. data/lib/pact/consumer/mock_service_interaction_expectation.rb +37 -0
  64. data/lib/pact/consumer/request.rb +27 -0
  65. data/lib/pact/consumer/rspec.rb +9 -19
  66. data/lib/pact/consumer/server.rb +90 -0
  67. data/lib/pact/consumer/spec_hooks.rb +38 -0
  68. data/lib/pact/consumer/world.rb +37 -0
  69. data/lib/pact/consumer.rb +10 -7
  70. data/lib/pact/consumer_contract/consumer_contract.rb +115 -0
  71. data/lib/pact/consumer_contract/consumer_contract_writer.rb +84 -0
  72. data/lib/pact/consumer_contract/file_name.rb +19 -0
  73. data/lib/pact/consumer_contract/headers.rb +51 -0
  74. data/lib/pact/consumer_contract/interaction.rb +67 -0
  75. data/lib/pact/consumer_contract/pact_file.rb +24 -0
  76. data/lib/pact/consumer_contract/request.rb +73 -0
  77. data/lib/pact/consumer_contract/service_consumer.rb +28 -0
  78. data/lib/pact/consumer_contract/service_provider.rb +28 -0
  79. data/lib/pact/consumer_contract.rb +1 -110
  80. data/lib/pact/doc/doc_file.rb +40 -0
  81. data/lib/pact/doc/generate.rb +11 -0
  82. data/lib/pact/doc/generator.rb +82 -0
  83. data/lib/pact/doc/interaction_view_model.rb +114 -0
  84. data/lib/pact/doc/markdown/consumer_contract_renderer.rb +56 -0
  85. data/lib/pact/doc/markdown/generator.rb +26 -0
  86. data/lib/pact/doc/markdown/index_renderer.rb +41 -0
  87. data/lib/pact/doc/markdown/interaction.erb +14 -0
  88. data/lib/pact/doc/markdown/interaction_renderer.rb +38 -0
  89. data/lib/pact/doc/sort_interactions.rb +17 -0
  90. data/lib/pact/matchers/actual_type.rb +16 -0
  91. data/lib/pact/matchers/base_difference.rb +37 -0
  92. data/lib/pact/matchers/differ.rb +153 -0
  93. data/lib/pact/matchers/difference.rb +13 -0
  94. data/lib/pact/matchers/difference_indicator.rb +26 -0
  95. data/lib/pact/matchers/embedded_diff_formatter.rb +62 -0
  96. data/lib/pact/matchers/expected_type.rb +35 -0
  97. data/lib/pact/matchers/index_not_found.rb +15 -0
  98. data/lib/pact/matchers/list_diff_formatter.rb +101 -0
  99. data/lib/pact/matchers/matchers.rb +91 -37
  100. data/lib/pact/matchers/no_diff_indicator.rb +18 -0
  101. data/lib/pact/matchers/regexp_difference.rb +13 -0
  102. data/lib/pact/matchers/type_difference.rb +16 -0
  103. data/lib/pact/matchers/unexpected_index.rb +11 -0
  104. data/lib/pact/matchers/unexpected_key.rb +11 -0
  105. data/lib/pact/matchers/unix_diff_formatter.rb +114 -0
  106. data/lib/pact/project_root.rb +7 -0
  107. data/lib/pact/provider/configuration/configuration_extension.rb +58 -0
  108. data/lib/pact/provider/configuration/dsl.rb +13 -0
  109. data/lib/pact/provider/configuration/pact_verification.rb +46 -0
  110. data/lib/pact/provider/configuration/service_provider_config.rb +16 -0
  111. data/lib/pact/provider/configuration/service_provider_dsl.rb +59 -0
  112. data/lib/pact/provider/configuration.rb +7 -0
  113. data/lib/pact/provider/context.rb +0 -0
  114. data/lib/pact/provider/matchers/messages.rb +45 -0
  115. data/lib/pact/provider/pact_helper_locator.rb +22 -0
  116. data/lib/pact/provider/pact_spec_runner.rb +123 -0
  117. data/lib/pact/provider/pact_verification.rb +17 -0
  118. data/lib/pact/provider/print_missing_provider_states.rb +30 -0
  119. data/lib/pact/provider/request.rb +70 -0
  120. data/lib/pact/provider/rspec/backtrace_formatter.rb +43 -0
  121. data/lib/pact/provider/rspec/custom_options_file +0 -0
  122. data/lib/pact/provider/rspec/formatter_rspec_2.rb +65 -0
  123. data/lib/pact/provider/rspec/formatter_rspec_3.rb +96 -0
  124. data/lib/pact/provider/rspec/matchers.rb +97 -0
  125. data/lib/pact/provider/rspec.rb +181 -0
  126. data/lib/pact/provider/state/provider_state.rb +181 -0
  127. data/lib/pact/provider/state/provider_state_configured_modules.rb +13 -0
  128. data/lib/pact/provider/state/provider_state_manager.rb +42 -0
  129. data/lib/pact/provider/state/provider_state_proxy.rb +39 -0
  130. data/lib/pact/provider/test_methods.rb +47 -0
  131. data/lib/pact/provider/verification_report.rb +36 -0
  132. data/lib/pact/provider/world.rb +23 -0
  133. data/lib/pact/provider.rb +3 -0
  134. data/lib/pact/reification.rb +6 -4
  135. data/lib/pact/rspec.rb +53 -0
  136. data/lib/pact/shared/active_support_support.rb +51 -0
  137. data/lib/pact/shared/dsl.rb +76 -0
  138. data/lib/pact/shared/jruby_support.rb +18 -0
  139. data/lib/pact/shared/json_differ.rb +15 -0
  140. data/lib/pact/shared/key_not_found.rb +15 -0
  141. data/lib/pact/shared/null_expectation.rb +31 -0
  142. data/lib/pact/shared/request.rb +80 -0
  143. data/lib/pact/shared/text_differ.rb +14 -0
  144. data/lib/pact/something_like.rb +49 -0
  145. data/lib/pact/symbolize_keys.rb +12 -0
  146. data/lib/pact/tasks/task_helper.rb +36 -0
  147. data/lib/pact/tasks/verification_task.rb +87 -0
  148. data/lib/pact/tasks.rb +2 -0
  149. data/lib/pact/templates/provider_state.erb +14 -0
  150. data/lib/pact/term.rb +47 -2
  151. data/lib/pact/version.rb +1 -1
  152. data/lib/pact.rb +1 -1
  153. data/lib/tasks/pact.rake +25 -2
  154. data/pact.gemspec +13 -11
  155. data/{scratchpad.txt → scratchpad.rb} +18 -2
  156. data/spec/features/consumption_spec.rb +24 -56
  157. data/spec/features/production_spec.rb +19 -24
  158. data/spec/features/{producer_states → provider_states}/zebras.rb +3 -3
  159. data/spec/integration/consumer_spec.rb +212 -0
  160. data/spec/integration/pact/consumer_configuration_spec.rb +66 -0
  161. data/spec/integration/pact/provider_configuration_spec.rb +25 -0
  162. data/spec/lib/pact/cli_spec.rb +47 -0
  163. data/spec/lib/pact/configuration_spec.rb +269 -7
  164. data/spec/lib/pact/consumer/app_manager_spec.rb +3 -3
  165. data/spec/lib/pact/consumer/configuration_spec.rb +57 -0
  166. data/spec/lib/pact/consumer/consumer_contract_builder_spec.rb +52 -70
  167. data/spec/lib/pact/consumer/interaction_builder_spec.rb +91 -0
  168. data/spec/lib/pact/consumer/interactions_spec.rb +64 -0
  169. data/spec/lib/pact/consumer/mock_service/app_spec.rb +52 -0
  170. data/spec/lib/pact/consumer/mock_service/interaction_list_spec.rb +78 -0
  171. data/spec/lib/pact/consumer/mock_service/interaction_mismatch_spec.rb +70 -0
  172. data/spec/lib/pact/consumer/mock_service/interaction_replay_spec.rb +12 -0
  173. data/spec/lib/pact/consumer/mock_service/rack_request_helper_spec.rb +88 -0
  174. data/spec/lib/pact/consumer/mock_service/verification_get_spec.rb +142 -0
  175. data/spec/lib/pact/consumer/mock_service_client_spec.rb +88 -0
  176. data/spec/lib/pact/consumer/mock_service_interaction_expectation_spec.rb +54 -0
  177. data/spec/lib/pact/consumer/request_spec.rb +24 -0
  178. data/spec/lib/pact/consumer/service_consumer_spec.rb +1 -1
  179. data/spec/lib/pact/consumer_contract/active_support_support_spec.rb +58 -0
  180. data/spec/lib/pact/consumer_contract/consumer_contract_spec.rb +180 -0
  181. data/spec/lib/pact/consumer_contract/consumer_contract_writer_spec.rb +111 -0
  182. data/spec/lib/pact/consumer_contract/headers_spec.rb +107 -0
  183. data/spec/lib/pact/consumer_contract/interaction_spec.rb +107 -0
  184. data/spec/lib/pact/{request_spec.rb → consumer_contract/request_spec.rb} +80 -67
  185. data/spec/lib/pact/doc/generator_spec.rb +84 -0
  186. data/spec/lib/pact/doc/interaction_view_model_spec.rb +132 -0
  187. data/spec/lib/pact/doc/markdown/consumer_contract_renderer_spec.rb +29 -0
  188. data/spec/lib/pact/doc/markdown/index_renderer_spec.rb +29 -0
  189. data/spec/lib/pact/matchers/differ_spec.rb +214 -0
  190. data/spec/lib/pact/matchers/difference_spec.rb +22 -0
  191. data/spec/lib/pact/matchers/embedded_diff_formatter_spec.rb +90 -0
  192. data/spec/lib/pact/matchers/index_not_found_spec.rb +21 -0
  193. data/spec/lib/pact/matchers/list_diff_formatter_spec.rb +114 -0
  194. data/spec/lib/pact/matchers/matchers_spec.rb +400 -254
  195. data/spec/lib/pact/matchers/regexp_difference_spec.rb +20 -0
  196. data/spec/lib/pact/matchers/type_difference_spec.rb +34 -0
  197. data/spec/lib/pact/matchers/unexpected_index_spec.rb +20 -0
  198. data/spec/lib/pact/matchers/unexpected_key_spec.rb +20 -0
  199. data/spec/lib/pact/matchers/unix_diff_formatter_spec.rb +216 -0
  200. data/spec/lib/pact/provider/configuration/configuration_extension_spec.rb +30 -0
  201. data/spec/lib/pact/provider/configuration/pact_verification_spec.rb +43 -0
  202. data/spec/lib/pact/provider/configuration/service_provider_config_spec.rb +21 -0
  203. data/spec/lib/pact/provider/configuration/service_provider_dsl_spec.rb +108 -0
  204. data/spec/lib/pact/provider/configuration_spec.rb +50 -0
  205. data/spec/lib/pact/provider/matchers/messages_spec.rb +116 -0
  206. data/spec/lib/pact/provider/pact_helper_locator_spec.rb +54 -0
  207. data/spec/lib/pact/provider/print_missing_provider_states_spec.rb +19 -0
  208. data/spec/lib/pact/provider/request_spec.rb +78 -0
  209. data/spec/lib/pact/provider/rspec/formatter_rspec_2_spec.rb +68 -0
  210. data/spec/lib/pact/provider/rspec/formatter_rspec_3_spec.rb +72 -0
  211. data/spec/lib/pact/provider/rspec_spec.rb +55 -0
  212. data/spec/lib/pact/provider/state/provider_state_manager_spec.rb +89 -0
  213. data/spec/lib/pact/provider/state/provider_state_proxy_spec.rb +80 -0
  214. data/spec/lib/pact/provider/state/provider_state_spec.rb +213 -0
  215. data/spec/lib/pact/provider/world_spec.rb +41 -0
  216. data/spec/lib/pact/reification_spec.rb +24 -0
  217. data/spec/lib/pact/shared/dsl_spec.rb +86 -0
  218. data/spec/lib/pact/shared/json_differ_spec.rb +36 -0
  219. data/spec/lib/pact/shared/key_not_found_spec.rb +20 -0
  220. data/spec/lib/pact/shared/request_spec.rb +111 -0
  221. data/spec/lib/pact/shared/text_differ_spec.rb +54 -0
  222. data/spec/lib/pact/something_like_spec.rb +21 -0
  223. data/spec/lib/pact/tasks/task_helper_spec.rb +74 -0
  224. data/spec/lib/pact/tasks/verification_task_spec.rb +75 -0
  225. data/spec/lib/pact/term_spec.rb +57 -4
  226. data/spec/pact_specification/compliance-1.0.0.rb +47 -0
  227. data/spec/spec_helper.rb +17 -0
  228. data/spec/standalone/consumer_fail_test.rb +55 -0
  229. data/spec/standalone/consumer_pass_test.rb +51 -0
  230. data/spec/support/a_consumer-a_producer.json +2 -4
  231. data/spec/support/a_consumer-a_provider.json +32 -0
  232. data/spec/support/active_support_if_configured.rb +6 -0
  233. data/spec/support/app_for_config_ru.rb +4 -0
  234. data/spec/support/consumer_contract_template.json +24 -0
  235. data/spec/support/dsl_spec_support.rb +7 -0
  236. data/spec/support/factories.rb +82 -0
  237. data/spec/support/generated_index.md +4 -0
  238. data/spec/support/generated_markdown.md +55 -0
  239. data/spec/support/interaction_view_model.json +63 -0
  240. data/spec/support/interaction_view_model_with_terms.json +50 -0
  241. data/spec/support/markdown_pact.json +48 -0
  242. data/spec/support/missing_provider_states_output.txt +25 -0
  243. data/spec/support/options.json +21 -0
  244. data/spec/support/options_app.rb +15 -0
  245. data/spec/support/pact_helper.rb +57 -0
  246. data/spec/support/shared_examples_for_request.rb +94 -0
  247. data/spec/support/spec_support.rb +20 -0
  248. data/spec/support/stubbing.json +22 -0
  249. data/spec/support/stubbing_using_allow.rb +29 -0
  250. data/spec/support/term.json +48 -0
  251. data/spec/support/test_app_fail.json +41 -2
  252. data/spec/support/test_app_pass.json +19 -2
  253. data/spec/support/test_app_with_right_content_type_differ.json +23 -0
  254. data/tasks/pact-test.rake +98 -8
  255. data/tasks/spec.rake +8 -0
  256. metadata +393 -95
  257. data/lib/pact/app.rb +0 -32
  258. data/lib/pact/consumer/configuration_dsl.rb +0 -71
  259. data/lib/pact/consumer/dsl.rb +0 -98
  260. data/lib/pact/consumer/interaction.rb +0 -70
  261. data/lib/pact/consumer/run_condor.rb +0 -4
  262. data/lib/pact/consumer/run_mock_contract_service.rb +0 -13
  263. data/lib/pact/consumer/service_consumer.rb +0 -22
  264. data/lib/pact/consumer/service_producer.rb +0 -23
  265. data/lib/pact/json_warning.rb +0 -23
  266. data/lib/pact/producer/configuration_dsl.rb +0 -62
  267. data/lib/pact/producer/matchers.rb +0 -22
  268. data/lib/pact/producer/pact_spec_runner.rb +0 -57
  269. data/lib/pact/producer/producer_state.rb +0 -81
  270. data/lib/pact/producer/rspec.rb +0 -127
  271. data/lib/pact/producer/test_methods.rb +0 -89
  272. data/lib/pact/producer.rb +0 -1
  273. data/lib/pact/rake_task.rb +0 -64
  274. data/lib/pact/request.rb +0 -109
  275. data/lib/pact/verification_task.rb +0 -57
  276. data/spec/integration/pact/configuration_spec.rb +0 -65
  277. data/spec/lib/pact/consumer/dsl_spec.rb +0 -52
  278. data/spec/lib/pact/consumer/interaction_spec.rb +0 -108
  279. data/spec/lib/pact/consumer/mock_service_spec.rb +0 -147
  280. data/spec/lib/pact/consumer_contract_spec.rb +0 -125
  281. data/spec/lib/pact/producer/configuration_dsl_spec.rb +0 -101
  282. data/spec/lib/pact/producer/producer_state_spec.rb +0 -103
  283. data/spec/lib/pact/producer/rspec_spec.rb +0 -48
  284. data/spec/lib/pact/verification_task_spec.rb +0 -64
  285. data/spec/support/pact_rake_support.rb +0 -41
@@ -1,340 +1,2 @@
1
- require 'net/http'
2
- require 'uri'
3
- require 'json'
4
- require 'hashie'
5
- require 'singleton'
6
- require 'logger'
7
- require 'awesome_print'
8
- require 'awesome_print/core_ext/logger' #For some reason we get an error indicating that the method 'ap' is private unless we load this specifically
9
- require 'json/add/regexp'
10
- require 'pact/matchers'
1
+ require 'pact/consumer/mock_service/app'
11
2
 
12
- AwesomePrint.defaults = {
13
- indent: -2,
14
- plain: true,
15
- index: false
16
- }
17
-
18
- module Pact
19
- module Consumer
20
-
21
- class InteractionList
22
- include Singleton
23
-
24
- attr_reader :interactions
25
- attr_reader :unexpected_requests
26
-
27
- def initialize
28
- clear
29
- end
30
-
31
- # For testing, sigh
32
- def clear
33
- @interactions = []
34
- @matched_interactions = []
35
- @unexpected_requests = []
36
- end
37
-
38
- def add interactions
39
- @interactions << interactions
40
- end
41
-
42
- def register_matched interaction
43
- @matched_interactions << interaction
44
- end
45
-
46
- def register_unexpected request
47
- @unexpected_requests << request
48
- end
49
-
50
- def all_matched?
51
- interaction_diffs.empty?
52
- end
53
-
54
- def missing_interactions
55
- @interactions - @matched_interactions
56
- end
57
-
58
- def interaction_diffs
59
- {
60
- :missing_interactions => missing_interactions,
61
- :unexpected_requests => unexpected_requests
62
- }.inject({}) do | hash, pair |
63
- hash[pair.first] = pair.last if pair.last.any?
64
- hash
65
- end
66
- end
67
-
68
- end
69
-
70
- module RackHelper
71
- def params_hash env
72
- env["QUERY_STRING"].split("&").collect{| param| param.split("=")}.inject({}){|params, param| params[param.first] = URI.decode(param.last); params }
73
- end
74
- end
75
-
76
- class StartupPoll
77
-
78
- def initialize name, logger
79
- @name = name
80
- @logger = logger
81
- end
82
-
83
- def match? env
84
- env['REQUEST_PATH'] == '/index.html' &&
85
- env['REQUEST_METHOD'] == 'GET'
86
- end
87
-
88
- def respond env
89
- @logger.info "#{@name} started up"
90
- [200, {}, ['Started up fine']]
91
- end
92
- end
93
-
94
- class CapybaraIdentify
95
-
96
- def initialize name, logger
97
- @name = name
98
- @logger = logger
99
- end
100
-
101
- def match? env
102
- env["PATH_INFO"] == "/__identify__"
103
- end
104
-
105
- def respond env
106
- [200, {}, [object_id.to_s]]
107
- end
108
- end
109
-
110
- class InteractionDelete
111
-
112
- include RackHelper
113
-
114
- def initialize name, logger
115
- @name = name
116
- @logger = logger
117
- end
118
-
119
- def match? env
120
- env['REQUEST_PATH'].start_with?('/interactions') &&
121
- env['REQUEST_METHOD'] == 'DELETE'
122
- end
123
-
124
- def respond env
125
- InteractionList.instance.clear
126
- @logger.info "Cleared interactions before example \"#{params_hash(env)['example_description']}\""
127
- [200, {}, ['Deleted interactions']]
128
- end
129
- end
130
-
131
- class InteractionPost
132
-
133
- def initialize name, logger
134
- @name = name
135
- @logger = logger
136
- end
137
-
138
- def match? env
139
- env['REQUEST_PATH'] == '/interactions' &&
140
- env['REQUEST_METHOD'] == 'POST'
141
- end
142
-
143
- def respond env
144
- interactions = Hashie::Mash.new(JSON.load(env['rack.input'].string))
145
- InteractionList.instance.add interactions
146
- @logger.info "Added interaction to #{@name}"
147
- @logger.ap interactions
148
- [200, {}, ['Added interactions']]
149
- end
150
- end
151
-
152
- module RequestExtractor
153
-
154
- REQUEST_KEYS = Hashie::Mash.new({
155
- 'REQUEST_METHOD' => :method,
156
- 'REQUEST_PATH' => :path,
157
- 'QUERY_STRING' => :query,
158
- 'rack.input' => :body
159
- })
160
-
161
- def request_from env
162
- request = env.inject({}) do |memo, (k, v)|
163
- request_key = REQUEST_KEYS[k]
164
- memo[request_key] = v if request_key
165
- memo
166
- end
167
-
168
- mashed_request = Hashie::Mash.new request
169
- mashed_request[:headers] = headers_from env
170
- body_string = mashed_request[:body].read
171
-
172
- if body_string.empty?
173
- mashed_request.delete :body
174
- else
175
- body_is_json = mashed_request[:headers]['Content-Type'] =~ /json/
176
- mashed_request[:body] = body_is_json ? JSON.parse(body_string) : body_string
177
- end
178
- mashed_request[:method] = mashed_request[:method].downcase
179
- mashed_request
180
- end
181
-
182
- def headers_from env
183
- headers = env.reject{ |key, value| !(key.start_with?("HTTP") || key == 'CONTENT_TYPE')}
184
- headers.inject({}) do | hash, header |
185
- hash[standardise_header(header.first)] = header.last
186
- hash
187
- end
188
- end
189
-
190
- def standardise_header header
191
- header.gsub(/^HTTP_/, '').split("_").collect{|word| word[0] + word[1..-1].downcase}.join("-")
192
- end
193
- end
194
-
195
- class InteractionReplay
196
- include Pact::Matchers
197
- include RequestExtractor
198
-
199
- def initialize name, logger
200
- @name = name
201
- @logger = logger
202
- end
203
-
204
- def match? env
205
- true # default handler
206
- end
207
-
208
- def respond env
209
- find_response request_from(env)
210
- end
211
-
212
- private
213
-
214
- def find_response raw_request
215
- actual_request = Request::Actual.from_hash(raw_request)
216
- @logger.info "#{@name} received request"
217
- @logger.ap actual_request.as_json
218
- candidates = []
219
- matching_interactions = InteractionList.instance.interactions.select do |interaction|
220
- expected_request = Request::Expected.from_hash(interaction.request.merge(:description => interaction.description))
221
- candidates << expected_request if expected_request.matches_route? actual_request
222
- expected_request.match actual_request
223
- end
224
- if matching_interactions.size > 1
225
- @logger.info "Multiple interactions found on #{@name}:"
226
- @logger.ap matching_interactions
227
- raise 'Multiple interactions found!'
228
- end
229
- if matching_interactions.empty?
230
- handle_unrecognised_request(actual_request, candidates)
231
- else
232
- response = response_from(matching_interactions.first.response)
233
- InteractionList.instance.register_matched matching_interactions.first
234
- @logger.info "Found matching response on #{@name}:"
235
- @logger.ap response
236
- response
237
- end
238
- end
239
-
240
- def handle_unrecognised_request request, candidates
241
- @logger.ap "No interaction found on #{@name} for request \"#{candidates.map(&:description).join(', ')}\""
242
- @logger.ap 'Interaction diffs for that route:'
243
- interaction_diff = candidates.map do |candidate|
244
- diff(candidate.as_json, request.as_json)
245
- end.to_a
246
- @logger.ap(interaction_diff)
247
- response = {message: "No interaction found for #{request.path}", interaction_diff: interaction_diff}
248
- [500, {'Content-Type' => 'application/json'}, [response.to_json]]
249
- end
250
-
251
- def response_from response
252
- [response.status, (response.headers || {}).to_hash, [render_body(response.body)]]
253
- end
254
-
255
- def render_body body
256
- return '' unless body
257
- body.kind_of?(String) ? body.force_encoding('utf-8') : body.to_json
258
- end
259
-
260
- def logger_info_ap msg
261
- @logger.info msg
262
- end
263
- end
264
-
265
- class VerificationGet
266
-
267
- include RackHelper
268
-
269
- def initialize name, logger, log_description
270
- @name = name
271
- @logger = logger
272
- @log_description = log_description
273
- end
274
-
275
- def match? env
276
- env['REQUEST_PATH'].start_with?('/verify') &&
277
- env['REQUEST_METHOD'] == 'GET'
278
- end
279
-
280
- def respond env
281
- if InteractionList.instance.all_matched?
282
- @logger.info "Veryifying - interactions matched for example \"#{example_description(env)}\""
283
- [200, {}, ['Interactions matched']]
284
- else
285
- @logger.warn "Verifying - actual interactions do not match expected interactions for example \"#{example_description(env)}\". Missing interactions:"
286
- @logger.ap InteractionList.instance.interaction_diffs, :warn
287
- [500, {}, ["Actual interactions do not match expected interactions for mock #{@name}. See #{@log_description} for details."]]
288
- end
289
- end
290
-
291
- def example_description env
292
- params_hash(env)['example_description']
293
- end
294
- end
295
-
296
- class MockService
297
-
298
- def initialize options = {}
299
- options = {log_file: STDOUT}.merge options
300
- log_stream = options[:log_file]
301
- @logger = Logger.new log_stream
302
-
303
- log_description = if log_stream.is_a? File
304
- File.absolute_path(log_stream).gsub(Dir.pwd + "/", '')
305
- else
306
- "standard out/err"
307
- end
308
-
309
- @name = options.fetch(:name, "MockService")
310
- @handlers = [
311
- StartupPoll.new(@name, @logger),
312
- CapybaraIdentify.new(@name, @logger),
313
- VerificationGet.new(@name, @logger, log_description),
314
- InteractionPost.new(@name, @logger),
315
- InteractionDelete.new(@name, @logger),
316
- InteractionReplay.new(@name, @logger)
317
- ]
318
- end
319
-
320
- def to_s
321
- "#{@name} #{super.to_s}"
322
- end
323
-
324
- def call env
325
- response = []
326
- begin
327
- relevant_handler = @handlers.detect { |handler| handler.match? env }
328
- response = relevant_handler.respond env
329
- rescue Exception => e
330
- @logger.ap 'Error ocurred in mock service:'
331
- @logger.ap e
332
- @logger.ap e.backtrace
333
- raise e
334
- end
335
- response
336
- end
337
-
338
- end
339
- end
340
- end
@@ -0,0 +1,65 @@
1
+ require 'net/http'
2
+ require 'pact/consumer/mock_service_interaction_expectation'
3
+
4
+ module Pact
5
+ module Consumer
6
+ class MockServiceClient
7
+
8
+ MOCK_SERVICE_ADMINISTRATON_HEADERS = {'X-Pact-Mock-Service' => 'true'}
9
+
10
+ def initialize port
11
+ @http = Net::HTTP.new('localhost', port)
12
+ end
13
+
14
+ def verify example_description
15
+ response = http.request_get("/interactions/verification?example_description=#{URI.encode(example_description)}", MOCK_SERVICE_ADMINISTRATON_HEADERS)
16
+ raise "\e[31m#{response.body}\e[m" unless response.is_a? Net::HTTPSuccess
17
+ end
18
+
19
+ def log msg
20
+ http.request_get("/log?msg=#{URI.encode(msg)}", MOCK_SERVICE_ADMINISTRATON_HEADERS)
21
+ end
22
+
23
+ def wait_for_interactions wait_max_seconds, poll_interval
24
+ wait_until_true(wait_max_seconds, poll_interval) do
25
+ response = http.request_get("/interactions/missing", MOCK_SERVICE_ADMINISTRATON_HEADERS)
26
+ JSON.parse(response.body)['size'] == 0
27
+ end
28
+ end
29
+
30
+ def clear_interactions example_description
31
+ http.delete("/interactions?example_description=#{URI.encode(example_description)}", MOCK_SERVICE_ADMINISTRATON_HEADERS)
32
+ end
33
+
34
+ def add_expected_interaction interaction
35
+ response = http.request_post('/interactions', MockServiceInteractionExpectation.new(interaction).to_json, MOCK_SERVICE_ADMINISTRATON_HEADERS.merge("Content-Type" => "application/json"))
36
+ raise "\e[31m#{response.body}\e[m" unless response.is_a? Net::HTTPSuccess
37
+ end
38
+
39
+ def self.clear_interactions port, example_description
40
+ Net::HTTP.new("localhost", port).delete("/interactions?example_description=#{URI.encode(example_description)}", MOCK_SERVICE_ADMINISTRATON_HEADERS)
41
+ end
42
+
43
+ def write_pact pacticipant_details
44
+ response = http.request_post("/pact", pacticipant_details.to_json, MOCK_SERVICE_ADMINISTRATON_HEADERS.merge("Content-Type" => "application/json"))
45
+ raise "\e[31m#{response.body}\e[m" unless response.is_a? Net::HTTPSuccess
46
+ response.body
47
+ end
48
+
49
+ private
50
+
51
+ attr_reader :http
52
+
53
+ #todo: in need a better home (where can we move it?)
54
+ def wait_until_true timeout=3, interval=0.1
55
+ time_limit = Time.now + timeout
56
+ loop do
57
+ result = yield
58
+ return if result || Time.now >= time_limit
59
+ sleep interval
60
+ end
61
+ end
62
+
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,37 @@
1
+ require 'pact/reification'
2
+
3
+ # Represents the Interaction in the form required by the MockService
4
+ # The json generated will be posted to the MockService to register the expectation
5
+ module Pact
6
+ module Consumer
7
+ class MockServiceInteractionExpectation
8
+
9
+
10
+ def initialize interaction
11
+ @interaction = interaction
12
+ end
13
+
14
+ def to_hash
15
+ hash = {:description => interaction.description}
16
+ hash[:provider_state] = interaction.provider_state if interaction.provider_state
17
+ options = interaction.request.options.empty? ? {} : { options: interaction.request.options}
18
+ hash[:request] = interaction.request.as_json.merge(options)
19
+ hash[:response] = interaction.response
20
+ hash
21
+ end
22
+
23
+ def as_json options = {}
24
+ to_hash
25
+ end
26
+
27
+ def to_json opts = {}
28
+ as_json.to_json(opts)
29
+ end
30
+
31
+ private
32
+
33
+ attr_reader :interaction
34
+
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,27 @@
1
+ require 'pact/shared/request'
2
+ require 'pact/shared/key_not_found'
3
+
4
+ module Pact
5
+ module Consumer
6
+ module Request
7
+ class Actual < Pact::Request::Base
8
+
9
+ def self.from_hash(hash)
10
+ sym_hash = symbolize_keys hash
11
+ method = sym_hash.fetch(:method)
12
+ path = sym_hash.fetch(:path)
13
+ query = sym_hash.fetch(:query)
14
+ headers = sym_hash.fetch(:headers)
15
+ body = sym_hash.fetch(:body, nil)
16
+ new(method, path, headers, body, query)
17
+ end
18
+
19
+ protected
20
+
21
+ def self.key_not_found
22
+ nil
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,7 +1,6 @@
1
- require_relative '../configuration'
2
- require_relative '../consumer'
3
- require_relative 'dsl'
4
- require_relative 'configuration_dsl'
1
+ require 'pact/consumer'
2
+ require 'pact/consumer/spec_hooks'
3
+ require 'pact/rspec'
5
4
 
6
5
  module Pact
7
6
  module Consumer
@@ -11,33 +10,24 @@ module Pact
11
10
  end
12
11
  end
13
12
 
13
+ hooks = Pact::Consumer::SpecHooks.new
14
+
14
15
  RSpec.configure do |config|
15
16
  config.include Pact::Consumer::RSpec, :pact => true
16
17
 
17
18
  config.before :all, :pact => true do
18
- Pact::Consumer::AppManager.instance.spawn_all
19
- FileUtils.mkdir_p Pact.configuration.pact_dir
19
+ hooks.before_all
20
20
  end
21
21
 
22
22
  config.before :each, :pact => true do | example |
23
- example_description = "#{example.example.example_group.description} #{example.example.description}"
24
- Pact.configuration.logger.info "Clearing all expectations"
25
- Pact::Consumer::AppManager.instance.ports_of_mock_services.each do | port |
26
- Net::HTTP.new("localhost", port).delete("/interactions?example_description=#{URI.encode(example_description)}")
27
- end
23
+ hooks.before_each Pact::RSpec.full_description(example)
28
24
  end
29
25
 
30
26
  config.after :each, :pact => true do | example |
31
- example_description = "#{example.example.example_group.description} #{example.example.description}"
32
- Pact.configuration.logger.info "Verifying interactions for #{example_description}"
33
- Pact.configuration.producer_verifications.each do | producer_verification |
34
- producer_verification.call example_description
35
- end
27
+ hooks.after_each Pact::RSpec.full_description(example)
36
28
  end
37
29
 
38
30
  config.after :suite do
39
- Pact.configuration.logger.info "After suite"
40
- Pact::Consumer::AppManager.instance.kill_all
41
- Pact::Consumer::AppManager.instance.clear_all
31
+ hooks.after_suite
42
32
  end
43
33
  end
@@ -0,0 +1,90 @@
1
+ require 'uri'
2
+ require 'net/http'
3
+ require 'rack'
4
+
5
+ # Copied shamelessly from Capybara
6
+ module Pact
7
+ class Server
8
+ class Middleware
9
+ attr_accessor :error
10
+
11
+ def initialize(app)
12
+ @app = app
13
+ end
14
+
15
+ def call(env)
16
+ if env["PATH_INFO"] == "/__identify__"
17
+ [200, {}, [@app.object_id.to_s]]
18
+ else
19
+ begin
20
+ @app.call(env)
21
+ rescue StandardError => e
22
+ @error = e unless @error
23
+ raise e
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ class << self
30
+ def ports
31
+ @ports ||= {}
32
+ end
33
+ end
34
+
35
+ attr_reader :app, :port
36
+
37
+ def initialize(app, port)
38
+ @app = app
39
+ @middleware = Middleware.new(@app)
40
+ @server_thread = nil # supress warnings
41
+ @port = port
42
+ end
43
+
44
+ def reset_error!
45
+ @middleware.error = nil
46
+ end
47
+
48
+ def error
49
+ @middleware.error
50
+ end
51
+
52
+ def host
53
+ "localhost"
54
+ end
55
+
56
+ def responsive?
57
+ return false if @server_thread && @server_thread.join(0)
58
+
59
+ res = Net::HTTP.start(host, @port) { |http| http.get('/__identify__') }
60
+
61
+ if res.is_a?(Net::HTTPSuccess) or res.is_a?(Net::HTTPRedirection)
62
+ return res.body == @app.object_id.to_s
63
+ end
64
+ rescue SystemCallError
65
+ return false
66
+ end
67
+
68
+ def run_default_server(app, port)
69
+ require 'rack/handler/webrick'
70
+ Rack::Handler::WEBrick.run(app, :Port => port, :AccessLog => [], :Logger => WEBrick::Log::new(nil, 0))
71
+ end
72
+
73
+ def boot
74
+ unless responsive?
75
+ Pact::Server.ports[@app.object_id] = @port
76
+
77
+ @server_thread = Thread.new do
78
+ run_default_server(@middleware, @port)
79
+ end
80
+
81
+ Timeout.timeout(60) { @server_thread.join(0.1) until responsive? }
82
+ end
83
+ rescue Timeout::Error
84
+ raise "Rack application timed out during boot"
85
+ else
86
+ self
87
+ end
88
+
89
+ end
90
+ end
@@ -0,0 +1,38 @@
1
+ require 'pact/doc/generate'
2
+ require 'pact/consumer/world'
3
+
4
+ module Pact
5
+ module Consumer
6
+ class SpecHooks
7
+
8
+ def before_all
9
+ Pact::Consumer::AppManager.instance.spawn_all
10
+ FileUtils.mkdir_p Pact.configuration.pact_dir
11
+ end
12
+
13
+ def before_each example_description
14
+ Pact.consumer_world.register_pact_example_ran
15
+ Pact.configuration.logger.info "Clearing all expectations"
16
+ Pact::Consumer::AppManager.instance.ports_of_mock_services.each do | port |
17
+ Pact::Consumer::MockServiceClient.clear_interactions port, example_description
18
+ end
19
+ end
20
+
21
+ def after_each example_description
22
+ Pact.configuration.logger.info "Verifying interactions for #{example_description}"
23
+ Pact.configuration.provider_verifications.each do | provider_verification |
24
+ provider_verification.call example_description
25
+ end
26
+ end
27
+
28
+ def after_suite
29
+ if Pact.consumer_world.any_pact_examples_ran?
30
+ Pact.consumer_world.consumer_contract_builders.each { | c | c.write_pact }
31
+ Pact::Doc::Generate.call
32
+ Pact::Consumer::AppManager.instance.kill_all
33
+ Pact::Consumer::AppManager.instance.clear_all
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,37 @@
1
+ module Pact
2
+
3
+ def self.consumer_world
4
+ @consumer_world ||= Pact::Consumer::World.new
5
+ end
6
+
7
+ # internal api, for testing only
8
+ def self.clear_consumer_world
9
+ @consumer_world = nil
10
+ end
11
+
12
+ module Consumer
13
+ class World
14
+
15
+ def initialize
16
+ @any_pact_examples_ran = false
17
+ end
18
+
19
+ def consumer_contract_builders
20
+ @consumer_contract_builders ||= []
21
+ end
22
+
23
+ def add_consumer_contract_builder consumer_contract_builder
24
+ consumer_contract_builders << consumer_contract_builder
25
+ end
26
+
27
+ def register_pact_example_ran
28
+ @any_pact_examples_ran = true
29
+ end
30
+
31
+ def any_pact_examples_ran?
32
+ @any_pact_examples_ran
33
+ end
34
+
35
+ end
36
+ end
37
+ end