subscriptions_test_kit 0.12.2 → 0.13.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/lib/subscriptions_test_kit/common/notification_conformance_verification.rb +6 -4
  3. data/lib/subscriptions_test_kit/common/subscription_conformance_verification.rb +4 -2
  4. data/lib/subscriptions_test_kit/docs/subscriptions_r5_backport_r4_client_suite_description.md +39 -18
  5. data/lib/subscriptions_test_kit/docs/subscriptions_r5_backport_r4_server_suite_description.md +16 -4
  6. data/lib/subscriptions_test_kit/endpoints/subscription_create_endpoint.rb +5 -33
  7. data/lib/subscriptions_test_kit/endpoints/subscription_delete_endpoint.rb +38 -0
  8. data/lib/subscriptions_test_kit/endpoints/subscription_patch_endpoint.rb +69 -0
  9. data/lib/subscriptions_test_kit/endpoints/subscription_read_endpoint.rb +5 -5
  10. data/lib/subscriptions_test_kit/endpoints/subscription_status_endpoint.rb +12 -17
  11. data/lib/subscriptions_test_kit/endpoints/subscription_update_endpoint.rb +81 -0
  12. data/lib/subscriptions_test_kit/fhir_validation.rb +9 -0
  13. data/lib/subscriptions_test_kit/requirements/generated/subscriptions_r5_backport_r4_server_requirements_coverage.csv +2 -2
  14. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_client/common/subscription_simulation_utils.rb +105 -11
  15. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_client/fixtures/capability_statement.json +12 -0
  16. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_client/update_delete_workflow/conformance_verification/delete_verification_test.rb +33 -0
  17. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_client/update_delete_workflow/conformance_verification/patch_update_verification_test.rb +44 -0
  18. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_client/update_delete_workflow/conformance_verification/put_update_verification_test.rb +36 -0
  19. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_client/update_delete_workflow/conformance_verification/update_request_loading.rb +61 -0
  20. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_client/update_delete_workflow/update_delete_conformance_verification_group.rb +18 -0
  21. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_client/update_delete_workflow/update_delete_interaction_test.rb +95 -0
  22. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_client/update_delete_workflow_group.rb +40 -0
  23. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_client/workflow/conformance_verification/notification_input_verification_test.rb +3 -1
  24. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_client/workflow/conformance_verification/processing_attestation_test.rb +1 -0
  25. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_client_suite.rb +24 -2
  26. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/common/interaction/subscription_conformance_test.rb +3 -2
  27. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/common/subscription_creation.rb +1 -0
  28. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/common/subscription_update_delete.rb +43 -0
  29. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/update_delete/delete_test.rb +30 -0
  30. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/update_delete/update_patch_test.rb +33 -0
  31. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/update_delete/update_put_test.rb +32 -0
  32. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/update_delete_group.rb +27 -0
  33. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/workflow_group.rb +6 -2
  34. data/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server_suite.rb +18 -2
  35. data/lib/subscriptions_test_kit/tags.rb +2 -0
  36. data/lib/subscriptions_test_kit/version.rb +2 -2
  37. metadata +36 -6
@@ -7,9 +7,16 @@ module SubscriptionsTestKit
7
7
  # application/json, so we'll allow that. Disallow XML for now.
8
8
  DEFAULT_MIME_TYPE = 'application/fhir+json'
9
9
  ALLOWED_MIME_TYPES = [DEFAULT_MIME_TYPE, 'application/json'].freeze
10
+ JSON_PATCH_MIME_TYPE = 'application/json-patch+json'
10
11
 
11
12
  def notification_bundle_input(test_result)
12
- JSON.parse(test_result.input_json).find { |i| i['name'] == 'notification_bundle' }['value']
13
+ input = JSON.parse(test_result.input_json).find { |i| i['name'] == 'notification_bundle' }
14
+ return input['value'] if input.present?
15
+
16
+ # Tests that wait after the Subscription workflow (e.g., update and delete) do not take the notification
17
+ # bundle as an input, so fall back to the value stored for the session.
18
+ Inferno::Repositories::SessionData.new.load(test_session_id: test_result.test_session_id,
19
+ name: 'notification_bundle')
13
20
  end
14
21
 
15
22
  def client_access_token_input(test_result)
@@ -69,19 +76,63 @@ module SubscriptionsTestKit
69
76
  end
70
77
  return unless request
71
78
 
72
- begin
73
- if as_json
74
- # needed to access primitive extensions
75
- JSON.parse(request.response_body)
76
- else
77
- FHIR.from_contents(request.response_body)
78
- end
79
- rescue StandardError
80
- nil
81
- end
79
+ parse_subscription(request.response_body, as_json:)
80
+ end
81
+
82
+ # The created Subscription as modified by any subsequent successful updates, or nil if it has been deleted.
83
+ # Updates and deletes are only accepted during the update and delete interaction test (see
84
+ # subscription_change_allowed?), so each run of that test starts again from the created Subscription.
85
+ def find_current_subscription(test_session_id)
86
+ subscription = find_subscription(test_session_id)
87
+ return if subscription.nil?
88
+
89
+ # Only updates and deletes are tagged with the Subscription id, so load them together
90
+ delete_requests, update_requests = requests_repo.tagged_requests(test_session_id, [subscription.id])
91
+ .partition { |r| r.tags.include?(SUBSCRIPTION_DELETE_TAG) }
92
+ return if delete_requests.any? { |r| r.status.between?(200, 299) }
93
+
94
+ update_request = update_requests.reverse.find { |r| r.status == 200 }
95
+ return subscription unless update_request
96
+
97
+ parse_subscription(update_request.response_body)
98
+ end
99
+
100
+ # For endpoints: whether the waiting test accepts changes (updates and deletes) to the Subscription, which
101
+ # a test indicates with the accepts_subscription_changes config option
102
+ def subscription_change_allowed?
103
+ test.config.options[:accepts_subscription_changes] == true
82
104
  end
83
105
 
106
+ # For endpoints: reject a change to the Subscription requested while a different test is waiting
107
+ def reject_subscription_change
108
+ fhir_error_response(422, 'business-rule', %(
109
+ Inferno only accepts Subscription updates and deletes during the Demonstrate Subscription Update and
110
+ Deletion tests.
111
+ ).squish)
112
+ end
113
+
114
+ # For endpoints: respond with an error status and an OperationOutcome describing the error
115
+ def fhir_error_response(status, code, text = nil)
116
+ response.format = :json
117
+ response.status = status
118
+ response.body = operation_outcome('error', code, text).to_json
119
+ end
120
+
121
+ # Inferno does not act on status changes requested by the client, except to honor requests to turn
122
+ # the Subscription off
123
+ def current_subscription_status_code(subscription)
124
+ return 'off' if subscription.status == 'off'
125
+
126
+ determine_subscription_status_code(subscription.id)
127
+ end
128
+
129
+ # Memoized because the handshakes do not change while an endpoint responds to a request
84
130
  def determine_subscription_status_code(subscription_id)
131
+ @subscription_status_codes ||= {}
132
+ @subscription_status_codes[subscription_id] ||= handshake_status_code(subscription_id)
133
+ end
134
+
135
+ def handshake_status_code(subscription_id)
85
136
  handshakes = requests_repo.tagged_requests(test_run.test_session_id, [REST_HOOK_HANDSHAKE_NOTIFICATION_TAG])
86
137
  handshake = handshakes.filter { |h| notification_subscription_id(h.request_body) == subscription_id }.last
87
138
 
@@ -106,8 +157,51 @@ module SubscriptionsTestKit
106
157
  end
107
158
  end
108
159
 
160
+ # Checks that Inferno is able to simulate the requested Subscription.
161
+ # Returns an OperationOutcome describing the problem, or nil if the Subscription is supported.
162
+ def verify_subscription(subscription)
163
+ unless subscription.is_a? FHIR::Subscription
164
+ return operation_outcome('error', 'invalid', 'No recognized R4 Subscription in request body')
165
+ end
166
+
167
+ unless subscription.channel&.type == 'rest-hook'
168
+ return operation_outcome('error', 'business-rule', 'channel.type must be rest-hook')
169
+ end
170
+
171
+ unless valid_channel_endpoint?(subscription.channel&.endpoint)
172
+ return operation_outcome('error', 'value', 'channel.endpoint is not recognized as a conformant URL')
173
+ end
174
+
175
+ heartbeat_period = find_heartbeat_period(subscription)
176
+ operation_outcome('error', 'not-supported', 'heartbeatPeriod is not supported') unless heartbeat_period.nil?
177
+ end
178
+
109
179
  private
110
180
 
181
+ def parse_subscription(subscription_json, as_json: false)
182
+ if as_json
183
+ # needed to access primitive extensions
184
+ JSON.parse(subscription_json)
185
+ else
186
+ FHIR.from_contents(subscription_json)
187
+ end
188
+ rescue StandardError
189
+ nil
190
+ end
191
+
192
+ def find_heartbeat_period(subscription)
193
+ subscription.channel&.extension&.find do |e|
194
+ e.url == 'http://hl7.org/fhir/uv/subscriptions-backport/StructureDefinition/backport-heartbeat-period'
195
+ end
196
+ end
197
+
198
+ def valid_channel_endpoint?(url)
199
+ uri = URI.parse(url)
200
+ %w[http https].include?(uri.scheme)
201
+ rescue URI::InvalidURIError
202
+ false
203
+ end
204
+
111
205
  def requests_repo
112
206
  @requests_repo ||= Inferno::Repositories::Requests.new
113
207
  end
@@ -9,6 +9,9 @@
9
9
  "format": [
10
10
  "json"
11
11
  ],
12
+ "patchFormat": [
13
+ "application/json-patch+json"
14
+ ],
12
15
  "software": {
13
16
  "name": "Inferno Subscriptions Test Kit Client Suite"
14
17
  },
@@ -42,6 +45,15 @@
42
45
  },
43
46
  {
44
47
  "code": "create"
48
+ },
49
+ {
50
+ "code": "update"
51
+ },
52
+ {
53
+ "code": "patch"
54
+ },
55
+ {
56
+ "code": "delete"
45
57
  }
46
58
  ],
47
59
  "operation": [
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../common/subscription_simulation_utils'
4
+
5
+ module SubscriptionsTestKit
6
+ module SubscriptionsR5BackportR4Client
7
+ class DeleteVerificationTest < Inferno::Test
8
+ include SubscriptionSimulationUtils
9
+
10
+ id :subscriptions_r4_client_delete_verification
11
+ title 'Client Subscription Delete Conformance Verification'
12
+ description %(
13
+ This test verifies that the client under test sent a conformant
14
+ [delete](https://hl7.org/fhir/R4/http.html#delete) request for the Subscription it created
15
+ in this session to Inferno's simulated FHIR server.
16
+ )
17
+
18
+ run do
19
+ subscription = find_subscription(test_session_id)
20
+ skip_if(subscription.nil?, 'No Subscription was created in this session')
21
+
22
+ load_tagged_requests(SUBSCRIPTION_DELETE_TAG)
23
+ skip_if(requests.none?, 'Inferno did not receive a Subscription delete request')
24
+
25
+ delete_request = requests.find { |r| r.tags.include?(subscription.id) }
26
+ assert(delete_request.present?, %(
27
+ Subscription delete request(s) did not target the created Subscription with id #{subscription.id}.
28
+ Requested URL(s): #{requests.map { |r| "`#{r.url}`" }.join(', ')}
29
+ ))
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../../../common/subscription_conformance_verification'
4
+ require_relative 'update_request_loading'
5
+
6
+ module SubscriptionsTestKit
7
+ module SubscriptionsR5BackportR4Client
8
+ class PatchUpdateVerificationTest < Inferno::Test
9
+ include SubscriptionConformanceVerification
10
+ include UpdateRequestLoading
11
+
12
+ id :subscriptions_r4_client_patch_update_verification
13
+ title 'Client Subscription PATCH Update Conformance Verification'
14
+ description %(
15
+ This test verifies that the [patch](https://hl7.org/fhir/R4/http.html#patch) (`PATCH`) requests
16
+ that the client under test sent to Inferno for the Subscription it created during this session
17
+ are conformant. Specifically, that each request
18
+ - was a [JSON Patch](https://datatracker.ietf.org/doc/html/rfc6902) that Inferno was able to
19
+ apply to the Subscription, and
20
+ - resulted in a patched Subscription that is conformant to the [R4/B Topic-Based Subscription
21
+ profile](https://hl7.org/fhir/uv/subscriptions-backport/STU1.1/StructureDefinition-backport-subscription.html)
22
+ and meets other requirements placed on it by the IG.
23
+
24
+ Requests sent for other Subscription ids are not considered.
25
+
26
+ Inferno manages the `Subscription.status` and `Subscription.channel.payload` values itself (see the
27
+ *Subscription Update and Delete Interaction* test), so the values that the patch gives those elements
28
+ are not verified and not used by Inferno.
29
+
30
+ Inferno does not support XML Patch or FHIRPath Patch requests.
31
+ )
32
+
33
+ run do
34
+ load_update_requests('patch')
35
+
36
+ verify_update_requests do |patch_request|
37
+ # The response contains the Subscription with the patch applied, except for the status and
38
+ # payload values that Inferno manages
39
+ subscription_verification(patch_request.response_body)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../../../common/subscription_conformance_verification'
4
+ require_relative 'update_request_loading'
5
+
6
+ module SubscriptionsTestKit
7
+ module SubscriptionsR5BackportR4Client
8
+ class PutUpdateVerificationTest < Inferno::Test
9
+ include SubscriptionConformanceVerification
10
+ include UpdateRequestLoading
11
+
12
+ id :subscriptions_r4_client_put_update_verification
13
+ title 'Client Subscription PUT Update Conformance Verification'
14
+ description %(
15
+ This test verifies that the [update](https://hl7.org/fhir/R4/http.html#update) (`PUT`) requests
16
+ that the client under test sent to Inferno for the Subscription it created during this session
17
+ are conformant. Specifically, that each request
18
+ - was accepted by Inferno, which requires, for example, that the `id` of the Subscription in the request
19
+ body matches the `id` in the request URL, and
20
+ - contained a Subscription in the request body that is conformant to the [R4/B Topic-Based Subscription
21
+ profile](https://hl7.org/fhir/uv/subscriptions-backport/STU1.1/StructureDefinition-backport-subscription.html)
22
+ and meets other requirements placed on it by the IG.
23
+
24
+ Requests sent for other Subscription ids are not considered.
25
+ )
26
+
27
+ run do
28
+ load_update_requests('put')
29
+
30
+ verify_update_requests do |update_request|
31
+ subscription_verification(update_request.request_body)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../common/subscription_simulation_utils'
4
+
5
+ module SubscriptionsTestKit
6
+ module SubscriptionsR5BackportR4Client
7
+ module UpdateRequestLoading
8
+ include SubscriptionSimulationUtils
9
+
10
+ # Loads the update requests for the created Subscription that used the given HTTP method. Requests that
11
+ # targeted other ids are ignored. Skips if the client did not send any such requests.
12
+ def load_update_requests(verb)
13
+ subscription = find_subscription(test_session_id)
14
+ skip_if(subscription.nil?, 'No Subscription was created in this session')
15
+
16
+ update_requests = requests_repo.tagged_requests(test_session_id, [SUBSCRIPTION_UPDATE_TAG, subscription.id])
17
+ requests.concat(update_requests.select { |r| r.verb.casecmp?(verb) })
18
+ skip_if(requests.none?, %(
19
+ Inferno did not receive a #{verb.upcase} request for the created Subscription with id #{subscription.id}
20
+ ))
21
+
22
+ subscription
23
+ end
24
+
25
+ # Verifies each loaded update request, using the given block for the checks specific to the HTTP method.
26
+ # Every request must have been accepted by Inferno and pass those checks. Each message is prefixed with
27
+ # the number of the request that it is about.
28
+ def verify_update_requests(&)
29
+ requests.each.with_index(1) do |update_request, request_number|
30
+ verify_update_request(update_request, request_number, &)
31
+ end
32
+
33
+ no_error_verification("Subscription #{requests.first.verb.upcase} update request is not conformant.")
34
+ end
35
+
36
+ def verify_update_request(update_request, request_number)
37
+ first_message_index = messages.length
38
+ begin
39
+ assert(update_request.status == 200, %(
40
+ Inferno could not apply the request (status #{update_request.status}):
41
+ #{error_response_text(update_request.response_body)}
42
+ ))
43
+ yield update_request
44
+ rescue Inferno::Exceptions::AssertionException => e
45
+ add_message('error', e.message)
46
+ end
47
+
48
+ messages[first_message_index..].each do |message|
49
+ message[:message] = "Request #{request_number}: #{message[:message]}"
50
+ end
51
+ end
52
+
53
+ def error_response_text(response_body)
54
+ outcome = JSON.parse(response_body)
55
+ (outcome.dig('issue', 0, 'details', 'text') if outcome.is_a?(Hash)) || 'no details provided'
56
+ rescue JSON::ParserError, TypeError
57
+ 'no details provided'
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'conformance_verification/put_update_verification_test'
4
+ require_relative 'conformance_verification/patch_update_verification_test'
5
+ require_relative 'conformance_verification/delete_verification_test'
6
+
7
+ module SubscriptionsTestKit
8
+ module SubscriptionsR5BackportR4Client
9
+ class UpdateDeleteConformanceVerificationGroup < Inferno::TestGroup
10
+ id :subscriptions_r4_client_update_delete_conformance_verification
11
+ title 'Subscription Update and Delete Conformance Verification'
12
+
13
+ test from: :subscriptions_r4_client_put_update_verification, optional: true
14
+ test from: :subscriptions_r4_client_patch_update_verification, optional: true
15
+ test from: :subscriptions_r4_client_delete_verification, optional: true
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../common/subscription_simulation_utils'
4
+
5
+ module SubscriptionsTestKit
6
+ module SubscriptionsR5BackportR4Client
7
+ class UpdateDeleteInteractionTest < Inferno::Test
8
+ include URLs
9
+ include SubscriptionSimulationUtils
10
+
11
+ id :subscriptions_r4_client_update_delete_interaction
12
+ title 'Subscription Update and Delete Interaction'
13
+ description %(
14
+ During this test, Inferno will wait while the client system changes the Subscription
15
+ that it created during the *Demonstrate the Rest-Hook Subscription Workflow* tests.
16
+ Inferno accepts the FHIR [update](https://hl7.org/fhir/R4/http.html#update),
17
+ [patch](https://hl7.org/fhir/R4/http.html#patch) (JSON only), and [delete](https://hl7.org/fhir/R4/http.html#delete)
18
+ interactions and testers can demonstrate system support for any or all of them.
19
+
20
+ Additional notes:
21
+ - Inferno will not send additional notifications or handshakes based on the updated Subscription.
22
+ - Inferno will modify the `Subscription.channel.payload` value on the updated Subscription to
23
+ `application/fhir+json` unless it is `application/json`.
24
+ - Inferno manages the `Subscription.status` value and will ignore requested changes to it, unless
25
+ the requested status is `off`.
26
+ - Inferno only accepts updates and deletes of the Subscription while this test is waiting.
27
+ - Each run of this test starts from the Subscription as created during the *Demonstrate the Rest-Hook
28
+ Subscription Workflow* tests. Changes made during earlier runs of this test, including deletion,
29
+ are not carried over.
30
+ )
31
+
32
+ input :access_token,
33
+ title: 'Access Token',
34
+ description: %(
35
+ The bearer token that the client under test will use when making Subscription update, delete, and other
36
+ requests to Inferno's simulated Subscriptions server.
37
+ )
38
+ # Inferno's Subscription update and delete endpoints only accept requests while this test is waiting
39
+ config options: { accepts_subscription_changes: true }
40
+
41
+ output :confirmation_url
42
+ output :fail_url
43
+ output :subscription_url
44
+
45
+ run do
46
+ subscription = find_subscription(test_session_id)
47
+ skip_if(subscription.nil?, %(
48
+ No Subscription was created in this session. Run the *Demonstrate the Rest-Hook Subscription Workflow*
49
+ tests before running these tests.
50
+ ))
51
+
52
+ subscription_url = "#{fhir_subscription_url}/#{subscription.id}"
53
+ output(subscription_url:)
54
+ confirmation_url = "#{resume_pass_url_client}?test_run_identifier=#{access_token}"
55
+ output(confirmation_url:)
56
+ fail_url = "#{resume_fail_url_client}?test_run_identifier=#{access_token}"
57
+ output(fail_url:)
58
+
59
+ wait(
60
+ identifier: access_token,
61
+ timeout: 600,
62
+ message: <<~MESSAGE
63
+ Inferno will wait while the client updates and/or deletes the Subscription created earlier
64
+ in the session. The Subscription is in the state it was created in: changes made during
65
+ earlier runs of these tests, including deletion, have not been kept. Inferno accepts the FHIR [update](https://hl7.org/fhir/R4/http.html#update),
66
+ [patch](https://hl7.org/fhir/R4/http.html#patch) (JSON only), and [delete](https://hl7.org/fhir/R4/http.html#delete)
67
+ interactions and testers can demonstrate system support for any or all of them by making
68
+ a request using the corresponding HTTP verb against `#{subscription_url}`:
69
+
70
+ - **PUT**: Change the Subscription with the update interaction by sending
71
+ a PUT request containing an updated Subscription resource in the request body.
72
+ - **PATCH**: Change the Subscription with a patch interaction by sending
73
+ a PATCH request containing a JSON Patch (`Content-Type: application/json-patch+json`)
74
+ in the request body
75
+ - **DELETE**: Remove the Subscription with a delete interaction by sending a
76
+ DELETE request with no body.
77
+
78
+ All requests must have the Authorization header set to:
79
+
80
+ `Bearer #{access_token}`
81
+
82
+ At any point while this test is waiting, Inferno will respond to Subscription GET and $status requests
83
+ for the Subscription until it has been deleted.
84
+
85
+ Once the client has demonstrated the Subscription interactions it supports,
86
+ [click here to complete the test](#{confirmation_url})
87
+
88
+ If at any point something has gone wrong and the client is unable to continue,
89
+ [click here to fail the test](#{fail_url})
90
+ MESSAGE
91
+ )
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'update_delete_workflow/update_delete_interaction_test'
4
+ require_relative 'update_delete_workflow/update_delete_conformance_verification_group'
5
+
6
+ module SubscriptionsTestKit
7
+ module SubscriptionsR5BackportR4Client
8
+ class UpdateDeleteWorkflowGroup < Inferno::TestGroup
9
+ id :subscriptions_r4_client_update_delete_workflow
10
+ title 'Demonstrate Subscription Update and Deletion'
11
+ description %(
12
+ During this test, the tester will demonstrate the ability of the client
13
+ to update and/or delete the Subscription instance that it created
14
+ during the *Demonstrate the Rest-Hook Subscription Workflow* tests.
15
+ Those tests must be run first in the same session.
16
+
17
+ Inferno will wait for the client to send update and delete requests and
18
+ will then verify that the requests are conformant. Note that
19
+ - Update and delete requests are only accepted by Inferno while
20
+ this group is waiting.
21
+ - If this group is re-run, Inferno will forget about the previous
22
+ update and delete interactions sent by the Subscriptions client.
23
+ The Subscriptions client can interact with it as it was after
24
+ it was created. If the Subscriptions client cannot reactivate
25
+ the deleted Subscription and make updates to it again, re-run
26
+ the *Demonstrate the Rest-Hook Subscription Workflow* group
27
+ to create a new Subscription.
28
+ )
29
+ run_as_group
30
+ optional
31
+
32
+ group 'Subscription Update and Delete Interaction' do
33
+ id :subscriptions_r4_client_update_delete_workflow_interaction
34
+ test from: :subscriptions_r4_client_update_delete_interaction
35
+ end
36
+
37
+ group from: :subscriptions_r4_client_update_delete_conformance_verification
38
+ end
39
+ end
40
+ end
@@ -14,13 +14,15 @@ module SubscriptionsTestKit
14
14
  to the [R4 Topic-Based Subscription Notification Bundle
15
15
  profile](https://hl7.org/fhir/uv/subscriptions-backport/STU1.1/StructureDefinition-backport-subscription-notification-r4.html).
16
16
  The content of the notification will be based on the Bundle provided by the tester.
17
+ To ensure that the subscriptions client is driving its behavior only based on
18
+ standard information, custom extensions are not allowed within the notifcation Bundle.
17
19
  )
18
20
  simulation_verification
19
21
 
20
22
  run do
21
23
  load_tagged_requests(REST_HOOK_EVENT_NOTIFICATION_TAG)
22
24
  skip_if(requests.none?, 'Inferno did not send an event notification')
23
- notification_verification(request.request_body, 'event-notification')
25
+ notification_verification(request.request_body, 'event-notification', validator: :no_custom_extensions)
24
26
  no_error_verification('Notification bundle was not conformant, see error messages')
25
27
  end
26
28
  end
@@ -16,6 +16,7 @@ module SubscriptionsTestKit
16
16
  of what should happen when the client receives an event notification for the requested
17
17
  Subscription.
18
18
  )
19
+ attestation
19
20
 
20
21
  output :attest_true_url
21
22
  output :attest_false_url
@@ -1,8 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'subscriptions_r5_backport_r4_client/workflow_group'
4
+ require_relative 'subscriptions_r5_backport_r4_client/update_delete_workflow_group'
5
+ require_relative '../fhir_validation'
4
6
  require_relative '../endpoints/subscription_create_endpoint'
5
7
  require_relative '../endpoints/subscription_read_endpoint'
8
+ require_relative '../endpoints/subscription_update_endpoint'
9
+ require_relative '../endpoints/subscription_patch_endpoint'
10
+ require_relative '../endpoints/subscription_delete_endpoint'
6
11
  require_relative '../endpoints/subscription_status_endpoint'
7
12
 
8
13
  module SubscriptionsTestKit
@@ -43,10 +48,23 @@ module SubscriptionsTestKit
43
48
 
44
49
  # All FHIR validation requests will use this FHIR validator
45
50
  fhir_resource_validator do
46
- igs 'hl7.fhir.uv.subscriptions-backport#1.1.0'
51
+ igs SUBSCRIPTIONS_BACKPORT_IG
47
52
 
48
53
  exclude_message do |message|
49
- message.message.match?(/\A\S+: \S+: URL value '.*' does not resolve/)
54
+ message.message.match?(UNRESOLVED_URL_VALIDATION_MESSAGE)
55
+ end
56
+ end
57
+
58
+ # Used to validate tester-provided content so that it cannot rely on extensions not defined in the IG
59
+ fhir_resource_validator :no_custom_extensions do
60
+ igs SUBSCRIPTIONS_BACKPORT_IG
61
+
62
+ validation_context do
63
+ extensions [] # no extensions not in the spec
64
+ end
65
+
66
+ exclude_message do |message|
67
+ message.message.match?(UNRESOLVED_URL_VALIDATION_MESSAGE)
50
68
  end
51
69
  end
52
70
 
@@ -61,6 +79,9 @@ module SubscriptionsTestKit
61
79
 
62
80
  suite_endpoint :post, FHIR_SUBSCRIPTION_PATH, SubscriptionCreateEndpoint
63
81
  suite_endpoint :get, FHIR_SUBSCRIPTION_INSTANCE_PATH, SubscriptionReadEndpoint
82
+ suite_endpoint :put, FHIR_SUBSCRIPTION_INSTANCE_PATH, SubscriptionUpdateEndpoint
83
+ suite_endpoint :patch, FHIR_SUBSCRIPTION_INSTANCE_PATH, SubscriptionPatchEndpoint
84
+ suite_endpoint :delete, FHIR_SUBSCRIPTION_INSTANCE_PATH, SubscriptionDeleteEndpoint
64
85
  suite_endpoint :post, FHIR_SUBSCRIPTION_INSTANCE_STATUS_PATH, SubscriptionStatusEndpoint
65
86
  suite_endpoint :get, FHIR_SUBSCRIPTION_INSTANCE_STATUS_PATH, SubscriptionStatusEndpoint
66
87
  suite_endpoint :post, FHIR_SUBSCRIPTION_RESOURCE_STATUS_PATH, SubscriptionStatusEndpoint
@@ -74,6 +95,7 @@ module SubscriptionsTestKit
74
95
  end
75
96
 
76
97
  group from: :subscriptions_r4_client_workflow
98
+ group from: :subscriptions_r4_client_update_delete_workflow
77
99
  end
78
100
  end
79
101
  end
@@ -11,7 +11,8 @@ module SubscriptionsTestKit
11
11
  This test accepts a Subscription resource as an input and verifies that it is conformant to the
12
12
  [R4/B Topic-Based Subscription profile](https://hl7.org/fhir/uv/subscriptions-backport/STU1.1/StructureDefinition-backport-subscription.html)
13
13
  and that it is constructed to make supported notifications to Inferno's simulated
14
- Subscriptions client.
14
+ Subscriptions client. To ensure that the subscriptions server is driving its behavior only based on
15
+ standard information, custom extensions are not allowed within the provided Subscription.
15
16
 
16
17
  For the server to successfuly deliver notifications recognized by this test session, the Subscription `channel`
17
18
  element should have it's subelements populated with the following information:
@@ -65,7 +66,7 @@ module SubscriptionsTestKit
65
66
  updated_subscription = subscription.to_json
66
67
  output(updated_subscription:)
67
68
 
68
- subscription_verification(updated_subscription)
69
+ subscription_verification(updated_subscription, validator: :no_custom_extensions)
69
70
  no_error_verification('Subscription resource is not conformant.')
70
71
  end
71
72
  end
@@ -63,6 +63,7 @@ module SubscriptionsTestKit
63
63
  tags = ['subscription_creation']
64
64
  payload_type = subscription_payload_type(subscription)
65
65
  tags.append(payload_type) if payload_type.present?
66
+ tags.append('workflow') if config.options[:workflow_subscription]
66
67
 
67
68
  fhir_operation('/Subscription', body: subscription, tags:)
68
69
  assert_response_status(201)
@@ -0,0 +1,43 @@
1
+ module SubscriptionsTestKit
2
+ module SubscriptionsR5BackportR4Server
3
+ module SubscriptionUpdateDelete
4
+ def created_subscription_id
5
+ creation_requests = load_tagged_requests('subscription_creation', 'workflow').select { |r| r.status == 201 }
6
+ skip_if creation_requests.empty?, %(
7
+ No successful Subscription creation request has been made during the "Demonstrate the Subscription Workflow"
8
+ group. Run that group successfully before re-running this one.
9
+ )
10
+
11
+ JSON.parse(creation_requests.first.response_body)['id']
12
+ end
13
+
14
+ # Returns the raw JSON hash so that primitive extensions, such as the payload content extension on
15
+ # Subscription.channel.payload, are preserved
16
+ def read_subscription_json(subscription_id)
17
+ fhir_read(:subscription, subscription_id)
18
+ assert_response_status(200)
19
+ assert_valid_json(request.response_body)
20
+ JSON.parse(request.response_body)
21
+ end
22
+
23
+ # FHIR models drop primitive extensions, so send the raw JSON as the body of the update
24
+ def raw_subscription_resource(subscription_hash)
25
+ FHIR::Subscription.new(subscription_hash).tap do |resource|
26
+ resource.define_singleton_method(:to_json) { |*| subscription_hash.to_json }
27
+ end
28
+ end
29
+
30
+ def updated_reason(subscription, interaction)
31
+ "#{subscription['reason']} (updated by Inferno using #{interaction} at #{Time.now.utc.iso8601})"
32
+ end
33
+
34
+ def verify_reason_updated(subscription_id, expected_reason)
35
+ subscription = read_subscription_json(subscription_id)
36
+ assert(subscription['reason'] == expected_reason, %(
37
+ Expected the `reason` of the updated Subscription to be `#{expected_reason}`, but was
38
+ `#{subscription['reason']}`.
39
+ ))
40
+ end
41
+ end
42
+ end
43
+ end