inferno_core 0.4.39 → 0.4.40

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1c569fc01e07576cb3aa512c37a69d730bc7b9848bd24ed95935b23a0d52759c
4
- data.tar.gz: 41165f4f56dcf7c7e552ec0c391b868e5020feed0957f12d508489835a91d140
3
+ metadata.gz: 62cf688066c4ae8b8855f9ba04cd376c78ec6ae3238d74c4e34f4fdf3a926aef
4
+ data.tar.gz: 301885b06d03b494d74f7b2f053b741d0fc5d546371fc2eaf602f5b73e00462c
5
5
  SHA512:
6
- metadata.gz: 03cb4fec0a859ba0967d891b62d4741b6a241839bce017ac0a9f212c06cabbec45e3d8c9464d8a75421c697c6e547af19e5bdbe8055ab7ea630344a781dfdf5e
7
- data.tar.gz: ad2fc2e5f42777535e912e4bc8217f48d9d90babb074b071b7437fb3b987f4f8470f505f9e1bd29b1b893f7b6ccc36800145ad62fe82664188e0eab965b71cf5
6
+ metadata.gz: 6081f531d0f06172ed695265ac0c772182fc1481a97290441758d40e1013b08f1d684683e38e496a4d8fae4ac05210ec11c07ee7d2ca5e4513eda0cc5eae3f43
7
+ data.tar.gz: 5fe4104c0e129a061f81ebaffe2bb6f993a8424aabe1b0118aeae49f1eea29e72f1c1ac1120c8d480e54514547386e47faa04b750988af1aaa7b8499965ef275
@@ -53,6 +53,9 @@ module Inferno
53
53
  # Should not need Content-Type header but GitHub Codespaces will not work without them.
54
54
  # This could be investigated and likely removed if addressed properly elsewhere.
55
55
  get '/', to: ->(_env) { [200, { 'Content-Type' => 'text/html' }, [client_page]] }
56
+ get '/jwks.json', to: lambda { |_env|
57
+ [200, { 'Content-Type' => 'application/json' }, [Inferno::JWKS.jwks_json]]
58
+ }, as: :jwks
56
59
 
57
60
  Inferno.routes.each do |route|
58
61
  cleaned_id = route[:suite].id.gsub(/[^a-zA-Z\d\-._~]/, '_')
@@ -13,6 +13,7 @@ module Inferno
13
13
  raw_js_host = ENV.fetch('JS_HOST', '')
14
14
  base_path = ENV.fetch('BASE_PATH', '')
15
15
  public_path = base_path.blank? ? '/public' : "/#{base_path}/public"
16
+ jwks_path = base_path.blank? ? '/jwks.json' : "/#{base_path}/jwks.json"
16
17
  js_host = raw_js_host.present? ? "#{raw_js_host}/public" : public_path
17
18
 
18
19
  Application.register('js_host', js_host)
@@ -21,6 +22,7 @@ module Inferno
21
22
  Application.register('async_jobs', ENV['ASYNC_JOBS'] != 'false')
22
23
  Application.register('inferno_host', ENV.fetch('INFERNO_HOST', 'http://localhost:4567'))
23
24
  Application.register('base_url', URI.join(Application['inferno_host'], base_path).to_s)
25
+ Application.register('jwks_url', URI.join(Application['inferno_host'], jwks_path).to_s)
24
26
  Application.register('cache_bust_token', SecureRandom.uuid)
25
27
 
26
28
  configure do |config|
@@ -1,4 +1,5 @@
1
1
  require_relative '../entities/attributes'
2
+ require_relative 'jwks'
2
3
 
3
4
  module Inferno
4
5
  module DSL
@@ -19,7 +20,7 @@ module Inferno
19
20
  # to normal inputs.
20
21
  #
21
22
  # The AuthInfo input type supports two different modes in the UI. Different
22
- # fields will be presented to the user dependengi on which mode is selected.
23
+ # fields will be presented to the user depending on which mode is selected.
23
24
  # - `auth` - This presents the inputs needed to perform authorization, and
24
25
  # is appropriate to use as an input to test groups which perform
25
26
  # authorization
@@ -161,13 +162,148 @@ module Inferno
161
162
 
162
163
  # @private
163
164
  def add_to_client(client)
164
- # TODO
165
- # client.auth = self
166
- # self.client = client
165
+ client.auth_info = self
166
+ self.client = client
167
+ # TODO: do we want to perform authorization if no access_token or rely on SMART/ other auth tests?
168
+ return unless access_token.present?
167
169
 
168
- # return unless access_token.present?
170
+ client.set_bearer_token(access_token)
171
+ end
172
+
173
+ # @private
174
+ def need_to_refresh?
175
+ return false if access_token.blank? || (!backend_services? && refresh_token.blank?)
176
+
177
+ return true if expires_in.blank?
178
+
179
+ issue_time.to_i + expires_in.to_i - DateTime.now.to_i < 60
180
+ end
181
+
182
+ # @private
183
+ def able_to_refresh?
184
+ token_url.present? && (backend_services? || refresh_token.present?)
185
+ end
186
+
187
+ # @private
188
+ def backend_services?
189
+ auth_type == 'backend_services'
190
+ end
191
+
192
+ # @private
193
+ def oauth2_refresh_params
194
+ case auth_type
195
+ when 'public'
196
+ public_auth_refresh_params
197
+ when 'symmetric'
198
+ symmetric_auth_refresh_params
199
+ when 'asymmetric'
200
+ asymmetric_auth_refresh_params
201
+ when 'backend_services'
202
+ backend_services_auth_refresh_params
203
+ end
204
+ end
205
+
206
+ # @private
207
+ def symmetric_auth_refresh_params
208
+ {
209
+ 'grant_type' => 'refresh_token',
210
+ 'refresh_token' => refresh_token
211
+ }
212
+ end
213
+
214
+ # @private
215
+ def public_auth_refresh_params
216
+ symmetric_auth_refresh_params.merge('client_id' => client_id)
217
+ end
218
+
219
+ # @private
220
+ def asymmetric_auth_refresh_params
221
+ symmetric_auth_refresh_params.merge(
222
+ 'client_assertion_type' => 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
223
+ 'client_assertion' => client_assertion
224
+ )
225
+ end
226
+
227
+ # @private
228
+ def backend_services_auth_refresh_params
229
+ {
230
+ 'grant_type' => 'client_credentials',
231
+ 'scope' => requested_scopes,
232
+ 'client_assertion_type' => 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
233
+ 'client_assertion' => client_assertion
234
+ }
235
+ end
236
+
237
+ # @private
238
+ def oauth2_refresh_headers
239
+ base_headers = { 'Content-Type' => 'application/x-www-form-urlencoded' }
240
+
241
+ return base_headers unless auth_type == 'symmetric'
242
+
243
+ credentials = "#{client_id}:#{client_secret}"
244
+
245
+ base_headers.merge(
246
+ 'Authorization' => "Basic #{Base64.strict_encode64(credentials)}"
247
+ )
248
+ end
249
+
250
+ # @private
251
+ def private_key
252
+ @private_key ||= JWKS.jwks(user_jwks: jwks)
253
+ .select { |key| key[:key_ops]&.include?('sign') }
254
+ .select { |key| key[:alg] == encryption_algorithm }
255
+ .find { |key| !kid || key[:kid] == kid }
256
+ end
257
+
258
+ # @private
259
+ def signing_key
260
+ if private_key.nil?
261
+ raise Inferno::Exceptions::AssertionException,
262
+ "No signing key found for inputs: encryption method = '#{encryption_algorithm}' and kid = '#{kid}'"
263
+ end
264
+
265
+ @private_key.signing_key
266
+ end
267
+
268
+ # @private
269
+ def auth_jwt_header
270
+ {
271
+ 'alg' => encryption_algorithm,
272
+ 'kid' => private_key['kid'],
273
+ 'typ' => 'JWT',
274
+ 'jku' => Inferno::Application['jwks_url']
275
+ }
276
+ end
277
+
278
+ # @private
279
+ def auth_jwt_claims
280
+ {
281
+ 'iss' => client_id,
282
+ 'sub' => client_id,
283
+ 'aud' => token_url,
284
+ 'exp' => 5.minutes.from_now.to_i,
285
+ 'jti' => SecureRandom.hex(32)
286
+ }
287
+ end
288
+
289
+ # @private
290
+ def client_assertion
291
+ JWT.encode auth_jwt_claims, signing_key, encryption_algorithm, auth_jwt_header
292
+ end
293
+
294
+ # @private
295
+ def update_from_response_body(request)
296
+ token_response_body = JSON.parse(request.response_body)
297
+
298
+ expires_in = token_response_body['expires_in'].is_a?(Numeric) ? token_response_body['expires_in'] : nil
299
+
300
+ self.access_token = token_response_body['access_token']
301
+ self.refresh_token = token_response_body['refresh_token'] if token_response_body['refresh_token'].present?
302
+ self.expires_in = expires_in
303
+ self.issue_time = DateTime.now
169
304
 
170
- # client.set_bearer_token(access_token)
305
+ add_to_client(client)
306
+ self
171
307
  end
172
308
  end
173
309
  end
@@ -347,7 +347,7 @@ module Inferno
347
347
 
348
348
  # @private
349
349
  def perform_refresh(client)
350
- credentials = client.oauth_credentials
350
+ credentials = client.auth_info || client.oauth_credentials
351
351
 
352
352
  post(
353
353
  credentials.token_url,
@@ -363,7 +363,7 @@ module Inferno
363
363
  Inferno::Repositories::SessionData.new.save(
364
364
  name: credentials.name,
365
365
  value: credentials,
366
- type: 'oauth_credentials',
366
+ type: credentials.is_a?(Inferno::DSL::AuthInfo) ? 'auth_info' : 'oauth_credentials',
367
367
  test_session_id:
368
368
  )
369
369
  end
@@ -20,6 +20,20 @@ module Inferno
20
20
  # url :url
21
21
  # bearer_token :access_token
22
22
  # end
23
+ #
24
+ # @example
25
+ # input :url
26
+ # input :fhir_auth,
27
+ # type: :auth_info,
28
+ # options: {
29
+ # mode: 'access'
30
+ # }
31
+ #
32
+ # fhir_client do
33
+ # url :url
34
+ # headers 'My-Custom_header' => 'CUSTOM_HEADER_VALUE'
35
+ # auth_info :fhir_auth
36
+ # end
23
37
  class FHIRClientBuilder
24
38
  attr_accessor :runnable
25
39
 
@@ -33,6 +47,7 @@ module Inferno
33
47
  client.default_json
34
48
  client.set_bearer_token bearer_token if bearer_token
35
49
  oauth_credentials&.add_to_client(client)
50
+ auth_info&.add_to_client(client)
36
51
  end
37
52
  end
38
53
 
@@ -80,6 +95,20 @@ module Inferno
80
95
  end
81
96
  end
82
97
 
98
+ # Define auth info for a client. Auth info contains info needed for client
99
+ # to perform authorization and refresh access token when necessary
100
+ #
101
+ # @param auth_info [Inferno::DSL::AuthInfo, Symbol]
102
+ # @return [void]
103
+ def auth_info(auth_info = nil)
104
+ @auth_info ||=
105
+ if auth_info.is_a? Symbol
106
+ runnable.send(auth_info)
107
+ else
108
+ auth_info
109
+ end
110
+ end
111
+
83
112
  # Define custom headers for a client
84
113
  #
85
114
  # @param headers [Hash]
@@ -277,32 +277,36 @@ module Inferno
277
277
  end
278
278
 
279
279
  # @private
280
- def operation_outcome_from_hl7_wrapped_response(response)
281
- res = JSON.parse(response)
282
- if res['sessionId'] != @session_id
283
- validator_session_repo.save(test_suite_id:, validator_session_id: res['sessionId'],
280
+ def operation_outcome_from_hl7_wrapped_response(response_hash)
281
+ if response_hash['sessionId'] && response_hash['sessionId'] != @session_id
282
+ validator_session_repo.save(test_suite_id:, validator_session_id: response_hash['sessionId'],
284
283
  validator_name: name.to_s, suite_options: requirements)
285
- @session_id = res['sessionId']
284
+ @session_id = response_hash['sessionId']
286
285
  end
287
286
 
288
287
  # assume for now that one resource -> one request
289
- issues = res['outcomes'][0]['issues']&.map do |i|
288
+ issues = response_hash['outcomes'][0]['issues']&.map do |i|
290
289
  { severity: i['level'].downcase, expression: i['location'], details: { text: i['message'] } }
291
290
  end
292
291
  # this is circuitous, ideally we would map this response directly to message_hashes
293
292
  FHIR::OperationOutcome.new(issue: issues)
294
293
  end
295
294
 
295
+ # @private
296
+ def remove_invalid_characters(string)
297
+ string.gsub(/[^[:print:]\r\n]+/, '')
298
+ end
299
+
296
300
  # @private
297
301
  def operation_outcome_from_validator_response(response, runnable)
298
- if response.body.start_with? '{'
299
- operation_outcome_from_hl7_wrapped_response(response.body)
300
- else
301
- runnable.add_message('error', "Validator Response: HTTP #{response.status}\n#{response.body}")
302
- raise Inferno::Exceptions::ErrorInValidatorException,
303
- 'Validator response was an unexpected format. ' \
304
- 'Review Messages tab or validator service logs for more information.'
305
- end
302
+ sanitized_body = remove_invalid_characters(response.body)
303
+
304
+ operation_outcome_from_hl7_wrapped_response(JSON.parse(sanitized_body))
305
+ rescue JSON::ParserError
306
+ runnable.add_message('error', "Validator Response: HTTP #{response.status}\n#{sanitized_body}")
307
+ raise Inferno::Exceptions::ErrorInValidatorException,
308
+ 'Validator response was an unexpected format. ' \
309
+ 'Review Messages tab or validator service logs for more information.'
306
310
  end
307
311
  end
308
312
 
@@ -124,7 +124,7 @@ module Inferno
124
124
  runnable.add_message('error', e.message)
125
125
  raise Inferno::Exceptions::ErrorInValidatorException, "Unable to connect to validator at #{url}."
126
126
  end
127
- outcome = operation_outcome_from_validator_response(response.body, runnable)
127
+ outcome = operation_outcome_from_validator_response(response, runnable)
128
128
 
129
129
  message_hashes = message_hashes_from_outcome(outcome, resource, profile_url)
130
130
 
@@ -212,16 +212,21 @@ module Inferno
212
212
  ).post('validate', resource.source_contents)
213
213
  end
214
214
 
215
+ # @private
216
+ def remove_invalid_characters(string)
217
+ string.gsub(/[^[:print:]\r\n]+/, '')
218
+ end
219
+
215
220
  # @private
216
221
  def operation_outcome_from_validator_response(response, runnable)
217
- if response.start_with? '{'
218
- FHIR::OperationOutcome.new(JSON.parse(response))
219
- else
220
- runnable.add_message('error', "Validator Response:\n#{response}")
221
- raise Inferno::Exceptions::ErrorInValidatorException,
222
- 'Validator response was an unexpected format. '\
223
- 'Review Messages tab or validator service logs for more information.'
224
- end
222
+ sanitized_body = remove_invalid_characters(response.body)
223
+
224
+ FHIR::OperationOutcome.new(JSON.parse(sanitized_body))
225
+ rescue JSON::ParserError
226
+ runnable.add_message('error', "Validator Response: HTTP #{response.status}\n#{sanitized_body}")
227
+ raise Inferno::Exceptions::ErrorInValidatorException,
228
+ 'Validator response was an unexpected format. ' \
229
+ 'Review Messages tab or validator service logs for more information.'
225
230
  end
226
231
  end
227
232
 
@@ -0,0 +1,121 @@
1
+ module Inferno
2
+ module DSL
3
+ # This module contains the methods needed to perform FHIRPath evaluations
4
+ # on FHIR resources/elements. The actual evaluation is typically performed by an external
5
+ # FHIRPath evaluation service.
6
+ #
7
+ # Tests can leverage the evaluation functionality by calling `evaluate_fhirpath` to retrieve
8
+ # results of FHIRPath expressions.
9
+ #
10
+ # @example
11
+ #
12
+ # results = evaluate_fhirpath(resource: patient_resource, path: 'Patient.name.given')
13
+ #
14
+ # results will be an array representing the result of evaluating the given
15
+ # expression against the given root element. Each "result" in the returned
16
+ # array will be in the form
17
+ # `{ "type": "[FHIR datatype of the result]", "element": "[result value of the FHIRPath expression]" }`.
18
+ # @note the `element` field can either be a primitive value (string, boolean, etc.) or a FHIR::Model.
19
+ module FhirpathEvaluation
20
+ def self.included(klass)
21
+ klass.extend ClassMethods
22
+ end
23
+
24
+ # Evaluates a fhirpath expression for a given FHIR resource
25
+ #
26
+ # @param resource [FHIR::Model] the root FHIR resource to use when evaluating the fhirpath expression.
27
+ # @param path [String] The FHIRPath expression to evaluate.
28
+ # @param url [String] the url of the fhirpath service to use.
29
+ # @return [Array<Hash>] An array of hashes representing the result of evaluating the given expression against
30
+ # the given root resource.
31
+ def evaluate_fhirpath(resource:, path:, url: nil)
32
+ self.class.evaluator(url).evaluate_fhirpath(resource, path, self)
33
+ end
34
+
35
+ class Evaluator
36
+ # @private
37
+ def initialize(url = nil)
38
+ url(url)
39
+ end
40
+
41
+ # @private
42
+ def default_fhirpath_url
43
+ ENV.fetch('FHIRPATH_URL')
44
+ end
45
+
46
+ # Set/Get the url of the fhirpath service
47
+ #
48
+ # @param fhirpath_url [String]
49
+ # @return [String]
50
+ def url(fhirpath_url = nil)
51
+ @url ||= fhirpath_url || default_fhirpath_url
52
+ end
53
+
54
+ # Evaluates a fhirpath expression for a given FHIR resource
55
+ #
56
+ # @param fhir_resource [FHIR::Model] the root FHIR resource to use when evaluating the fhirpath expression.
57
+ # @param fhirpath_expression [String] The FHIRPath expression to evaluate.
58
+ # @param runnable [Inferno::Test] to add any error message that occurs.
59
+ # @return [Array<Hash>] An array hashes representing the result of evaluating the given expression against
60
+ # the given root resource. Each "result" in the returned array will be in the form
61
+ # `{ "type": "[FHIR datatype of the result]", "element": "[result value of the FHIRPath expression]" }`.
62
+ # @note the `element` field can either be a primitive value (string, boolean, etc.) or a FHIR::Model.
63
+ def evaluate_fhirpath(fhir_resource, fhirpath_expression, runnable)
64
+ begin
65
+ response = call_fhirpath_service(fhir_resource, fhirpath_expression)
66
+ rescue StandardError => e
67
+ # This could be a complete failure to connect (fhirpath service isn't running)
68
+ # or a timeout (fhirpath service took too long to respond).
69
+ runnable.add_message('error', e.message)
70
+ raise Inferno::Exceptions::ErrorInFhirpathException, "Unable to connect to FHIRPath service at #{url}."
71
+ end
72
+
73
+ sanitized_body = remove_invalid_characters(response.body)
74
+ return transform_fhirpath_results(JSON.parse(sanitized_body)) if response.status.to_s.start_with? '2'
75
+
76
+ runnable.add_message('error', "FHIRPath service Response: HTTP #{response.status}\n#{sanitized_body}")
77
+ raise Inferno::Exceptions::ErrorInFhirpathException,
78
+ 'FHIRPath service call failed. Review Messages tab for more information.'
79
+ rescue JSON::ParserError
80
+ runnable.add_message('error', "Invalid FHIRPath service response format:\n#{sanitized_body}")
81
+ raise Inferno::Exceptions::ErrorInFhirpathException,
82
+ 'Error occurred in the FHIRPath service. Review Messages tab for more information.'
83
+ end
84
+
85
+ # @private
86
+ def transform_fhirpath_results(fhirpath_results)
87
+ fhirpath_results.each do |result|
88
+ klass = FHIR.const_get(result['type'])
89
+ result['element'] = klass.new(result['element'])
90
+ rescue NameError
91
+ next
92
+ end
93
+ fhirpath_results
94
+ end
95
+
96
+ def call_fhirpath_service(fhir_resource, fhirpath_expression)
97
+ Faraday.new(
98
+ url,
99
+ request: { timeout: 600 }
100
+ ).post(
101
+ "evaluate?path=#{fhirpath_expression}",
102
+ fhir_resource.to_json,
103
+ content_type: 'application/json'
104
+ )
105
+ end
106
+
107
+ # @private
108
+ def remove_invalid_characters(string)
109
+ string.gsub(/[^[:print:]\r\n]+/, '')
110
+ end
111
+ end
112
+
113
+ module ClassMethods
114
+ # @private
115
+ def evaluator(url = nil)
116
+ @evaluator ||= Inferno::DSL::FhirpathEvaluation::Evaluator.new(url)
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,79 @@
1
+ module Inferno
2
+ module DSL
3
+ # The JWKS class provides methods to handle JSON Web Key Sets (JWKS)
4
+ # within Inferno.
5
+ #
6
+ # This class allows users to fetch, parse, and manage JWKS, ensuring
7
+ # that the necessary keys for verifying tokens are available.
8
+ class JWKS
9
+ class << self
10
+ # Returns a formatted JSON string of the JWKS public keys that are used for verification.
11
+ # This method filters out keys that do not have the 'verify' operation.
12
+ #
13
+ # @return [String] The formatted JSON string of the JWKS public keys.
14
+ #
15
+ # @example
16
+ # jwks_json = Inferno::JWKS.jwks_json
17
+ # puts jwks_json
18
+ def jwks_json
19
+ @jwks_json ||=
20
+ JSON.pretty_generate(
21
+ { keys: jwks.export[:keys].select { |key| key[:key_ops]&.include?('verify') } }
22
+ )
23
+ end
24
+
25
+ # Provides the default file path to the JWKS file.
26
+ # This method is primarily used internally to locate the default JWKS file.
27
+ #
28
+ # @return [String] The default JWKS file path.
29
+ #
30
+ # @private
31
+ def default_jwks_path
32
+ @default_jwks_path ||= File.join(__dir__, 'jwks.json')
33
+ end
34
+
35
+ # Fetches the JWKS file path from the environment variable `INFERNO_JWKS_PATH`.
36
+ # If the environment variable is not set, it falls back to the default path
37
+ # provided by `.default_jwks_path`.
38
+ #
39
+ # @return [String] The JWKS file path.
40
+ #
41
+ # @private
42
+ def jwks_path
43
+ @jwks_path ||=
44
+ ENV.fetch('INFERNO_JWKS_PATH', default_jwks_path)
45
+ end
46
+
47
+ # Reads the JWKS content from the file located at the JWKS path.
48
+ #
49
+ # @return [String] The json content of the JWKS file.
50
+ #
51
+ # @private
52
+ def default_jwks_json
53
+ @default_jwks_json ||= File.read(jwks_path)
54
+ end
55
+
56
+ # Parses and returns a `JWT::JWK::Set` object from the provided JWKS string
57
+ # or from the file located at the JWKS path. If a user-provided JWKS string
58
+ # is not available, it reads the JWKS from the file.
59
+ #
60
+ # @param user_jwks [String, nil] An optional json containing the JWKS.
61
+ # If not provided, the method reads from the file.
62
+ # @return [JWT::JWK::Set] The parsed JWKS set.
63
+ #
64
+ # @example
65
+ # # Using a user-provided JWKS string
66
+ # user_jwks = '{"keys":[...]}'
67
+ # jwks_set = Inferno::JWKS.jwks(user_jwks: user_jwks)
68
+ #
69
+ # # Using the default JWKS file
70
+ # jwks_set = Inferno::JWKS.jwks
71
+ def jwks(user_jwks: nil)
72
+ JWT::JWK::Set.new(JSON.parse(user_jwks.presence || default_jwks_json))
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ JWKS = DSL::JWKS
79
+ end
@@ -0,0 +1,71 @@
1
+ require_relative '../utils/markdown_formatter'
2
+
3
+ module Inferno
4
+ module DSL
5
+ # This module contains methods to add meessages to runnable results
6
+ module Messages
7
+ include Inferno::Utils::MarkdownFormatter
8
+ # @private
9
+ def messages
10
+ @messages ||= []
11
+ end
12
+
13
+ # Add a message to the result.
14
+ #
15
+ # @param type [String] error, warning, or info
16
+ # @param message [String]
17
+ # @return [void]
18
+ def add_message(type, message)
19
+ messages << { type: type.to_s, message: format_markdown(message) }
20
+ end
21
+
22
+ # Add an informational message to the results of a test. If passed a
23
+ # block, a failed assertion will become an info message and test execution
24
+ # will continue.
25
+ #
26
+ # @param message [String]
27
+ # @return [void]
28
+ # @example
29
+ # # Add an info message
30
+ # info 'This message will be added to the test results'
31
+ #
32
+ # # The message for the failed assertion will be treated as an info
33
+ # # message. Test exection will continue.
34
+ # info { assert false == true }
35
+ def info(message = nil)
36
+ unless block_given?
37
+ add_message('info', message) unless message.nil?
38
+ return
39
+ end
40
+
41
+ yield
42
+ rescue Exceptions::AssertionException => e
43
+ add_message('info', e.message)
44
+ end
45
+
46
+ # Add a warning message to the results of a test. If passed a block, a
47
+ # failed assertion will become a warning message and test execution will
48
+ # continue.
49
+ #
50
+ # @param message [String]
51
+ # @return [void]
52
+ # @example
53
+ # # Add a warning message
54
+ # warning 'This message will be added to the test results'
55
+ #
56
+ # # The message for the failed assertion will be treated as a warning
57
+ # # message. Test exection will continue.
58
+ # warning { assert false == true }
59
+ def warning(message = nil)
60
+ unless block_given?
61
+ add_message('warning', message) unless message.nil?
62
+ return
63
+ end
64
+
65
+ yield
66
+ rescue Exceptions::AssertionException => e
67
+ add_message('warning', e.message)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -291,6 +291,18 @@ module Inferno
291
291
  to_s
292
292
  end
293
293
 
294
+ # Set/Get the block that is executed when a runnable is run
295
+ #
296
+ # @param block [Proc]
297
+ # @return [Proc] the block that is executed when a runnable is run
298
+ def block(&block)
299
+ return @block unless block_given?
300
+
301
+ @block = block
302
+ end
303
+
304
+ alias run block
305
+
294
306
  # @private
295
307
  def all_children
296
308
  @all_children ||= []