pact_broker-client 1.14.1 → 1.15.0

Sign up to get free protection for your applications and to get access to all the features.
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