pact_broker 2.95.0 → 2.97.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -0
  3. data/Gemfile +1 -0
  4. data/docs/CONFIGURATION.md +98 -66
  5. data/lib/db.rb +2 -7
  6. data/lib/pact_broker/api/middleware/http_debug_logs.rb +36 -0
  7. data/lib/pact_broker/api/resources/base_resource.rb +273 -1
  8. data/lib/pact_broker/api/resources/default_base_resource.rb +0 -280
  9. data/lib/pact_broker/app.rb +1 -7
  10. data/lib/pact_broker/config/basic_auth_configuration.rb +7 -0
  11. data/lib/pact_broker/config/runtime_configuration.rb +20 -3
  12. data/lib/pact_broker/config/runtime_configuration_coercion_methods.rb +11 -0
  13. data/lib/pact_broker/config/runtime_configuration_database_methods.rb +6 -1
  14. data/lib/pact_broker/config/runtime_configuration_logging_methods.rb +13 -2
  15. data/lib/pact_broker/configuration.rb +3 -17
  16. data/lib/pact_broker/db/models.rb +2 -2
  17. data/lib/pact_broker/index/service.rb +1 -2
  18. data/lib/pact_broker/integrations/integration.rb +21 -6
  19. data/lib/pact_broker/integrations/service.rb +1 -1
  20. data/lib/pact_broker/matrix/repository.rb +11 -12
  21. data/lib/pact_broker/matrix/service.rb +0 -1
  22. data/lib/pact_broker/metrics/service.rb +2 -2
  23. data/lib/pact_broker/pacts/lazy_loaders.rb +26 -0
  24. data/lib/pact_broker/pacts/pact_publication.rb +16 -31
  25. data/lib/pact_broker/pacts/pact_version.rb +24 -28
  26. data/lib/pact_broker/pacts/pact_version_association_loaders.rb +36 -0
  27. data/lib/pact_broker/pacts/pacts_for_verification_repository.rb +16 -12
  28. data/lib/pact_broker/pacts/repository.rb +29 -24
  29. data/lib/pact_broker/repositories/helpers.rb +8 -0
  30. data/lib/pact_broker/test/http_test_data_builder.rb +8 -1
  31. data/lib/pact_broker/test/test_data_builder.rb +2 -1
  32. data/lib/pact_broker/ui/controllers/matrix.rb +14 -11
  33. data/lib/pact_broker/version.rb +1 -1
  34. data/pact_broker.gemspec +1 -1
  35. metadata +9 -16
  36. data/lib/pact_broker/matrix/aggregated_row.rb +0 -79
  37. data/lib/pact_broker/matrix/head_row.rb +0 -80
  38. data/lib/pact_broker/matrix/row.rb +0 -287
@@ -0,0 +1,36 @@
1
+ require "pact_broker/logging"
2
+
3
+ module PactBroker
4
+ module Api
5
+ module Middleware
6
+ class HttpDebugLogs
7
+ include PactBroker::Logging
8
+
9
+ EXCLUDE_HEADERS = ["puma.", "rack.", "pactbroker."]
10
+ RACK_SESSION = "rack.session"
11
+
12
+ def initialize(app)
13
+ @app = app
14
+ @logger = logger
15
+ end
16
+
17
+ def call(env)
18
+ env_to_log = env.reject { | header, _ | header.start_with?(*EXCLUDE_HEADERS) }
19
+ env_to_log["rack.session"] = env["rack.session"].to_hash if env["rack.session"]
20
+ env_to_log["rack.input"] = request_body(env) if env["rack.input"]
21
+ logger.debug("env", payload: env_to_log)
22
+ status, headers, body = @app.call(env)
23
+ logger.debug("response", payload: { "status" => status, "headers" => headers, "body" => body })
24
+ [status, headers, body]
25
+ end
26
+
27
+ def request_body(env)
28
+ buffer = env["rack.input"]
29
+ request_body = buffer.read
30
+ buffer.respond_to?(:rewind) && buffer.rewind
31
+ JSON.parse(request_body) rescue request_body
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -1,9 +1,281 @@
1
+ # frozen_string_literal: true
1
2
  require "pact_broker/configuration"
3
+ require "webmachine"
4
+ require "pact_broker/services"
5
+ require "pact_broker/api/decorators"
6
+ require "pact_broker/logging"
7
+ require "pact_broker/api/pact_broker_urls"
8
+ require "pact_broker/json"
9
+ require "pact_broker/pacts/pact_params"
10
+ require "pact_broker/api/resources/authentication"
11
+ require "pact_broker/api/resources/authorization"
12
+ require "pact_broker/errors"
2
13
 
3
14
  module PactBroker
4
15
  module Api
5
16
  module Resources
6
- BaseResource = PactBroker.configuration.base_resource_class_factory.call
17
+ class InvalidJsonError < PactBroker::Error ; end
18
+
19
+ class BaseResource < Webmachine::Resource
20
+ include PactBroker::Services
21
+ include PactBroker::Api::PactBrokerUrls
22
+ include PactBroker::Api::Resources::Authentication
23
+ include PactBroker::Api::Resources::Authorization
24
+
25
+ include PactBroker::Logging
26
+
27
+ attr_accessor :user
28
+
29
+ def initialize
30
+ PactBroker.configuration.before_resource.call(self)
31
+ application_context.before_resource&.call(self)
32
+ end
33
+
34
+ def options
35
+ { "Access-Control-Allow-Methods" => allowed_methods.join(", ")}
36
+ end
37
+
38
+ def known_methods
39
+ super + ["PATCH"]
40
+ end
41
+
42
+ def finish_request
43
+ application_context.after_resource&.call(self)
44
+ PactBroker.configuration.after_resource.call(self)
45
+ end
46
+
47
+ def is_authorized?(authorization_header)
48
+ authenticated?(self, authorization_header)
49
+ end
50
+
51
+ def forbidden?
52
+ if application_context.resource_authorizer
53
+ !application_context.resource_authorizer.call(self)
54
+ elsif PactBroker.configuration.authorize
55
+ !PactBroker.configuration.authorize.call(self, {})
56
+ else
57
+ false
58
+ end
59
+ end
60
+
61
+ # The path_info segments aren't URL decoded
62
+ def identifier_from_path
63
+ @identifier_from_path ||= request.path_info.each_with_object({}) do | (key, value), hash|
64
+ if value.is_a?(String)
65
+ hash[key] = URI.decode(value)
66
+ elsif value.is_a?(Symbol) || value.is_a?(Numeric)
67
+ hash[key] = value
68
+ end
69
+ end
70
+ end
71
+
72
+ alias_method :path_info, :identifier_from_path
73
+
74
+ def base_url
75
+ # Have to use something for the base URL here - we can't use an empty string as we can in the UI.
76
+ # Can't work out if cache poisoning is a vulnerability for APIs or not.
77
+ # Using the request base URI as a fallback if the base_url is not configured may be a vulnerability,
78
+ # but the documentation recommends that the
79
+ # base_url should be set in the configuration to mitigate this.
80
+ request.env["pactbroker.base_url"] || request.base_uri.to_s.chomp("/")
81
+ end
82
+
83
+ # See comments for base_url in lib/pact_broker/doc/controllers/app.rb
84
+ def ui_base_url
85
+ request.env["pactbroker.base_url"] || ""
86
+ end
87
+
88
+ def charsets_provided
89
+ [["utf-8", :encode]]
90
+ end
91
+
92
+ # We only use utf-8 so leave encoding as it is
93
+ def encode(body)
94
+ body
95
+ end
96
+
97
+ def resource_url
98
+ request.uri.to_s.gsub(/\?.*/, "").chomp("/")
99
+ end
100
+
101
+ def decorator_context options = {}
102
+ application_context.decorator_context_creator.call(self, options)
103
+ end
104
+
105
+ def decorator_options options = {}
106
+ { user_options: decorator_context(options) }
107
+ end
108
+
109
+ def handle_exception(error)
110
+ error_reference = PactBroker::Errors.generate_error_reference
111
+ application_context.error_logger.call(error, error_reference, request.env)
112
+ if PactBroker::Errors.reportable_error?(error)
113
+ PactBroker::Errors.report(error, error_reference, request.env)
114
+ end
115
+ response.body = application_context.error_response_body_generator.call(error, error_reference, request.env)
116
+ end
117
+
118
+ # rubocop: disable Metrics/CyclomaticComplexity
119
+ def params(options = {})
120
+ return options[:default] if options.key?(:default) && request_body.empty?
121
+
122
+ symbolize_names = !options.key?(:symbolize_names) || options[:symbolize_names]
123
+ if symbolize_names
124
+ @params_with_symbol_keys ||= JSON.parse(request_body, { symbolize_names: true }.merge(PACT_PARSING_OPTIONS)) #Not load! Otherwise it will try to load Ruby classes.
125
+ else
126
+ @params_with_string_keys ||= JSON.parse(request_body, { symbolize_names: false }.merge(PACT_PARSING_OPTIONS)) #Not load! Otherwise it will try to load Ruby classes.
127
+ end
128
+ rescue JSON::JSONError => e
129
+ raise InvalidJsonError.new("Error parsing JSON - #{e.message}")
130
+ end
131
+ # rubocop: enable Metrics/CyclomaticComplexity
132
+
133
+ def params_with_string_keys
134
+ params(symbolize_names: false)
135
+ end
136
+
137
+ def pact_params
138
+ @pact_params ||= PactBroker::Pacts::PactParams.from_request request, identifier_from_path
139
+ end
140
+
141
+ def set_json_error_message message
142
+ response.headers["Content-Type"] = "application/hal+json;charset=utf-8"
143
+ response.body = { error: message }.to_json
144
+ end
145
+
146
+ def set_json_validation_error_messages errors
147
+ response.headers["Content-Type"] = "application/hal+json;charset=utf-8"
148
+ response.body = { errors: errors }.to_json
149
+ end
150
+
151
+ def request_body
152
+ @request_body ||= request.body.to_s
153
+ end
154
+
155
+ def consumer_name
156
+ identifier_from_path[:consumer_name]
157
+ end
158
+
159
+ def consumer_version_number
160
+ identifier_from_path[:consumer_version_number]
161
+ end
162
+
163
+ def pacticipant_version_number
164
+ identifier_from_path[:pacticipant_version_number]
165
+ end
166
+
167
+ def consumer_specified?
168
+ identifier_from_path.key?(:consumer_name)
169
+ end
170
+
171
+ def provider_specified?
172
+ identifier_from_path.key?(:provider_name)
173
+ end
174
+
175
+ def provider_name
176
+ identifier_from_path[:provider_name]
177
+ end
178
+
179
+ def pacticipant_name
180
+ identifier_from_path[:pacticipant_name]
181
+ end
182
+
183
+ def pacticipant_specified?
184
+ identifier_from_path.key?(:pacticipant_name)
185
+ end
186
+
187
+ def invalid_json?
188
+ begin
189
+ params
190
+ false
191
+ rescue StandardError => e
192
+ logger.info "Error parsing JSON #{e} - #{request_body}"
193
+ set_json_error_message "Error parsing JSON - #{e.message}"
194
+ response.headers["Content-Type"] = "application/hal+json;charset=utf-8"
195
+ true
196
+ end
197
+ end
198
+
199
+ def validation_errors? model
200
+ if (errors = model.validate).any?
201
+ set_json_validation_error_messages errors
202
+ true
203
+ else
204
+ false
205
+ end
206
+ end
207
+
208
+ def contract_validation_errors? contract, params
209
+ if (invalid = !contract.validate(params))
210
+ set_json_validation_error_messages contract.errors.messages
211
+ end
212
+ invalid
213
+ end
214
+
215
+ def find_pacticipant name, role
216
+ pacticipant_service.find_pacticipant_by_name(name).tap do | pacticipant |
217
+ set_json_error_message("No #{role} with name '#{name}' found") if pacticipant.nil?
218
+ end
219
+ end
220
+
221
+ def consumer
222
+ @consumer ||= identifier_from_path[:consumer_name] && find_pacticipant(identifier_from_path[:consumer_name], "consumer")
223
+ end
224
+
225
+ def provider
226
+ @provider ||= identifier_from_path[:provider_name] && find_pacticipant(identifier_from_path[:provider_name], "provider")
227
+ end
228
+
229
+ def pacticipant
230
+ @pacticipant ||= identifier_from_path[:pacticipant_name] && find_pacticipant(identifier_from_path[:pacticipant_name], "pacticipant")
231
+ end
232
+
233
+ def pact
234
+ @pact ||= pact_service.find_pact(pact_params)
235
+ end
236
+
237
+ # Not necessarily an existing integration
238
+ def integration
239
+ if consumer_specified? && provider_specified?
240
+ OpenStruct.new(consumer: consumer, provider: provider)
241
+ else
242
+ nil
243
+ end
244
+ end
245
+
246
+ def database_connector
247
+ request.env["pactbroker.database_connector"]
248
+ end
249
+
250
+ def application_context
251
+ request.path_info[:application_context]
252
+ end
253
+
254
+ def decorator_class(name)
255
+ application_context.decorator_configuration.class_for(name)
256
+ end
257
+
258
+ def api_contract_class(name)
259
+ application_context.api_contract_configuration.class_for(name)
260
+ end
261
+
262
+ def schema
263
+ nil
264
+ end
265
+
266
+ def validation_errors_for_schema?(schema_to_use = schema, params_to_validate = params)
267
+ if (errors = schema_to_use.call(params_to_validate)).any?
268
+ set_json_validation_error_messages(errors)
269
+ true
270
+ else
271
+ false
272
+ end
273
+ end
274
+
275
+ def malformed_request_for_json_with_schema?(schema_to_use = schema, params_to_validate = params)
276
+ invalid_json? || validation_errors_for_schema?(schema_to_use, params_to_validate)
277
+ end
278
+ end
7
279
  end
8
280
  end
9
281
  end
@@ -1,280 +0,0 @@
1
- # frozen_string_literal: true
2
- require "webmachine"
3
- require "pact_broker/services"
4
- require "pact_broker/api/decorators"
5
- require "pact_broker/logging"
6
- require "pact_broker/api/pact_broker_urls"
7
- require "pact_broker/json"
8
- require "pact_broker/pacts/pact_params"
9
- require "pact_broker/api/resources/authentication"
10
- require "pact_broker/api/resources/authorization"
11
- require "pact_broker/errors"
12
-
13
- module PactBroker
14
- module Api
15
- module Resources
16
- class InvalidJsonError < PactBroker::Error ; end
17
-
18
- class DefaultBaseResource < Webmachine::Resource
19
- include PactBroker::Services
20
- include PactBroker::Api::PactBrokerUrls
21
- include PactBroker::Api::Resources::Authentication
22
- include PactBroker::Api::Resources::Authorization
23
-
24
- include PactBroker::Logging
25
-
26
- attr_accessor :user
27
-
28
- def initialize
29
- PactBroker.configuration.before_resource.call(self)
30
- application_context.before_resource&.call(self)
31
- end
32
-
33
- def options
34
- { "Access-Control-Allow-Methods" => allowed_methods.join(", ")}
35
- end
36
-
37
- def known_methods
38
- super + ["PATCH"]
39
- end
40
-
41
- def finish_request
42
- application_context.after_resource&.call(self)
43
- PactBroker.configuration.after_resource.call(self)
44
- end
45
-
46
- def is_authorized?(authorization_header)
47
- authenticated?(self, authorization_header)
48
- end
49
-
50
- def forbidden?
51
- if application_context.resource_authorizer
52
- !application_context.resource_authorizer.call(self)
53
- elsif PactBroker.configuration.authorize
54
- !PactBroker.configuration.authorize.call(self, {})
55
- else
56
- false
57
- end
58
- end
59
-
60
- # The path_info segments aren't URL decoded
61
- def identifier_from_path
62
- @identifier_from_path ||= request.path_info.each_with_object({}) do | (key, value), hash|
63
- if value.is_a?(String)
64
- hash[key] = URI.decode(value)
65
- elsif value.is_a?(Symbol) || value.is_a?(Numeric)
66
- hash[key] = value
67
- end
68
- end
69
- end
70
-
71
- alias_method :path_info, :identifier_from_path
72
-
73
- def base_url
74
- # Have to use something for the base URL here - we can't use an empty string as we can in the UI.
75
- # Can't work out if cache poisoning is a vulnerability for APIs or not.
76
- # Using the request base URI as a fallback if the base_url is not configured may be a vulnerability,
77
- # but the documentation recommends that the
78
- # base_url should be set in the configuration to mitigate this.
79
- request.env["pactbroker.base_url"] || request.base_uri.to_s.chomp("/")
80
- end
81
-
82
- # See comments for base_url in lib/pact_broker/doc/controllers/app.rb
83
- def ui_base_url
84
- request.env["pactbroker.base_url"] || ""
85
- end
86
-
87
- def charsets_provided
88
- [["utf-8", :encode]]
89
- end
90
-
91
- # We only use utf-8 so leave encoding as it is
92
- def encode(body)
93
- body
94
- end
95
-
96
- def resource_url
97
- request.uri.to_s.gsub(/\?.*/, "").chomp("/")
98
- end
99
-
100
- def decorator_context options = {}
101
- application_context.decorator_context_creator.call(self, options)
102
- end
103
-
104
- def decorator_options options = {}
105
- { user_options: decorator_context(options) }
106
- end
107
-
108
- def handle_exception(error)
109
- error_reference = PactBroker::Errors.generate_error_reference
110
- application_context.error_logger.call(error, error_reference, request.env)
111
- if PactBroker::Errors.reportable_error?(error)
112
- PactBroker::Errors.report(error, error_reference, request.env)
113
- end
114
- response.body = application_context.error_response_body_generator.call(error, error_reference, request.env)
115
- end
116
-
117
- # rubocop: disable Metrics/CyclomaticComplexity
118
- def params(options = {})
119
- return options[:default] if options.key?(:default) && request_body.empty?
120
-
121
- symbolize_names = !options.key?(:symbolize_names) || options[:symbolize_names]
122
- if symbolize_names
123
- @params_with_symbol_keys ||= JSON.parse(request_body, { symbolize_names: true }.merge(PACT_PARSING_OPTIONS)) #Not load! Otherwise it will try to load Ruby classes.
124
- else
125
- @params_with_string_keys ||= JSON.parse(request_body, { symbolize_names: false }.merge(PACT_PARSING_OPTIONS)) #Not load! Otherwise it will try to load Ruby classes.
126
- end
127
- rescue JSON::JSONError => e
128
- raise InvalidJsonError.new("Error parsing JSON - #{e.message}")
129
- end
130
- # rubocop: enable Metrics/CyclomaticComplexity
131
-
132
- def params_with_string_keys
133
- params(symbolize_names: false)
134
- end
135
-
136
- def pact_params
137
- @pact_params ||= PactBroker::Pacts::PactParams.from_request request, identifier_from_path
138
- end
139
-
140
- def set_json_error_message message
141
- response.headers["Content-Type"] = "application/hal+json;charset=utf-8"
142
- response.body = { error: message }.to_json
143
- end
144
-
145
- def set_json_validation_error_messages errors
146
- response.headers["Content-Type"] = "application/hal+json;charset=utf-8"
147
- response.body = { errors: errors }.to_json
148
- end
149
-
150
- def request_body
151
- @request_body ||= request.body.to_s
152
- end
153
-
154
- def consumer_name
155
- identifier_from_path[:consumer_name]
156
- end
157
-
158
- def consumer_version_number
159
- identifier_from_path[:consumer_version_number]
160
- end
161
-
162
- def pacticipant_version_number
163
- identifier_from_path[:pacticipant_version_number]
164
- end
165
-
166
- def consumer_specified?
167
- identifier_from_path.key?(:consumer_name)
168
- end
169
-
170
- def provider_specified?
171
- identifier_from_path.key?(:provider_name)
172
- end
173
-
174
- def provider_name
175
- identifier_from_path[:provider_name]
176
- end
177
-
178
- def pacticipant_name
179
- identifier_from_path[:pacticipant_name]
180
- end
181
-
182
- def pacticipant_specified?
183
- identifier_from_path.key?(:pacticipant_name)
184
- end
185
-
186
- def invalid_json?
187
- begin
188
- params
189
- false
190
- rescue StandardError => e
191
- logger.info "Error parsing JSON #{e} - #{request_body}"
192
- set_json_error_message "Error parsing JSON - #{e.message}"
193
- response.headers["Content-Type"] = "application/hal+json;charset=utf-8"
194
- true
195
- end
196
- end
197
-
198
- def validation_errors? model
199
- if (errors = model.validate).any?
200
- set_json_validation_error_messages errors
201
- true
202
- else
203
- false
204
- end
205
- end
206
-
207
- def contract_validation_errors? contract, params
208
- if (invalid = !contract.validate(params))
209
- set_json_validation_error_messages contract.errors.messages
210
- end
211
- invalid
212
- end
213
-
214
- def find_pacticipant name, role
215
- pacticipant_service.find_pacticipant_by_name(name).tap do | pacticipant |
216
- set_json_error_message("No #{role} with name '#{name}' found") if pacticipant.nil?
217
- end
218
- end
219
-
220
- def consumer
221
- @consumer ||= identifier_from_path[:consumer_name] && find_pacticipant(identifier_from_path[:consumer_name], "consumer")
222
- end
223
-
224
- def provider
225
- @provider ||= identifier_from_path[:provider_name] && find_pacticipant(identifier_from_path[:provider_name], "provider")
226
- end
227
-
228
- def pacticipant
229
- @pacticipant ||= identifier_from_path[:pacticipant_name] && find_pacticipant(identifier_from_path[:pacticipant_name], "pacticipant")
230
- end
231
-
232
- def pact
233
- @pact ||= pact_service.find_pact(pact_params)
234
- end
235
-
236
- # Not necessarily an existing integration
237
- def integration
238
- if consumer_specified? && provider_specified?
239
- OpenStruct.new(consumer: consumer, provider: provider)
240
- else
241
- nil
242
- end
243
- end
244
-
245
- def database_connector
246
- request.env["pactbroker.database_connector"]
247
- end
248
-
249
- def application_context
250
- request.path_info[:application_context]
251
- end
252
-
253
- def decorator_class(name)
254
- application_context.decorator_configuration.class_for(name)
255
- end
256
-
257
- def api_contract_class(name)
258
- application_context.api_contract_configuration.class_for(name)
259
- end
260
-
261
- def schema
262
- nil
263
- end
264
-
265
- def validation_errors_for_schema?(schema_to_use = schema, params_to_validate = params)
266
- if (errors = schema_to_use.call(params_to_validate)).any?
267
- set_json_validation_error_messages(errors)
268
- true
269
- else
270
- false
271
- end
272
- end
273
-
274
- def malformed_request_for_json_with_schema?(schema_to_use = schema, params_to_validate = params)
275
- invalid_json? || validation_errors_for_schema?(schema_to_use, params_to_validate)
276
- end
277
- end
278
- end
279
- end
280
- end
@@ -134,17 +134,11 @@ module PactBroker
134
134
  # Keep this configuration in sync with lib/db.rb
135
135
  configuration.database_connection ||= PactBroker.create_database_connection(configuration.database_configuration, configuration.logger)
136
136
  PactBroker::DB.connection = configuration.database_connection
137
- PactBroker::DB.connection.extend_datasets do
138
- # rubocop: disable Lint/NestedMethodDefinition
139
- def any?
140
- !empty?
141
- end
142
- # rubocop: enable Lint/NestedMethodDefinition
143
- end
144
137
  PactBroker::DB.validate_connection_config if configuration.validate_database_connection_config
145
138
  PactBroker::DB.set_mysql_strict_mode_if_mysql
146
139
  PactBroker::DB.connection.extension(:pagination)
147
140
  PactBroker::DB.connection.extension(:statement_timeout)
141
+ PactBroker::DB.connection.extension(:any_not_empty)
148
142
  PactBroker::DB.connection.timezone = :utc
149
143
  Sequel.datetime_class = DateTime
150
144
  Sequel.database_timezone = :utc # Store all dates in UTC, assume any date without a TZ is UTC
@@ -20,6 +20,13 @@ module PactBroker
20
20
 
21
21
  sensitive_values(:basic_auth_password, :basic_auth_read_only_password)
22
22
 
23
+ coerce_types(
24
+ basic_auth_username: :string,
25
+ basic_auth_password: :string,
26
+ basic_auth_read_only_username: :string,
27
+ basic_auth_read_only_password: :string
28
+ )
29
+
23
30
  def basic_auth_credentials_provided?
24
31
  basic_auth_username&.not_blank? && basic_auth_password&.not_blank?
25
32
  end
@@ -52,9 +52,12 @@ module PactBroker
52
52
  webhook_scheme_whitelist: ["https"],
53
53
  webhook_host_whitelist: [],
54
54
  disable_ssl_verification: false,
55
- webhook_certificates: [],
56
- user_agent: "Pact Broker v#{PactBroker::VERSION}",
55
+ user_agent: "Pact Broker v#{PactBroker::VERSION}"
57
56
  )
57
+ # no default, if you set it to [] or nil, then anyway config blows up when it tries to merge in the
58
+ # numerically indexed hash from the environment variables.
59
+ attr_config :webhook_certificates
60
+ on_load :set_webhook_attribute_defaults
58
61
 
59
62
  # resource attributes
60
63
  attr_config(
@@ -178,8 +181,14 @@ module PactBroker
178
181
  def webhook_certificates= webhook_certificates
179
182
  if webhook_certificates.is_a?(Array)
180
183
  super(webhook_certificates.collect(&:symbolize_keys))
184
+ elsif webhook_certificates.is_a?(Hash)
185
+ if all_keys_are_number_strings?(webhook_certificates)
186
+ super(convert_hash_with_number_string_keys_to_array(webhook_certificates).collect(&:symbolize_keys))
187
+ else
188
+ raise_validation_error("webhook_certificates must be an array, or a hash where each key is an integer in string format.")
189
+ end
181
190
  elsif !webhook_certificates.nil?
182
- raise_validation_error("webhook_certificates must be an array")
191
+ raise_validation_error("webhook_certificates cannot be set using a #{webhook_certificates.class}")
183
192
  end
184
193
  end
185
194
 
@@ -208,6 +217,14 @@ module PactBroker
208
217
  def raise_validation_error(msg)
209
218
  raise PactBroker::ConfigurationError, msg
210
219
  end
220
+
221
+ def set_webhook_attribute_defaults
222
+ # can't set a default on this, or anyway config blows up when trying to merge the
223
+ # hash from the env vars into an array/nil.
224
+ if webhook_certificates.nil?
225
+ self.webhook_certificates = []
226
+ end
227
+ end
211
228
  end
212
229
  end
213
230
  end
@@ -4,6 +4,17 @@ require "pact_broker/config/space_delimited_integer_list"
4
4
  module PactBroker
5
5
  module Config
6
6
  module RuntimeConfigurationCoercionMethods
7
+
8
+ def all_keys_are_number_strings?(hash)
9
+ hash.keys.all? { | k | k.to_s.to_i.to_s == k } # is an integer as a string
10
+ end
11
+
12
+ def convert_hash_with_number_string_keys_to_array(hash)
13
+ hash.keys.collect{ |k| [k, k.to_i]}.sort_by(&:last).collect(&:first).collect do | key |
14
+ hash[key]
15
+ end
16
+ end
17
+
7
18
  def value_to_string_array value, property_name
8
19
  if value.is_a?(String)
9
20
  PactBroker::Config::SpaceDelimitedStringList.parse(value)