pact_broker-client 1.14.1 → 1.15.0

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.md +21 -0
  4. data/README.md +27 -1
  5. data/Rakefile +9 -0
  6. data/doc/pacts/markdown/Pact Broker Client - Pact Broker.md +521 -10
  7. data/lib/pact_broker/client/base_client.rb +1 -1
  8. data/lib/pact_broker/client/cli/broker.rb +63 -0
  9. data/lib/pact_broker/client/cli/can_i_deploy_long_desc.txt +1 -1
  10. data/lib/pact_broker/client/cli/create_webhook_long_desc.txt +1 -0
  11. data/lib/pact_broker/client/command_result.rb +12 -0
  12. data/lib/pact_broker/client/hal.rb +4 -0
  13. data/lib/pact_broker/client/hal/entity.rb +78 -0
  14. data/lib/pact_broker/client/hal/entry_point.rb +13 -0
  15. data/lib/pact_broker/client/hal/http_client.rb +82 -0
  16. data/lib/pact_broker/client/hal/link.rb +79 -0
  17. data/lib/pact_broker/client/pacts.rb +7 -1
  18. data/lib/pact_broker/client/version.rb +1 -1
  19. data/lib/pact_broker/client/versions.rb +0 -1
  20. data/lib/pact_broker/client/webhooks/create.rb +114 -0
  21. data/spec/integration/can_i_deploy_spec.rb +1 -3
  22. data/spec/integration/create_version_tag_spec.rb +1 -3
  23. data/spec/lib/pact_broker/client/can_i_deploy_spec.rb +1 -1
  24. data/spec/lib/pact_broker/client/cli/broker_create_webhook_spec.rb +190 -0
  25. data/spec/lib/pact_broker/client/hal/entity_spec.rb +93 -0
  26. data/spec/lib/pact_broker/client/hal/http_client_spec.rb +105 -0
  27. data/spec/lib/pact_broker/client/hal/link_spec.rb +108 -0
  28. data/spec/lib/pact_broker/client/webhooks/create_spec.rb +61 -0
  29. data/spec/pacts/pact_broker_client-pact_broker.json +499 -9
  30. data/spec/service_providers/pact_broker_client_publish_spec.rb +6 -1
  31. data/spec/service_providers/pact_broker_client_retrieve_all_pacts_for_provider_spec.rb +1 -1
  32. data/spec/service_providers/{pact_broker_client_retrive_pact_spec.rb → pact_broker_client_retrieve_pact_spec.rb} +2 -3
  33. data/spec/service_providers/pact_broker_client_versions_spec.rb +1 -1
  34. data/spec/service_providers/pact_helper.rb +46 -0
  35. data/spec/service_providers/webhooks_create_spec.rb +255 -0
  36. data/spec/spec_helper.rb +32 -0
  37. data/spec/support/latest_pacts_for_provider.json +1 -1
  38. data/spec/support/shared_context.rb +6 -3
  39. metadata +24 -4
@@ -48,7 +48,7 @@ module PactBroker
48
48
  end
49
49
 
50
50
  def default_request_headers
51
- {'Accept' => 'application/json, application/hal+json'}
51
+ {'Accept' => 'application/hal+json, application/json'}
52
52
  end
53
53
 
54
54
  def default_get_headers
@@ -14,6 +14,7 @@ module PactBroker
14
14
  module CLI
15
15
  # Thor::Error will have its backtrace hidden
16
16
  class PactPublicationError < ::Thor::Error; end
17
+ class WebhookCreationError < ::Thor::Error; end
17
18
 
18
19
  class Broker < CustomThor
19
20
  desc 'can-i-deploy', ''
@@ -99,6 +100,68 @@ module PactBroker
99
100
  exit(1) unless result.success
100
101
  end
101
102
 
103
+ method_option :request, aliases: "-X", desc: "HTTP method", required: true
104
+ method_option :header, aliases: "-H", type: :array, desc: "Header"
105
+ method_option :data, aliases: "-d", desc: "Data"
106
+ method_option :user, aliases: "-u", desc: "Basic auth username and password eg. username:password"
107
+ method_option :consumer, desc: "Consumer name"
108
+ method_option :provider, desc: "Provider name"
109
+ method_option :broker_base_url, required: true, aliases: "-b", desc: "The base URL of the Pact Broker"
110
+ method_option :broker_username, aliases: "-u", desc: "Pact Broker basic auth username"
111
+ method_option :broker_password, aliases: "-p", desc: "Pact Broker basic auth password"
112
+ method_option :contract_content_changed, type: :boolean, desc: "Trigger this webhook when the pact content changes"
113
+ method_option :provider_verification_published, type: :boolean, desc: "Trigger this webhook when a provider verification result is published"
114
+ method_option :verbose, aliases: "-v", type: :boolean, default: false, required: false, desc: "Verbose output. Default: false"
115
+
116
+ desc 'create-webhook URL', 'Creates a webhook using the same switches as a curl request.'
117
+ long_desc File.read(File.join(File.dirname(__FILE__), 'create_webhook_long_desc.txt'))
118
+ def create_webhook webhook_url
119
+ require 'pact_broker/client/webhooks/create'
120
+
121
+ if !(options.contract_content_changed || options.provider_verification_published)
122
+ raise PactBroker::Client::Error.new("You must select at least one of --contract-content-changed or --provider-verification-published")
123
+ end
124
+
125
+ username = options.user ? options.user.split(":", 2).first : nil
126
+ password = options.user ? options.user.split(":", 2).last : nil
127
+
128
+ headers = (options.header || []).each_with_object({}) { | header, headers | headers[header.split(":", 2).first.strip] = header.split(":", 2).last.strip }
129
+
130
+ body = options.data
131
+ if body && body.start_with?("@")
132
+ filepath = body[1..-1]
133
+ begin
134
+ body = File.read(filepath)
135
+ rescue StandardError => e
136
+ raise PactBroker::Client::Error.new("Couldn't read data from file \"#{filepath}\" due to #{e.class} #{e.message}")
137
+ end
138
+ end
139
+
140
+ events = []
141
+ events << 'contract_content_changed' if options.contract_content_changed
142
+ events << 'provider_verification_published' if options.provider_verification_published
143
+
144
+ params = {
145
+ http_method: options.request,
146
+ url: webhook_url,
147
+ headers: headers,
148
+ username: username,
149
+ password: password,
150
+ body: body,
151
+ consumer: options.consumer,
152
+ provider: options.provider,
153
+ events: events
154
+ }
155
+
156
+ begin
157
+ result = PactBroker::Client::Webhooks::Create.call(params, options.broker_base_url, pact_broker_client_options)
158
+ $stdout.puts result.message
159
+ exit(1) unless result.success
160
+ rescue PactBroker::Client::Error => e
161
+ raise WebhookCreationError, "#{e.class} - #{e.message}"
162
+ end
163
+ end
164
+
102
165
  desc 'version', "Show the pact_broker-client gem version"
103
166
  def version
104
167
  $stdout.puts PactBroker::Client::VERSION
@@ -1,6 +1,6 @@
1
1
  Returns exit code 0 or 1, indicating whether or not the specified pacticipant versions are compatible. Prints out the relevant pact/verification details.
2
2
 
3
- The environment variables PACT_BROKER_BASE_URL_BASE_URL, PACT_BROKER_BASE_URL_USERNAME and PACT_BROKER_BASE_URL_PASSWORD may be used instead of their respective command line options.
3
+ The environment variables PACT_BROKER_BASE_URL, PACT_BROKER_BASE_URL_USERNAME and PACT_BROKER_BASE_URL_PASSWORD may be used instead of their respective command line options.
4
4
 
5
5
  SCENARIOS
6
6
 
@@ -0,0 +1 @@
1
+ Create a curl command that executes the request that you want your webhook to execute, then replace "curl" with "pact-broker create-webhook" and add the consumer, provider, event types and broker details. Note that the URL must be the first parameter when executing create-webhook.
@@ -0,0 +1,12 @@
1
+ module PactBroker
2
+ module Client
3
+ class CommandResult
4
+ attr_reader :success, :message
5
+
6
+ def initialize success, message = nil
7
+ @success = success
8
+ @message = message
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,4 @@
1
+ require 'pact_broker/client/hal/link'
2
+ require 'pact_broker/client/hal/entry_point'
3
+ require 'pact_broker/client/hal/http_client'
4
+ require 'pact_broker/client/hal/entity'
@@ -0,0 +1,78 @@
1
+ require 'uri'
2
+ require 'delegate'
3
+ require 'pact_broker/client/hal/link'
4
+
5
+ module PactBroker
6
+ module Client
7
+ module Hal
8
+ class Entity
9
+ def initialize(data, http_client, response = nil)
10
+ @data = data
11
+ @links = (@data || {}).fetch("_links", {})
12
+ @client = http_client
13
+ @response = response
14
+ end
15
+
16
+ def get(key, *args)
17
+ _link(key).get(*args)
18
+ end
19
+
20
+ def post(key, *args)
21
+ _link(key).post(*args)
22
+ end
23
+
24
+ def put(key, *args)
25
+ _link(key).put(*args)
26
+ end
27
+
28
+ def can?(key)
29
+ @links.key? key.to_s
30
+ end
31
+
32
+ def follow(key, http_method, *args)
33
+ Link.new(@links[key].merge(method: http_method), @client).run(*args)
34
+ end
35
+
36
+ def _link(key)
37
+ if @links[key]
38
+ Link.new(@links[key], @client)
39
+ else
40
+ nil
41
+ end
42
+ end
43
+
44
+ def success?
45
+ true
46
+ end
47
+
48
+ def response
49
+ @response
50
+ end
51
+
52
+ def fetch(key)
53
+ @links[key]
54
+ end
55
+
56
+ def method_missing(method_name, *args, &block)
57
+ if @data.key?(method_name.to_s)
58
+ @data[method_name.to_s]
59
+ elsif @links.key?(method_name)
60
+ Link.new(@links[method_name], @client).run(*args)
61
+ else
62
+ super
63
+ end
64
+ end
65
+
66
+ def respond_to_missing?(method_name, include_private = false)
67
+ @data.key?(method_name) || @links.key?(method_name)
68
+ end
69
+ end
70
+
71
+ class ErrorEntity < Entity
72
+ def success?
73
+ false
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,13 @@
1
+ require 'pact_broker/client/hal/link'
2
+
3
+ module PactBroker
4
+ module Client
5
+ module Hal
6
+ class EntryPoint < Link
7
+ def initialize(url, http_client)
8
+ super({ "href" => url }, http_client)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,82 @@
1
+ require 'pact_broker/client/retry'
2
+
3
+ module PactBroker
4
+ module Client
5
+ module Hal
6
+ class HttpClient
7
+ attr_reader :username, :password, :verbose
8
+
9
+ def initialize options
10
+ @username = options[:username]
11
+ @password = options[:password]
12
+ @verbose = options[:verbose]
13
+ end
14
+
15
+ def get href, params = {}, headers = {}
16
+ query = params.collect{ |(key, value)| "#{CGI::escape(key)}=#{CGI::escape(value)}" }.join("&")
17
+ uri = URI(href)
18
+ uri.query = query
19
+ perform_request(create_request(uri, 'Get', nil, headers), uri)
20
+ end
21
+
22
+ def put href, body = nil, headers = {}
23
+ uri = URI(href)
24
+ perform_request(create_request(uri, 'Put', body, headers), uri)
25
+ end
26
+
27
+ def post href, body = nil, headers = {}
28
+ uri = URI(href)
29
+ perform_request(create_request(uri, 'Post', body, headers), uri)
30
+ end
31
+
32
+ def create_request uri, http_method, body = nil, headers = {}
33
+ request = Net::HTTP.const_get(http_method).new(uri.request_uri)
34
+ request['Content-Type'] = "application/json" if ['Post', 'Put', 'Patch'].include?(http_method)
35
+ request['Accept'] = "application/hal+json"
36
+ headers.each do | key, value |
37
+ request[key] = value
38
+ end
39
+
40
+ request.body = body if body
41
+ request.basic_auth username, password if username
42
+ request
43
+ end
44
+
45
+ def perform_request request, uri
46
+ response = Retry.until_true do
47
+ http = Net::HTTP.new(uri.host, uri.port, :ENV)
48
+ http.set_debug_output($stderr) if verbose
49
+ http.use_ssl = (uri.scheme == 'https')
50
+ http.start do |http|
51
+ http.request request
52
+ end
53
+ end
54
+ Response.new(response)
55
+ end
56
+
57
+ class Response < SimpleDelegator
58
+ def body
59
+ bod = __getobj__().body
60
+ if bod && bod != ''
61
+ JSON.parse(bod)
62
+ else
63
+ nil
64
+ end
65
+ end
66
+
67
+ def raw_body
68
+ __getobj__().body
69
+ end
70
+
71
+ def status
72
+ code.to_i
73
+ end
74
+
75
+ def success?
76
+ __getobj__().code.start_with?("2")
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,79 @@
1
+ require 'uri'
2
+ require 'delegate'
3
+
4
+ module PactBroker
5
+ module Client
6
+ module Hal
7
+ class Link
8
+ attr_reader :request_method, :href
9
+
10
+ def initialize(attrs, http_client)
11
+ @attrs = attrs
12
+ @request_method = attrs.fetch(:method, :get).to_sym
13
+ @href = attrs.fetch('href')
14
+ @http_client = http_client
15
+ end
16
+
17
+ def run(payload = nil)
18
+ response = case request_method
19
+ when :get
20
+ get(payload)
21
+ when :put
22
+ put(payload)
23
+ when :post
24
+ post(payload)
25
+ end
26
+ end
27
+
28
+ def title_or_name
29
+ title || name
30
+ end
31
+
32
+ def title
33
+ @attrs['title']
34
+ end
35
+
36
+ def name
37
+ @attrs['name']
38
+ end
39
+
40
+ def get(payload = {}, headers = {})
41
+ wrap_response(@http_client.get(href, payload, headers))
42
+ end
43
+
44
+ def put(payload = nil, headers = {})
45
+ wrap_response(@http_client.put(href, payload ? JSON.dump(payload) : nil, headers))
46
+ end
47
+
48
+ def post(payload = nil, headers = {})
49
+ wrap_response(@http_client.post(href, payload ? JSON.dump(payload) : nil, headers))
50
+ end
51
+
52
+ def expand(params)
53
+ expanded_url = expand_url(params, href)
54
+ new_attrs = @attrs.merge('href' => expanded_url)
55
+ Link.new(new_attrs, @http_client)
56
+ end
57
+
58
+ private
59
+
60
+ def wrap_response(http_response)
61
+ require 'pact_broker/client/hal/entity' # avoid circular reference
62
+ if http_response.success?
63
+ Entity.new(http_response.body, @http_client, http_response)
64
+ else
65
+ ErrorEntity.new(http_response.body, @http_client, http_response)
66
+ end
67
+ end
68
+
69
+ def expand_url(params, url)
70
+ new_url = url
71
+ params.each do | key, value |
72
+ new_url = new_url.gsub('{' + key.to_s + '}', URI.escape(value))
73
+ end
74
+ new_url
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -49,7 +49,7 @@ module PactBroker
49
49
  url = get_latest_provider_contracts(options)
50
50
  response = self.class.get(url, headers: {})
51
51
  handle_response(response) do
52
- map_latest_provider_pacts_to_hash JSON.parse(response.body)["_links"]["pacts"]
52
+ map_latest_provider_pacts_to_hash(pact_links(response))
53
53
  end
54
54
  end
55
55
 
@@ -63,6 +63,12 @@ module PactBroker
63
63
 
64
64
  private
65
65
 
66
+ def pact_links response
67
+ body = JSON.parse(response.body)
68
+ # pacts relation is deprecated
69
+ body["_links"]["pb:pacts"] || body["_links"]["pacts"]
70
+ end
71
+
66
72
  #TODO Move into mapper class
67
73
  def map_pact_list_to_hash pacts_list
68
74
  pacts_list.collect do | pact_hash |
@@ -1,5 +1,5 @@
1
1
  module PactBroker
2
2
  module Client
3
- VERSION = '1.14.1'
3
+ VERSION = '1.15.0'
4
4
  end
5
5
  end
@@ -14,7 +14,6 @@ module PactBroker
14
14
  end
15
15
 
16
16
  def latest options
17
- puts options
18
17
  url = if options[:tag]
19
18
  url_for_relation('pb:latest-tagged-version', pacticipant: options.fetch(:pacticipant), tag: options.fetch(:tag))
20
19
  else
@@ -0,0 +1,114 @@
1
+ require 'uri'
2
+ require 'pact_broker/client/hal'
3
+ require 'ostruct'
4
+ require 'json'
5
+ require 'pact_broker/client/command_result'
6
+
7
+ module PactBroker
8
+ module Client
9
+ module Webhooks
10
+ class Create
11
+
12
+ WEBHOOKS_WITH_OPTIONAL_PACTICICPANTS_NOT_SUPPORTED = "Your version of the Pact Broker requires that both consumer and provider are specified for a webhook. Please upgrade your broker to >= 2.22.0 to create a webhook with optional consumer and provider."
13
+
14
+ attr_reader :params, :pact_broker_base_url, :basic_auth_options, :verbose
15
+
16
+ def self.call(params, pact_broker_base_url, pact_broker_client_options)
17
+ new(params, pact_broker_base_url, pact_broker_client_options).call
18
+ end
19
+
20
+ def initialize(params, pact_broker_base_url, pact_broker_client_options)
21
+ @params = OpenStruct.new(params)
22
+ @pact_broker_base_url = pact_broker_base_url
23
+ @basic_auth_options = pact_broker_client_options[:basic_auth] || {}
24
+ @verbose = pact_broker_client_options[:verbose]
25
+ @http_client = PactBroker::Client::Hal::HttpClient.new(basic_auth_options.merge(verbose: verbose))
26
+ end
27
+
28
+ def call
29
+ if params.consumer && params.provider
30
+ create_webhook_with_consumer_and_provider
31
+ else
32
+ create_webhook_with_optional_consumer_and_provider
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ attr_reader :http_client
39
+
40
+ def create_webhook_with_consumer_and_provider
41
+ webhook_entity = webhook_link.expand(consumer: params.consumer, provider: params.provider).post(request_body)
42
+ handle_response(webhook_entity)
43
+ end
44
+
45
+ def create_webhook_with_optional_consumer_and_provider
46
+ webhook_entity = index_link.get._link("pb:webhooks").post(request_body_with_optional_consumer_and_provider)
47
+
48
+ if webhook_entity.response.status == 405
49
+ raise PactBroker::Client::Error.new(WEBHOOKS_WITH_OPTIONAL_PACTICICPANTS_NOT_SUPPORTED)
50
+ end
51
+
52
+ handle_response(webhook_entity)
53
+ end
54
+
55
+ def request_body
56
+ webhook_request_body = JSON.parse(params.body) rescue params.body
57
+ {
58
+ events: params.events.collect{ | event | { "name" => event }},
59
+ request: {
60
+ url: params.url,
61
+ method: params.http_method,
62
+ headers: params.headers,
63
+ body: webhook_request_body,
64
+ username: params.username,
65
+ password: params.password
66
+ }
67
+ }
68
+ end
69
+
70
+ def request_body_with_optional_consumer_and_provider
71
+ body = request_body
72
+
73
+ if params.consumer
74
+ body[:consumer] = { name: params.consumer }
75
+ end
76
+
77
+ if params.provider
78
+ body[:provider] = { name: params.provider }
79
+ end
80
+
81
+ body
82
+ end
83
+
84
+ def webhook_for_consumer_and_provider_url
85
+ "#{pact_broker_base_url.chomp("/")}/webhooks/provider/{provider}/consumer/{consumer}"
86
+ end
87
+
88
+ def handle_response(webhook_entity)
89
+ if webhook_entity.success?
90
+ success_result(webhook_entity)
91
+ else
92
+ error_result(webhook_entity)
93
+ end
94
+ end
95
+
96
+ def success_result(webhook_entity)
97
+ CommandResult.new(true, "Webhook #{webhook_entity._link('self').title_or_name.inspect} created")
98
+ end
99
+
100
+ def error_result(webhook_entity)
101
+ CommandResult.new(false, "Error creating webhook. response status=#{webhook_entity.response.status} body=#{webhook_entity.response.raw_body}")
102
+ end
103
+
104
+ def index_link
105
+ PactBroker::Client::Hal::EntryPoint.new(pact_broker_base_url, http_client)
106
+ end
107
+
108
+ def webhook_link
109
+ PactBroker::Client::Hal::EntryPoint.new(webhook_for_consumer_and_provider_url, http_client)
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end