From 7a1cf3b24746554d71d8157b475016355cd87a83 Mon Sep 17 00:00:00 2001 From: Honza2 Date: Sun, 23 Feb 2020 23:55:43 +0100 Subject: [PATCH 1/2] feat(generators): Pass context and add ProviderState generator --- lib/pact/provider/generators.rb | 26 +++++++++ .../provider/generators/provider_state.rb | 58 +++++++++++++++++++ lib/pact/provider/request.rb | 9 ++- lib/pact/provider/rspec.rb | 10 +++- lib/pact/provider/test_methods.rb | 12 +++- 5 files changed, 108 insertions(+), 7 deletions(-) create mode 100644 lib/pact/provider/generators.rb create mode 100644 lib/pact/provider/generators/provider_state.rb diff --git a/lib/pact/provider/generators.rb b/lib/pact/provider/generators.rb new file mode 100644 index 00000000..bbafd09a --- /dev/null +++ b/lib/pact/provider/generators.rb @@ -0,0 +1,26 @@ + +require 'pact/provider/generators/provider_state'; + +module Pact + module Provider + class Generators + def self.add_generator generator + generators.unshift(generator) + end + + def self.generators + @generators ||= [] + end + + def self.execute_generators object, interaction_context = nil + generators.each do | parser | + return parser.call(object, interaction_context) if parser.can_generate?(object) + end + + raise Pact::UnrecognizePactFormatError.new("This document does not use a recognised Pact generator: #{object}") + end + + add_generator(ProviderStateGenerator.new) + end + end +end diff --git a/lib/pact/provider/generators/provider_state.rb b/lib/pact/provider/generators/provider_state.rb new file mode 100644 index 00000000..fc86b67d --- /dev/null +++ b/lib/pact/provider/generators/provider_state.rb @@ -0,0 +1,58 @@ +require 'pact/provider/generators' + +module Pact + module Provider + class ProviderStateGenerator + + + # rewrite of https://github.com/DiUS/pact-jvm/blob/master/core/support/src/main/kotlin/au/com/dius/pact/core/support/expressions/ExpressionParser.kt#L27 + VALUES_SEPARATOR = "," + START_EXPRESSION = "\${" + END_EXPRESSION = '}' + def parse_expression expression, params + + return_string = [] + + buffer = expression; + # initial value + position = buffer.index(START_EXPRESSION) + + while (position && position >= 0) + if (position > 0) + # add string + return_string.push(buffer[0...position]) + end + end_position = buffer.index(END_EXPRESSION, position) + if (end_position < 0) + raise "Missing closing brace in expression string \"#{$value}\"" + end + + variable = "" + + if (end_position - position > 2) + expression = params[buffer[position+2...end_position]] || "" + end + return_string.push(expression) + + buffer = buffer[end_position + 1...-1] + position = buffer.index(START_EXPRESSION) + end + + return_string.join("") + end + + def call hash, interaction_context = nil + params = interaction_context.state_params || {} + + parse_expression hash["expression"], params + end + + def can_generate?(hash) + hash.key?('type') && hash['type'] === 'ProviderState' + end + end + end +end + + + diff --git a/lib/pact/provider/request.rb b/lib/pact/provider/request.rb index ad4b1998..3701421a 100644 --- a/lib/pact/provider/request.rb +++ b/lib/pact/provider/request.rb @@ -1,6 +1,7 @@ require 'json' require 'pact/reification' require 'pact/shared/null_expectation' +require 'pact/provider/generators' module Pact module Provider @@ -10,8 +11,9 @@ class Replayable # See https://github.com/rack/rack/blob/e7d741c6282ca4cf4e01506f5681e6e6b14c0b32/SPEC#L87-89 NO_HTTP_PREFIX = ["CONTENT-TYPE", "CONTENT-LENGTH"] - def initialize expected_request + def initialize expected_request, interaction_context = nil @expected_request = expected_request + @interaction_context = interaction_context end def method @@ -19,6 +21,11 @@ def method end def path + if expected_request.methods.include? :generators + if expected_request.generators["path"] + return Pact::Provider::Generators.execute_generators(expected_request.generators["path"], @interaction_context) + end + end expected_request.full_path end diff --git a/lib/pact/provider/rspec.rb b/lib/pact/provider/rspec.rb index a6e7372c..57ed8acc 100644 --- a/lib/pact/provider/rspec.rb +++ b/lib/pact/provider/rspec.rb @@ -25,6 +25,7 @@ def honour_pactfile pact_source, pact_json, options pact_uri = pact_source.uri Pact.configuration.output_stream.puts "INFO: Reading pact at #{pact_uri}" consumer_contract = Pact::ConsumerContract.from_json(pact_json) + suffix = pact_uri.metadata[:pending] ? " [PENDING]": "" example_group_description = "Verifying a pact between #{consumer_contract.consumer.name} and #{consumer_contract.provider.name}#{suffix}" example_group_metadata = { pactfile_uri: pact_uri, pact_criteria: options[:criteria] } @@ -77,7 +78,6 @@ def describe_interaction_with_provider_state interaction, options end def describe_interaction interaction, options - # pact_uri and pact_interaction are used by # Pact::Provider::RSpec::PactBrokerFormatter @@ -103,8 +103,9 @@ def describe_interaction interaction, options before do | example | interaction_context.run_once :before do Pact.configuration.logger.info "Running example '#{Pact::RSpec.full_description(example)}'" - set_up_provider_states interaction.provider_states, options[:consumer] - replay_interaction interaction, options[:request_customizer] + state_params = set_up_provider_states interaction.provider_states, options[:consumer] + interaction_context.state_params = state_params + replay_interaction interaction, options[:request_customizer], interaction_context interaction_context.last_response = last_response end end @@ -129,6 +130,7 @@ def describe_message expected_response, interaction_context include Pact::RSpec::Matchers extend Pact::Matchers::Messages + let(:expected_contents) { expected_response.body[:contents].as_json } let(:response) { interaction_context.last_response } let(:differ) { Pact.configuration.body_differ_for_content_type diff_content_type } @@ -214,6 +216,8 @@ class InteractionContext attr_accessor :last_response + attr_accessor :state_params + def initialize @already_run = [] end diff --git a/lib/pact/provider/test_methods.rb b/lib/pact/provider/test_methods.rb index f27466af..94e8207c 100644 --- a/lib/pact/provider/test_methods.rb +++ b/lib/pact/provider/test_methods.rb @@ -14,8 +14,8 @@ module TestMethods include Pact::Logging include Rack::Test::Methods - def replay_interaction interaction, request_customizer = nil - request = Request::Replayable.new(interaction.request) + def replay_interaction interaction, request_customizer = nil, interaction_context + request = Request::Replayable.new(interaction.request, interaction_context) request = request_customizer.call(request, interaction) if request_customizer args = [request.path, request.body, request.headers] @@ -42,11 +42,17 @@ def parse_body_from_response rack_response end def set_up_provider_states provider_states, consumer, options = {} + state_params = {}; # If there are no provider state, execute with an nil state to ensure global and base states are executed Pact.configuration.provider_state_set_up.call(nil, consumer, options) if provider_states.nil? || provider_states.empty? provider_states.each do | provider_state | - Pact.configuration.provider_state_set_up.call(provider_state.name, consumer, options.merge(params: provider_state.params)) + result = Pact.configuration.provider_state_set_up.call(provider_state.name, consumer, options.merge(params: provider_state.params)) + if result.is_a?(Hash) + state_params = state_params.merge(result) + end end + + state_params end def tear_down_provider_states provider_states, consumer, options = {} From f55adf1dff5d53575cf54c50cde9c11f46e5fb91 Mon Sep 17 00:00:00 2001 From: Steve Taylor Date: Thu, 15 Sep 2022 10:26:45 +1000 Subject: [PATCH 2/2] feat(generators): Add more generators as per spec --- lib/pact/provider/generator/boolean.rb | 16 +++ lib/pact/provider/generator/date.rb | 64 +++++++++ lib/pact/provider/generator/datetime.rb | 18 +++ lib/pact/provider/generator/provider_state.rb | 57 ++++++++ lib/pact/provider/generator/random_decimal.rb | 39 ++++++ .../provider/generator/random_hexadecimal.rb | 21 +++ lib/pact/provider/generator/random_int.rb | 18 +++ lib/pact/provider/generator/random_string.rb | 18 +++ lib/pact/provider/generator/regex.rb | 19 +++ lib/pact/provider/generator/time.rb | 18 +++ lib/pact/provider/generator/uuid.rb | 21 +++ lib/pact/provider/generators.rb | 62 +++++++-- .../provider/generators/provider_state.rb | 58 -------- lib/pact/provider/request.rb | 20 ++- lib/pact/provider/rspec.rb | 10 +- lib/pact/provider/test_methods.rb | 10 +- pact.gemspec | 4 +- .../pact/provider/generator/boolean_spec.rb | 20 +++ spec/lib/pact/provider/generator/date_spec.rb | 20 +++ .../pact/provider/generator/datetime_spec.rb | 21 +++ .../provider/generator/provider_state_spec.rb | 37 ++++++ .../provider/generator/random_decimal_spec.rb | 27 ++++ .../generator/random_hexadecimal_spec.rb | 25 ++++ .../provider/generator/random_int_spec.rb | 25 ++++ .../provider/generator/random_string_spec.rb | 25 ++++ .../lib/pact/provider/generator/regex_spec.rb | 20 +++ spec/lib/pact/provider/generator/time_spec.rb | 20 +++ spec/lib/pact/provider/generator/uuid_spec.rb | 20 +++ spec/lib/pact/provider/generators_spec.rb | 124 ++++++++++++++++++ 29 files changed, 765 insertions(+), 92 deletions(-) create mode 100644 lib/pact/provider/generator/boolean.rb create mode 100644 lib/pact/provider/generator/date.rb create mode 100644 lib/pact/provider/generator/datetime.rb create mode 100644 lib/pact/provider/generator/provider_state.rb create mode 100644 lib/pact/provider/generator/random_decimal.rb create mode 100644 lib/pact/provider/generator/random_hexadecimal.rb create mode 100644 lib/pact/provider/generator/random_int.rb create mode 100644 lib/pact/provider/generator/random_string.rb create mode 100644 lib/pact/provider/generator/regex.rb create mode 100644 lib/pact/provider/generator/time.rb create mode 100644 lib/pact/provider/generator/uuid.rb delete mode 100644 lib/pact/provider/generators/provider_state.rb create mode 100644 spec/lib/pact/provider/generator/boolean_spec.rb create mode 100644 spec/lib/pact/provider/generator/date_spec.rb create mode 100644 spec/lib/pact/provider/generator/datetime_spec.rb create mode 100644 spec/lib/pact/provider/generator/provider_state_spec.rb create mode 100644 spec/lib/pact/provider/generator/random_decimal_spec.rb create mode 100644 spec/lib/pact/provider/generator/random_hexadecimal_spec.rb create mode 100644 spec/lib/pact/provider/generator/random_int_spec.rb create mode 100644 spec/lib/pact/provider/generator/random_string_spec.rb create mode 100644 spec/lib/pact/provider/generator/regex_spec.rb create mode 100644 spec/lib/pact/provider/generator/time_spec.rb create mode 100644 spec/lib/pact/provider/generator/uuid_spec.rb create mode 100644 spec/lib/pact/provider/generators_spec.rb diff --git a/lib/pact/provider/generator/boolean.rb b/lib/pact/provider/generator/boolean.rb new file mode 100644 index 00000000..499e326a --- /dev/null +++ b/lib/pact/provider/generator/boolean.rb @@ -0,0 +1,16 @@ +module Pact + module Provider + module Generator + # Boolean provides the boolean generator which will give a true or false value + class Boolean + def can_generate?(hash) + hash.key?('type') && hash['type'] == 'Boolean' + end + + def call(_hash, _params = nil, _example_value = nil) + [true, false].sample + end + end + end + end +end diff --git a/lib/pact/provider/generator/date.rb b/lib/pact/provider/generator/date.rb new file mode 100644 index 00000000..7b05c247 --- /dev/null +++ b/lib/pact/provider/generator/date.rb @@ -0,0 +1,64 @@ +require 'date' + +module Pact + module Provider + module Generator + # Date provides the time generator which will give the current date in the defined format + class Date + def can_generate?(hash) + hash.key?('type') && hash['type'] == type + end + + def call(hash, _params = nil, _example_value = nil) + format = hash['format'] || default_format + ::Time.now.strftime(convert_from_java_simple_date_format(format)) + end + + def type + 'Date' + end + + def default_format + 'yyyy-MM-dd' + end + + # Format for the pact specficiation should be the Java DateTimeFormmater + # This tries to convert to something Ruby can format. + def convert_from_java_simple_date_format(format) + # Year + format.sub!(/(?= 0 + if position.positive? + # add string + return_string.push(buffer[0...position]) + end + end_position = buffer.index(END_EXPRESSION, position) + raise 'Missing closing brace in expression string' if !end_position || end_position.negative? + + variable = buffer[position + 2...end_position] + + if !params[variable] + logger.info "Could not subsitute provider state key #{variable}, have #{params}" + end + + expression = params[variable] || '' + return_string.push(expression) + + buffer = buffer[end_position + 1...-1] + position = buffer.index(START_EXPRESSION) + end + + return_string.join('') + end + end + end + end +end diff --git a/lib/pact/provider/generator/random_decimal.rb b/lib/pact/provider/generator/random_decimal.rb new file mode 100644 index 00000000..5a756552 --- /dev/null +++ b/lib/pact/provider/generator/random_decimal.rb @@ -0,0 +1,39 @@ +require 'bigdecimal' + +module Pact + module Provider + module Generator + # RandomDecimal provides the random decimal generator which will generate a decimal value of digits length + class RandomDecimal + def can_generate?(hash) + hash.key?('type') && hash['type'] == 'RandomDecimal' + end + + def call(hash, _params = nil, _example_value = nil) + digits = hash['digits'] || 6 + + raise 'RandomDecimalGenerator digits must be > 0, got $digits' if digits < 1 + + return rand(0..9) if digits == 1 + + return rand(0..9) + rand(1..9) / 10 if digits == 2 + + pos = rand(1..digits - 1) + precision = digits - pos + integers = '' + decimals = '' + while pos.positive? + integers += String(rand(1..9)) + pos -= 1 + end + while precision.positive? + decimals += String(rand(1..9)) + precision -= 1 + end + + Float("#{integers}.#{decimals}") + end + end + end + end +end diff --git a/lib/pact/provider/generator/random_hexadecimal.rb b/lib/pact/provider/generator/random_hexadecimal.rb new file mode 100644 index 00000000..f2e4f4db --- /dev/null +++ b/lib/pact/provider/generator/random_hexadecimal.rb @@ -0,0 +1,21 @@ +require 'securerandom' + +module Pact + module Provider + module Generator + # RandomHexadecimal provides the random hexadecimal generator which will generate a hexadecimal + class RandomHexadecimal + def can_generate?(hash) + hash.key?('type') && hash['type'] == 'RandomHexadecimal' + end + + def call(hash, _params = nil, _example_value = nil) + digits = hash['digits'] || 8 + bytes = (digits / 2).ceil + string = SecureRandom.hex(bytes) + string[0, digits] + end + end + end + end +end diff --git a/lib/pact/provider/generator/random_int.rb b/lib/pact/provider/generator/random_int.rb new file mode 100644 index 00000000..5f749359 --- /dev/null +++ b/lib/pact/provider/generator/random_int.rb @@ -0,0 +1,18 @@ +module Pact + module Provider + module Generator + # RandomInt provides the random int generator which generate a random integer, with a min/max + class RandomInt + def can_generate?(hash) + hash.key?('type') && hash['type'] == 'RandomInt' + end + + def call(hash, _params = nil, _example_value = nil) + min = hash['min'] || 0 + max = hash['max'] || 2_147_483_647 + rand(min..max) + end + end + end + end +end diff --git a/lib/pact/provider/generator/random_string.rb b/lib/pact/provider/generator/random_string.rb new file mode 100644 index 00000000..5c56b08d --- /dev/null +++ b/lib/pact/provider/generator/random_string.rb @@ -0,0 +1,18 @@ +module Pact + module Provider + module Generator + # RandomString provides the random string generator which generate a random string of size length + class RandomString + def can_generate?(hash) + hash.key?('type') && hash['type'] == 'RandomString' + end + + def call(hash, _params = nil, _example_value = nil) + size = hash['size'] || 20 + string = rand(36**(size + 2)).to_s(36) + string[0, size] + end + end + end + end +end diff --git a/lib/pact/provider/generator/regex.rb b/lib/pact/provider/generator/regex.rb new file mode 100644 index 00000000..1415cf87 --- /dev/null +++ b/lib/pact/provider/generator/regex.rb @@ -0,0 +1,19 @@ +require 'string_pattern' + +module Pact + module Provider + module Generator + # Regex provides the regex generator which will generate a value based on the regex pattern provided + class Regex + def can_generate?(hash) + hash.key?('type') && hash['type'] == 'Regex' + end + + def call(hash, _params = nil, _example_value = nil) + pattern = hash['pattern'] || '' + StringPattern.generate(Regexp.new(pattern)) + end + end + end + end +end diff --git a/lib/pact/provider/generator/time.rb b/lib/pact/provider/generator/time.rb new file mode 100644 index 00000000..0db4d214 --- /dev/null +++ b/lib/pact/provider/generator/time.rb @@ -0,0 +1,18 @@ +require 'date' + +module Pact + module Provider + module Generator + # Time provides the time generator which will give the current time in the defined format + class Time < Date + def type + 'Time' + end + + def default_format + 'HH:mm' + end + end + end + end +end diff --git a/lib/pact/provider/generator/uuid.rb b/lib/pact/provider/generator/uuid.rb new file mode 100644 index 00000000..f1441340 --- /dev/null +++ b/lib/pact/provider/generator/uuid.rb @@ -0,0 +1,21 @@ +require 'securerandom' + +module Pact + module Provider + module Generator + # Uuid provides the uuid generator + class Uuid + def can_generate?(hash) + hash.key?('type') && hash['type'] == 'Uuid' + end + + # If we had the example value, we could determine what type of uuid + # to send, this is what pact-jvm does + # See https://github.com/pact-foundation/pact-jvm/blob/master/core/model/src/main/kotlin/au/com/dius/pact/core/model/generators/Generator.kt + def call(_hash, _params = nil, _example_value = nil) + SecureRandom.uuid + end + end + end + end +end diff --git a/lib/pact/provider/generators.rb b/lib/pact/provider/generators.rb index bbafd09a..2cd77cdb 100644 --- a/lib/pact/provider/generators.rb +++ b/lib/pact/provider/generators.rb @@ -1,26 +1,66 @@ - -require 'pact/provider/generators/provider_state'; +require 'pact/provider/generator/boolean' +require 'pact/provider/generator/date' +require 'pact/provider/generator/datetime' +require 'pact/provider/generator/provider_state' +require 'pact/provider/generator/random_decimal' +require 'pact/provider/generator/random_hexadecimal' +require 'pact/provider/generator/random_int' +require 'pact/provider/generator/random_string' +require 'pact/provider/generator/regex' +require 'pact/provider/generator/time' +require 'pact/provider/generator/uuid' +require 'pact/matching_rules/jsonpath' +require 'pact/matching_rules/v3/extract' +require 'jsonpath' module Pact - module Provider + module Provider class Generators - def self.add_generator generator + def self.add_generator(generator) generators.unshift(generator) end - def self.generators + def self.generators @generators ||= [] end - def self.execute_generators object, interaction_context = nil - generators.each do | parser | - return parser.call(object, interaction_context) if parser.can_generate?(object) + def self.execute_generators(object, state_params = nil, example_value = nil) + generators.each do |parser| + return parser.call(object, state_params, example_value) if parser.can_generate?(object) + end + + raise Pact::UnrecognizePactFormatError, "This document does not use a recognised Pact generator: #{object}" + end + + def self.apply_generators(expected_request, component, example_value, state_params) + # Latest pact-support is required to have generators exposed + if expected_request.methods.include?(:generators) && expected_request.generators[component] + # Some component will have single generator without selectors, i.e. path + generators = expected_request.generators[component] + if generators.is_a?(Hash) && generators.key?('type') + return execute_generators(generators, state_params, example_value) + end + + generators.each do |selector, generator| + val = JsonPath.new(selector).on(example_value) + replace = execute_generators(generator, state_params, val) + example_value = JsonPath.for(example_value).gsub(selector) { |_v| replace }.to_hash + end end - - raise Pact::UnrecognizePactFormatError.new("This document does not use a recognised Pact generator: #{object}") + example_value end - add_generator(ProviderStateGenerator.new) + add_generator(Generator::Boolean.new) + add_generator(Generator::Date.new) + add_generator(Generator::DateTime.new) + add_generator(Generator::ProviderState.new) + add_generator(Generator::RandomDecimal.new) + add_generator(Generator::RandomHexadecimal.new) + add_generator(Generator::RandomInt.new) + add_generator(Generator::RandomString.new) + add_generator(Generator::Regex.new) + add_generator(Generator::Time.new) + add_generator(Generator::Uuid.new) end end end diff --git a/lib/pact/provider/generators/provider_state.rb b/lib/pact/provider/generators/provider_state.rb deleted file mode 100644 index fc86b67d..00000000 --- a/lib/pact/provider/generators/provider_state.rb +++ /dev/null @@ -1,58 +0,0 @@ -require 'pact/provider/generators' - -module Pact - module Provider - class ProviderStateGenerator - - - # rewrite of https://github.com/DiUS/pact-jvm/blob/master/core/support/src/main/kotlin/au/com/dius/pact/core/support/expressions/ExpressionParser.kt#L27 - VALUES_SEPARATOR = "," - START_EXPRESSION = "\${" - END_EXPRESSION = '}' - def parse_expression expression, params - - return_string = [] - - buffer = expression; - # initial value - position = buffer.index(START_EXPRESSION) - - while (position && position >= 0) - if (position > 0) - # add string - return_string.push(buffer[0...position]) - end - end_position = buffer.index(END_EXPRESSION, position) - if (end_position < 0) - raise "Missing closing brace in expression string \"#{$value}\"" - end - - variable = "" - - if (end_position - position > 2) - expression = params[buffer[position+2...end_position]] || "" - end - return_string.push(expression) - - buffer = buffer[end_position + 1...-1] - position = buffer.index(START_EXPRESSION) - end - - return_string.join("") - end - - def call hash, interaction_context = nil - params = interaction_context.state_params || {} - - parse_expression hash["expression"], params - end - - def can_generate?(hash) - hash.key?('type') && hash['type'] === 'ProviderState' - end - end - end -end - - - diff --git a/lib/pact/provider/request.rb b/lib/pact/provider/request.rb index 3701421a..561e24a6 100644 --- a/lib/pact/provider/request.rb +++ b/lib/pact/provider/request.rb @@ -11,9 +11,9 @@ class Replayable # See https://github.com/rack/rack/blob/e7d741c6282ca4cf4e01506f5681e6e6b14c0b32/SPEC#L87-89 NO_HTTP_PREFIX = ["CONTENT-TYPE", "CONTENT-LENGTH"] - def initialize expected_request, interaction_context = nil + def initialize expected_request, state_params = nil @expected_request = expected_request - @interaction_context = interaction_context + @state_params = state_params end def method @@ -21,12 +21,7 @@ def method end def path - if expected_request.methods.include? :generators - if expected_request.generators["path"] - return Pact::Provider::Generators.execute_generators(expected_request.generators["path"], @interaction_context) - end - end - expected_request.full_path + Pact::Provider::Generators.apply_generators(expected_request, "path", expected_request.full_path, @state_params) end def body @@ -34,17 +29,20 @@ def body when String then expected_request.body when NullExpectation then '' else - reified_body + Pact::Provider::Generators.apply_generators(expected_request, "body", reified_body, @state_params) end end def headers request_headers = {} return request_headers if expected_request.headers.is_a?(Pact::NullExpectation) + expected_request.headers.each do |key, value| - request_headers[rack_request_header_for(key)] = Pact::Reification.from_term(value) + request_headers[key] = Pact::Reification.from_term(value) end - request_headers + + request_headers = Pact::Provider::Generators.apply_generators(expected_request, "header", request_headers, @state_params) + request_headers.map{ |key,value| [rack_request_header_for(key), value]}.to_h end private diff --git a/lib/pact/provider/rspec.rb b/lib/pact/provider/rspec.rb index 57ed8acc..b498def5 100644 --- a/lib/pact/provider/rspec.rb +++ b/lib/pact/provider/rspec.rb @@ -25,7 +25,7 @@ def honour_pactfile pact_source, pact_json, options pact_uri = pact_source.uri Pact.configuration.output_stream.puts "INFO: Reading pact at #{pact_uri}" consumer_contract = Pact::ConsumerContract.from_json(pact_json) - + suffix = pact_uri.metadata[:pending] ? " [PENDING]": "" example_group_description = "Verifying a pact between #{consumer_contract.consumer.name} and #{consumer_contract.provider.name}#{suffix}" example_group_metadata = { pactfile_uri: pact_uri, pact_criteria: options[:criteria] } @@ -103,9 +103,9 @@ def describe_interaction interaction, options before do | example | interaction_context.run_once :before do Pact.configuration.logger.info "Running example '#{Pact::RSpec.full_description(example)}'" - state_params = set_up_provider_states interaction.provider_states, options[:consumer] - interaction_context.state_params = state_params - replay_interaction interaction, options[:request_customizer], interaction_context + provider_states_result = set_up_provider_states interaction.provider_states, options[:consumer] + state_params = provider_states_result[interaction.provider_state]; + replay_interaction interaction, options[:request_customizer], state_params interaction_context.last_response = last_response end end @@ -216,8 +216,6 @@ class InteractionContext attr_accessor :last_response - attr_accessor :state_params - def initialize @already_run = [] end diff --git a/lib/pact/provider/test_methods.rb b/lib/pact/provider/test_methods.rb index 94e8207c..1619633d 100644 --- a/lib/pact/provider/test_methods.rb +++ b/lib/pact/provider/test_methods.rb @@ -14,8 +14,8 @@ module TestMethods include Pact::Logging include Rack::Test::Methods - def replay_interaction interaction, request_customizer = nil, interaction_context - request = Request::Replayable.new(interaction.request, interaction_context) + def replay_interaction interaction, request_customizer = nil, state_params = nil + request = Request::Replayable.new(interaction.request, state_params) request = request_customizer.call(request, interaction) if request_customizer args = [request.path, request.body, request.headers] @@ -42,17 +42,17 @@ def parse_body_from_response rack_response end def set_up_provider_states provider_states, consumer, options = {} - state_params = {}; + provider_states_result = {}; # If there are no provider state, execute with an nil state to ensure global and base states are executed Pact.configuration.provider_state_set_up.call(nil, consumer, options) if provider_states.nil? || provider_states.empty? provider_states.each do | provider_state | result = Pact.configuration.provider_state_set_up.call(provider_state.name, consumer, options.merge(params: provider_state.params)) if result.is_a?(Hash) - state_params = state_params.merge(result) + provider_states_result[provider_state.name] = result end end - state_params + provider_states_result end def tear_down_provider_states provider_states, consumer, options = {} diff --git a/pact.gemspec b/pact.gemspec index e3a0c556..6456a809 100644 --- a/pact.gemspec +++ b/pact.gemspec @@ -31,8 +31,10 @@ Gem::Specification.new do |gem| gem.add_runtime_dependency 'thor', '>= 0.20', '< 2.0' gem.add_runtime_dependency 'webrick', '~> 1.3' gem.add_runtime_dependency 'term-ansicolor', '~> 1.0' + gem.add_runtime_dependency 'string_pattern', '~> 2.0' + gem.add_runtime_dependency 'jsonpath', '~> 1.0' - gem.add_runtime_dependency 'pact-support', '~> 1.16', '>= 1.16.9' + gem.add_runtime_dependency 'pact-support', '~> 1.19', '>= 1.19.0' gem.add_runtime_dependency 'pact-mock_service', '~> 3.0', '>= 3.3.1' gem.add_development_dependency 'rake', '~> 13.0' diff --git a/spec/lib/pact/provider/generator/boolean_spec.rb b/spec/lib/pact/provider/generator/boolean_spec.rb new file mode 100644 index 00000000..0ef0d7e0 --- /dev/null +++ b/spec/lib/pact/provider/generator/boolean_spec.rb @@ -0,0 +1,20 @@ +require 'pact/provider/generator/boolean' + +describe Pact::Provider::Generator::Boolean do + generator = Pact::Provider::Generator::Boolean.new + + it 'can_generate with a supported hash' do + hash = { 'type' => 'Boolean' } + expect(generator.can_generate?(hash)).to be true + end + + it 'can_generate with a unsupported hash' do + hash = { 'type' => 'unknown' } + expect(generator.can_generate?(hash)).to be false + end + + it 'call' do + hash = { 'type' => 'Boolean' } + expect(generator.call(hash)).to eq(true).or eq(false) + end +end diff --git a/spec/lib/pact/provider/generator/date_spec.rb b/spec/lib/pact/provider/generator/date_spec.rb new file mode 100644 index 00000000..46b32d16 --- /dev/null +++ b/spec/lib/pact/provider/generator/date_spec.rb @@ -0,0 +1,20 @@ +require 'pact/provider/generator/date' + +describe Pact::Provider::Generator::Date do + generator = Pact::Provider::Generator::Date.new + + it 'can_generate with a supported hash' do + hash = { 'type' => 'Date' } + expect(generator.can_generate?(hash)).to be true + end + + it 'can_generate with a unsupported hash' do + hash = { 'type' => 'unknown' } + expect(generator.can_generate?(hash)).to be false + end + + it 'call' do + hash = { 'type' => 'Date' } + expect(generator.call(hash).length).to eq(10) + end +end diff --git a/spec/lib/pact/provider/generator/datetime_spec.rb b/spec/lib/pact/provider/generator/datetime_spec.rb new file mode 100644 index 00000000..32c8c1b5 --- /dev/null +++ b/spec/lib/pact/provider/generator/datetime_spec.rb @@ -0,0 +1,21 @@ +require 'pact/provider/generator/datetime' + +describe Pact::Provider::Generator::DateTime do + generator = Pact::Provider::Generator::DateTime.new + + it 'can_generate with a supported hash' do + hash = { 'type' => 'DateTime' } + expect(generator.can_generate?(hash)).to be true + end + + it 'can_generate with a unsupported hash' do + hash = { 'type' => 'unknown' } + expect(generator.can_generate?(hash)).to be false + end + + it 'call' do + hash = { 'type' => 'DateTime' } + p generator.call(hash) + expect(generator.call(hash).length).to eq(16) + end +end diff --git a/spec/lib/pact/provider/generator/provider_state_spec.rb b/spec/lib/pact/provider/generator/provider_state_spec.rb new file mode 100644 index 00000000..3e3d4519 --- /dev/null +++ b/spec/lib/pact/provider/generator/provider_state_spec.rb @@ -0,0 +1,37 @@ +require 'pact/provider/generator/provider_state' + +describe Pact::Provider::Generator::ProviderState do + generator = Pact::Provider::Generator::ProviderState.new + + it 'can_generate with a supported hash' do + hash = { 'type' => 'ProviderState' } + expect(generator.can_generate?(hash)).to be true + end + + it 'can_generate with a unsupported hash' do + hash = { 'type' => 'unknown' } + expect(generator.can_generate?(hash)).to be false + end + + it 'call without params' do + hash = { 'type' => 'ProviderState', 'expression' => 'Bearer ${access_token}' } + expect(generator.call(hash)).to eq 'Bearer ' + end + + it 'call with correct params' do + hash = { 'type' => 'ProviderState', 'expression' => 'Bearer ${access_token}' } + params = { 'access_token' => 'ABC' } + expect(generator.call(hash, params)).to eq 'Bearer ABC' + end + + it 'call with wrong params' do + hash = { 'type' => 'ProviderState', 'expression' => 'Bearer ${access_token}' } + params = { 'refresh_token' => 'ABC' } + expect(generator.call(hash, params)).to eq 'Bearer ' + end + + it 'call with incomplete expression' do + hash = { 'type' => 'ProviderState', 'expression' => 'Bearer ${access_token' } + expect { generator.call(hash) }.to raise_error('Missing closing brace in expression string') + end +end diff --git a/spec/lib/pact/provider/generator/random_decimal_spec.rb b/spec/lib/pact/provider/generator/random_decimal_spec.rb new file mode 100644 index 00000000..eb495367 --- /dev/null +++ b/spec/lib/pact/provider/generator/random_decimal_spec.rb @@ -0,0 +1,27 @@ +require 'pact/provider/generator/random_decimal' + +describe Pact::Provider::Generator::RandomDecimal do + generator = Pact::Provider::Generator::RandomDecimal.new + + it 'can_generate with a supported hash' do + hash = { 'type' => 'RandomDecimal' } + expect(generator.can_generate?(hash)).to be true + end + + it 'can_generate with a unsupported hash' do + hash = { 'type' => 'unknown' } + expect(generator.can_generate?(hash)).to be false + end + + it 'call' do + hash = { 'type' => 'RandomDecimal' } + value = generator.call(hash) + expect(String(value).length).to eq 7 + end + + it 'call with digits' do + hash = { 'type' => 'RandomDecimal', 'digits' => 10 } + value = generator.call(hash) + expect(String(value).length).to eq 11 + end +end diff --git a/spec/lib/pact/provider/generator/random_hexadecimal_spec.rb b/spec/lib/pact/provider/generator/random_hexadecimal_spec.rb new file mode 100644 index 00000000..48bd6f94 --- /dev/null +++ b/spec/lib/pact/provider/generator/random_hexadecimal_spec.rb @@ -0,0 +1,25 @@ +require 'pact/provider/generator/random_hexadecimal' + +describe Pact::Provider::Generator::RandomHexadecimal do + generator = Pact::Provider::Generator::RandomHexadecimal.new + + it 'can_generate with a supported hash' do + hash = { 'type' => 'RandomHexadecimal' } + expect(generator.can_generate?(hash)).to be true + end + + it 'can_generate with a unsupported hash' do + hash = { 'type' => 'unknown' } + expect(generator.can_generate?(hash)).to be false + end + + it 'call' do + hash = { 'type' => 'RandomHexadecimal' } + expect(generator.call(hash).length).to eq(8) + end + + it 'call with size' do + hash = { 'type' => 'RandomHexadecimal', 'digits' => 2 } + expect(generator.call(hash).length).to eq(2) + end +end diff --git a/spec/lib/pact/provider/generator/random_int_spec.rb b/spec/lib/pact/provider/generator/random_int_spec.rb new file mode 100644 index 00000000..e267c79b --- /dev/null +++ b/spec/lib/pact/provider/generator/random_int_spec.rb @@ -0,0 +1,25 @@ +require 'pact/provider/generator/random_int' + +describe Pact::Provider::Generator::RandomInt do + generator = Pact::Provider::Generator::RandomInt.new + + it 'can_generate with a supported hash' do + hash = { 'type' => 'RandomInt' } + expect(generator.can_generate?(hash)).to be true + end + + it 'can_generate with a unsupported hash' do + hash = { 'type' => 'unknown' } + expect(generator.can_generate?(hash)).to be false + end + + it 'call' do + hash = { 'type' => 'RandomInt' } + expect(generator.call(hash).instance_of?(Integer)).to be true + end + + it 'call with min/max' do + hash = { 'type' => 'RandomInt', 'min' => 5, 'max' => 5 } + expect(generator.call(hash)).to eq 5 + end +end diff --git a/spec/lib/pact/provider/generator/random_string_spec.rb b/spec/lib/pact/provider/generator/random_string_spec.rb new file mode 100644 index 00000000..098563ec --- /dev/null +++ b/spec/lib/pact/provider/generator/random_string_spec.rb @@ -0,0 +1,25 @@ +require 'pact/provider/generator/random_string' + +describe Pact::Provider::Generator::RandomString do + generator = Pact::Provider::Generator::RandomString.new + + it 'can_generate with a supported hash' do + hash = { 'type' => 'RandomString' } + expect(generator.can_generate?(hash)).to be true + end + + it 'can_generate with a unsupported hash' do + hash = { 'type' => 'unknown' } + expect(generator.can_generate?(hash)).to be false + end + + it 'call' do + hash = { 'type' => 'RandomString' } + expect(generator.call(hash).length).to eq(20) + end + + it 'call with size' do + hash = { 'type' => 'RandomString', 'size' => 30 } + expect(generator.call(hash).length).to eq(30) + end +end diff --git a/spec/lib/pact/provider/generator/regex_spec.rb b/spec/lib/pact/provider/generator/regex_spec.rb new file mode 100644 index 00000000..db2738a1 --- /dev/null +++ b/spec/lib/pact/provider/generator/regex_spec.rb @@ -0,0 +1,20 @@ +require 'pact/provider/generator/regex' + +describe Pact::Provider::Generator::Regex do + generator = Pact::Provider::Generator::Regex.new + + it 'can_generate with a supported hash' do + hash = { 'type' => 'Regex' } + expect(generator.can_generate?(hash)).to be true + end + + it 'can_generate with a unsupported hash' do + hash = { 'type' => 'unknown' } + expect(generator.can_generate?(hash)).to be false + end + + it 'call' do + hash = { 'type' => 'Regex', 'pattern' => '(one|two)' } + expect(generator.call(hash)).to eq('one').or eq('two') + end +end diff --git a/spec/lib/pact/provider/generator/time_spec.rb b/spec/lib/pact/provider/generator/time_spec.rb new file mode 100644 index 00000000..9baa6b9b --- /dev/null +++ b/spec/lib/pact/provider/generator/time_spec.rb @@ -0,0 +1,20 @@ +require 'pact/provider/generator/time' + +describe Pact::Provider::Generator::Time do + generator = Pact::Provider::Generator::Time.new + + it 'can_generate with a supported hash' do + hash = { 'type' => 'Time' } + expect(generator.can_generate?(hash)).to be true + end + + it 'can_generate with a unsupported hash' do + hash = { 'type' => 'unknown' } + expect(generator.can_generate?(hash)).to be false + end + + it 'call' do + hash = { 'type' => 'Time' } + expect(generator.call(hash).length).to eq(5) + end +end diff --git a/spec/lib/pact/provider/generator/uuid_spec.rb b/spec/lib/pact/provider/generator/uuid_spec.rb new file mode 100644 index 00000000..c19665bb --- /dev/null +++ b/spec/lib/pact/provider/generator/uuid_spec.rb @@ -0,0 +1,20 @@ +require 'pact/provider/generator/uuid' + +describe Pact::Provider::Generator::Uuid do + generator = Pact::Provider::Generator::Uuid.new + + it 'can_generate with a supported hash' do + hash = { 'type' => 'Uuid' } + expect(generator.can_generate?(hash)).to be true + end + + it 'can_generate with a unsupported hash' do + hash = { 'type' => 'unknown' } + expect(generator.can_generate?(hash)).to be false + end + + it 'call' do + hash = { 'type' => 'Uuid' } + expect(generator.call(hash).length).to eq(36) + end +end diff --git a/spec/lib/pact/provider/generators_spec.rb b/spec/lib/pact/provider/generators_spec.rb new file mode 100644 index 00000000..198e1068 --- /dev/null +++ b/spec/lib/pact/provider/generators_spec.rb @@ -0,0 +1,124 @@ +require 'pact/provider/generators' +require 'pact/provider/request' + +describe Pact::Provider::Generators do + it 'execute_generators with Boolean' do + hash = { 'type' => 'Boolean' } + expect(Pact::Provider::Generators.execute_generators(hash)).to eq(true).or eq(false) + end + + it 'execute_generators with Date' do + hash = { 'type' => 'Date' } + expect(Pact::Provider::Generators.execute_generators(hash).length).to eq(10) + end + + it 'execute_generators with DateTime' do + hash = { 'type' => 'DateTime' } + expect(Pact::Provider::Generators.execute_generators(hash).length).to eq(16) + end + + it 'execute_generators with ProviderState' do + hash = { 'type' => 'ProviderState', 'expression' => 'Bearer ${access_token}' } + params = { 'access_token' => 'ABC' } + expect(Pact::Provider::Generators.execute_generators(hash, params)).to eq('Bearer ABC') + end + + it 'execute_generators with RandomDecimal' do + hash = { 'type' => 'RandomDecimal' } + expect(String(Pact::Provider::Generators.execute_generators(hash)).length).to eq(7) + end + + it 'execute_generators with RandomHexadecimal' do + hash = { 'type' => 'RandomHexadecimal' } + expect(Pact::Provider::Generators.execute_generators(hash).length).to eq(8) + end + + it 'execute_generators with RandomInt' do + hash = { 'type' => 'RandomInt' } + expect(Pact::Provider::Generators.execute_generators(hash).instance_of?(Integer)).to be true + end + + it 'execute_generators with RandomString' do + hash = { 'type' => 'RandomString' } + expect(Pact::Provider::Generators.execute_generators(hash).length).to eq(20) + end + + it 'execute_generators with Regex' do + hash = { 'type' => 'Regex', 'pattern' => '(one|two)' } + expect(Pact::Provider::Generators.execute_generators(hash)).to eq('one').or eq('two') + end + + it 'execute_generators with Time' do + hash = { 'type' => 'Time' } + expect(Pact::Provider::Generators.execute_generators(hash).length).to eq(5) + end + + it 'execute_generators with Uuid' do + hash = { 'type' => 'Uuid' } + expect(Pact::Provider::Generators.execute_generators(hash).length).to eq(36) + end + + it 'apply_generators for path' do + expected_request = Pact::Request::Expected.from_hash({ + method: 'GET', + path: '/path/1', + generators: { + 'path' => { + 'type' => 'ProviderState', + 'expression' => '/path/${itemID}' + } + } + }) + state_params = { + 'itemID' => 2 + } + request = Pact::Provider::Request::Replayable.new(expected_request, state_params) + expect(request.path).to eq('/path/2') + end + + it 'apply_generators for headers' do + expected_request = Pact::Request::Expected.from_hash({ + method: 'GET', + path: '/path/1', + headers: { + 'Authorization' => 'Bearer 123' + }, + generators: { + 'header' => { + '$.Authorization' => { + 'expression' => 'Bearer ${accessToken}', + 'type' => 'ProviderState' + } + } + } + }) + state_params = { + 'accessToken' => 'ABC' + } + request = Pact::Provider::Request::Replayable.new(expected_request, state_params) + expect(request.headers).to eq({ + 'HTTP_AUTHORIZATION' => 'Bearer ABC' + }) + end + + it 'apply_generators for body' do + expected_request = Pact::Request::Expected.from_hash({ + method: 'GET', + path: '/path/1', + body: { + 'result' => [ + '12345F' + ] + }, + generators: { + 'body' => { + '$.result[0]' => { + 'type' => 'RandomHexadecimal' + } + } + } + }) + request = Pact::Provider::Request::Replayable.new(expected_request) + expect(request.body['result'][0].length).to eq(8) + end +end