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.
- 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
|