inferno_core 0.4.35 → 0.4.37.dev

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: 30182f3e68640375fa7109325e45456b1a51017df274fa367e209d4fdef9a7de
4
- data.tar.gz: 5c2a06c4171424120d8b0d760c8b295c8410ec2ba0b47473a5a14ae8c894faf6
3
+ metadata.gz: ccd64a44d96c18439d7d844bd6ec467d9eb0c23d5964b0928539fd70a0e806f0
4
+ data.tar.gz: b72e0ac46e7aca9d496fe23f6b89537c4a5c2054acc20690e089dc17d3d2528d
5
5
  SHA512:
6
- metadata.gz: 62d86aba85c02d2a40bf46e01dc6b31d6b2d6bf91af05bf75f0c9d183dcdffbe854b196a5c21fd7512b59db19e06429dcd42a173e51c84ac8b3a537940a2e930
7
- data.tar.gz: e13f07fa322182878878fabe04b293f33596733dc926381fcbfbf589fda5cef517aaabe2ab4107876f67188d554aec241de5354ce45f3d738f5106329f73721e
6
+ metadata.gz: 33d89c3291db6f4bfaf92b704eb13177b961e49b2b5c9c358661e062bde12f0eb00f5ab55e91945e089a7d2d83a13002926153c53b75b30b1a18926b69791496
7
+ data.tar.gz: b6878fd8a73f017dccf8f027ac1ddfb7811a122f287a55ea6fe393b7e4d5dd0f2885d4e55d1a540f6bd5ec723619af8c00bdb8d633fd3ce187f9dd0d63790a97
@@ -1,3 +1,5 @@
1
+ require 'hanami/action/mime/request_mime_weight'
2
+
1
3
  module Inferno
2
4
  module Web
3
5
  module Controllers
@@ -11,12 +13,14 @@ module Inferno
11
13
 
12
14
  subclass.include Import[repo: "inferno.repositories.#{subclass.resource_name}"]
13
15
 
14
- subclass.config.default_request_format = :json
15
- subclass.config.default_response_format = :json
16
-
17
16
  subclass.define_method(:serialize) do |*args|
18
17
  Inferno::Web::Serializers.const_get(self.class.resource_class).render(*args)
19
18
  end
19
+
20
+ # Hanami Controller 2.0.0 removes the ability to set a default
21
+ # Content-Type response header, so set it manually if it hasn't been
22
+ # set.
23
+ subclass.after { |_req, res| res.format = :json if res.format == :all && res.body&.first&.first == '{' }
20
24
  end
21
25
 
22
26
  def self.resource_name
@@ -3,8 +3,6 @@ module Inferno
3
3
  module Controllers
4
4
  module TestSessions
5
5
  class ClientShow < Controller
6
- config.default_response_format = :html
7
-
8
6
  CLIENT_PAGE =
9
7
  ERB.new(
10
8
  File.read(
@@ -31,6 +29,7 @@ module Inferno
31
29
 
32
30
  halt 404 if test_suite.nil?
33
31
 
32
+ res.format = :html
34
33
  res.body = CLIENT_PAGE
35
34
  end
36
35
  end
@@ -1,6 +1,6 @@
1
1
  require 'active_support/all'
2
2
  require 'dotenv'
3
- require 'dry/system/container'
3
+ require 'dry/system'
4
4
  require 'sequel'
5
5
  require_relative 'boot'
6
6
 
@@ -25,7 +25,7 @@ module Inferno
25
25
 
26
26
  configure do |config|
27
27
  config.root = File.expand_path('../../..', __dir__)
28
- config.bootable_dirs = [File.join('lib', 'inferno', 'config', 'boot')]
28
+ config.provider_dirs = [File.join('lib', 'inferno', 'config', 'boot')]
29
29
  config.component_dirs.add 'lib'
30
30
  end
31
31
  end
@@ -1,9 +1,9 @@
1
1
  require 'sequel'
2
2
  require 'erb'
3
3
 
4
- Inferno::Application.boot(:db) do
5
- init do
6
- use :logging
4
+ Inferno::Application.register_provider(:db) do
5
+ prepare do
6
+ target_container.start :logging
7
7
 
8
8
  require 'yaml'
9
9
 
@@ -1,5 +1,5 @@
1
- Inferno::Application.boot(:logging) do
2
- init do
1
+ Inferno::Application.register_provider(:logging) do
2
+ prepare do
3
3
  logger =
4
4
  if Inferno::Application.env == :test
5
5
  log_file_directory = File.join(Dir.pwd, 'tmp')
@@ -1,8 +1,8 @@
1
1
  require_relative '../../repositories/presets'
2
2
 
3
- Inferno::Application.boot(:presets) do
4
- init do
5
- use :suites
3
+ Inferno::Application.register_provider(:presets) do
4
+ prepare do
5
+ target_container.start :suites
6
6
 
7
7
  files_to_load = Dir.glob(['config/presets/*.json', 'config/presets/*.json.erb'])
8
8
  files_to_load.map! { |path| File.realpath(path) }
@@ -1,7 +1,7 @@
1
1
  require 'sidekiq'
2
2
 
3
- Inferno::Application.boot(:sidekiq) do
4
- init do
3
+ Inferno::Application.register_provider(:sidekiq) do
4
+ prepare do
5
5
  if Inferno::Application['async_jobs']
6
6
  Sidekiq.configure_server do |config|
7
7
  config.redis = { url: ENV.fetch('REDIS_URL', 'redis://127.0.0.1:6379/0') }
@@ -1,10 +1,11 @@
1
- Inferno::Application.boot(:suites) do
2
- init do
3
- use :logging
1
+ Inferno::Application.register_provider(:suites) do
2
+ prepare do
3
+ target_container.start :logging
4
4
 
5
5
  require 'inferno/entities/test'
6
6
  require 'inferno/entities/test_group'
7
7
  require 'inferno/entities/test_suite'
8
+ require 'inferno/entities/test_kit'
8
9
 
9
10
  files_to_load = Dir.glob(File.join(Dir.pwd, 'lib', '*.rb'))
10
11
 
@@ -18,13 +19,24 @@ Inferno::Application.boot(:suites) do
18
19
  files_to_load.concat Dir.glob(File.join(Inferno::Application.root, 'spec', 'fixtures', '**', '*.rb'))
19
20
  end
20
21
 
22
+ # Whenever the definition of a Runnable class ends, add it to the
23
+ # appropriate repository.
24
+ in_memory_entities_trace = TracePoint.trace(:end) do |trace|
25
+ if trace.self < Inferno::Entities::Test ||
26
+ trace.self < Inferno::Entities::TestGroup ||
27
+ trace.self < Inferno::Entities::TestSuite ||
28
+ trace.self < Inferno::Entities::TestKit
29
+ trace.self.add_self_to_repository
30
+ end
31
+ end
32
+
21
33
  files_to_load.map! { |path| File.realpath(path) }
22
34
 
23
35
  files_to_load.each do |path|
24
36
  require_relative path
25
37
  end
26
38
 
27
- ObjectSpace.each_object(TracePoint, &:disable)
39
+ in_memory_entities_trace.disable
28
40
 
29
41
  Inferno::Entities::TestSuite.descendants.each do |descendant|
30
42
  # When ID not assigned in custom test suites, Runnable.id will return default ID
@@ -1,6 +1,6 @@
1
- Inferno::Application.boot(:validator) do
2
- init do
3
- use :suites
1
+ Inferno::Application.register_provider(:validator) do
2
+ prepare do
3
+ target_container.start :suites
4
4
 
5
5
  # This process should only run once, to start one job per validator,
6
6
  # so skipping it on workers will start it only once from the "web" process
@@ -1,5 +1,5 @@
1
- Inferno::Application.boot(:web) do |_app|
2
- init do
1
+ Inferno::Application.register_provider(:web) do |_app|
2
+ prepare do
3
3
  require 'blueprinter'
4
4
  require 'hanami/router'
5
5
  require 'hanami/controller'
@@ -26,17 +26,6 @@ module Inferno
26
26
 
27
27
  extending_class.define_singleton_method(:inherited) do |subclass|
28
28
  copy_instance_variables(subclass)
29
-
30
- # Whenever the definition of a Runnable class ends, keep track of the
31
- # file it came from. Once the Suite loader successfully loads a file,
32
- # it will add all of the Runnable classes from that file to the
33
- # appropriate repositories.
34
- TracePoint.trace(:end) do |trace|
35
- if trace.self == subclass
36
- subclass.add_self_to_repository
37
- trace.disable
38
- end
39
- end
40
29
  end
41
30
  end
42
31
 
@@ -360,6 +349,24 @@ module Inferno
360
349
  route(method, path, route_class)
361
350
  end
362
351
 
352
+ # Create an endpoint to receive incoming requests during a Test Run.
353
+ #
354
+ # @see Inferno::DSL::SuiteEndpoint
355
+ # @example
356
+ # suite_endpoint :post, '/my_suite_endpoint', MySuiteEndpoint
357
+ # @param method [Symbol] the HTTP request type (:get, :post, etc.) for the
358
+ # incoming request
359
+ # @param path [String] the path for this request. The route will be served
360
+ # with a prefix of `/custom/TEST_SUITE_ID` to prevent path conflicts.
361
+ # [Any of the path options available in Hanami
362
+ # Router](https://github.com/hanami/router/tree/f41001d4c3ee9e2d2c7bb142f74b43f8e1d3a265#a-beautiful-dsl)
363
+ # can be used here.
364
+ # @param [Class] a subclass of Inferno::DSL::SuiteEndpoint
365
+ # @return [void]
366
+ def suite_endpoint(method, path, endpoint_class)
367
+ route(method, path, endpoint_class)
368
+ end
369
+
363
370
  # Create a route to handle a request
364
371
  #
365
372
  # @param method [Symbol] the HTTP request type (:get, :post, etc.) for the
@@ -0,0 +1,330 @@
1
+ require 'hanami/controller'
2
+ require 'rack/request'
3
+ require_relative '../ext/rack'
4
+
5
+ module Inferno
6
+ module DSL
7
+ # A base class for creating endpoints to test client requests. This class is
8
+ # based on Hanami::Action, and may be used similarly to [a normal Hanami
9
+ # endpoint](https://github.com/hanami/controller/tree/v2.0.0).
10
+ #
11
+ # @example
12
+ # class AuthorizedEndpoint < Inferno::DSL::SuiteEndpoint
13
+ # # Identify the incoming request based on a bearer token
14
+ # def test_run_identifier
15
+ # request.header['authorization']&.delete_prefix('Bearer ')
16
+ # end
17
+ #
18
+ # # Return a json FHIR Patient resource
19
+ # def make_response
20
+ # response.status = 200
21
+ # response.body = FHIR::Patient.new(id: 'abcdef').to_json
22
+ # response.format = :json
23
+ # end
24
+ #
25
+ # # Update the waiting test to pass when the incoming request is received.
26
+ # # This will resume the test run.
27
+ # def update_result
28
+ # results_repo.update(result.id, result: 'pass')
29
+ # end
30
+ #
31
+ # # Apply the 'authorized' tag to the incoming request so that it may be
32
+ # # used by later tests.
33
+ # def tags
34
+ # ['authorized']
35
+ # end
36
+ # end
37
+ #
38
+ # class AuthorizedRequestSuite < Inferno::TestSuite
39
+ # id :authorized_suite
40
+ # suite_endpoint :get, '/authorized_endpoint', AuthorizedEndpoint
41
+ #
42
+ # group do
43
+ # title 'Authorized Request Group'
44
+ #
45
+ # test do
46
+ # title 'Wait for authorized request'
47
+ #
48
+ # input :bearer_token
49
+ #
50
+ # run do
51
+ # wait(
52
+ # identifier: bearer_token,
53
+ # message: "Waiting to receive a request with bearer_token: #{bearer_token}" \
54
+ # "at `#{Inferno::Application['base_url']}/custom/authorized_suite/authorized_endpoint`"
55
+ # )
56
+ # end
57
+ # end
58
+ # end
59
+ # end
60
+ class SuiteEndpoint < Hanami::Action
61
+ attr_reader :req, :res
62
+
63
+ # @!group Overrides These methods should be overridden by subclasses to
64
+ # define the behavior of the endpoint
65
+
66
+ # Override this method to determine a test run's identifier based on an
67
+ # incoming request.
68
+ #
69
+ # @return [String]
70
+ #
71
+ # @example
72
+ # def test_run_identifier
73
+ # # Identify the test session of an incoming request based on the bearer
74
+ # # token
75
+ # request.headers['authorization']&.delete_prefix('Bearer ')
76
+ # end
77
+ def test_run_identifier
78
+ nil
79
+ end
80
+
81
+ # Override this method to build the response.
82
+ #
83
+ # @return [Void]
84
+ #
85
+ # @example
86
+ # def make_response
87
+ # response.status = 200
88
+ # response.body = { abc: 123 }.to_json
89
+ # response.format = :json
90
+ # end
91
+ def make_response
92
+ nil
93
+ end
94
+
95
+ # Override this method to define the tags which will be applied to the
96
+ # request.
97
+ #
98
+ # @return [Array<String>]
99
+ def tags
100
+ @tags ||= []
101
+ end
102
+
103
+ # Override this method to assign a name to the request
104
+ #
105
+ # @return [String]
106
+ def name
107
+ result&.runnable&.incoming_request_name
108
+ end
109
+
110
+ # Override this method to update the current waiting result. To resume the
111
+ # test run, set the result to something other than 'waiting'.
112
+ #
113
+ # @return [Void]
114
+ #
115
+ # @example
116
+ # def update_result
117
+ # results_repo.update(result.id, result: 'pass')
118
+ # end
119
+ def update_result
120
+ nil
121
+ end
122
+
123
+ # Override this method to specify whether this request should be
124
+ # persisted. Defaults to true.
125
+ #
126
+ # @return [Boolean]
127
+ def persist_request?
128
+ true
129
+ end
130
+
131
+ # @!endgroup
132
+
133
+ # @private
134
+ def self.call(...)
135
+ new.call(...)
136
+ end
137
+
138
+ # @return [Inferno::Repositories::Requests]
139
+ def requests_repo
140
+ @requests_repo ||= Inferno::Repositories::Requests.new
141
+ end
142
+
143
+ # @return [Inferno::Repositories::Results]
144
+ def results_repo
145
+ @results_repo ||= Inferno::Repositories::Results.new
146
+ end
147
+
148
+ # @return [Inferno::Repositories::TestRuns]
149
+ def test_runs_repo
150
+ @test_runs_repo ||= Inferno::Repositories::TestRuns.new
151
+ end
152
+
153
+ # @return [Inferno::Repositories::Tests]
154
+ def tests_repo
155
+ @tests_repo ||= Inferno::Repositories::Tests.new
156
+ end
157
+
158
+ # @private
159
+ def initialize(config: self.class.config) # rubocop:disable Lint/MissingSuper
160
+ @config = config
161
+ end
162
+
163
+ # The incoming request as a `Hanami::Action::Request`
164
+ #
165
+ # @return [Hanami::Action::Request]
166
+ #
167
+ # @example
168
+ # request.params # Get url/query params
169
+ # request.body.read # Get body
170
+ # request.headers['accept'] # Get Accept header
171
+ def request
172
+ req
173
+ end
174
+
175
+ # The response as a `Hanami::Action::Response`. Modify this to build the
176
+ # response to the incoming request.
177
+ #
178
+ # @return [Hanami::Action::Response]
179
+ #
180
+ # @example
181
+ # response.status = 200 # Set the status
182
+ # response.body = 'Ok' # Set the body
183
+ # # Set headers
184
+ # response.headers.merge!('X-Custom-Header' => 'CUSTOM_HEADER_VALUE')
185
+ def response
186
+ res
187
+ end
188
+
189
+ # The test run which is waiting for incoming requests
190
+ #
191
+ # @return [Inferno::Entities::TestRun]
192
+ def test_run
193
+ @test_run ||=
194
+ test_runs_repo.find_latest_waiting_by_identifier(find_test_run_identifier).tap do |test_run|
195
+ halt 500, "Unable to find test run with identifier '#{test_run_identifier}'." if test_run.nil?
196
+ end
197
+ end
198
+
199
+ # The result which is waiting for incoming requests for the current test
200
+ # run
201
+ #
202
+ # @return [Inferno::Entities::Result]
203
+ def result
204
+ @result ||= find_result
205
+ end
206
+
207
+ # The test which is currently waiting for incoming requests
208
+ #
209
+ # @return [Inferno::Entities::Test]
210
+ def test
211
+ @test ||= tests_repo.find(result.test_id)
212
+ end
213
+
214
+ # @return [Logger] Inferno's logger
215
+ def logger
216
+ @logger ||= Application['logger']
217
+ end
218
+
219
+ # @private
220
+ def find_test_run_identifier
221
+ @test_run_identifier ||= test_run_identifier
222
+ rescue StandardError => e
223
+ halt 500, "Unable to determine test run identifier:\n#{e.full_message}"
224
+ end
225
+
226
+ # @private
227
+ def find_result
228
+ results_repo.find_waiting_result(test_run_id: test_run.id)
229
+ end
230
+
231
+ # @private
232
+ # The actual persisting happens in
233
+ # Inferno::Utils::Middleware::RequestRecorder, which allows the response
234
+ # to include response headers added by other parts of the rack stack
235
+ # rather than only the response headers explicitly added in the endpoint.
236
+ def persist_request
237
+ req.env['inferno.test_session_id'] = test_run.test_session_id
238
+ req.env['inferno.result_id'] = result.id
239
+ req.env['inferno.tags'] = tags
240
+ req.env['inferno.name'] = name if name.present?
241
+
242
+ add_persistence_callback
243
+ end
244
+
245
+ # @private
246
+ def resume_test_run?
247
+ find_result&.result != 'waiting'
248
+ end
249
+
250
+ # @private
251
+ # Inferno::Utils::Middleware::RequestRecorder actually resumes the
252
+ # TestRun. If it were resumed here, it would be resuming prior to the
253
+ # Request being persisted.
254
+ def resume
255
+ req.env['inferno.resume_test_run'] = true
256
+ req.env['inferno.test_run_id'] = test_run.id
257
+ end
258
+
259
+ # @private
260
+ def handle(req, res)
261
+ @req = req
262
+ @res = res
263
+ test_run
264
+
265
+ persist_request if persist_request?
266
+
267
+ update_result
268
+
269
+ resume if resume_test_run?
270
+
271
+ make_response
272
+ rescue StandardError => e
273
+ halt 500, e.full_message
274
+ end
275
+
276
+ # @private
277
+ def add_persistence_callback # rubocop:disable Metrics/CyclomaticComplexity
278
+ logger = Application['logger']
279
+ env = req.env
280
+ env['rack.after_reply'] ||= []
281
+ env['rack.after_reply'] << proc do
282
+ repo = Inferno::Repositories::Requests.new
283
+
284
+ uri = URI('http://example.com')
285
+ uri.scheme = env['rack.url_scheme']
286
+ uri.host = env['SERVER_NAME']
287
+ uri.port = env['SERVER_PORT']
288
+ uri.path = env['REQUEST_PATH'] || ''
289
+ uri.query = env['rack.request.query_string'] if env['rack.request.query_string'].present?
290
+ url = uri&.to_s
291
+ verb = env['REQUEST_METHOD']
292
+ logger.info('get body')
293
+ request_body = env['rack.input']
294
+ request_body.rewind if env['rack.input'].respond_to? :rewind
295
+ request_body = request_body.instance_of?(Puma::NullIO) ? nil : request_body.string
296
+
297
+ request_headers = ::Rack::Request.new(env).headers.to_h.map { |name, value| { name:, value: } }
298
+
299
+ status, response_headers, response_body = env['inferno.response']
300
+
301
+ response_headers = response_headers.map { |name, value| { name:, value: } }
302
+
303
+ repo.create(
304
+ verb:,
305
+ url:,
306
+ direction: 'incoming',
307
+ name: env['inferno.name'],
308
+ status:,
309
+ request_body:,
310
+ response_body: response_body.join,
311
+ result_id: env['inferno.result_id'],
312
+ test_session_id: env['inferno.test_session_id'],
313
+ request_headers:,
314
+ response_headers:,
315
+ tags: env['inferno.tags']
316
+ )
317
+
318
+ if env['inferno.resume_test_run']
319
+ test_run_id = env['inferno.test_run_id']
320
+ Inferno::Repositories::TestRuns.new.mark_as_no_longer_waiting(test_run_id)
321
+
322
+ Inferno::Jobs.perform(Jobs::ResumeTestRun, test_run_id)
323
+ end
324
+ rescue StandardError => e
325
+ logger.error(e.full_message)
326
+ end
327
+ end
328
+ end
329
+ end
330
+ end
data/lib/inferno/dsl.rb CHANGED
@@ -5,6 +5,7 @@ require_relative 'dsl/fhir_resource_validation'
5
5
  require_relative 'dsl/http_client'
6
6
  require_relative 'dsl/results'
7
7
  require_relative 'dsl/runnable'
8
+ require_relative 'dsl/suite_endpoint'
8
9
 
9
10
  module Inferno
10
11
  # The DSL for writing tests.