subscriptions_test_kit 0.9.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.
Files changed (67) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +201 -0
  3. data/lib/inferno_requirements_tools/ext/inferno_core/runnable.rb +22 -0
  4. data/lib/inferno_requirements_tools/tasks/collect_requirements.rb +222 -0
  5. data/lib/inferno_requirements_tools/tasks/requirements_coverage.rb +264 -0
  6. data/lib/subscriptions_test_kit/common/notification_conformance_verification.rb +310 -0
  7. data/lib/subscriptions_test_kit/common/subscription_conformance_verification.rb +87 -0
  8. data/lib/subscriptions_test_kit/docs/samples/Subscription_empty.json +37 -0
  9. data/lib/subscriptions_test_kit/docs/samples/Subscription_full-resource.json +37 -0
  10. data/lib/subscriptions_test_kit/docs/samples/Subscription_id-only.json +38 -0
  11. data/lib/subscriptions_test_kit/docs/subscriptions_r5_backport_r4_client_suite_description.md +120 -0
  12. data/lib/subscriptions_test_kit/docs/subscriptions_r5_backport_r4_server_suite_description.md +170 -0
  13. data/lib/subscriptions_test_kit/endpoints/subscription_create_endpoint.rb +90 -0
  14. data/lib/subscriptions_test_kit/endpoints/subscription_read_endpoint.rb +32 -0
  15. data/lib/subscriptions_test_kit/endpoints/subscription_rest_hook_endpoint.rb +66 -0
  16. data/lib/subscriptions_test_kit/endpoints/subscription_status_endpoint.rb +70 -0
  17. data/lib/subscriptions_test_kit/igs/README.md +21 -0
  18. data/lib/subscriptions_test_kit/jobs/send_subscription_notifications.rb +130 -0
  19. data/lib/subscriptions_test_kit/requirements/generated/subscriptions-test-kit_requirements_coverage.csv +136 -0
  20. data/lib/subscriptions_test_kit/requirements/subscriptions-test-kit_out_of_scope_requirements.csv +23 -0
  21. data/lib/subscriptions_test_kit/requirements/subscriptions-test-kit_requirements.csv +145 -0
  22. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_client/common/subscription_simulation_utils.rb +140 -0
  23. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_client/fixtures/capability_statement.json +57 -0
  24. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_client/workflow/conformance_verification/notification_input_payload_verification_test.rb +50 -0
  25. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_client/workflow/conformance_verification/notification_input_verification_test.rb +25 -0
  26. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_client/workflow/conformance_verification/processing_attestation_test.rb +38 -0
  27. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_client/workflow/conformance_verification/subscription_verification_test.rb +28 -0
  28. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_client/workflow/conformance_verification_group.rb +20 -0
  29. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_client/workflow/interaction_test.rb +128 -0
  30. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_client/workflow/interaction_verification/event_notification_verification_test.rb +40 -0
  31. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_client/workflow/interaction_verification/handshake_notification_verification_test.rb +40 -0
  32. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_client/workflow/interaction_verification_group.rb +16 -0
  33. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_client/workflow_group.rb +33 -0
  34. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_client_suite.rb +66 -0
  35. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/common/interaction/creation_response_conformance_test.rb +51 -0
  36. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/common/interaction/notification_delivery_test.rb +56 -0
  37. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/common/interaction/subscription_conformance_test.rb +72 -0
  38. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/common/interaction_group.rb +24 -0
  39. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/common/interaction_verification/notification_conformance_test.rb +73 -0
  40. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/common/interaction_verification_group.rb +19 -0
  41. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/common/subscription_creation.rb +63 -0
  42. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/common/subscription_status_operation.rb +58 -0
  43. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/coverage/capability_statement/cs_conformance_test.rb +49 -0
  44. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/coverage/capability_statement/topic_discovery_test.rb +106 -0
  45. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/coverage/capability_statement_group.rb +21 -0
  46. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/coverage/event_notification/empty_content/empty_conformance_test.rb +63 -0
  47. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/coverage/event_notification/empty_content_group.rb +58 -0
  48. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/coverage/event_notification/full_resource_content/full_resource_conformance_test.rb +68 -0
  49. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/coverage/event_notification/full_resource_content_group.rb +58 -0
  50. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/coverage/event_notification/id_only_content/id_only_conformance_test.rb +66 -0
  51. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/coverage/event_notification/id_only_content_group.rb +58 -0
  52. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/coverage/event_notification_group.rb +25 -0
  53. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/coverage/handshake_heartbeat/handshake_conformance_test.rb +67 -0
  54. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/coverage/handshake_heartbeat/heartbeat_conformance_test.rb +74 -0
  55. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/coverage/handshake_heartbeat_group.rb +19 -0
  56. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/coverage/status_operation/status_invocation_test.rb +43 -0
  57. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/coverage/status_operation_group.rb +15 -0
  58. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/coverage/subscription_rejection/reject_subscriptions_test.rb +181 -0
  59. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/coverage/subscription_rejection_group.rb +21 -0
  60. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/coverage_group.rb +26 -0
  61. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/workflow_group.rb +27 -0
  62. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server_suite.rb +79 -0
  63. data/lib/subscriptions_test_kit/tags.rb +10 -0
  64. data/lib/subscriptions_test_kit/urls.rb +37 -0
  65. data/lib/subscriptions_test_kit/version.rb +5 -0
  66. data/lib/subscriptions_test_kit.rb +3 -0
  67. metadata +194 -0
@@ -0,0 +1,264 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'CSV'
4
+ require 'roo'
5
+ require_relative '../ext/inferno_core/runnable'
6
+
7
+ module InfernoRequirementsTools
8
+ module Tasks
9
+ # This class manages the mapping of test kit tests to requirements that they verify
10
+ # and creates a CSV file with the tests that cover each requirement.
11
+ # It expects a CSV file in the repo at `lib/[test kit id]/requirements/[test kit id]_requirements.csv`
12
+ # that serves as the source of the requirement set for the test kit. The requirements in
13
+ # this files are identified by a requirement set and an id and tests, groups, and suites
14
+ # within in the test kit can claim that they verify a requirement by including a reference
15
+ # to that requirementin the form <requirement set>@<id> in their `verifies_requirements` field.
16
+ # Requirements that are out of scope can be listed in a companion file
17
+ # `lib/[test kit id]/requirements/[test kit id]_out_of_scope_requirements.csv`.
18
+ #
19
+ # The `run` method generates a CSV file at
20
+ # `lib/[test kit id]/requirements/generated/[test kit id]_requirements_coverage.csv``.
21
+ # This file will be identical to the input spreadsheet, plus an additional column which holds a comma separated
22
+ # list of inferno test IDs that test each requirement. These test IDs are Inferno short form IDs that represent the
23
+ # position of the test within its group and suite. For example, the fifth test in the second group will have an ID
24
+ # of 2.05. This ID is also shown in the Inferno web UI.
25
+ #
26
+ # The `run_check` method will check whether the previously generated file is up-to-date.
27
+ class RequirementsCoverage
28
+ # Update these constants based on the test kit.
29
+ TEST_KIT_ID = 'subscriptions-test-kit'
30
+ TEST_SUITES = [SubscriptionsTestKit::SubscriptionsR5BackportR4Client::SubscriptionsR5BackportR4ClientSuite,
31
+ SubscriptionsTestKit::SubscriptionsR5BackportR4Server::SubscriptionsR5BackportR4ServerSuite].freeze
32
+ SUITE_ID_TO_ACTOR_MAP = {
33
+ 'subscriptions_r5_backport_r4_client' => 'Client',
34
+ 'subscriptions_r5_backport_r4_server' => 'Server'
35
+ }.freeze
36
+
37
+ # Derivative constants
38
+ TEST_KIT_CODE_FOLDER = TEST_KIT_ID.gsub('-', '_')
39
+ INPUT_HEADERS = [
40
+ 'Req Set',
41
+ 'ID',
42
+ 'URL',
43
+ 'Requirement',
44
+ 'Conformance',
45
+ 'Actor',
46
+ 'Sub-Requirement(s)',
47
+ 'Conditionality'
48
+ ].freeze
49
+ SHORT_ID_HEADER = 'Short ID(s)'
50
+ FULL_ID_HEADER = 'Full ID(s)'
51
+ INPUT_FILE_NAME = "#{TEST_KIT_ID}_requirements.csv".freeze
52
+ INPUT_FILE = File.join('lib', TEST_KIT_CODE_FOLDER, 'requirements', INPUT_FILE_NAME).freeze
53
+ NOT_TESTED_FILE_NAME = "#{TEST_KIT_ID}_out_of_scope_requirements.csv".freeze
54
+ NOT_TESTED_FILE = File.join('lib', TEST_KIT_CODE_FOLDER, 'requirements', NOT_TESTED_FILE_NAME).freeze
55
+ OUTPUT_FILE_NAME = "#{TEST_KIT_ID}_requirements_coverage.csv".freeze
56
+ OUTPUT_FILE = File.join('lib', TEST_KIT_CODE_FOLDER, 'requirements', 'generated', OUTPUT_FILE_NAME).freeze
57
+
58
+ def input_rows
59
+ @input_rows ||=
60
+ CSV.parse(File.open(INPUT_FILE, "r:bom|utf-8"), headers: true).map do |row|
61
+ row.to_h.slice(*INPUT_HEADERS)
62
+ end
63
+ end
64
+
65
+ def not_tested_requirements_map
66
+ @not_tested_requirements_map ||= load_not_tested_requirements
67
+ end
68
+
69
+ def load_not_tested_requirements
70
+ return {} unless File.exists?(NOT_TESTED_FILE)
71
+
72
+ not_tested_requirements = {}
73
+ CSV.parse(File.open(NOT_TESTED_FILE, "r:bom|utf-8"), headers: true).each do |row|
74
+ row_hash = row.to_h
75
+ not_tested_requirements[ "#{row_hash['Req Set']}@#{row_hash['ID']}"] = row_hash
76
+ end
77
+
78
+ not_tested_requirements
79
+ end
80
+
81
+ # Of the form:
82
+ # {
83
+ # 'req-id-1': [
84
+ # { short_id: 'short-id-1', full_id: 'long-id-1', suite_id: 'suite-id-1' },
85
+ # { short_id: 'short-id-2', full_id: 'long-id-2', suite_id: 'suite-id-2' }
86
+ # ],
87
+ # 'req-id-2': [{ short_id: 'short-id-3', full_id: 'long-id-3', suite_id: 'suite-id-3' }],
88
+ # ...
89
+ # }
90
+ def inferno_requirements_map
91
+ @inferno_requirements_map ||= TEST_SUITES.each_with_object({}) do |suite, requirements_map|
92
+ serialize_requirements(suite, 'suite', suite.id, requirements_map)
93
+ suite.groups.each do |group|
94
+ map_group_requirements(group, suite.id, requirements_map)
95
+ end
96
+ end
97
+ end
98
+
99
+ def new_csv
100
+ @new_csv ||=
101
+ CSV.generate(+"\xEF\xBB\xBF") do |csv|
102
+ output_headers = TEST_SUITES.each_with_object(INPUT_HEADERS.dup) do |suite, headers|
103
+ headers << "#{suite.title} #{SHORT_ID_HEADER}"
104
+ headers << "#{suite.title} #{FULL_ID_HEADER}"
105
+ end
106
+
107
+ csv << output_headers
108
+ input_rows.each do |row| # note: use row order from source file
109
+ next if row['Conformance'] == 'DEPRECATED' # filter out deprecated rows
110
+ row_actor = row['Actor']
111
+ TEST_SUITES.each do |suite|
112
+ suite_actor = SUITE_ID_TO_ACTOR_MAP[suite.id]
113
+ if row_actor&.include?(suite_actor)
114
+ set_and_req_id = "#{row['Req Set']}@#{row['ID']}"
115
+ suite_requirement_items = inferno_requirements_map[set_and_req_id]&.filter { |item| item[:suite_id] == suite.id}
116
+ short_ids = suite_requirement_items&.map { |item| item[:short_id] }
117
+ full_ids = suite_requirement_items&.map { |item| item[:full_id] }
118
+ if short_ids.blank? && not_tested_requirements_map.has_key?(set_and_req_id)
119
+ row["#{suite.title} #{SHORT_ID_HEADER}"] = 'Not Tested'
120
+ row["#{suite.title} #{FULL_ID_HEADER}"] = 'Not Tested'
121
+ else
122
+ row["#{suite.title} #{SHORT_ID_HEADER}"] = short_ids&.join(', ')
123
+ row["#{suite.title} #{FULL_ID_HEADER}"] = full_ids&.join(', ')
124
+ end
125
+ else
126
+ row["#{suite.title} #{SHORT_ID_HEADER}"] = 'NA'
127
+ row["#{suite.title} #{FULL_ID_HEADER}"] = 'NA'
128
+ end
129
+ end
130
+
131
+ csv << row.values
132
+ end
133
+ end
134
+ end
135
+
136
+ def input_requirement_ids
137
+ @input_requirement_ids ||= input_rows.map { |row| "#{row['Req Set']}@#{row['ID']}" }
138
+ end
139
+
140
+ # The requirements present in Inferno that aren't in the input spreadsheet
141
+ def unmatched_requirements_map
142
+ @unmatched_requirements_map ||= inferno_requirements_map.filter do |requirement_id, _|
143
+ !input_requirement_ids.include?(requirement_id)
144
+ end
145
+ end
146
+
147
+ def old_csv
148
+ @old_csv ||= File.read(OUTPUT_FILE)
149
+ end
150
+
151
+ def run
152
+ unless File.exist?(INPUT_FILE)
153
+ puts "Could not find input file: #{INPUT_FILE}. Aborting requirements coverage generation..."
154
+ exit(1)
155
+ end
156
+
157
+ if unmatched_requirements_map.any?
158
+ puts "WARNING: The following requirements indicated in the test kit are not present in #{INPUT_FILE_NAME}"
159
+ output_requirements_map_table(unmatched_requirements_map)
160
+ end
161
+
162
+ if File.exist?(OUTPUT_FILE)
163
+ if old_csv == new_csv
164
+ puts "'#{OUTPUT_FILE_NAME}' file is up to date."
165
+ return
166
+ else
167
+ puts 'Requirements coverage has changed.'
168
+ end
169
+ else
170
+ puts "No existing #{OUTPUT_FILE_NAME}."
171
+ end
172
+
173
+ puts "Writing to file #{OUTPUT_FILE}..."
174
+ File.write(OUTPUT_FILE, new_csv)
175
+ puts 'Done.'
176
+ end
177
+
178
+ def run_check
179
+ unless File.exist?(INPUT_FILE)
180
+ puts "Could not find input file: #{INPUT_FILE}. Aborting requirements coverage check..."
181
+ exit(1)
182
+ end
183
+
184
+ if unmatched_requirements_map.any?
185
+ puts "The following requirements indicated in the test kit are not present in #{INPUT_FILE_NAME}"
186
+ output_requirements_map_table(unmatched_requirements_map)
187
+ end
188
+
189
+ if File.exist?(OUTPUT_FILE)
190
+ if old_csv == new_csv
191
+ puts "'#{OUTPUT_FILE_NAME}' file is up to date."
192
+ return unless unmatched_requirements_map.any?
193
+ else
194
+ puts <<~MESSAGE
195
+ #{OUTPUT_FILE_NAME} file is out of date.
196
+ To regenerate the file, run:
197
+
198
+ bundle exec rake requirements:generate_coverage
199
+
200
+ MESSAGE
201
+ end
202
+ else
203
+ puts <<~MESSAGE
204
+ No existing #{OUTPUT_FILE_NAME} file.
205
+ To generate the file, run:
206
+
207
+ bundle exec rake requirements:generate_coverage
208
+
209
+ MESSAGE
210
+ end
211
+
212
+ puts 'Check failed.'
213
+ exit(1)
214
+ end
215
+
216
+ def map_group_requirements(group, suite_id, requirements_map)
217
+ serialize_requirements(group, group.short_id, suite_id, requirements_map)
218
+ group.tests&.each { |test| serialize_requirements(test, test.short_id, suite_id, requirements_map) }
219
+ group.groups&.each { |subgroup| map_group_requirements(subgroup, suite_id, requirements_map) }
220
+ end
221
+
222
+ def serialize_requirements(runnable, short_id, suite_id, requirements_map)
223
+ runnable.verifies_requirements&.each do |requirement_id|
224
+ requirement_id_string = requirement_id.to_s
225
+
226
+ requirements_map[requirement_id_string] ||= []
227
+ requirements_map[requirement_id_string] << { short_id:, full_id: runnable.id, suite_id: }
228
+ end
229
+ end
230
+
231
+ # Output the requirements in the map like so:
232
+ #
233
+ # requirement_id | short_id | full_id
234
+ # ---------------+------------+----------
235
+ # req-id-1 | short-id-1 | full-id-1
236
+ # req-id-2 | short-id-2 | full-id-2
237
+ def output_requirements_map_table(requirements_map)
238
+ headers = %w[requirement_id short_id full_id]
239
+ col_widths = headers.map(&:length)
240
+ col_widths[0] = [col_widths[0], requirements_map.keys.map(&:length).max].max
241
+ col_widths[1] = ([col_widths[1]] + requirements_map.values.flatten.map { |item| item[:short_id].length }).max
242
+ col_widths[2] = ([col_widths[2]] + requirements_map.values.flatten.map { |item| item[:full_id].length }).max
243
+ col_widths.map { |width| width + 3 }
244
+
245
+ puts [
246
+ headers[0].ljust(col_widths[0]),
247
+ headers[1].ljust(col_widths[1]),
248
+ headers[2].ljust(col_widths[2])
249
+ ].join(' | ')
250
+ puts col_widths.map { |width| '-' * width }.join('-+-')
251
+ requirements_map.each do |requirement_id, runnables|
252
+ runnables.each do |runnable|
253
+ puts [
254
+ requirement_id.ljust(col_widths[0]),
255
+ runnable[:short_id].ljust(col_widths[1]),
256
+ runnable[:full_id].ljust(col_widths[2])
257
+ ].join(' | ')
258
+ end
259
+ end
260
+ puts
261
+ end
262
+ end
263
+ end
264
+ end
@@ -0,0 +1,310 @@
1
+ module SubscriptionsTestKit
2
+ module NotificationConformanceVerification
3
+ def no_error_verification(message)
4
+ assert messages.none? { |msg| msg[:type] == 'error' }, message
5
+ end
6
+
7
+ def find_all_elems(resource_array, param_name)
8
+ resource_array.select do |param|
9
+ param.name == param_name
10
+ end
11
+ end
12
+
13
+ def find_elem(resource_array, param_name)
14
+ resource_array.find do |param|
15
+ param.name == param_name
16
+ end
17
+ end
18
+
19
+ def check_entry_request_and_response(entry, entry_num)
20
+ if entry.request.blank?
21
+ add_message('error', %(
22
+ The `entry.request` field is mandatory for history Bundles, but was not included in entry #{entry_num}))
23
+ end
24
+ return unless entry.response.blank?
25
+
26
+ add_message('error', %(
27
+ The `entry.response` field is mandatory for history Bundles, but was not included in entry #{entry_num}))
28
+ end
29
+
30
+ def check_history_bundle_request_response(bundle, subscription_status_entry, subscription_id)
31
+ check_entry_request_and_response(subscription_status_entry, 1)
32
+
33
+ unless subscription_status_entry.request.present? &&
34
+ (
35
+ subscription_id ?
36
+ subscription_status_entry.request.url.end_with?("Subscription/#{subscription_id}/$status") :
37
+ subscription_status_entry.request.url.match?(%r{Subscription/[^/]+/\$status\z})
38
+ )
39
+
40
+ add_message('error',
41
+ 'The SubscriptionStatus `request` SHALL be filled out to match a request to the $status operation')
42
+ end
43
+
44
+ bundle.entry.drop(1).each_with_index do |entry, index|
45
+ check_entry_request_and_response(entry, index + 2)
46
+ end
47
+ end
48
+
49
+ def parameters_verification(subscription_status)
50
+ resource_type = subscription_status.resourceType
51
+ if resource_type == 'Parameters'
52
+ resource_is_valid?(resource: subscription_status, profile_url: 'http://hl7.org/fhir/uv/subscriptions-backport/StructureDefinition/backport-subscription-status-r4')
53
+ else
54
+ add_message('error',
55
+ "Unexpected resource type: Expected `Parameters`. Got `#{resource_type}`")
56
+ end
57
+ end
58
+
59
+ def notification_verification(notification_bundle, notification_type, subscription_id: nil, status: nil)
60
+ assert_valid_json(notification_bundle)
61
+ bundle = FHIR.from_contents(notification_bundle)
62
+ assert bundle.present?, 'Not a FHIR resource'
63
+ assert_resource_type(:bundle, resource: bundle)
64
+ unless bundle.type == 'history'
65
+ add_message('error', "Notification should be a history Bundle, instead was #{bundle.type}")
66
+ end
67
+
68
+ if bundle.entry.empty?
69
+ add_message('error', 'Notification Bundle is empty.')
70
+ return
71
+ end
72
+ subscription_status_entry = bundle.entry[0]
73
+
74
+ check_history_bundle_request_response(bundle, subscription_status_entry, subscription_id)
75
+
76
+ subscription_status = subscription_status_entry.resource
77
+ parameters_verification(subscription_status)
78
+
79
+ return unless subscription_status.respond_to?(:parameter)
80
+
81
+ subscription_type = find_elem(subscription_status.parameter, 'type')
82
+
83
+ unless subscription_type.valueCode == notification_type
84
+ add_message('error', %(
85
+ The Subscription resource should have it's `type` set to '#{notification_type}', was
86
+ '#{subscription_type.valueCode}'))
87
+ end
88
+
89
+ return unless status.present?
90
+
91
+ subscription_param_status = find_elem(subscription_status.parameter, 'status')
92
+ return if subscription_param_status.valueCode == status
93
+
94
+ add_message('error', %(
95
+ The Subscription resource should have it's `status` set to '#{status}', was
96
+ '#{subscription_param_status.valueCode}'))
97
+ end
98
+
99
+ def empty_notification_event_references(notification_events)
100
+ empty_req_check = true
101
+ notification_events.each do |notification_event|
102
+ empty_req_check = notification_event.part.none? do |part|
103
+ part.name == 'focus' || part.name == 'additional-context'
104
+ end
105
+ break unless empty_req_check
106
+ end
107
+ empty_req_check
108
+ end
109
+
110
+ def empty_notification_verification(bundle_entries, notification_events)
111
+ unless bundle_entries.empty?
112
+ add_message('error', %(
113
+ When the content type is empty, notification bundles SHALL not contain Bundle.entry elements other than
114
+ the SubscriptionStatus for the notification.))
115
+ end
116
+
117
+ if notification_events.empty?
118
+ add_message('error', %(
119
+ Events are required for empty notifications, but the SubscriptionStatus does not contain event-notifications
120
+ ))
121
+ return
122
+ end
123
+
124
+ empty_req_check = empty_notification_event_references(notification_events)
125
+ return if empty_req_check
126
+
127
+ add_message('error', %(
128
+ When populating the SubscriptionStatus.notificationEvent structure for a notification with an empty
129
+ payload, a server SHALL NOT include references to resources))
130
+ end
131
+
132
+ def verify_id_only_notification_bundle_entries(bundle_entries)
133
+ bundle_entries.each do |entry|
134
+ if entry.resource.present?
135
+ add_message('error', 'Each Bundle.entry for id-only notification SHALL not contain the `resource` field')
136
+ end
137
+ if entry.fullUrl.blank?
138
+ add_message('error', %(
139
+ Each Bundle.entry for id-only notification SHALL contain a relevant resource URL in the fullUrl))
140
+ end
141
+ end
142
+ end
143
+
144
+ def verify_full_resource_notification_bundle_entries(bundle_entries)
145
+ bundle_entries.each do |entry|
146
+ resource_is_valid?(resource: entry.resource) if entry.resource.present?
147
+ end
148
+ end
149
+
150
+ def check_bundle_entry_reference(bundle_entries, reference)
151
+ referenced_entry = bundle_entries.find do |entry|
152
+ reference.include?("#{entry.resource.resourceType}/#{entry.resource.id}")
153
+ end
154
+ referenced_entry.present?
155
+ end
156
+
157
+ def check_notification_event_focus(focus_elem, bundle_entries)
158
+ if focus_elem.blank?
159
+ add_message('error', %(
160
+ When the content type is `full-resource`, notification bundles SHALL include references to
161
+ the appropriate focus resources in the SubscriptionStatus.notificationEvent.focus element))
162
+ else
163
+ unless check_bundle_entry_reference(bundle_entries, focus_elem.valueReference.reference)
164
+ add_message('error', %(
165
+ The Notification Bundle does not include a resource entry for the reference found in
166
+ SubscriptionStatus.notificationEvent.focus with id #{focus_elem.valueReference.reference}))
167
+ end
168
+ end
169
+ end
170
+
171
+ def check_notification_event_additional_context(additional_context_list, bundle_entries)
172
+ return if additional_context_list.empty?
173
+
174
+ additional_context_list.each do |additional_context|
175
+ next if check_bundle_entry_reference(bundle_entries, additional_context.valueReference.reference)
176
+
177
+ add_message('error', %(
178
+ The Notification Bundle does not include a resource entry for the reference found in
179
+ SubscriptionStatus.notificationEvent.additional-context with id
180
+ #{additional_context.valueReference.reference}))
181
+ end
182
+ end
183
+
184
+ def full_resource_notification_event_parameter_verification(notification_events, bundle_entries)
185
+ notification_events.each do |notification_event|
186
+ focus_elem = find_elem(notification_event.part, 'focus')
187
+ additional_context_list = find_all_elems(notification_event.part, 'additional-context')
188
+
189
+ check_notification_event_focus(focus_elem, bundle_entries)
190
+ check_notification_event_additional_context(additional_context_list, bundle_entries)
191
+ end
192
+ end
193
+
194
+ def id_only_notification_event_parameter_verification(notification_events, criteria_resource_type)
195
+ notification_events.each do |notification_event|
196
+ focus_elem = find_elem(notification_event.part, 'focus')
197
+
198
+ if focus_elem.blank?
199
+ add_message('error', %(
200
+ When the content type is `id-only`, notification bundles SHALL include references to
201
+ the appropriate focus resources in the SubscriptionStatus.notificationEvent.focus element))
202
+ break
203
+ else
204
+ break unless criteria_resource_type.present?
205
+
206
+ unless focus_elem.valueReference.reference.include?(criteria_resource_type)
207
+ add_message('error', %(
208
+ The SubscriptionStatus.notificationEvent.focus should include a reference to a #{criteria_resource_type}
209
+ resource, the resource type the Subscription is focused on))
210
+ break
211
+ end
212
+ end
213
+ end
214
+ end
215
+
216
+ def full_resource_notification_criteria_resource_check(bundle_entries, criteria_resource_type)
217
+ relevant_resource = bundle_entries.any? do |entry|
218
+ entry.resource.resourceType == criteria_resource_type || entry.request.url.include?(criteria_resource_type)
219
+ end
220
+ return if relevant_resource
221
+
222
+ add_message('error', %(
223
+ The notification bundle of type `full-resource` must include at least one #{criteria_resource_type}
224
+ resource in the entry.resource element.))
225
+ end
226
+
227
+ def subscription_criteria(subscription)
228
+ return unless subscription['_criteria']
229
+
230
+ criteria_extension = subscription['_criteria']['extension'].find do |ext|
231
+ ext['url'].ends_with?('/backport-filter-criteria')
232
+ end
233
+ criteria_extension['valueString'].split('?').first
234
+ end
235
+
236
+ def empty_event_notification_verification(notification_bundle)
237
+ assert_valid_json(notification_bundle)
238
+ bundle = FHIR.from_contents(notification_bundle)
239
+ assert bundle.present?, 'Not a FHIR resource'
240
+
241
+ subscription_status = bundle.entry[0].resource
242
+
243
+ parameter_topic = find_elem(subscription_status.parameter, 'topic')
244
+ if parameter_topic.present?
245
+ add_message('warning',
246
+ 'Parameters.parameter:topic.value[x]: This value SHOULD NOT be present when using empty payloads')
247
+ end
248
+
249
+ notification_events = find_all_elems(subscription_status.parameter, 'notification-event')
250
+ bundle_entries = bundle.entry.drop(1)
251
+
252
+ empty_notification_verification(bundle_entries, notification_events)
253
+ end
254
+
255
+ def full_resource_event_notification_verification(notification_bundle, criteria_resource_type)
256
+ assert_valid_json(notification_bundle)
257
+ bundle = FHIR.from_contents(notification_bundle)
258
+ assert bundle.present?, 'Not a FHIR resource'
259
+
260
+ subscription_status = bundle.entry[0].resource
261
+
262
+ parameter_topic = find_elem(subscription_status.parameter, 'topic')
263
+ if parameter_topic.blank?
264
+ add_message('warning', %(
265
+ Parameters.parameter:topic.value[x]: This value SHOULD be present when using full-resource payloads))
266
+ end
267
+
268
+ notification_events = find_all_elems(subscription_status.parameter, 'notification-event')
269
+ bundle_entries = bundle.entry.drop(1)
270
+
271
+ if criteria_resource_type.present?
272
+ full_resource_notification_criteria_resource_check(bundle_entries, criteria_resource_type)
273
+ end
274
+
275
+ if notification_events.empty?
276
+ add_message('error', %(
277
+ The notification event parameter must be present in `full-resource` notification bundles.))
278
+ else
279
+ full_resource_notification_event_parameter_verification(notification_events, bundle_entries)
280
+ end
281
+
282
+ verify_full_resource_notification_bundle_entries(bundle_entries)
283
+ end
284
+
285
+ def id_only_event_notification_verification(notification_bundle, criteria_resource_type)
286
+ assert_valid_json(notification_bundle)
287
+ bundle = FHIR.from_contents(notification_bundle)
288
+ assert bundle.present?, 'Not a FHIR resource'
289
+
290
+ subscription_status = bundle.entry[0].resource
291
+
292
+ parameter_topic = find_elem(subscription_status.parameter, 'topic')
293
+ add_message('info', %(
294
+ Parameters.parameter:topic.value[x] is #{'not ' if parameter_topic.blank?}populated in `id-only` Notification.
295
+ This value MAY be present when using id-only payloads))
296
+
297
+ notification_events = find_all_elems(subscription_status.parameter, 'notification-event')
298
+ bundle_entries = bundle.entry.drop(1)
299
+
300
+ if notification_events.empty?
301
+ add_message('error', %(
302
+ The notification event parameter must be present in `id-only` notification bundles.))
303
+ else
304
+ id_only_notification_event_parameter_verification(notification_events, criteria_resource_type)
305
+ end
306
+
307
+ verify_id_only_notification_bundle_entries(bundle_entries)
308
+ end
309
+ end
310
+ end
@@ -0,0 +1,87 @@
1
+ require_relative '../urls'
2
+
3
+ module SubscriptionsTestKit
4
+ module SubscriptionConformanceVerification
5
+ include URLs
6
+
7
+ def no_error_verification(message)
8
+ assert messages.none? { |msg| msg[:type] == 'error' }, message
9
+ end
10
+
11
+ def channel_field_matches?(subscription_channel, field_name, expected_entry)
12
+ subscription_channel[field_name].present? && subscription_channel[field_name] == expected_entry
13
+ end
14
+
15
+ def cross_version_extension?(url)
16
+ url.match?(%r{http://hl7\.org/fhir/[0-9]+\.[0-9]+/StructureDefinition/extension-Subscription\..+})
17
+ end
18
+
19
+ def check_extension(extensions)
20
+ extensions.each do |extension|
21
+ next unless cross_version_extension?(extension['url'])
22
+
23
+ add_message('warning', %(
24
+ Cross-version extensions SHOULD NOT be used on R4 subscriptions to describe any elements also described by
25
+ this guide, but found the #{extension['url']} extension on the Subscription resource
26
+ ))
27
+ end
28
+ end
29
+
30
+ def cross_version_extension_check(subscription)
31
+ subscription.each do |key, value|
32
+ if value.is_a?(Array) && key == 'extension'
33
+ check_extension(value)
34
+ elsif value.is_a?(Hash) || value.is_a?(Array)
35
+ cross_version_extension_check(value)
36
+ end
37
+ end
38
+ end
39
+
40
+ def subscription_verification(subscription_resource)
41
+ assert_valid_json(subscription_resource)
42
+ subscription = JSON.parse(subscription_resource)
43
+
44
+ subscription_channel = subscription['channel']
45
+ assert(channel_field_matches?(subscription_channel, 'type', 'rest-hook'), %(
46
+ The `type` field on the Subscription resource must be set to `rest-hook`, the `#{subscription_channel['type']}`
47
+ channel type is unsupported.))
48
+
49
+ subscription_resource = FHIR.from_contents(subscription.to_json)
50
+ assert_resource_type('Subscription', resource: subscription_resource)
51
+ assert_valid_resource(resource: subscription_resource,
52
+ profile_url: 'http://hl7.org/fhir/uv/subscriptions-backport/StructureDefinition/backport-subscription')
53
+
54
+ cross_version_extension_check(subscription)
55
+ subscription
56
+ end
57
+
58
+ def server_check_channel(subscription, access_token)
59
+ subscription_channel = subscription['channel']
60
+ unless channel_field_matches?(subscription_channel, 'endpoint', subscription_channel_url)
61
+ add_message('warning', %(
62
+ The subscription url was changed from #{subscription_channel['endpoint']} to
63
+ #{subscription_channel_url}))
64
+ subscription_channel['endpoint'] = subscription_channel_url
65
+ end
66
+
67
+ unless channel_field_matches?(subscription_channel, 'payload', 'application/json')
68
+ add_message('warning', %(
69
+ The `type` field on the Subscription resource should be set to `application/json`. Inferno will only accept
70
+ resources in requests with this content type.
71
+ ))
72
+ end
73
+
74
+ unless subscription_channel['header'].present? &&
75
+ subscription_channel['header'].include?("Authorization: Bearer #{access_token}")
76
+ add_message('warning', %(
77
+ Added the Authorization header field with a Bearer token set to #{access_token} to the `header` field on the
78
+ Subscription resource in order to connect successfully with the Inferno subscription channel.
79
+ ))
80
+ subscription['header'] = [] unless subscription['header'].present?
81
+ subscription['header'].append("Authorization: Bearer #{access_token}")
82
+ end
83
+ subscription['channel'] = subscription_channel
84
+ subscription
85
+ end
86
+ end
87
+ end