inferno_core 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/lib/inferno.rb +4 -0
  3. data/lib/inferno/apps/web/controllers/test_runs/create.rb +17 -10
  4. data/lib/inferno/apps/web/controllers/test_runs/show.rb +10 -0
  5. data/lib/inferno/apps/web/controllers/test_sessions/create.rb +1 -0
  6. data/lib/inferno/apps/web/controllers/test_sessions/last_test_run.rb +22 -0
  7. data/lib/inferno/apps/web/controllers/test_sessions/results/index.rb +6 -1
  8. data/lib/inferno/apps/web/controllers/test_sessions/session_data/index.rb +21 -0
  9. data/lib/inferno/apps/web/router.rb +15 -0
  10. data/lib/inferno/apps/web/serializers/request.rb +1 -0
  11. data/lib/inferno/apps/web/serializers/result.rb +8 -0
  12. data/lib/inferno/apps/web/serializers/session_data.rb +10 -0
  13. data/lib/inferno/apps/web/serializers/test.rb +1 -3
  14. data/lib/inferno/apps/web/serializers/test_group.rb +2 -3
  15. data/lib/inferno/apps/web/serializers/test_run.rb +1 -0
  16. data/lib/inferno/apps/web/serializers/test_session.rb +1 -1
  17. data/lib/inferno/apps/web/serializers/test_suite.rb +1 -0
  18. data/lib/inferno/config/application.rb +4 -2
  19. data/lib/inferno/config/boot.rb +2 -0
  20. data/lib/inferno/config/boot/db.rb +10 -1
  21. data/lib/inferno/config/boot/sidekiq.rb +11 -0
  22. data/lib/inferno/db/migrations/001_create_initial_structure.rb +0 -21
  23. data/lib/inferno/db/migrations/002_add_wait_support.rb +7 -0
  24. data/lib/inferno/db/migrations/003_update_session_data.rb +18 -0
  25. data/lib/inferno/db/migrations/004_add_request_results_table.rb +9 -0
  26. data/lib/inferno/db/migrations/005_add_updated_at_index_to_results.rb +5 -0
  27. data/lib/inferno/db/schema.rb +154 -0
  28. data/lib/inferno/dsl.rb +1 -3
  29. data/lib/inferno/dsl/fhir_client_builder.rb +16 -0
  30. data/lib/inferno/dsl/request_storage.rb +12 -0
  31. data/lib/inferno/dsl/results.rb +49 -0
  32. data/lib/inferno/dsl/resume_test_route.rb +89 -0
  33. data/lib/inferno/dsl/runnable.rb +96 -7
  34. data/lib/inferno/entities.rb +1 -1
  35. data/lib/inferno/entities/header.rb +7 -7
  36. data/lib/inferno/entities/message.rb +8 -6
  37. data/lib/inferno/entities/request.rb +40 -14
  38. data/lib/inferno/entities/result.rb +34 -18
  39. data/lib/inferno/entities/session_data.rb +33 -0
  40. data/lib/inferno/entities/test.rb +20 -7
  41. data/lib/inferno/entities/test_run.rb +13 -6
  42. data/lib/inferno/entities/test_session.rb +8 -8
  43. data/lib/inferno/exceptions.rb +12 -0
  44. data/lib/inferno/jobs.rb +16 -0
  45. data/lib/inferno/jobs/execute_test_run.rb +14 -0
  46. data/lib/inferno/jobs/resume_test_run.rb +14 -0
  47. data/lib/inferno/public/bundle.js +1 -1
  48. data/lib/inferno/repositories/repository.rb +13 -0
  49. data/lib/inferno/repositories/requests.rb +5 -4
  50. data/lib/inferno/repositories/results.rb +151 -3
  51. data/lib/inferno/repositories/session_data.rb +47 -0
  52. data/lib/inferno/repositories/test_runs.rb +66 -0
  53. data/lib/inferno/test_runner.rb +121 -29
  54. data/lib/inferno/utils/middleware/request_logger.rb +16 -3
  55. data/lib/inferno/version.rb +1 -1
  56. data/spec/factories/result.rb +8 -0
  57. data/spec/factories/test_run.rb +2 -0
  58. metadata +32 -5
  59. data/lib/inferno/dsl/fhir_manipulation.rb +0 -25
  60. data/lib/inferno/entities/test_input.rb +0 -20
data/lib/inferno/dsl.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  require_relative 'dsl/assertions'
2
2
  require_relative 'dsl/fhir_client'
3
- require_relative 'dsl/fhir_manipulation'
4
3
  require_relative 'dsl/fhir_validation'
5
4
  require_relative 'dsl/http_client'
6
5
  require_relative 'dsl/results'
@@ -14,8 +13,7 @@ module Inferno
14
13
  FHIRClient,
15
14
  HTTPClient,
16
15
  Results,
17
- FHIRValidation,
18
- FHIRManipulation
16
+ FHIRValidation
19
17
  ].freeze
20
18
 
21
19
  EXTENDABLE_DSL_MODULES = [
@@ -14,6 +14,7 @@ module Inferno
14
14
  FHIR::Client.new(url).tap do |client|
15
15
  client.additional_headers = headers if headers
16
16
  client.default_json
17
+ client.set_bearer_token bearer_token if bearer_token
17
18
  end
18
19
  end
19
20
 
@@ -32,6 +33,21 @@ module Inferno
32
33
  end
33
34
  end
34
35
 
36
+ # Define the bearer token for a client. A string or symbol can be provided.
37
+ # A string is interpreted as a token. A symbol is interpreted as the name of
38
+ # an input to the Runnable.
39
+ #
40
+ # @param bearer_token [String, Symbol]
41
+ # @return [void]
42
+ def bearer_token(bearer_token = nil)
43
+ @bearer_token ||=
44
+ if bearer_token.is_a? Symbol
45
+ runnable.send(bearer_token)
46
+ else
47
+ bearer_token
48
+ end
49
+ end
50
+
35
51
  # Define custom headers for a client
36
52
  #
37
53
  # @param headers [Hash]
@@ -89,6 +89,18 @@ module Inferno
89
89
  named_requests_made.concat(names)
90
90
  end
91
91
 
92
+ # Specify the name for a request received by a test
93
+ #
94
+ # @param *names [Symbol] one or more Symbols
95
+ def receives_request(name)
96
+ @incoming_request_name = name
97
+ end
98
+
99
+ # @api private
100
+ def incoming_request_name
101
+ @incoming_request_name
102
+ end
103
+
92
104
  # Specify the named requests used by a test
93
105
  #
94
106
  # @param *names [Symbol] one or more Symbols
@@ -49,6 +49,55 @@ module Inferno
49
49
  def omit_if(test, message = '')
50
50
  raise Exceptions::OmitException, message if test
51
51
  end
52
+
53
+ # Halt execution of the current test and wait for execution to resume.
54
+ #
55
+ # @see Inferno::DSL::Runnable#resume_test_route
56
+ # @example
57
+ # resume_test_route :get, '/launch' do
58
+ # request.query_parameters['iss']
59
+ # end
60
+ #
61
+ # test do
62
+ # input :issuer
63
+ # receives_request :launch
64
+ #
65
+ # run do
66
+ # wait(
67
+ # identifier: issuer,
68
+ # message: "Wating to receive a request with an issuer of #{issuer}"
69
+ # )
70
+ # end
71
+ # end
72
+ # @param identifier [String] An identifier which can uniquely identify
73
+ # this test run based on an incoming request. This is necessary so that
74
+ # the correct test run can be resumed.
75
+ # @param message [String]
76
+ # @param timeout [Integer] Number of seconds to wait for an incoming
77
+ # request
78
+ def wait(identifier:, message: '', timeout: 300)
79
+ identifier(identifier)
80
+ wait_timeout(timeout)
81
+
82
+ raise Exceptions::WaitException, message
83
+ end
84
+
85
+ def identifier(identifier = nil)
86
+ @identifier ||= identifier
87
+ end
88
+
89
+ def wait_timeout(timeout = nil)
90
+ @wait_timeout ||= timeout
91
+ end
92
+
93
+ # Halt execution of the current test. This provided for testing purposes
94
+ # and should not be used in real tests.
95
+ #
96
+ # @param message [String]
97
+ # @api private
98
+ def cancel(message = '')
99
+ raise Exceptions::CancelException, message
100
+ end
52
101
  end
53
102
  end
54
103
  end
@@ -0,0 +1,89 @@
1
+ require 'hanami-controller'
2
+
3
+ module Inferno
4
+ module DSL
5
+ # A base class for creating routes to resume test execution upon receiving
6
+ # an incoming request.
7
+ # @api private
8
+ # @see Inferno::DSL::Runnable#resume_test_route
9
+ class ResumeTestRoute
10
+ include Hanami::Action
11
+ include Import[
12
+ requests_repo: 'repositories.requests',
13
+ results_repo: 'repositories.results',
14
+ test_runs_repo: 'repositories.test_runs',
15
+ tests_repo: 'repositories.tests'
16
+ ]
17
+
18
+ def self.call(params)
19
+ new.call(params)
20
+ end
21
+
22
+ # The incoming request
23
+ #
24
+ # @return [Inferno::Entities::Request]
25
+ def request
26
+ @request ||= Inferno::Entities::Request.from_rack_env(@params.env)
27
+ end
28
+
29
+ # @api private
30
+ def test_run
31
+ @test_run ||=
32
+ test_runs_repo.find_latest_waiting_by_identifier(test_run_identifier)
33
+ end
34
+
35
+ # @api private
36
+ def waiting_result
37
+ @waiting_result ||= results_repo.find_waiting_result(test_run_id: test_run.id)
38
+ end
39
+
40
+ # @api private
41
+ def update_result
42
+ results_repo.pass_waiting_result(waiting_result.id)
43
+ end
44
+
45
+ # @api private
46
+ def persist_request
47
+ requests_repo.create(
48
+ request.to_hash.merge(
49
+ test_session_id: test_run.test_session_id,
50
+ result_id: waiting_result.id,
51
+ name: test.incoming_request_name
52
+ )
53
+ )
54
+ end
55
+
56
+ # @api private
57
+ def redirect_route
58
+ "/test_sessions/#{test_run.test_session_id}##{waiting_group_id}"
59
+ end
60
+
61
+ # @api private
62
+ def test
63
+ @test ||= tests_repo.find(waiting_result.test_id)
64
+ end
65
+
66
+ # @api private
67
+ def waiting_group_id
68
+ test.parent.id
69
+ end
70
+
71
+ # @api private
72
+ def call(_params)
73
+ if test_run.nil?
74
+ status(500, "Unable to find test run with identifier '#{test_run_identifier}'.")
75
+ return
76
+ end
77
+
78
+ test_runs_repo.mark_as_no_longer_waiting(test_run.id)
79
+
80
+ update_result
81
+ persist_request
82
+
83
+ Jobs.perform(Jobs::ResumeTestRun, test_run.id)
84
+
85
+ redirect_to redirect_route
86
+ end
87
+ end
88
+ end
89
+ end
@@ -1,3 +1,5 @@
1
+ require_relative 'resume_test_route'
2
+
1
3
  module Inferno
2
4
  module DSL
3
5
  # This module contains the DSL for defining child entities in the test
@@ -37,7 +39,7 @@ module Inferno
37
39
  # @api private
38
40
  def copy_instance_variables(subclass)
39
41
  instance_variables.each do |variable|
40
- next if [:@id, :@groups, :@tests, :@parent, :@children].include?(variable)
42
+ next if [:@id, :@groups, :@tests, :@parent, :@children, :@test_count].include?(variable)
41
43
 
42
44
  subclass.instance_variable_set(variable, instance_variable_get(variable).dup)
43
45
  end
@@ -125,9 +127,9 @@ module Inferno
125
127
  # @api private
126
128
  def configure_child_class(klass, hash_args) # rubocop:disable Metrics/CyclomaticComplexity
127
129
  inputs.each do |input_definition|
128
- next if klass.inputs.include? input_definition
130
+ next if klass.inputs.any? { |input| input[:name] == input_definition[:name] }
129
131
 
130
- klass.input input_definition
132
+ klass.input input_definition[:name], input_definition
131
133
  end
132
134
 
133
135
  outputs.each do |output_definition|
@@ -196,12 +198,28 @@ module Inferno
196
198
 
197
199
  # Define inputs
198
200
  #
199
- # @param inputs [Symbol]
201
+ # @param name [Symbol] name of the input
202
+ # @param other_names [Symbol] array of symbols if specifying multiple inputs
203
+ # @param input_definition [Hash] options for input such as type, description, or title
204
+ # @option input_definition [String] :title Human readable title for input
205
+ # @option input_definition [String] :description Description for the input
206
+ # @option input_definition [String] :type text | textarea
207
+ # @option input_definition [String] :default The default value for the input
200
208
  # @return [void]
201
209
  # @example
202
- # input :patient_id, :bearer_token
203
- def input(*input_definitions)
204
- inputs.concat(input_definitions)
210
+ # input :patientid, title: 'Patient ID', description: 'The ID of the patient being searched for',
211
+ # default: 'default_patient_id'
212
+ # @example
213
+ # input :textarea, title: 'Textarea Input Example', type: 'textarea'
214
+ def input(name, *other_names, **input_definition)
215
+ if other_names.present?
216
+ [name, *other_names].each do |input_name|
217
+ inputs.push({ name: input_name, title: nil, description: nil, type: 'text' })
218
+ end
219
+ else
220
+ input_definition[:type] = 'text' unless input_definition.key? :type
221
+ inputs.push({ name: name }.merge(input_definition))
222
+ end
205
223
  end
206
224
 
207
225
  # Define outputs
@@ -229,6 +247,7 @@ module Inferno
229
247
  @outputs ||= []
230
248
  end
231
249
 
250
+ # @api private
232
251
  def child_types
233
252
  return [] if ancestors.include? Inferno::Entities::Test
234
253
  return [:groups] if ancestors.include? Inferno::Entities::TestSuite
@@ -236,6 +255,7 @@ module Inferno
236
255
  [:groups, :tests]
237
256
  end
238
257
 
258
+ # @api private
239
259
  def children
240
260
  @children ||= []
241
261
  end
@@ -245,6 +265,75 @@ module Inferno
245
265
 
246
266
  @validator_url = url
247
267
  end
268
+
269
+ # @api private
270
+ def suite
271
+ return self if ancestors.include? Inferno::Entities::TestSuite
272
+
273
+ parent.suite
274
+ end
275
+
276
+ # Create a route which will resume a test run when a request is received
277
+ #
278
+ # @see Inferno::DSL::Results#wait
279
+ # @example
280
+ # resume_test_route :get, '/launch' do
281
+ # request.query_parameters['iss']
282
+ # end
283
+ #
284
+ # test do
285
+ # input :issuer
286
+ # receives_request :launch
287
+ #
288
+ # run do
289
+ # wait(
290
+ # identifier: issuer,
291
+ # message: "Wating to receive a request with an issuer of #{issuer}"
292
+ # )
293
+ # end
294
+ # end
295
+ #
296
+ # @param method [Symbol] the HTTP request type (:get, :post, etc.) for the
297
+ # incoming request
298
+ # @param path [String] the path for this request. The route will be served
299
+ # with a prefix of `/custom/TEST_SUITE_ID` to prevent path conflicts.
300
+ # [Any of the path options available in Hanami
301
+ # Router](https://github.com/hanami/router/tree/f41001d4c3ee9e2d2c7bb142f74b43f8e1d3a265#a-beautiful-dsl)
302
+ # can be used here.
303
+ # @yield This method takes a block which must return the identifier
304
+ # defined when a test was set to wait for the test run that hit this
305
+ # route. The block has access to the `request` method which returns a
306
+ # {Inferno::DSL::Request} object with the information for the incoming
307
+ # request.
308
+ def resume_test_route(method, path, &block)
309
+ route_class = Class.new(ResumeTestRoute) do
310
+ define_method(:test_run_identifier, &block)
311
+ define_method(:request_name, -> { options[:name] })
312
+ end
313
+
314
+ route(method, path, route_class)
315
+ end
316
+
317
+ # Create a route to handle a request
318
+ #
319
+ # @param method [Symbol] the HTTP request type (:get, :post, etc.) for the
320
+ # incoming request. `:all` will accept all HTTP request types.
321
+ # @param path [String] the path for this request. The route will be served
322
+ # with a prefix of `/custom/TEST_SUITE_ID` to prevent path conflicts.
323
+ # [Any of the path options available in Hanami
324
+ # Router](https://github.com/hanami/router/tree/f41001d4c3ee9e2d2c7bb142f74b43f8e1d3a265#a-beautiful-dsl)
325
+ # can be used here.
326
+ # @param handler [#call] the route handler. This can be any Rack
327
+ # compatible object (e.g. a `Proc` object, a [Sinatra
328
+ # app](http://sinatrarb.com/)) as described in the [Hanami Router
329
+ # documentation.](https://github.com/hanami/router/tree/f41001d4c3ee9e2d2c7bb142f74b43f8e1d3a265#mount-rack-applications)
330
+ def route(method, path, handler)
331
+ Inferno.routes << { method: method, path: path, handler: handler, suite: suite }
332
+ end
333
+
334
+ def test_count
335
+ @test_count ||= children&.reduce(0) { |sum, child| sum + child.test_count } || 0
336
+ end
248
337
  end
249
338
  end
250
339
  end
@@ -4,9 +4,9 @@ require_relative 'entities/header'
4
4
  require_relative 'entities/message'
5
5
  require_relative 'entities/request'
6
6
  require_relative 'entities/result'
7
+ require_relative 'entities/session_data'
7
8
  require_relative 'entities/test'
8
9
  require_relative 'entities/test_group'
9
- require_relative 'entities/test_input'
10
10
  require_relative 'entities/test_run'
11
11
  require_relative 'entities/test_session'
12
12
  require_relative 'entities/test_suite'
@@ -2,13 +2,13 @@ module Inferno
2
2
  module Entities
3
3
  # A `Header` represents an HTTP request/response header
4
4
  #
5
- # @attr_reader [String] id of the header
6
- # @attr_reader [String] request_id index of the HTTP request
7
- # @attr_reader [String] name header name
8
- # @attr_reader [String] value header value
9
- # @attr_reader [String] type request/response
10
- # @attr_reader [Time] created_at
11
- # @attr_reader [Time] updated_at
5
+ # @attr_accessor [String] id of the header
6
+ # @attr_accessor [String] request_id index of the HTTP request
7
+ # @attr_accessor [String] name header name
8
+ # @attr_accessor [String] value header value
9
+ # @attr_accessor [String] type request/response
10
+ # @attr_accessor [Time] created_at
11
+ # @attr_accessor [Time] updated_at
12
12
  class Header < Entity
13
13
  ATTRIBUTES = [:id, :request_id, :name, :type, :value, :created_at, :updated_at].freeze
14
14
 
@@ -2,12 +2,14 @@ module Inferno
2
2
  module Entities
3
3
  # A `Message` represents a message generated during a test.
4
4
  #
5
- # @attr_reader [String] id of the message
6
- # @attr_reader [String] index of the message. Used for ordering.
7
- # @attr_reader [String] result_id
8
- # @attr_reader [Inferno::Entities::Result] result
9
- # @attr_reader [String] type
10
- # @attr_reader [String] message
5
+ # @attr_accessor [String] id of the message
6
+ # @attr_accessor [String] index of the message. Used for ordering.
7
+ # @attr_accessor [String] result_id
8
+ # @attr_accessor [Inferno::Entities::Result] result
9
+ # @attr_accessor [String] type
10
+ # @attr_accessor [String] message
11
+ # @attr_accessor [Time] created_at
12
+ # @attr_accessor [Time] updated_at
11
13
  class Message < Entity
12
14
  ATTRIBUTES = [:id, :index, :message, :result_id, :result, :type, :created_at, :updated_at].freeze
13
15
  TYPES = ['error', 'warning', 'info'].freeze
@@ -2,21 +2,21 @@ module Inferno
2
2
  module Entities
3
3
  # A `Request` represents a request and response issued during a test.
4
4
  #
5
- # @attr_reader [String] id of the request
6
- # @attr_reader [String] index of the request. Used for ordering.
7
- # @attr_reader [String] verb http verb
8
- # @attr_reader [String] url request url
9
- # @attr_reader [String] direction incoming/outgoing
10
- # @attr_reader [String] name name for the request
11
- # @attr_reader [String] status http response status code
12
- # @attr_reader [String] request_body body of the http request
13
- # @attr_reader [String] response_body body of the http response
14
- # @attr_reader [Array<Inferno::Entities::Header>] headers http
5
+ # @attr_accessor [String] id of the request
6
+ # @attr_accessor [String] index of the request. Used for ordering.
7
+ # @attr_accessor [String] verb http verb
8
+ # @attr_accessor [String] url request url
9
+ # @attr_accessor [String] direction incoming/outgoing
10
+ # @attr_accessor [String] name name for the request
11
+ # @attr_accessor [String] status http response status code
12
+ # @attr_accessor [String] request_body body of the http request
13
+ # @attr_accessor [String] response_body body of the http response
14
+ # @attr_accessor [Array<Inferno::Entities::Header>] headers http
15
15
  # request/response headers
16
- # @attr_reader [String] result_id id of the result for this request
17
- # @attr_reader [String] test_session_id id of the test session for this request
18
- # @attr_reader [Time] created_at creation timestamp
19
- # @attr_reader [Time] updated_at update timestamp
16
+ # @attr_accessor [String] result_id id of the result for this request
17
+ # @attr_accessor [String] test_session_id id of the test session for this request
18
+ # @attr_accessor [Time] created_at creation timestamp
19
+ # @attr_accessor [Time] updated_at update timestamp
20
20
  class Request < Entity
21
21
  ATTRIBUTES = [
22
22
  :id, :index, :verb, :url, :direction, :name, :status,
@@ -36,6 +36,11 @@ module Inferno
36
36
  @headers = params[:headers]&.map { |header| header.is_a?(Hash) ? Header.new(header) : header } || []
37
37
  end
38
38
 
39
+ # @return [Hash<String, String>]
40
+ def query_parameters
41
+ Addressable::URI.parse(url).query_values || {}
42
+ end
43
+
39
44
  # Find a response header
40
45
  #
41
46
  # @param name [String] the header name
@@ -117,6 +122,27 @@ module Inferno
117
122
  end
118
123
 
119
124
  class << self
125
+ # @api private
126
+ def from_rack_env(env, name: nil)
127
+ rack_request = env['router.request'].rack_request
128
+ url = "#{rack_request.base_url}#{rack_request.path}"
129
+ url += "?#{rack_request.query_string}" if rack_request.query_string.present?
130
+ request_headers =
131
+ env
132
+ .select { |key, _| key.start_with? 'HTTP_' }
133
+ .transform_keys { |key| key.delete_prefix('HTTP_').tr('_', '-').downcase }
134
+ .map { |header_name, value| Header.new(name: header_name, value: value, type: 'request') }
135
+
136
+ new(
137
+ verb: rack_request.request_method.downcase,
138
+ url: url,
139
+ direction: 'incoming',
140
+ name: name,
141
+ request_body: rack_request.body.string,
142
+ headers: request_headers
143
+ )
144
+ end
145
+
120
146
  # @api private
121
147
  def from_http_response(response, test_session_id:, direction: 'outgoing', name: nil)
122
148
  request_headers =