inferno_core 0.4.36 → 0.4.37.dev

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c25196d5fe3eb33c100d2556c8376c9f4b2a321b9b2c1e301e4463aa07dcc406
4
- data.tar.gz: 160b379221c16275d9699d062d1cb8a76daeaf14ba1cd0bb403196071668d0a1
3
+ metadata.gz: ccd64a44d96c18439d7d844bd6ec467d9eb0c23d5964b0928539fd70a0e806f0
4
+ data.tar.gz: b72e0ac46e7aca9d496fe23f6b89537c4a5c2054acc20690e089dc17d3d2528d
5
5
  SHA512:
6
- metadata.gz: 83188900a5e5fda1d7d2652b92786c441296fdb6896ba3ea3964bc0a7beee277058692e5965bfab695f6a22706765ed599fd0c6be35ce96cb92251748241a729
7
- data.tar.gz: 31361076ae6839d4dd7f7d3ec9cb8332e1f55cc03db2c79efa7d09139b27258caf93cd37a03a1583b99b6ee092d9cec0b493aafe1dd7c291e8e875d775bb3d2a
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.