subscriptions_test_kit 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +201 -0
- data/lib/inferno_requirements_tools/ext/inferno_core/runnable.rb +22 -0
- data/lib/inferno_requirements_tools/tasks/collect_requirements.rb +222 -0
- data/lib/inferno_requirements_tools/tasks/requirements_coverage.rb +264 -0
- data/lib/subscriptions_test_kit/common/notification_conformance_verification.rb +310 -0
- data/lib/subscriptions_test_kit/common/subscription_conformance_verification.rb +87 -0
- data/lib/subscriptions_test_kit/docs/samples/Subscription_empty.json +37 -0
- data/lib/subscriptions_test_kit/docs/samples/Subscription_full-resource.json +37 -0
- data/lib/subscriptions_test_kit/docs/samples/Subscription_id-only.json +38 -0
- data/lib/subscriptions_test_kit/docs/subscriptions_r5_backport_r4_client_suite_description.md +120 -0
- data/lib/subscriptions_test_kit/docs/subscriptions_r5_backport_r4_server_suite_description.md +170 -0
- data/lib/subscriptions_test_kit/endpoints/subscription_create_endpoint.rb +90 -0
- data/lib/subscriptions_test_kit/endpoints/subscription_read_endpoint.rb +32 -0
- data/lib/subscriptions_test_kit/endpoints/subscription_rest_hook_endpoint.rb +66 -0
- data/lib/subscriptions_test_kit/endpoints/subscription_status_endpoint.rb +70 -0
- data/lib/subscriptions_test_kit/igs/README.md +21 -0
- data/lib/subscriptions_test_kit/jobs/send_subscription_notifications.rb +130 -0
- data/lib/subscriptions_test_kit/requirements/generated/subscriptions-test-kit_requirements_coverage.csv +136 -0
- data/lib/subscriptions_test_kit/requirements/subscriptions-test-kit_out_of_scope_requirements.csv +23 -0
- data/lib/subscriptions_test_kit/requirements/subscriptions-test-kit_requirements.csv +145 -0
- data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_client/common/subscription_simulation_utils.rb +140 -0
- data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_client/fixtures/capability_statement.json +57 -0
- data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_client/workflow/conformance_verification/notification_input_payload_verification_test.rb +50 -0
- data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_client/workflow/conformance_verification/notification_input_verification_test.rb +25 -0
- data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_client/workflow/conformance_verification/processing_attestation_test.rb +38 -0
- data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_client/workflow/conformance_verification/subscription_verification_test.rb +28 -0
- data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_client/workflow/conformance_verification_group.rb +20 -0
- data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_client/workflow/interaction_test.rb +128 -0
- data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_client/workflow/interaction_verification/event_notification_verification_test.rb +40 -0
- data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_client/workflow/interaction_verification/handshake_notification_verification_test.rb +40 -0
- data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_client/workflow/interaction_verification_group.rb +16 -0
- data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_client/workflow_group.rb +33 -0
- data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_client_suite.rb +66 -0
- data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/common/interaction/creation_response_conformance_test.rb +51 -0
- data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/common/interaction/notification_delivery_test.rb +56 -0
- data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/common/interaction/subscription_conformance_test.rb +72 -0
- data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/common/interaction_group.rb +24 -0
- data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/common/interaction_verification/notification_conformance_test.rb +73 -0
- data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/common/interaction_verification_group.rb +19 -0
- data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/common/subscription_creation.rb +63 -0
- data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/common/subscription_status_operation.rb +58 -0
- data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/coverage/capability_statement/cs_conformance_test.rb +49 -0
- data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/coverage/capability_statement/topic_discovery_test.rb +106 -0
- data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/coverage/capability_statement_group.rb +21 -0
- data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/coverage/event_notification/empty_content/empty_conformance_test.rb +63 -0
- data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/coverage/event_notification/empty_content_group.rb +58 -0
- data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/coverage/event_notification/full_resource_content/full_resource_conformance_test.rb +68 -0
- data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/coverage/event_notification/full_resource_content_group.rb +58 -0
- data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/coverage/event_notification/id_only_content/id_only_conformance_test.rb +66 -0
- data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/coverage/event_notification/id_only_content_group.rb +58 -0
- data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/coverage/event_notification_group.rb +25 -0
- data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/coverage/handshake_heartbeat/handshake_conformance_test.rb +67 -0
- data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/coverage/handshake_heartbeat/heartbeat_conformance_test.rb +74 -0
- data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/coverage/handshake_heartbeat_group.rb +19 -0
- data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/coverage/status_operation/status_invocation_test.rb +43 -0
- data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/coverage/status_operation_group.rb +15 -0
- data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/coverage/subscription_rejection/reject_subscriptions_test.rb +181 -0
- data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/coverage/subscription_rejection_group.rb +21 -0
- data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/coverage_group.rb +26 -0
- data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/workflow_group.rb +27 -0
- data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server_suite.rb +79 -0
- data/lib/subscriptions_test_kit/tags.rb +10 -0
- data/lib/subscriptions_test_kit/urls.rb +37 -0
- data/lib/subscriptions_test_kit/version.rb +5 -0
- data/lib/subscriptions_test_kit.rb +3 -0
- 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
|