pact 0.1.28

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 (72) hide show
  1. data/.gitignore +28 -0
  2. data/.rspec +2 -0
  3. data/Gemfile +5 -0
  4. data/Gemfile.lock +83 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +238 -0
  7. data/Rakefile +33 -0
  8. data/bin/pact +4 -0
  9. data/lib/pact/app.rb +32 -0
  10. data/lib/pact/configuration.rb +54 -0
  11. data/lib/pact/consumer/app_manager.rb +177 -0
  12. data/lib/pact/consumer/configuration_dsl.rb +71 -0
  13. data/lib/pact/consumer/consumer_contract_builder.rb +79 -0
  14. data/lib/pact/consumer/consumer_contract_builders.rb +10 -0
  15. data/lib/pact/consumer/dsl.rb +98 -0
  16. data/lib/pact/consumer/interaction.rb +70 -0
  17. data/lib/pact/consumer/mock_service.rb +340 -0
  18. data/lib/pact/consumer/rspec.rb +43 -0
  19. data/lib/pact/consumer/run_condor.rb +4 -0
  20. data/lib/pact/consumer/run_mock_contract_service.rb +13 -0
  21. data/lib/pact/consumer/service_consumer.rb +22 -0
  22. data/lib/pact/consumer/service_producer.rb +23 -0
  23. data/lib/pact/consumer.rb +7 -0
  24. data/lib/pact/consumer_contract.rb +110 -0
  25. data/lib/pact/json_warning.rb +23 -0
  26. data/lib/pact/logging.rb +14 -0
  27. data/lib/pact/matchers/matchers.rb +85 -0
  28. data/lib/pact/matchers.rb +1 -0
  29. data/lib/pact/producer/configuration_dsl.rb +62 -0
  30. data/lib/pact/producer/matchers.rb +22 -0
  31. data/lib/pact/producer/pact_spec_runner.rb +57 -0
  32. data/lib/pact/producer/producer_state.rb +81 -0
  33. data/lib/pact/producer/rspec.rb +127 -0
  34. data/lib/pact/producer/test_methods.rb +89 -0
  35. data/lib/pact/producer.rb +1 -0
  36. data/lib/pact/rake_task.rb +64 -0
  37. data/lib/pact/reification.rb +26 -0
  38. data/lib/pact/request.rb +109 -0
  39. data/lib/pact/term.rb +40 -0
  40. data/lib/pact/verification_task.rb +57 -0
  41. data/lib/pact/version.rb +3 -0
  42. data/lib/pact.rb +5 -0
  43. data/lib/tasks/pact.rake +6 -0
  44. data/pact.gemspec +36 -0
  45. data/scratchpad.txt +36 -0
  46. data/spec/features/consumption_spec.rb +146 -0
  47. data/spec/features/producer_states/zebras.rb +28 -0
  48. data/spec/features/production_spec.rb +160 -0
  49. data/spec/integration/pact/configuration_spec.rb +65 -0
  50. data/spec/lib/pact/configuration_spec.rb +35 -0
  51. data/spec/lib/pact/consumer/app_manager_spec.rb +41 -0
  52. data/spec/lib/pact/consumer/consumer_contract_builder_spec.rb +87 -0
  53. data/spec/lib/pact/consumer/dsl_spec.rb +52 -0
  54. data/spec/lib/pact/consumer/interaction_spec.rb +108 -0
  55. data/spec/lib/pact/consumer/mock_service_spec.rb +147 -0
  56. data/spec/lib/pact/consumer/service_consumer_spec.rb +11 -0
  57. data/spec/lib/pact/consumer_contract_spec.rb +125 -0
  58. data/spec/lib/pact/matchers/matchers_spec.rb +354 -0
  59. data/spec/lib/pact/producer/configuration_dsl_spec.rb +101 -0
  60. data/spec/lib/pact/producer/producer_state_spec.rb +103 -0
  61. data/spec/lib/pact/producer/rspec_spec.rb +48 -0
  62. data/spec/lib/pact/reification_spec.rb +43 -0
  63. data/spec/lib/pact/request_spec.rb +316 -0
  64. data/spec/lib/pact/term_spec.rb +36 -0
  65. data/spec/lib/pact/verification_task_spec.rb +64 -0
  66. data/spec/spec_helper.rb +5 -0
  67. data/spec/support/a_consumer-a_producer.json +34 -0
  68. data/spec/support/pact_rake_support.rb +41 -0
  69. data/spec/support/test_app_fail.json +22 -0
  70. data/spec/support/test_app_pass.json +21 -0
  71. data/tasks/pact-test.rake +19 -0
  72. metadata +381 -0
@@ -0,0 +1,43 @@
1
+ require_relative '../configuration'
2
+ require_relative '../consumer'
3
+ require_relative 'dsl'
4
+ require_relative 'configuration_dsl'
5
+
6
+ module Pact
7
+ module Consumer
8
+ module RSpec
9
+ include Pact::Consumer::ConsumerContractBuilders
10
+ end
11
+ end
12
+ end
13
+
14
+ RSpec.configure do |config|
15
+ config.include Pact::Consumer::RSpec, :pact => true
16
+
17
+ config.before :all, :pact => true do
18
+ Pact::Consumer::AppManager.instance.spawn_all
19
+ FileUtils.mkdir_p Pact.configuration.pact_dir
20
+ end
21
+
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
28
+ end
29
+
30
+ 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
36
+ end
37
+
38
+ 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
42
+ end
43
+ end
@@ -0,0 +1,4 @@
1
+ require_relative 'spawn_app'
2
+ require_relative '../../../boot'
3
+
4
+ spawn_app CondorAppAssets.new, APP_PORT
@@ -0,0 +1,13 @@
1
+ require 'net/http'
2
+ require_relative 'spawn_app'
3
+ require_relative 'mock_service'
4
+
5
+ spawn_app MockService.new, 1234
6
+
7
+ RSpec.configure do |c|
8
+ c.before(:each, :type => :feature) do
9
+ http = Net::HTTP.new('localhost', 1234)
10
+ request = Net::HTTP::Delete.new('/interactions')
11
+ response = http.request(request)
12
+ end
13
+ end
@@ -0,0 +1,22 @@
1
+ module Pact
2
+ module Consumer
3
+ class ServiceConsumer
4
+ attr_accessor :name
5
+ def initialize options
6
+ @name = options[:name]
7
+ end
8
+
9
+ def to_s
10
+ name
11
+ end
12
+
13
+ def as_json options = {}
14
+ {name: name}
15
+ end
16
+
17
+ def self.from_hash obj
18
+ ServiceConsumer.new(:name => obj['name'])
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,23 @@
1
+ module Pact
2
+ module Consumer
3
+ # This is a crap name, it's really just a object for serializing to JSON
4
+ class ServiceProducer
5
+ attr_accessor :name
6
+ def initialize options
7
+ @name = options[:name] || '[producer name unknown - please update the pact gem in the consumer project to the latest version and regenerate the pacts]'
8
+ end
9
+
10
+ def to_s
11
+ name
12
+ end
13
+
14
+ def as_json options = {}
15
+ {name: name}
16
+ end
17
+
18
+ def self.from_hash obj
19
+ ServiceProducer.new(:name => obj['name'])
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,7 @@
1
+ require_relative 'consumer/interaction'
2
+ require_relative 'consumer/consumer_contract_builder'
3
+ require_relative 'consumer/mock_service'
4
+ require_relative 'consumer/app_manager'
5
+ require_relative 'term'
6
+ require_relative 'request'
7
+ require_relative 'consumer_contract'
@@ -0,0 +1,110 @@
1
+ require 'pact/consumer/service_consumer'
2
+ require 'pact/consumer/service_producer'
3
+ require 'pact/consumer/interaction'
4
+ require 'pact/logging'
5
+ require 'pact/json_warning'
6
+ require 'date'
7
+
8
+ module Pact
9
+ class ConsumerContract
10
+
11
+ include Pact::Logging
12
+ include Pact::JsonWarning
13
+
14
+ attr_accessor :interactions
15
+ attr_accessor :consumer
16
+ attr_accessor :producer
17
+
18
+ def initialize(attributes = {})
19
+ @interactions = attributes[:interactions] || []
20
+ @consumer = attributes[:consumer]
21
+ @producer = attributes[:producer]
22
+ end
23
+
24
+ def as_json(options = {})
25
+ {
26
+ producer: @producer.as_json,
27
+ consumer: @consumer.as_json,
28
+ interactions: @interactions.collect(&:as_json),
29
+ metadata: {
30
+ pact_gem: {
31
+ version: Pact::VERSION
32
+ }
33
+ }
34
+ }
35
+ end
36
+
37
+ def to_json(options = {})
38
+ as_json(options).to_json(options)
39
+ end
40
+
41
+ def self.from_hash(obj)
42
+ new({
43
+ :interactions => obj['interactions'].collect { |hash| Pact::Consumer::Interaction.from_hash(hash)},
44
+ :consumer => Pact::Consumer::ServiceConsumer.from_hash(obj['consumer']),
45
+ :producer => Pact::Consumer::ServiceProducer.from_hash(obj['producer'] || {})
46
+ })
47
+ end
48
+
49
+ def self.from_json string
50
+ deserialised_object = JSON.load(string)
51
+ from_hash(deserialised_object)
52
+ end
53
+
54
+ def find_interaction criteria
55
+ interactions = find_interactions criteria
56
+ if interactions.size == 0
57
+ raise "Could not find interaction matching #{criteria} in pact file between #{consumer.name} and #{producer.name}."
58
+ elsif interactions.size > 1
59
+ raise "Found more than 1 interaction matching #{criteria} in pact file between #{consumer.name} and #{producer.name}."
60
+ end
61
+ interactions.first
62
+ end
63
+
64
+ def find_interactions criteria
65
+ interactions.select{ | interaction| match_criteria? interaction, criteria}
66
+ end
67
+
68
+ def each
69
+ interactions.each do | interaction |
70
+ yield interaction
71
+ end
72
+ end
73
+
74
+ def match_criteria? interaction, criteria
75
+ criteria.each do | key, value |
76
+ unless match_criterion interaction[key.to_s], value
77
+ return false
78
+ end
79
+ end
80
+ true
81
+ end
82
+
83
+ def match_criterion target, criterion
84
+ target == criterion || (criterion.is_a?(Regexp) && criterion.match(target))
85
+ end
86
+
87
+ def pact_file_name
88
+ "#{filenamify(consumer.name)}-#{filenamify(producer.name)}.json"
89
+ end
90
+
91
+ def pactfile_path
92
+ raise 'You must first specify a consumer and service name' unless (consumer && consumer.name && producer && producer.name)
93
+ @pactfile_path ||= File.join(Pact.configuration.pact_dir, pact_file_name)
94
+ end
95
+
96
+ def update_pactfile
97
+ logger.debug "Updating pact file for #{producer.name} at #{pactfile_path}"
98
+ check_for_active_support_json
99
+ File.open(pactfile_path, 'w') do |f|
100
+ f.write JSON.pretty_generate(self)
101
+ end
102
+ end
103
+
104
+ private
105
+
106
+ def filenamify name
107
+ name.downcase.gsub(/\s/, '_')
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,23 @@
1
+ require 'logger'
2
+ require 'json/add/regexp'
3
+
4
+ module Pact
5
+ module JsonWarning
6
+ def check_for_active_support_json
7
+ # Active support clobbers the as_json methods defined in the json/add directory of the json gem.
8
+ # These methods are required to serialize and deserialize the Regexp and Symbol classes properly.
9
+ # You can potentially fix this by making sure the json gem is required AFTER the active_support/json gem
10
+ # OR if you don't use the json part of activesupport you could only require the parts of active support you really need
11
+ # OR you can only use strings in your pacts.
12
+ # Good luck.
13
+
14
+ # If someone knows how to make sure the pact gem uses the json gem as_json methods when activesupport/json is used in the calling code,
15
+ # without breaking the calling code, which may depend on activesupport/json... then please fix this.
16
+ # Note: we can probably do this in Ruby 2.0 with refinements, but for now, we're all stuck on 1.9 :(
17
+
18
+ unless Regexp.new('').as_json.is_a?(Hash)
19
+ Logger.new($stderr).warn("It appears you are using ActiveSupport json in your project. You are now in rubygems hell. Please see Pact::JsonWarning for more info.")
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,14 @@
1
+ require 'logger'
2
+ require_relative 'configuration'
3
+
4
+ module Pact
5
+ module Logging
6
+ def self.included(base)
7
+ base.extend(self)
8
+ end
9
+
10
+ def logger
11
+ Pact.configuration.logger
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,85 @@
1
+ require 'awesome_print'
2
+ require 'pact/term'
3
+
4
+ module Pact
5
+ module Matchers
6
+
7
+ NO_DIFF_INDICATOR = 'no difference here!'
8
+
9
+ def diff expected, actual, options = {}
10
+ case expected
11
+ when Hash then hash_diff(expected, actual, options)
12
+ when Array then array_diff(expected, actual, options)
13
+ when Pact::Term then diff(expected.matcher, actual, options)
14
+ when Regexp then regexp_diff(expected, actual, options)
15
+ else object_diff(expected, actual, options)
16
+ end
17
+ end
18
+
19
+ def structure_diff expected, actual
20
+ diff expected, actual, {structure: true}
21
+ end
22
+
23
+ def regexp_diff regexp, actual, options
24
+ if actual != nil && regexp.match(actual)
25
+ {}
26
+ else
27
+ {expected: regexp, actual: actual}
28
+ end
29
+ end
30
+
31
+ def array_diff expected, actual, options
32
+ if actual.is_a? Array
33
+ if expected.length == actual.length
34
+ difference = []
35
+ diff_found = false
36
+ expected.each_with_index do | item, index|
37
+ if (item_diff = diff(item, actual[index], options)).any?
38
+ diff_found = true
39
+ difference << item_diff
40
+ else
41
+ difference << NO_DIFF_INDICATOR
42
+ end
43
+ end
44
+ diff_found ? difference : {}
45
+ else
46
+ {expected: expected, actual: actual}
47
+ end
48
+ else
49
+ {expected: expected, actual: actual}
50
+ end
51
+ end
52
+
53
+ def hash_diff expected, actual, options
54
+ if actual.is_a? Hash
55
+ expected.keys.inject({}) do |diff, key|
56
+ if (diff_at_key = diff(expected[key], actual[key], options)).any?
57
+ diff[key] = diff_at_key
58
+ end
59
+ diff
60
+ end
61
+ else
62
+ {expected: expected, actual: actual}
63
+ end
64
+ end
65
+
66
+ def class_diff expected, actual
67
+ if expected.class != actual.class
68
+ actual_display = actual.nil? ? nil : {:class => actual.class, :value => actual }
69
+ expected_display = expected.nil? ? nil : {:class => expected.class, eg: expected}
70
+ {:expected => expected_display, :actual => actual_display}
71
+ else
72
+ {}
73
+ end
74
+ end
75
+
76
+ def object_diff expected, actual, options
77
+ return class_diff(expected, actual) if options[:structure]
78
+ if expected != actual
79
+ {:expected => expected, :actual => actual}
80
+ else
81
+ {}
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1 @@
1
+ require_relative 'matchers/matchers'
@@ -0,0 +1,62 @@
1
+ require 'ostruct'
2
+
3
+
4
+ module Pact
5
+ module Producer
6
+ module ConfigurationDSL
7
+
8
+ def producer &block
9
+ @producer ||= nil
10
+ if block_given?
11
+ @producer = ProducerDSL.new(&block).create_producer_config
12
+ elsif @producer
13
+ @producer
14
+ else
15
+ raise "Please configure your producer. See the Producer section in the README for examples."
16
+ end
17
+ end
18
+
19
+ class ProducerConfig
20
+ attr_accessor :name
21
+
22
+ def initialize name, &app_block
23
+ @name = name
24
+ @app_block = app_block
25
+ end
26
+
27
+ def app
28
+ @app_block.call
29
+ end
30
+ end
31
+
32
+ class ProducerDSL
33
+
34
+ def initialize &block
35
+ @app = nil
36
+ @name = nil
37
+ instance_eval(&block)
38
+ end
39
+
40
+ def validate
41
+ raise "Please provide a name for the Producer" unless @name
42
+ raise "Please configure an app for the Producer" unless @app_block
43
+ end
44
+
45
+ def name name
46
+ @name = name
47
+ end
48
+
49
+ def app &block
50
+ @app_block = block
51
+ end
52
+
53
+ def create_producer_config
54
+ validate
55
+ ProducerConfig.new(@name, &@app_block)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ Pact::Configuration.send(:include, Pact::Producer::ConfigurationDSL)
@@ -0,0 +1,22 @@
1
+ require 'pact/term'
2
+ require 'awesome_print'
3
+ require 'pact/matchers'
4
+ require 'awesome_print'
5
+
6
+ RSpec::Matchers.define :match_term do |expected|
7
+ include Pact::Matchers
8
+
9
+ match do |actual|
10
+ if (difference = diff(expected, actual)).any?
11
+ @message = difference
12
+ false
13
+ else
14
+ true
15
+ end
16
+ end
17
+
18
+ failure_message_for_should do | actual |
19
+ @message.ai
20
+ end
21
+
22
+ end
@@ -0,0 +1,57 @@
1
+ require 'open-uri'
2
+ require 'rspec'
3
+ require 'rspec/core'
4
+ require 'rspec/core/formatters/documentation_formatter'
5
+ require_relative 'rspec'
6
+
7
+ module Pact
8
+ module Producer
9
+ class PactSpecRunner
10
+
11
+ extend Pact::Producer::RSpec::ClassMethods
12
+
13
+ def self.run(spec_definitions, options = {})
14
+ initialize_specs spec_definitions
15
+ configure_rspec options
16
+ run_specs
17
+ end
18
+
19
+ private
20
+
21
+ def self.initialize_specs spec_definitions
22
+ spec_definitions.each do | spec_definition |
23
+ require spec_definition[:support_file] if spec_definition[:support_file]
24
+ options = {consumer: spec_definition[:consumer], save_pactfile_to_tmp: true}
25
+ honour_pactfile spec_definition[:uri], options
26
+ end
27
+ end
28
+
29
+ def self.configure_rspec options
30
+ config = ::RSpec.configuration
31
+ config.color = true
32
+
33
+ unless options[:silent]
34
+ config.error_stream = $stderr
35
+ config.output_stream = $stdout
36
+ end
37
+
38
+ formatter = ::RSpec::Core::Formatters::DocumentationFormatter.new(config.output)
39
+ reporter = ::RSpec::Core::Reporter.new(formatter)
40
+ config.instance_variable_set(:@reporter, reporter)
41
+ end
42
+
43
+ def self.run_specs
44
+ config = ::RSpec.configuration
45
+ world = ::RSpec::world
46
+ config.reporter.report(world.example_count, nil) do |reporter|
47
+ begin
48
+ config.run_hook(:before, :suite)
49
+ world.example_groups.ordered.map {|g| g.run(reporter)}.all? ? 0 : config.failure_exit_code
50
+ ensure
51
+ config.run_hook(:after, :suite)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,81 @@
1
+ module Pact
2
+ module Producer
3
+
4
+ module DSL
5
+ def producer_state name, &block
6
+ ProducerState.producer_state(name, &block).register
7
+ end
8
+
9
+ def with_consumer name, &block
10
+ ProducerState.current_namespaces << name
11
+ instance_eval(&block)
12
+ ProducerState.current_namespaces.pop
13
+ end
14
+ end
15
+
16
+ class ProducerState
17
+
18
+ attr_accessor :name
19
+ attr_accessor :namespace
20
+
21
+ def self.producer_state name, &block
22
+ ProducerState.new(name, current_namespaces.join('.'), &block)
23
+ end
24
+
25
+ def self.register name, producer_state
26
+ producer_states[name] = producer_state
27
+ end
28
+
29
+ def self.producer_states
30
+ @@producer_states ||= {}
31
+ end
32
+
33
+ def self.current_namespaces
34
+ @@current_namespaces ||= []
35
+ end
36
+
37
+ def self.get name, options = {}
38
+ fullname = options[:for] ? "#{options[:for]}.#{name}" : name
39
+ (producer_states[fullname] || producer_states[fullname.to_sym]) || producer_states[name]
40
+ end
41
+
42
+ def register
43
+ self.class.register(namespaced(name), self)
44
+ end
45
+
46
+ def initialize name, namespace, &block
47
+ @name = name
48
+ @namespace = namespace
49
+ instance_eval(&block)
50
+ end
51
+
52
+ def set_up &block
53
+ if block_given?
54
+ @set_up_block = block
55
+ elsif @set_up_block
56
+ instance_eval &@set_up_block
57
+ end
58
+ end
59
+
60
+ def tear_down &block
61
+ if block_given?
62
+ @tear_down_block = block
63
+ elsif @tear_down_block
64
+ instance_eval &@tear_down_block
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def namespaced(name)
71
+ if namespace.empty?
72
+ name
73
+ else
74
+ "#{namespace}.#{name}"
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+
81
+ Pact.send(:extend, Pact::Producer::DSL)