inferno_core 0.4.4 → 0.4.5

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: 0fc53d7cfc20506e263bf358bba6d477fb6dff4b0523530bd2aef3d191283c28
4
- data.tar.gz: aff080de4856187943390ee5478dfdd791a8962e21067be89aa7a5d40708e84c
3
+ metadata.gz: 77c4a279df5216b705b84640b636cb3084f27a27493668e4a57a99ab699a3ae3
4
+ data.tar.gz: 1d35829be39575c26962588ee44482535962be25c2dabbd7016cf727077ed3c7
5
5
  SHA512:
6
- metadata.gz: b1f8fb8df13079cc0f8f9007fb1d550ec96760fe9438bc4281115c26c1a6beb55e9e7dd826f87f6dea9d4b5c97c0546cb6090fbf2370febf5c2568c333f0fda4
7
- data.tar.gz: 9136dc23b84c4da21bbfd0c79cddab3d16e2aa25528a88b5636f47968aa8e801b657a1daac56f211ca0fd9c567484fcea8d1556ec8a0bc9fb206aa97ee5b8822
6
+ metadata.gz: a2546687c4f061eb080944bcff64dfe3aa9dc03534e808795157ad3c2b56dd6e7eee38c34821f7c5f16b551933d88acaa750d557f6c5125fe75730845d758541
7
+ data.tar.gz: 13681873050e6864506231c87f623007e8e09ea1ea9ae8486e703f9a4f9b417572f3c1d433ee31fe23462c3ab79c799e03d9001ed0c18f4c7ef4eb3eef8e2708
@@ -2,6 +2,10 @@ require_relative '../exceptions'
2
2
 
3
3
  module Inferno
4
4
  module DSL
5
+ # This module contains the assertions used within tests to verify the
6
+ # behavior of the systems under test. Failing an assertion causes a test to
7
+ # immediately stop execution and receive a `fail` result. Additional
8
+ # assertions added to this module will be available in all tests.
5
9
  module Assertions
6
10
  # Make an assertion
7
11
  #
@@ -159,6 +163,10 @@ module Inferno
159
163
  assert uri =~ /\A#{URI::DEFAULT_PARSER.make_regexp(['http', 'https'])}\z/, error_message
160
164
  end
161
165
 
166
+ # Check the Content-Type header of a response
167
+ #
168
+ # @param type [String]
169
+ # @param request [Inferno::Entities::Request]
162
170
  def assert_response_content_type(type, request: self.request)
163
171
  header = request.response_header('Content-Type')
164
172
  assert header.present?, no_content_type_message
@@ -166,10 +174,12 @@ module Inferno
166
174
  assert header.value.start_with?(type), bad_content_type_message(type, header.value)
167
175
  end
168
176
 
177
+ # @private
169
178
  def no_content_type_message
170
179
  'Response did not contain a `Content-Type` header.'
171
180
  end
172
181
 
182
+ # @private
173
183
  def bad_content_type_message(expected, received)
174
184
  "Expected `Content-Type` to be `#{expected}`, but found `#{received}`"
175
185
  end
@@ -2,13 +2,85 @@ require_relative '../entities/input'
2
2
 
3
3
  module Inferno
4
4
  module DSL
5
- # This module contains the DSL for managing runnable configuration.
5
+ # This module contains the DSL for managing runnable configuration. Runnable
6
+ # configuration provides a way to modify test behavior at boot time.
7
+ #
8
+ # The main features enabled by configuration are:
9
+ # - Modifying the properties of a runnable's inputs. This could include
10
+ # locking a particular input, making a particular input optional/required,
11
+ # or changing an input's value.
12
+ # - Renaming an input/output/request to avoid name collisions when a test
13
+ # suite uses the same test multiple times.
14
+ # - Tests can define custom configuration options to enable different
15
+ # - testing behavior.
16
+ #
17
+ # @example
18
+ # test do
19
+ # id :json_request_test
20
+ #
21
+ # input :url
22
+ # output :response_body
23
+ # makes_request :json_request
24
+ #
25
+ # run do
26
+ # if config.options[:include_content_type]
27
+ # get url, headers: { 'Content-Type' => 'application/json' }
28
+ # else
29
+ # get url
30
+ # end
31
+ #
32
+ # assert_response_status(200)
33
+ # output response_body: request.response_body
34
+ # assert_valid_json
35
+ # end
36
+ # end
37
+ #
38
+ # group do
39
+ # test from :json_request_test do
40
+ # id :json_request_without_content_type
41
+ #
42
+ # config(
43
+ # inputs: {
44
+ # url: { name: :url_without_content_type }
45
+ # },
46
+ # outputs: {
47
+ # response_body: { name: :response_body_without_content_type }
48
+ # },
49
+ # requests: {
50
+ # json_request: { name: :json_request_without_content_type }
51
+ # }
52
+ # )
53
+ # end
54
+ #
55
+ # test from :json_request_test do
56
+ # id :json_request_with_content_type
57
+ #
58
+ # config(
59
+ # options: {
60
+ # include_content_type: true
61
+ # },
62
+ # inputs: {
63
+ # url: { name: :url_with_content_type }
64
+ # },
65
+ # outputs: {
66
+ # response_body: { name: :response_body_with_content_type }
67
+ # },
68
+ # requests: {
69
+ # json_request: { name: :json_request_with_content_type }
70
+ # }
71
+ # )
72
+ # end
73
+ # end
6
74
  module Configurable
7
75
  def self.extended(klass)
8
76
  klass.extend Forwardable
9
77
  klass.def_delegator 'self.class', :config
10
78
  end
11
79
 
80
+ # Define/update/get the configuration for a runnable. This configuration
81
+ # will be applied to the runnable and all of its children.
82
+ #
83
+ # @param new_configuration [Hash]
12
84
  def config(new_configuration = {})
13
85
  @config ||= Configuration.new
14
86
 
@@ -21,14 +93,18 @@ module Inferno
21
93
  @config
22
94
  end
23
95
 
24
- # @private
96
+ # This class stores a runnable's configuration. It should never be
97
+ # directly instantiated within a test suite. Instead, a runnable's
98
+ # configuration can be modified or retrieved using the `config` method.
25
99
  class Configuration
26
100
  attr_accessor :configuration
27
101
 
102
+ # @private
28
103
  def initialize(configuration = {})
29
104
  self.configuration = configuration
30
105
  end
31
106
 
107
+ # @private
32
108
  def apply(new_configuration)
33
109
  config_to_apply =
34
110
  if new_configuration.is_a? Configuration
@@ -44,16 +120,23 @@ module Inferno
44
120
  end
45
121
  end
46
122
 
123
+ # The configuration options defined for this runnable.
124
+ #
125
+ # @return [Hash]
47
126
  def options
48
127
  configuration[:options] ||= {}
49
128
  end
50
129
 
51
130
  ### Input Configuration ###
52
131
 
132
+ # The input configuration for this runnable.
133
+ #
134
+ # @return [Hash]
53
135
  def inputs
54
136
  configuration[:inputs] ||= {}
55
137
  end
56
138
 
139
+ # @private
57
140
  def add_input(identifier, new_config = {})
58
141
  existing_config = input(identifier)
59
142
 
@@ -67,81 +150,103 @@ module Inferno
67
150
  .merge(Entities::Input.new(**new_config))
68
151
  end
69
152
 
153
+ # @private
70
154
  def default_input_params(identifier)
71
155
  { name: identifier, type: 'text' }
72
156
  end
73
157
 
158
+ # @private
74
159
  def input_exists?(identifier)
75
160
  inputs.key? identifier
76
161
  end
77
162
 
163
+ # @private
78
164
  def input(identifier)
79
165
  inputs[identifier]
80
166
  end
81
167
 
168
+ # @private
82
169
  def input_name(identifier)
83
170
  inputs[identifier]&.name
84
171
  end
85
172
 
173
+ # @private
86
174
  def input_type(identifier)
87
175
  inputs[identifier]&.type
88
176
  end
89
177
 
90
178
  ### Output Configuration ###
91
179
 
180
+ # The output configuration for this runnable.
181
+ #
182
+ # @return [Hash]
92
183
  def outputs
93
184
  configuration[:outputs] ||= {}
94
185
  end
95
186
 
187
+ # @private
96
188
  def add_output(identifier, new_config = {})
97
189
  existing_config = output_config(identifier) || {}
98
190
  outputs[identifier] = default_output_config(identifier).merge(existing_config, new_config)
99
191
  end
100
192
 
193
+ # @private
101
194
  def default_output_config(identifier)
102
195
  { name: identifier, type: 'text' }
103
196
  end
104
197
 
198
+ # @private
105
199
  def output_config_exists?(identifier)
106
200
  outputs.key? identifier
107
201
  end
108
202
 
203
+ # @private
109
204
  def output_config(identifier)
110
205
  outputs[identifier]
111
206
  end
112
207
 
208
+ # @private
113
209
  def output_name(identifier)
114
210
  outputs.dig(identifier, :name) || identifier
115
211
  end
116
212
 
213
+ # @private
117
214
  def output_type(identifier)
118
215
  outputs.dig(identifier, :type)
119
216
  end
120
217
 
121
218
  ### Request Configuration ###
122
219
 
220
+ # The request configuration for this runnable.
221
+ #
222
+ # @return [Hash]
123
223
  def requests
124
224
  configuration[:requests] ||= {}
125
225
  end
126
226
 
227
+ # @private
127
228
  def add_request(identifier)
128
229
  return if request_config_exists?(identifier)
129
230
 
130
231
  requests[identifier] = default_request_config(identifier)
131
232
  end
132
233
 
234
+ # @private
133
235
  def default_request_config(identifier)
134
236
  { name: identifier }
135
237
  end
136
238
 
239
+ # @private
137
240
  def request_config_exists?(identifier)
138
241
  requests.key? identifier
139
242
  end
140
243
 
244
+ # @private
141
245
  def request_config(identifier)
142
246
  requests[identifier]
143
247
  end
144
248
 
249
+ # @private
145
250
  def request_name(identifier)
146
251
  requests.dig(identifier, :name) || identifier
147
252
  end
@@ -34,7 +34,7 @@ module Inferno
34
34
  # end
35
35
  # end
36
36
  # end
37
- # @see Inferno::FHIRClientBuilder Documentation for the client
37
+ # @see Inferno::DSL::FHIRClientBuilder Documentation for the client
38
38
  # configuration DSL
39
39
  module FHIRClient
40
40
  # @private
@@ -43,15 +43,13 @@ module Inferno
43
43
  klass.extend Forwardable
44
44
  klass.include RequestStorage
45
45
  klass.include TCPExceptionHandler
46
-
47
- klass.def_delegators 'self.class', :profile_url, :validator_url
48
46
  end
49
47
 
50
48
  # Return a previously defined FHIR client
51
49
  #
52
50
  # @param client [Symbol] the name of the client
53
51
  # @return [FHIR::Client]
54
- # @see Inferno::FHIRClientBuilder
52
+ # @see Inferno::DSL::FHIRClientBuilder
55
53
  def fhir_client(client = :default)
56
54
  fhir_clients[client] ||=
57
55
  FHIRClientBuilder.new.build(self, self.class.fhir_client_definitions[client])
@@ -101,6 +99,21 @@ module Inferno
101
99
  end
102
100
  end
103
101
 
102
+ # Perform a FHIR create interaction.
103
+ #
104
+ # @param resource [FHIR::Model]
105
+ # @param client [Symbol]
106
+ # @param name [Symbol] Name for this request to allow it to be used by
107
+ # other tests
108
+ # @return [Inferno::Entities::Request]
109
+ def fhir_create(resource, client: :default, name: nil)
110
+ store_request_and_refresh_token(fhir_client(client), name) do
111
+ tcp_exception_handler do
112
+ fhir_client(client).create(resource)
113
+ end
114
+ end
115
+ end
116
+
104
117
  # Perform a FHIR read interaction.
105
118
  #
106
119
  # @param resource_type [String, Symbol, Class]
@@ -160,7 +173,7 @@ module Inferno
160
173
 
161
174
  # @todo Make this a FHIR class method? Something like
162
175
  # FHIR.class_for(resource_type)
163
- # @api private
176
+ # @private
164
177
  def fhir_class_from_resource_type(resource_type)
165
178
  FHIR.const_get(resource_type.to_s.camelize)
166
179
  end
@@ -168,7 +181,7 @@ module Inferno
168
181
  # This method wraps a request to automatically refresh its access token if
169
182
  # expired. It's combined with `store_request` so that all of the fhir
170
183
  # request methods don't have to be wrapped twice.
171
- # @api private
184
+ # @private
172
185
  def store_request_and_refresh_token(client, name, &block)
173
186
  store_request('outgoing', name) do
174
187
  perform_refresh(client) if client.need_to_refresh? && client.able_to_refresh?
@@ -176,7 +189,7 @@ module Inferno
176
189
  end
177
190
  end
178
191
 
179
- # @api private
192
+ # @private
180
193
  def perform_refresh(client)
181
194
  credentials = client.oauth_credentials
182
195
 
@@ -203,7 +216,7 @@ module Inferno
203
216
  end
204
217
 
205
218
  module ClassMethods
206
- # @api private
219
+ # @private
207
220
  def fhir_client_definitions
208
221
  @fhir_client_definitions ||= {}
209
222
  end
@@ -4,6 +4,22 @@ require_relative '../ext/fhir_client'
4
4
  module Inferno
5
5
  module DSL
6
6
  # DSL for configuring FHIR clients
7
+ #
8
+ # @example
9
+ # input :url
10
+ # input :fhir_credentials, type: :oauth_credentials
11
+ # input :access_token
12
+ #
13
+ # fhir_client do
14
+ # url :url
15
+ # headers 'My-Custom_header' => 'CUSTOM_HEADER_VALUE'
16
+ # oauth_credentials :fhir_credentials
17
+ # end
18
+ #
19
+ # fhir_client :with_bearer_token do
20
+ # url :url
21
+ # bearer_token :access_token
22
+ # end
7
23
  class FHIRClientBuilder
8
24
  attr_accessor :runnable
9
25
 
@@ -28,7 +28,7 @@ module Inferno
28
28
  # end
29
29
  # end
30
30
  # end
31
- # @see Inferno::FHIRClientBuilder Documentation for the client
31
+ # @see Inferno::DSL::HTTPClientBuilder Documentation for the client
32
32
  # configuration DSL
33
33
  module HTTPClient
34
34
  # @private
@@ -2,6 +2,9 @@ require_relative '../entities/attributes'
2
2
 
3
3
  module Inferno
4
4
  module DSL
5
+ # OAuthCredentials provide a user with a single input which allows a fhir
6
+ # client to use a bearer token and automatically refresh the token when it
7
+ # expires.
5
8
  class OAuthCredentials
6
9
  ATTRIBUTES = [
7
10
  :access_token,
@@ -18,6 +21,16 @@ module Inferno
18
21
 
19
22
  attr_accessor :client
20
23
 
24
+ # @!attribute [rw] access_token
25
+ # @!attribute [rw] refresh_token
26
+ # @!attribute [rw] token_url
27
+ # @!attribute [rw] client_id
28
+ # @!attribute [rw] client_secret
29
+ # @!attribute [rw] token_retrieval_time
30
+ # @!attribute [rw] expires_in
31
+ # @!attribute [rw] name
32
+
33
+ # @private
21
34
  def initialize(raw_attributes_hash)
22
35
  attributes_hash = raw_attributes_hash.symbolize_keys
23
36
 
@@ -34,7 +47,7 @@ module Inferno
34
47
  self.token_retrieval_time = DateTime.now if access_token.present? && token_retrieval_time.blank?
35
48
  end
36
49
 
37
- # @api private
50
+ # @private
38
51
  def to_hash
39
52
  self.class::ATTRIBUTES.each_with_object({}) do |attribute, hash|
40
53
  value = send(attribute)
@@ -46,12 +59,12 @@ module Inferno
46
59
  end
47
60
  end
48
61
 
49
- # @api private
62
+ # @private
50
63
  def to_s
51
64
  JSON.generate(to_hash)
52
65
  end
53
66
 
54
- # @api private
67
+ # @private
55
68
  def add_to_client(client)
56
69
  client.oauth_credentials = self
57
70
  self.client = client
@@ -61,7 +74,7 @@ module Inferno
61
74
  client.set_bearer_token(access_token)
62
75
  end
63
76
 
64
- # @api private
77
+ # @private
65
78
  def need_to_refresh?
66
79
  return false if access_token.blank? || refresh_token.blank?
67
80
 
@@ -70,17 +83,17 @@ module Inferno
70
83
  token_retrieval_time.to_i + expires_in - DateTime.now.to_i < 60
71
84
  end
72
85
 
73
- # @api private
86
+ # @private
74
87
  def able_to_refresh?
75
88
  refresh_token.present? && token_url.present?
76
89
  end
77
90
 
78
- # @api private
91
+ # @private
79
92
  def confidential_client?
80
93
  client_id.present? && client_secret.present?
81
94
  end
82
95
 
83
- # @api private
96
+ # @private
84
97
  def oauth2_refresh_params
85
98
  {
86
99
  'grant_type' => 'refresh_token',
@@ -88,7 +101,7 @@ module Inferno
88
101
  }
89
102
  end
90
103
 
91
- # @api private
104
+ # @private
92
105
  def oauth2_refresh_headers
93
106
  base_headers = { 'Content-Type' => 'application/x-www-form-urlencoded' }
94
107
 
@@ -101,6 +114,7 @@ module Inferno
101
114
  )
102
115
  end
103
116
 
117
+ # @private
104
118
  def update_from_response_body(request)
105
119
  token_response_body = JSON.parse(request.response_body)
106
120
 
@@ -16,7 +16,7 @@ module Inferno
16
16
 
17
17
  # Returns the most recent FHIR/HTTP request
18
18
  #
19
- # @return [Inferno::Entities::Request]
19
+ # @return [Inferno::Entities::Request, nil]
20
20
  def request
21
21
  requests.last
22
22
  end
@@ -36,7 +36,7 @@ module Inferno
36
36
  request&.resource
37
37
  end
38
38
 
39
- # TODO: do a check in the test runner
39
+ # @private
40
40
  def named_request(name)
41
41
  requests.find { |request| request.name == self.class.config.request_name(name.to_sym) }
42
42
  end
@@ -82,10 +82,12 @@ module Inferno
82
82
  raise Exceptions::WaitException, message
83
83
  end
84
84
 
85
+ # @private
85
86
  def identifier(identifier = nil)
86
87
  @identifier ||= identifier
87
88
  end
88
89
 
90
+ # @private
89
91
  def wait_timeout(timeout = nil)
90
92
  @wait_timeout ||= timeout
91
93
  end
@@ -46,6 +46,7 @@ module Inferno
46
46
  # copied, and will need to be repopulated from scratch on the new class.
47
47
  # Any child Runnable classes will themselves need to be subclassed so that
48
48
  # their parent can be updated.
49
+ # @private
49
50
  VARIABLES_NOT_TO_COPY = [
50
51
  :@id, # New runnable will have a different id
51
52
  :@parent, # New runnable unlikely to have the same parent
@@ -307,12 +308,6 @@ module Inferno
307
308
  @all_children ||= []
308
309
  end
309
310
 
310
- def validator_url(url = nil)
311
- return @validator_url ||= parent&.validator_url if url.nil?
312
-
313
- @validator_url = url
314
- end
315
-
316
311
  # @private
317
312
  def suite
318
313
  return self if ancestors.include? Inferno::Entities::TestSuite
@@ -397,6 +392,28 @@ module Inferno
397
392
  (parent.user_runnable? && !parent.run_as_group?)
398
393
  end
399
394
 
395
+ # Set/get suite options required for this runnable to be executed.
396
+ #
397
+ # @param suite_option_requirements [Hash]
398
+ # @example
399
+ # suite_option :ig_version,
400
+ # list_options: [
401
+ # {
402
+ # label: 'IG v1',
403
+ # value: 'ig_v1'
404
+ # },
405
+ # {
406
+ # label: 'IG v2',
407
+ # value: 'ig_v2'
408
+ # }
409
+ # ]
410
+ #
411
+ # group from: :ig_v1_group,
412
+ # required_suite_options: { ig_version: 'ig_v1' }
413
+ #
414
+ # group from: :ig_v2_group do
415
+ # required_suite_options ig_version: 'ig_v2'
416
+ # end
400
417
  def required_suite_options(suite_option_requirements)
401
418
  @suite_option_requirements =
402
419
  suite_option_requirements.map do |key, value|
@@ -404,6 +421,7 @@ module Inferno
404
421
  end
405
422
  end
406
423
 
424
+ # @private
407
425
  def children(selected_suite_options = [])
408
426
  return all_children if selected_suite_options.blank?
409
427
 
@@ -417,6 +435,16 @@ module Inferno
417
435
  end
418
436
  end
419
437
  end
438
+
439
+ def inspect
440
+ non_dynamic_ancestor = ancestors.find { |ancestor| !ancestor.to_s.start_with? '#' }
441
+ "#<#{non_dynamic_ancestor}".tap do |inspect_string|
442
+ inspect_string.concat(" @id=#{id.inspect},")
443
+ inspect_string.concat(" @short_id=#{short_id.inspect},") if respond_to? :short_id
444
+ inspect_string.concat(" @title=#{title.inspect}")
445
+ inspect_string.concat('>')
446
+ end
447
+ end
420
448
  end
421
449
  end
422
450
  end
@@ -2,6 +2,11 @@ require_relative '../entities/attributes'
2
2
 
3
3
  module Inferno
4
4
  module DSL
5
+ # This class is used to represent TestSuite-level options which are selected
6
+ # by the user, and can affect which tests/groups are displayed and run as
7
+ # well as the behavior of those tests.
8
+ #
9
+ # @see Inferno::Entities::TestSuite.suite_option
5
10
  class SuiteOption
6
11
  ATTRIBUTES = [
7
12
  :id,
@@ -13,6 +18,13 @@ module Inferno
13
18
 
14
19
  include Entities::Attributes
15
20
 
21
+ # @!attribute [rw] id
22
+ # @!attribute [rw] title
23
+ # @!attribute [rw] description
24
+ # @!attribute [rw] list_options
25
+ # @!attribute [rw] value
26
+
27
+ # @private
16
28
  def initialize(raw_params)
17
29
  params = raw_params.deep_symbolize_keys
18
30
  bad_params = params.keys - ATTRIBUTES
@@ -26,10 +38,12 @@ module Inferno
26
38
  self.id = id.to_sym if id.is_a? String
27
39
  end
28
40
 
41
+ # @private
29
42
  def ==(other)
30
43
  id == other.id && value == other.value
31
44
  end
32
45
 
46
+ # @private
33
47
  def to_hash
34
48
  self.class::ATTRIBUTES.each_with_object({}) do |attribute, hash|
35
49
  hash[attribute] = send(attribute)
@@ -1,5 +1,6 @@
1
1
  module Inferno
2
2
  module DSL
3
+ # @private
3
4
  module TCPExceptionHandler
4
5
  def tcp_exception_handler(&block)
5
6
  block.call
@@ -1,5 +1,6 @@
1
1
  module Inferno
2
2
  module Entities
3
+ # @private
3
4
  module Attributes
4
5
  def self.included(klass)
5
6
  klass.attr_accessor(*klass::ATTRIBUTES)
@@ -42,6 +42,7 @@ module Inferno
42
42
 
43
43
  include Attributes
44
44
 
45
+ # @private
45
46
  def initialize(params)
46
47
  super(params, ATTRIBUTES - [:headers, :name])
47
48
 
@@ -86,7 +87,8 @@ module Inferno
86
87
 
87
88
  # Return a hash of the request parameters
88
89
  #
89
- # @return [Hash]
90
+ # @return [Hash] A Hash with `:verb`, `:url`, `:headers`, and `:body`
91
+ # fields
90
92
  def request
91
93
  {
92
94
  verb:,
@@ -98,7 +100,7 @@ module Inferno
98
100
 
99
101
  # Return a hash of the response parameters
100
102
  #
101
- # @return [Hash]
103
+ # @return [Hash] A Hash with `:status`, `:headers`, and `:body` fields
102
104
  def response
103
105
  {
104
106
  status:,
@@ -47,7 +47,7 @@ module Inferno
47
47
  end
48
48
  end
49
49
 
50
- # @api private
50
+ # @private
51
51
  # A hash containing outputs that have been set during execution and need
52
52
  # to be persisted. A test may not always update all outputs, so this is
53
53
  # used to prevent overwriting an output with nil when it wasn't updated.