bulk_data_test_kit 0.13.2 → 0.14.0
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 +4 -4
- data/config/presets/bulk_data_v400_submit_consumer_demo.json.erb +65 -0
- data/config/presets/bulk_data_v400_submit_provider_demo.json +22 -0
- data/lib/bulk_data_test_kit/metadata.rb +20 -4
- data/lib/bulk_data_test_kit/requirements/generated/bulk_data_v101_requirements_coverage.csv +1 -1
- data/lib/bulk_data_test_kit/requirements/generated/bulk_data_v200_client_requirements_coverage.csv +1 -1
- data/lib/bulk_data_test_kit/requirements/generated/bulk_data_v200_requirements_coverage.csv +1 -1
- data/lib/bulk_data_test_kit/requirements/generated/bulk_data_v400_submit_consumer_requirements_coverage.csv +18 -0
- data/lib/bulk_data_test_kit/requirements/generated/bulk_data_v400_submit_provider_requirements_coverage.csv +4 -0
- data/lib/bulk_data_test_kit/v1.0.1/bulk_data_test_suite.rb +3 -3
- data/lib/bulk_data_test_kit/v2.0.0/bulk_data_test_suite.rb +3 -3
- data/lib/bulk_data_test_kit/v2.0.0_client/bulk_data_client_test_suite.rb +1 -1
- data/lib/bulk_data_test_kit/v2.0.0_client/docs/suite_description.md +4 -4
- data/lib/bulk_data_test_kit/v4.0.0/submit/consumer/backend_services_group.rb +50 -0
- data/lib/bulk_data_test_kit/v4.0.0/submit/consumer/complete_poll_group.rb +105 -0
- data/lib/bulk_data_test_kit/v4.0.0/submit/consumer/docs/suite_description.md +49 -0
- data/lib/bulk_data_test_kit/v4.0.0/submit/consumer/endpoints/file_download.rb +39 -0
- data/lib/bulk_data_test_kit/v4.0.0/submit/consumer/endpoints/manifest.rb +56 -0
- data/lib/bulk_data_test_kit/v4.0.0/submit/consumer/helpers.rb +78 -0
- data/lib/bulk_data_test_kit/v4.0.0/submit/consumer/manifest_and_file_retrieval.rb +59 -0
- data/lib/bulk_data_test_kit/v4.0.0/submit/consumer/poll_group.rb +66 -0
- data/lib/bulk_data_test_kit/v4.0.0/submit/consumer/processing_wait_group.rb +53 -0
- data/lib/bulk_data_test_kit/v4.0.0/submit/consumer/status_submit_group.rb +78 -0
- data/lib/bulk_data_test_kit/v4.0.0/submit/consumer/submit_completed_group.rb +65 -0
- data/lib/bulk_data_test_kit/v4.0.0/submit/consumer/submit_group.rb +66 -0
- data/lib/bulk_data_test_kit/v4.0.0/submit/consumer/tags.rb +12 -0
- data/lib/bulk_data_test_kit/v4.0.0/submit/consumer/test_suite.rb +116 -0
- data/lib/bulk_data_test_kit/v4.0.0/submit/consumer/urls.rb +43 -0
- data/lib/bulk_data_test_kit/v4.0.0/submit/provider/client_registration_group.rb +42 -0
- data/lib/bulk_data_test_kit/v4.0.0/submit/provider/docs/suite_description.md +50 -0
- data/lib/bulk_data_test_kit/v4.0.0/submit/provider/endpoints/poll.rb +90 -0
- data/lib/bulk_data_test_kit/v4.0.0/submit/provider/endpoints/status.rb +63 -0
- data/lib/bulk_data_test_kit/v4.0.0/submit/provider/endpoints/submit.rb +35 -0
- data/lib/bulk_data_test_kit/v4.0.0/submit/provider/helpers.rb +222 -0
- data/lib/bulk_data_test_kit/v4.0.0/submit/provider/manifest_and_file_retrieval.rb +165 -0
- data/lib/bulk_data_test_kit/v4.0.0/submit/provider/poll_group.rb +36 -0
- data/lib/bulk_data_test_kit/v4.0.0/submit/provider/processing_wait_group.rb +83 -0
- data/lib/bulk_data_test_kit/v4.0.0/submit/provider/retrieval.rb +139 -0
- data/lib/bulk_data_test_kit/v4.0.0/submit/provider/status_submit_group.rb +96 -0
- data/lib/bulk_data_test_kit/v4.0.0/submit/provider/submit_group.rb +314 -0
- data/lib/bulk_data_test_kit/v4.0.0/submit/provider/submit_stopped_group.rb +43 -0
- data/lib/bulk_data_test_kit/v4.0.0/submit/provider/tags.rb +18 -0
- data/lib/bulk_data_test_kit/v4.0.0/submit/provider/test_suite.rb +122 -0
- data/lib/bulk_data_test_kit/v4.0.0/submit/provider/urls.rb +53 -0
- data/lib/bulk_data_test_kit/v4.0.0/submit/provider/wait_group.rb +73 -0
- data/lib/bulk_data_test_kit/v4.0.0/submit/shared/jwks.rb +27 -0
- data/lib/bulk_data_test_kit/v4.0.0/submit/shared/submission_status.rb +14 -0
- data/lib/bulk_data_test_kit/v4.0.0/submit/shared/submitter_identifier.rb +14 -0
- data/lib/bulk_data_test_kit/version.rb +2 -2
- data/lib/bulk_data_test_kit.rb +2 -0
- metadata +41 -2
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
The Bulk Data Access v4.0.0-ballot Submit - Provider test suite validates a Data Provider
|
|
2
|
+
implementation of the [Bulk Submit Operation](https://hl7.org/fhir/uv/bulkdata/4.0.0-202609-ballot/en/)
|
|
3
|
+
within the Bulk Data Access v4.0.0-ballot IG.
|
|
4
|
+
|
|
5
|
+
## Scope
|
|
6
|
+
|
|
7
|
+
These tests target the
|
|
8
|
+
[Bulk Data Access v4.0.0-ballot IG](https://hl7.org/fhir/uv/bulkdata/4.0.0-202609-ballot/en/).
|
|
9
|
+
The ballot specification is subject to change before final publication.
|
|
10
|
+
These tests are intended to allow implementers to perform
|
|
11
|
+
preliminary checks of their systems against the requirements stated for Bulk Submit actors
|
|
12
|
+
and [provide feedback](https://github.com/inferno-framework/bulk-data-test-kit/issues)
|
|
13
|
+
on the tests. Future versions of these tests may verify other
|
|
14
|
+
requirements and may change the test verification logic.
|
|
15
|
+
|
|
16
|
+
## Running the Tests
|
|
17
|
+
|
|
18
|
+
### Quick Start
|
|
19
|
+
|
|
20
|
+
Bulk data access requires the use of SMART Backend Services for authentication.
|
|
21
|
+
In order to interact with Inferno's simulated bulk server, the tester must provide
|
|
22
|
+
the JSON Web Key Set (JWKS) containing the asymmetric signing key in the
|
|
23
|
+
**SMART JSON Web Key Set (JWKS)** input as either a URL that resolves
|
|
24
|
+
to a JWKS or a raw JWKS in JSON format. Additionally, testers may provide
|
|
25
|
+
a **Client Id** if they want their client assigned a specific one.
|
|
26
|
+
|
|
27
|
+
Once the client has been registered, it will need to obtain an access token
|
|
28
|
+
following the SMART Backend Services flow and use it when making bulk data requests.
|
|
29
|
+
If the client is not able to obtain an access token, see the *Demonstration* section
|
|
30
|
+
below for how to use the SMART server tests to obtain an access token that the client can use.
|
|
31
|
+
|
|
32
|
+
### Demonstration
|
|
33
|
+
|
|
34
|
+
You can run the Data Provider and Data Consumer suites against each other to
|
|
35
|
+
see how the tests work:
|
|
36
|
+
|
|
37
|
+
1. Start a session in the Bulk Data Access v4.0.0-ballot Submit - Provider suite.
|
|
38
|
+
1. Select the "Demo: Run Data Provider Against Inferno Data Consumer" preset.
|
|
39
|
+
1. Click the "Run All Tests" button.
|
|
40
|
+
1. In another browser tab, start a session in the Bulk Data Access v4.0.0-ballot
|
|
41
|
+
Submit - Consumer suite.
|
|
42
|
+
1. Select the "Demo: Run Data Consumer Against Inferno Data Provider" preset.
|
|
43
|
+
1. Click the "Run All Tests" button.
|
|
44
|
+
1. When the Data Consumer suite pauses after sending `completed`, advance the
|
|
45
|
+
Data Provider suite from its initial wait.
|
|
46
|
+
1. The Data Provider suite will retrieve the manifest and file synchronously,
|
|
47
|
+
then pause while waiting for the final status poll.
|
|
48
|
+
1. Advance the Data Consumer suite. It will make one final status poll.
|
|
49
|
+
1. After that poll completes, advance the Data Provider suite to finish the
|
|
50
|
+
paired workflow.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'smart_app_launch_test_kit'
|
|
4
|
+
require_relative '../helpers'
|
|
5
|
+
|
|
6
|
+
module BulkDataTestKit
|
|
7
|
+
module BulkDataV400
|
|
8
|
+
module Submit
|
|
9
|
+
module Provider
|
|
10
|
+
module Endpoints
|
|
11
|
+
class Poll < Inferno::DSL::SuiteEndpoint
|
|
12
|
+
include URLs
|
|
13
|
+
include Helpers
|
|
14
|
+
|
|
15
|
+
def test_run_identifier
|
|
16
|
+
return request.params[:session_path] if request.params[:session_path].present?
|
|
17
|
+
|
|
18
|
+
SMARTAppLaunch::MockSMARTServer.issued_token_to_client_id(
|
|
19
|
+
request.headers['authorization']&.delete_prefix('Bearer ')
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def make_response
|
|
24
|
+
case submission_outcome[:status]
|
|
25
|
+
when 'succeeded'
|
|
26
|
+
make_completed_response
|
|
27
|
+
when 'failed'
|
|
28
|
+
make_error_response(500, submission_outcome[:diagnostic])
|
|
29
|
+
else
|
|
30
|
+
make_in_progress_response
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def submission_id
|
|
35
|
+
submission_outcome[:submission_id]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def submission_outcome
|
|
39
|
+
@submission_outcome ||= submission_outcomes
|
|
40
|
+
.fetch(request.params[:submission_key].to_s, {})
|
|
41
|
+
.deep_symbolize_keys
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def submission_outcomes
|
|
45
|
+
raw_outcomes = Inferno::Repositories::SessionData.new.load(
|
|
46
|
+
test_session_id: test_run.test_session_id,
|
|
47
|
+
name: :provider_submission_outcomes
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
JSON.parse(raw_outcomes.presence || '{}')
|
|
51
|
+
rescue JSON::ParserError
|
|
52
|
+
{}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def make_in_progress_response
|
|
56
|
+
response.status = 202
|
|
57
|
+
response.headers['Retry-After'] = '1'
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def make_completed_response
|
|
61
|
+
response.status = 200
|
|
62
|
+
response.headers['Content-Type'] = 'application/json'
|
|
63
|
+
response.body = complete_manifest(submission_id).to_json
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def make_error_response(status, diagnostic)
|
|
67
|
+
operation_outcome = FHIR::R4::OperationOutcome.new(
|
|
68
|
+
issue: [
|
|
69
|
+
FHIR::R4::OperationOutcome::Issue.new(
|
|
70
|
+
severity: 'error',
|
|
71
|
+
code: 'processing',
|
|
72
|
+
diagnostics: diagnostic
|
|
73
|
+
)
|
|
74
|
+
]
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
response.status = status
|
|
78
|
+
response.headers['Content-Type'] = 'application/fhir+json'
|
|
79
|
+
response.body = operation_outcome.to_json
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def tags
|
|
83
|
+
[POLL_TAG]
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'smart_app_launch_test_kit'
|
|
4
|
+
require_relative '../helpers'
|
|
5
|
+
require_relative '../../shared/submission_status'
|
|
6
|
+
|
|
7
|
+
module BulkDataTestKit
|
|
8
|
+
module BulkDataV400
|
|
9
|
+
module Submit
|
|
10
|
+
module Provider
|
|
11
|
+
module Endpoints
|
|
12
|
+
class Status < Inferno::DSL::SuiteEndpoint
|
|
13
|
+
include URLs
|
|
14
|
+
include Helpers
|
|
15
|
+
|
|
16
|
+
def test_run_identifier
|
|
17
|
+
return request.params[:session_path] if request.params[:session_path].present?
|
|
18
|
+
|
|
19
|
+
SMARTAppLaunch::MockSMARTServer.issued_token_to_client_id(
|
|
20
|
+
request.headers['authorization']&.delete_prefix('Bearer ')
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def make_response
|
|
25
|
+
if identity.nil?
|
|
26
|
+
response.status = 400
|
|
27
|
+
response.headers['Content-Type'] = 'application/fhir+json'
|
|
28
|
+
response.body = FHIR::R4::OperationOutcome.new(
|
|
29
|
+
issue: [
|
|
30
|
+
FHIR::R4::OperationOutcome::Issue.new(
|
|
31
|
+
severity: 'error',
|
|
32
|
+
code: 'invalid',
|
|
33
|
+
diagnostics: 'The request did not identify a Bulk Submit submission.'
|
|
34
|
+
)
|
|
35
|
+
]
|
|
36
|
+
).to_json
|
|
37
|
+
else
|
|
38
|
+
response.status = 202
|
|
39
|
+
response.headers['Content-Location'] = poll_url(submission_key(identity))
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def identity
|
|
44
|
+
@identity ||= submission_identity(request_parameters)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def request_parameters
|
|
48
|
+
body = request.body.read
|
|
49
|
+
request.body.rewind
|
|
50
|
+
FHIR.from_contents(body)
|
|
51
|
+
rescue JSON::ParserError, TypeError
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def tags
|
|
56
|
+
[STATUS_SUBMIT_TAG]
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../tags'
|
|
4
|
+
|
|
5
|
+
require 'smart_app_launch_test_kit'
|
|
6
|
+
|
|
7
|
+
module BulkDataTestKit
|
|
8
|
+
module BulkDataV400
|
|
9
|
+
module Submit
|
|
10
|
+
module Provider
|
|
11
|
+
module Endpoints
|
|
12
|
+
class Submit < Inferno::DSL::SuiteEndpoint
|
|
13
|
+
include URLs
|
|
14
|
+
|
|
15
|
+
def test_run_identifier
|
|
16
|
+
return request.params[:session_path] if request.params[:session_path].present?
|
|
17
|
+
|
|
18
|
+
SMARTAppLaunch::MockSMARTServer.issued_token_to_client_id(
|
|
19
|
+
request.headers['authorization']&.delete_prefix('Bearer ')
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def make_response
|
|
24
|
+
response.status = 200
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def tags
|
|
28
|
+
[SUBMIT_TAG]
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require_relative '../shared/submission_status'
|
|
5
|
+
|
|
6
|
+
module BulkDataTestKit
|
|
7
|
+
module BulkDataV400
|
|
8
|
+
module Submit
|
|
9
|
+
module Provider
|
|
10
|
+
module Helpers
|
|
11
|
+
TERMINAL_SUBMISSION_STATUSES = %w[completed stopped].freeze
|
|
12
|
+
|
|
13
|
+
def submission_identity(parameters)
|
|
14
|
+
parameter_list = parameters.respond_to?(:parameter) ? parameters.parameter : []
|
|
15
|
+
submitter = parameter_list.find { |parameter| parameter.name == 'submitter' }&.valueIdentifier
|
|
16
|
+
submission_id = parameter_list.find do |parameter|
|
|
17
|
+
parameter.name == 'submissionId'
|
|
18
|
+
end&.valueString
|
|
19
|
+
|
|
20
|
+
return if submitter&.system.blank? || submitter&.value.blank? || submission_id.blank?
|
|
21
|
+
|
|
22
|
+
{
|
|
23
|
+
submitter_system: submitter.system,
|
|
24
|
+
submitter_value: submitter.value,
|
|
25
|
+
submission_id:
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def submission_status(parameters)
|
|
30
|
+
parameter_list = parameters.respond_to?(:parameter) ? parameters.parameter : []
|
|
31
|
+
status_parameters = parameter_list.select do |parameter|
|
|
32
|
+
parameter.name == 'submissionStatus'
|
|
33
|
+
end
|
|
34
|
+
return unless status_parameters.one?
|
|
35
|
+
|
|
36
|
+
coding = status_parameters.first.valueCoding
|
|
37
|
+
|
|
38
|
+
return unless coding&.system == Shared::SubmissionStatus::SYSTEM
|
|
39
|
+
|
|
40
|
+
coding.code
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def parsed_submission_parameters(requests)
|
|
44
|
+
requests.filter_map do |request|
|
|
45
|
+
parameters = FHIR.from_contents(request.request_body)
|
|
46
|
+
parameters if parameters.is_a?(FHIR::R4::Parameters)
|
|
47
|
+
rescue JSON::ParserError, TypeError
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def submission_timelines(parameters_list)
|
|
53
|
+
parameters_list.each_with_object({}) do |parameters, timelines|
|
|
54
|
+
identity = submission_identity(parameters)
|
|
55
|
+
next unless identity
|
|
56
|
+
|
|
57
|
+
(timelines[identity] ||= []) << parameters
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def valid_completed_submission_timelines(parameters_list)
|
|
62
|
+
submission_timelines(parameters_list).select do |_identity, timeline|
|
|
63
|
+
terminal_indexes = timeline.each_index.select do |index|
|
|
64
|
+
TERMINAL_SUBMISSION_STATUSES.include?(submission_status(timeline[index]))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
terminal_indexes == [timeline.length - 1] &&
|
|
68
|
+
submission_status(timeline.last) == 'completed'
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def valid_completed_submission_parameters(requests)
|
|
73
|
+
valid_completed_submission_timelines(
|
|
74
|
+
parsed_submission_parameters(requests)
|
|
75
|
+
).values.flatten
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def validate_terminal_submission_order(requests)
|
|
79
|
+
assert requests.present?, 'No $bulk-submit requests were received.'
|
|
80
|
+
|
|
81
|
+
submission_timelines(parsed_submission_parameters(requests)).each_value do |timeline|
|
|
82
|
+
terminal_index = timeline.index do |parameters|
|
|
83
|
+
TERMINAL_SUBMISSION_STATUSES.include?(submission_status(parameters))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
assert terminal_index.nil? || terminal_index == timeline.length - 1,
|
|
87
|
+
'No `$bulk-submit` request may follow a terminal submission status ' \
|
|
88
|
+
'for the same `submitter` and `submissionId`.'
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def submission_key(identity)
|
|
93
|
+
Digest::SHA256.hexdigest(
|
|
94
|
+
[identity[:submitter_system], identity[:submitter_value], identity[:submission_id]].to_json
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def complete_manifest(submission_id)
|
|
99
|
+
{
|
|
100
|
+
submissionId: submission_id,
|
|
101
|
+
transactionTime: DateTime.now,
|
|
102
|
+
requiresAccessToken: true
|
|
103
|
+
}
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def validate_submitter_identifiers(requests, operation_name)
|
|
107
|
+
assert requests.present?, "No #{operation_name} requests were received."
|
|
108
|
+
|
|
109
|
+
requests.each do |request|
|
|
110
|
+
parameters = FHIR.from_contents(request.request_body)
|
|
111
|
+
submitter_parameters = parameters.parameter.select do |parameter|
|
|
112
|
+
parameter.name == 'submitter'
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
assert submitter_parameters.one?,
|
|
116
|
+
"Every #{operation_name} request must include exactly one `submitter` parameter."
|
|
117
|
+
|
|
118
|
+
submitter_identifier = submitter_parameters.first.valueIdentifier
|
|
119
|
+
assert submitter_identifier.present?,
|
|
120
|
+
'The `submitter` parameter must have type `Identifier`.'
|
|
121
|
+
assert submitter_identifier.system.present?,
|
|
122
|
+
'The `submitter` Identifier must include a system.'
|
|
123
|
+
assert submitter_identifier.value.present?,
|
|
124
|
+
'The `submitter` Identifier must include a value.'
|
|
125
|
+
|
|
126
|
+
[submitter_identifier.system, submitter_identifier.value]
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def validate_status_submission_identities(status_requests, submit_requests)
|
|
131
|
+
validate_submitter_identifiers(status_requests, '$bulk-submit-status')
|
|
132
|
+
validate_submission_ids(status_requests, '$bulk-submit-status')
|
|
133
|
+
validate_submitter_identifiers(submit_requests, '$bulk-submit')
|
|
134
|
+
validate_submission_ids(submit_requests, '$bulk-submit')
|
|
135
|
+
|
|
136
|
+
submitted_identities = submit_requests.map do |request|
|
|
137
|
+
submission_identity(FHIR.from_contents(request.request_body))
|
|
138
|
+
end
|
|
139
|
+
status_identities = status_requests.map do |request|
|
|
140
|
+
submission_identity(FHIR.from_contents(request.request_body))
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
assert status_identities.all? { |identity| submitted_identities.include?(identity) },
|
|
144
|
+
'Every `$bulk-submit-status` request must match the `submitter` and ' \
|
|
145
|
+
'`submissionId` of a `$bulk-submit` request.'
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def validate_submission_ids(requests, operation_name)
|
|
149
|
+
assert requests.present?, "No #{operation_name} requests were received."
|
|
150
|
+
|
|
151
|
+
requests.each do |request|
|
|
152
|
+
parameters = FHIR.from_contents(request.request_body)
|
|
153
|
+
submission_id_parameters = parameters.parameter.select do |parameter|
|
|
154
|
+
parameter.name == 'submissionId'
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
assert submission_id_parameters.one?,
|
|
158
|
+
"Every #{operation_name} request must include exactly one `submissionId` parameter."
|
|
159
|
+
assert submission_id_parameters.first.valueString.present?,
|
|
160
|
+
'The `submissionId` parameter must have type `string` and contain a value.'
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def submission_status_parameters(request)
|
|
165
|
+
FHIR.from_contents(request.request_body).parameter.select do |parameter|
|
|
166
|
+
parameter.name == 'submissionStatus'
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def request_has_submission_status?(request, code)
|
|
171
|
+
status_parameters = submission_status_parameters(request)
|
|
172
|
+
return code == 'in-progress' if status_parameters.empty?
|
|
173
|
+
return false unless status_parameters.one?
|
|
174
|
+
|
|
175
|
+
coding = status_parameters.first.valueCoding
|
|
176
|
+
coding.present? &&
|
|
177
|
+
coding.system == Shared::SubmissionStatus::SYSTEM &&
|
|
178
|
+
coding.code == code
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def validate_submission_statuses(requests)
|
|
182
|
+
assert requests.present?, 'No $bulk-submit requests were received.'
|
|
183
|
+
|
|
184
|
+
requests.each do |request|
|
|
185
|
+
status_parameters = submission_status_parameters(request)
|
|
186
|
+
|
|
187
|
+
assert status_parameters.length <= 1,
|
|
188
|
+
'Every $bulk-submit request must include at most one `submissionStatus` parameter.'
|
|
189
|
+
next if status_parameters.empty?
|
|
190
|
+
|
|
191
|
+
coding = status_parameters.first.valueCoding
|
|
192
|
+
assert coding.present?, '`submissionStatus` must have type `Coding`.'
|
|
193
|
+
assert Shared::SubmissionStatus::CODES.include?(coding.code),
|
|
194
|
+
"#{coding.code} is not a valid submissionStatus code"
|
|
195
|
+
assert coding.system == Shared::SubmissionStatus::SYSTEM,
|
|
196
|
+
"#{coding.system} is not the submissionStatus code system"
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def validate_optional_part_parameter(requests, parameter_name)
|
|
201
|
+
parameters = requests.flat_map do |request|
|
|
202
|
+
FHIR.from_contents(request.request_body).parameter
|
|
203
|
+
end.select do |parameter|
|
|
204
|
+
parameter.name == parameter_name
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
skip_if parameters.empty?,
|
|
208
|
+
"No submission included the optional `#{parameter_name}` parameter"
|
|
209
|
+
|
|
210
|
+
all_parts = parameters.all? do |parameter|
|
|
211
|
+
serialized = parameter.to_hash
|
|
212
|
+
parameter.part.present? &&
|
|
213
|
+
serialized.keys.none? { |key| key.start_with?('value') || key == 'resource' }
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
assert all_parts, "`#{parameter_name}` must have type `part`"
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
require_relative 'tags'
|
|
2
|
+
|
|
3
|
+
module BulkDataTestKit
|
|
4
|
+
module BulkDataV400
|
|
5
|
+
module Submit
|
|
6
|
+
module Provider
|
|
7
|
+
class ManifestAndFileRetrieval < Inferno::TestGroup
|
|
8
|
+
include Helpers
|
|
9
|
+
|
|
10
|
+
title 'Manifest and File Retrieval'
|
|
11
|
+
id :bulk_data_v400_submit_provider_manifest_file_retrieval
|
|
12
|
+
|
|
13
|
+
input :provider_submission_outcomes, type: :textarea, optional: true
|
|
14
|
+
|
|
15
|
+
test do
|
|
16
|
+
id :bulk_data_v400_submit_provider_completed_submissions_processed
|
|
17
|
+
title 'Completed submissions were processed successfully'
|
|
18
|
+
description <<~DESCRIPTION
|
|
19
|
+
This test verifies that Inferno successfully retrieved every
|
|
20
|
+
completed submission.
|
|
21
|
+
DESCRIPTION
|
|
22
|
+
|
|
23
|
+
run do
|
|
24
|
+
outcomes = JSON.parse(provider_submission_outcomes.presence || '{}')
|
|
25
|
+
skip_if outcomes.empty?, 'No completed submissions were processed'
|
|
26
|
+
|
|
27
|
+
failures = outcomes.values.select { |outcome| outcome['status'] == 'failed' }
|
|
28
|
+
assert failures.empty?, 'Inferno was unable to retrieve one or more completed submissions.'
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
test do
|
|
33
|
+
id :bulk_data_v400_submit_provider_oauth_metadata_retrieved
|
|
34
|
+
title 'OAuth metadata retrieved'
|
|
35
|
+
description <<~DESCRIPTION
|
|
36
|
+
This test verifies that the provider's OAuth metadata could be
|
|
37
|
+
retrieved.
|
|
38
|
+
DESCRIPTION
|
|
39
|
+
|
|
40
|
+
run do
|
|
41
|
+
submit_requests = load_tagged_requests Provider::SUBMIT_TAG
|
|
42
|
+
completed_submissions = valid_completed_submission_parameters(submit_requests)
|
|
43
|
+
oauth_metadata_submitted = completed_submissions.any? do |parameters|
|
|
44
|
+
manifest_submitted = parameters.parameter.any? do |parameter|
|
|
45
|
+
parameter.name == 'manifestUrl' && parameter.valueUrl.present?
|
|
46
|
+
end
|
|
47
|
+
oauth_metadata_url_submitted = parameters.parameter.any? do |parameter|
|
|
48
|
+
parameter.name == 'oauthMetadataUrl' && parameter.valueUrl.present?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
manifest_submitted && oauth_metadata_url_submitted
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
skip_if !oauth_metadata_submitted, 'No OAuth metadata URL was submitted'
|
|
55
|
+
|
|
56
|
+
oauth_metadata_requests = load_tagged_requests Provider::OAUTH_METADATA_TAG
|
|
57
|
+
|
|
58
|
+
assert oauth_metadata_requests.present?, 'No OAuth Metadata request made'
|
|
59
|
+
|
|
60
|
+
failed_requests = oauth_metadata_requests.reject { |request| request.status.to_i == 200 }
|
|
61
|
+
assert failed_requests.empty?,
|
|
62
|
+
"OAuth metadata retrieval failed: #{failed_requests.map { |request| "#{request.url} returned #{request.status}" }.join(', ')}"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
test do
|
|
67
|
+
id :bulk_data_v400_submit_provider_token_request_success
|
|
68
|
+
title 'Successful token request made'
|
|
69
|
+
description <<~DESCRIPTION
|
|
70
|
+
This test verifies that all files included in the manifest
|
|
71
|
+
were retrieved.
|
|
72
|
+
DESCRIPTION
|
|
73
|
+
|
|
74
|
+
run do
|
|
75
|
+
submit_requests = load_tagged_requests Provider::SUBMIT_TAG
|
|
76
|
+
completed_submissions = valid_completed_submission_parameters(submit_requests)
|
|
77
|
+
oauth_metadata_submitted = completed_submissions.any? do |parameters|
|
|
78
|
+
manifest_submitted = parameters.parameter.any? do |parameter|
|
|
79
|
+
parameter.name == 'manifestUrl' && parameter.valueUrl.present?
|
|
80
|
+
end
|
|
81
|
+
oauth_metadata_url_submitted = parameters.parameter.any? do |parameter|
|
|
82
|
+
parameter.name == 'oauthMetadataUrl' && parameter.valueUrl.present?
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
manifest_submitted && oauth_metadata_url_submitted
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
skip_if !oauth_metadata_submitted, 'No OAuth metadata URL was submitted'
|
|
89
|
+
|
|
90
|
+
token_requests = load_tagged_requests Provider::TOKEN_TAG
|
|
91
|
+
|
|
92
|
+
assert token_requests.present?, 'No token request made'
|
|
93
|
+
|
|
94
|
+
failed_requests = token_requests.reject { |request| request.status.to_i == 200 }
|
|
95
|
+
assert failed_requests.empty?,
|
|
96
|
+
"Token request failed: #{failed_requests.map { |request| "#{request.url} returned #{request.status}" }.join(', ')}"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
test do
|
|
101
|
+
id :bulk_data_v400_submit_provider_manifests_retrieved
|
|
102
|
+
title 'Manifests for each submission were retrieved'
|
|
103
|
+
description <<~DESCRIPTION
|
|
104
|
+
This test verifies that all manifests included in the submissions
|
|
105
|
+
were retrieved.
|
|
106
|
+
DESCRIPTION
|
|
107
|
+
|
|
108
|
+
run do
|
|
109
|
+
submit_requests = load_tagged_requests Provider::SUBMIT_TAG
|
|
110
|
+
completed_submissions = valid_completed_submission_parameters(submit_requests)
|
|
111
|
+
expected_manifest_count = completed_submissions.sum do |parameters|
|
|
112
|
+
parameters.parameter.count do |parameter|
|
|
113
|
+
parameter.name == 'manifestUrl' && parameter.valueUrl.present?
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
skip_if expected_manifest_count.zero?, 'No manifest URLs were submitted'
|
|
118
|
+
|
|
119
|
+
manifest_requests = load_tagged_requests Provider::MANIFEST_TAG
|
|
120
|
+
|
|
121
|
+
assert manifest_requests.count == expected_manifest_count,
|
|
122
|
+
"Expected #{expected_manifest_count} manifest retrievals, " \
|
|
123
|
+
"but received #{manifest_requests.count}"
|
|
124
|
+
|
|
125
|
+
failed_requests = manifest_requests.reject { |request| request.status.to_i == 200 }
|
|
126
|
+
assert failed_requests.empty?,
|
|
127
|
+
"Manifest retrieval failed: #{failed_requests.map { |request| "#{request.url} returned #{request.status}" }.join(', ')}"
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
test do
|
|
132
|
+
id :bulk_data_v400_submit_provider_files_retrieved
|
|
133
|
+
title 'Files retrieved'
|
|
134
|
+
description <<~DESCRIPTION
|
|
135
|
+
This test verifies that all files included in the manifest
|
|
136
|
+
were retrieved.
|
|
137
|
+
DESCRIPTION
|
|
138
|
+
|
|
139
|
+
run do
|
|
140
|
+
manifest_requests = load_tagged_requests Provider::MANIFEST_TAG
|
|
141
|
+
|
|
142
|
+
expected_file_count = manifest_requests
|
|
143
|
+
.select { |request| request.status.to_i == 200 }
|
|
144
|
+
.sum do |request|
|
|
145
|
+
manifest = JSON.parse(request.response_body)
|
|
146
|
+
Array(manifest['output']).count
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
skip_if expected_file_count.zero?, 'No files were listed in the retrieved manifests'
|
|
150
|
+
|
|
151
|
+
file_requests = load_tagged_requests Provider::FILE_DOWNLOAD_TAG
|
|
152
|
+
|
|
153
|
+
assert file_requests.count == expected_file_count,
|
|
154
|
+
"Expected #{expected_file_count} file downloads, but received #{file_requests.count}"
|
|
155
|
+
|
|
156
|
+
failed_requests = file_requests.reject { |request| request.status.to_i == 200 }
|
|
157
|
+
assert failed_requests.empty?,
|
|
158
|
+
"File retrieval failed: #{failed_requests.map { |request| "#{request.url} returned #{request.status}" }.join(', ')}"
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BulkDataTestKit
|
|
4
|
+
module BulkDataV400
|
|
5
|
+
module Submit
|
|
6
|
+
module Provider
|
|
7
|
+
class PollGroup < Inferno::TestGroup
|
|
8
|
+
title 'Bulk Data Status Polling Request'
|
|
9
|
+
|
|
10
|
+
description %(
|
|
11
|
+
This group verifies that a provider successfully polled for status.
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
id :bulk_data_v400_submit_provider_poll
|
|
15
|
+
|
|
16
|
+
run_as_group
|
|
17
|
+
|
|
18
|
+
optional
|
|
19
|
+
|
|
20
|
+
test do
|
|
21
|
+
id :bulk_data_v400_submit_provider_poll_request
|
|
22
|
+
title 'Poll Request Was Made'
|
|
23
|
+
|
|
24
|
+
description %(
|
|
25
|
+
This test verifies that at least one poll request was made.
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
run do
|
|
29
|
+
assert load_tagged_requests(POLL_TAG).any?, 'Did not receive a poll request.'
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|