subscriptions_test_kit 0.12.1 → 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 +40 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 10ffb662d80a36170cabd608e69c7a776d7aab12ed7c32e347e7b32f68d8b8dd
4
- data.tar.gz: 30b825cba5b772ed7265385ee247f3a22648aa07ea1d31dfb86917f1cee1a610
3
+ metadata.gz: 22bedb9e7ab1b3784b17117262e36654dd9bd9d652a2ae81db58abfcd6ca8b04
4
+ data.tar.gz: 72a233c69e0554ef66d8031504f3cbb962cef548e8507ecc95dbdd18ee94998b
5
5
  SHA512:
6
- metadata.gz: ae18d19fe351b026841bf6627195a09ad96156ddbc9fd28a36e9501a93c52a424cdba615edcd8c915f99e24572171633a3780e86936b5d4a6e82024e3d773978
7
- data.tar.gz: 020a331e5cd162b763a6adeb3ebb6076828f63104faa8849b5b464ccc5c53e1a740eb73b03c35c95ff4a8bab9e319924662b32e3af02a59d6bcca83ace512f14
6
+ metadata.gz: 6161dbfb63aa5950300c2d3a092acef3e42dc43c9e976b11d1491da3eaaa937fcb0cafa553ac0b0f76f8d906c228f2874b0bbb4cc3bc6309d3f12df92b5c6925
7
+ data.tar.gz: 0f5ced8f4e1ebf1d856d391cda49f1e5a50f90bd2e6c36e621ced9f839043ca78f8c55b05bd71533271f132937f9e9ac0ebfe3aae6a3f32f08082aedeb660de1
@@ -48,17 +48,19 @@ module SubscriptionsTestKit
48
48
  end
49
49
  end
50
50
 
51
- def parameters_verification(subscription_status)
51
+ def parameters_verification(subscription_status, validator: :default)
52
52
  resource_type = subscription_status.resourceType
53
53
  if resource_type == 'Parameters'
54
- resource_is_valid?(resource: subscription_status, profile_url: 'http://hl7.org/fhir/uv/subscriptions-backport/StructureDefinition/backport-subscription-status-r4')
54
+ resource_is_valid?(resource: subscription_status, validator:,
55
+ profile_url: 'http://hl7.org/fhir/uv/subscriptions-backport/StructureDefinition/backport-subscription-status-r4')
55
56
  else
56
57
  add_message('error',
57
58
  "Unexpected resource type: Expected `Parameters`. Got `#{resource_type}`")
58
59
  end
59
60
  end
60
61
 
61
- def notification_verification(notification_bundle, notification_type, subscription_id: nil, status: nil)
62
+ def notification_verification(notification_bundle, notification_type, subscription_id: nil, status: nil,
63
+ validator: :default)
62
64
  assert_valid_json(notification_bundle)
63
65
  bundle = FHIR.from_contents(notification_bundle)
64
66
  assert bundle.present?, 'Not a FHIR resource'
@@ -76,7 +78,7 @@ module SubscriptionsTestKit
76
78
  check_history_bundle_request_response(bundle, subscription_status_entry, subscription_id)
77
79
 
78
80
  subscription_status = subscription_status_entry.resource
79
- parameters_verification(subscription_status)
81
+ parameters_verification(subscription_status, validator:)
80
82
 
81
83
  return unless subscription_status.respond_to?(:parameter)
82
84
 
@@ -44,11 +44,13 @@ module SubscriptionsTestKit
44
44
  false
45
45
  end
46
46
 
47
- def subscription_verification(subscription_resource)
47
+ def subscription_verification(subscription_resource, validator: :default)
48
48
  assert_valid_json(subscription_resource)
49
49
  subscription = JSON.parse(subscription_resource)
50
+ assert(subscription.is_a?(Hash), 'The Subscription must be a JSON object.')
50
51
 
51
52
  subscription_channel = subscription['channel']
53
+ assert(subscription_channel.is_a?(Hash), 'The Subscription must include a `channel` element.')
52
54
  assert(channel_field_matches?(subscription_channel, 'type', 'rest-hook'), %(
53
55
  The `type` element on the Subscription resource must be set to `rest-hook`,
54
56
  the `#{subscription_channel['type']}` channel type is unsupported.))
@@ -62,7 +64,7 @@ module SubscriptionsTestKit
62
64
  end
63
65
  subscription_resource = FHIR.from_contents(subscription.to_json)
64
66
  assert_resource_type('Subscription', resource: subscription_resource)
65
- assert_valid_resource(resource: subscription_resource,
67
+ assert_valid_resource(resource: subscription_resource, validator:,
66
68
  profile_url: 'http://hl7.org/fhir/uv/subscriptions-backport/StructureDefinition/backport-subscription')
67
69
 
68
70
  cross_version_extension_check(subscription)
@@ -13,20 +13,26 @@ requirements and may change the test verification logic.
13
13
 
14
14
  ## Test Methodology
15
15
 
16
- For these tests Inferno simulates a Subscriptions server application. Inferno will wait for
17
- the client under test to request a Subscription and then will send notifications
18
- back to the client based on that Subscription using additional information provided by the tester.
19
- Using the interactions between Inferno and the client under test, Inferno will verify that
16
+ For these tests Inferno simulates a Subscriptions server application. The client contains
17
+ two groups:
18
+ - *Demonstrate the Rest-Hook Subscription Workflow*, which tests the client's ability to
19
+ create a Subscription using the rest-hook channel and receive notifications based on it.
20
+ - *Demonstrate Subscription Update and Deletion*, which tests the client's ability to
21
+ update and delete the Subscription created during the first group.
22
+
23
+ During each group, Inferno will wait for the client system to make requests against Inferno.
24
+ Inferno will react to those requests, triggering handshakes and notifications.
25
+ Using the interactions between Inferno and the client system, Inferno will verify that
20
26
  the client meets the obligations from the implementation guide both on individual interactions
21
27
  and in aggregate.
22
28
 
23
- Inferno does not implement a real data repository that recognizes resource-level changes
24
- and so relies on the tester to provide an event notification that Inferno will send back
25
- to the client under test. Inferno will manipulate this provided notification to generate other
26
- notifications, such as handshakes and `$status` operation responses, to send
27
- and respond with as directed by the IG. Inferno will verify that the provided event
28
- notification is conformant and that it matches the requirements for the Subscription
29
- sent by the client under test.
29
+ Inferno does not implement a real data repository that recognizes resource-level changes.
30
+ So, for the first group, Inferno relies on the tester to provide an event notification that
31
+ Inferno will send back to the client system. Inferno will manipulate this provided
32
+ notification to generate other notifications, such as handshakes and `$status` operation
33
+ responses, to send and respond with as directed by the IG. Inferno will verify that the
34
+ provided event notification is conformant and that it matches the requirements for the
35
+ Subscription sent by the client system.
30
36
 
31
37
  The content of all Subscription interactions sent by the client,
32
38
  as well as tester-provided notifications, will be checked
@@ -41,7 +47,7 @@ HL7® FHIR® resources are checked for conformance using the Java validator with
41
47
 
42
48
  The following inputs must be provided by the tester at a minimum to execute
43
49
  any tests in this suite:
44
- 1. *Access Token*: A `Bearer` token that the client under test will send in the
50
+ 1. *Access Token*: A `Bearer` token that the client system will send in the
45
51
  `Authorization` header of HTTP requests made against Inferno. Inferno uses the
46
52
  value to identify incoming requests that belong to the testing session.
47
53
  1. *Notification Bundle*: an `event-notification` conforming to the [R4 Topic-Based
@@ -49,7 +55,7 @@ any tests in this suite:
49
55
  Inferno uses this as the basis for responses and notifications that it will send
50
56
  to the client.
51
57
 
52
- All other details needed to interact with the client under test, such as where to send notifications,
58
+ All other details needed to interact with the client system, such as where to send notifications,
53
59
  will be determined from the subscription that the client will create. Once the testing start,
54
60
  Inferno will wait for a Subscription creation request and then send the relevant handshakes
55
61
  and notification requests to the client and verify the interaction.
@@ -81,8 +87,14 @@ To run the client tests against the server tests:
81
87
  1. Wait 10-15 seconds while the client session makes requests,
82
88
  then click the "Click here" link in the wait dialog to evaluate the notifications.
83
89
  1. Back in the client session, click the "click here to complete the test" link
84
- and respond to the attestation based on the results in the server session and review
85
- the results.
90
+ and respond to the attestation based on the results in the server session.
91
+ 1. The client session will then wait for the Subscription to be updated and deleted. The server
92
+ session's *Subscription Update and Deletion Verification* tests have already made these requests,
93
+ but they will have failed as Inferno only accepts these interactions when made during this test.
94
+ Return to the server session and re-run the *Subscription Update and Deletion Verification*
95
+ group.
96
+ 1. Once the server run has completed, return to the client session and click the "click here to
97
+ complete the test" link and review the results.
86
98
 
87
99
  NOTE: Inferno uses the `Bearer` token sent in the `Authorization` HTTP header
88
100
  to associate requests with sessions. If multiple concurrent sessions are configured
@@ -99,7 +111,7 @@ that appears when initiating a test run.
99
111
 
100
112
  ### Client Notification Access Token
101
113
 
102
- If the client under test needs Inferno to send a specific `Bearer` token in the HTTP `Authorization` header
114
+ If the client system needs Inferno to send a specific `Bearer` token in the HTTP `Authorization` header
103
115
  when sending notifications and it cannot be specified in the `channel.header` element of the Subscription
104
116
  instance that the client sends to Inferno, then a value can be provided here.
105
117
 
@@ -111,7 +123,7 @@ that the test kit covers and does not cover in the [Requirements
111
123
  Coverage](https://github.com/inferno-framework/subscriptions-test-kit/blob/main/lib/subscriptions_test_kit/requirements/generated/subscriptions-test-kit_requirements_coverage.csv)
112
124
  CSV document.
113
125
 
114
- Specific limitations to highlight include
126
+ Specific limitations to highlight include:
115
127
  - Inferno supports only the `rest-hook` channel type. Support for other channels may be added in the future.
116
128
  If there is a channel type that you would like to see verified, please
117
129
  [provide feedback](https://github.com/inferno-framework/subscriptions-test-kit/issues) to that effect.
@@ -123,4 +135,13 @@ Specific limitations to highlight include
123
135
  are defined within the SubscriptionTopic which is not available in FHIR R4.
124
136
  - When sending notifications, Inferno supports only JSON payloads and will always use `application/fhir+json`
125
137
  as the value of the `content-type` HTTP header, unless the client system explicitly asks for
126
- `application/json` using the `Subscription.channel.payload` element.
138
+ `application/json` using the `Subscription.channel.payload` element.
139
+ - Inferno supports Subscription `PATCH` requests only when they use JSON Patch. XML Patch and FHIRPath Patch
140
+ are not supported.
141
+ - Inferno does not support re-creating a deleted Subscription with a `PUT` request. Once the client deletes the
142
+ Subscription, Inferno responds to `PUT` and `PATCH` requests for it with `404 Not Found`, and the update
143
+ conformance verification tests will report those requests as failures.
144
+ - Inferno records each request it receives just after responding to it, and its simulated Subscription reflects
145
+ only the requests it has recorded. As a result, a request sent immediately after an update or delete, for
146
+ example in parallel or over a separate connection, may be answered as if that change had not happened yet.
147
+ If Inferno's responses appear inconsistent with earlier changes, pause briefly between requests.
@@ -24,13 +24,17 @@ Over the course of a test session, Inferno may request multiple Subscriptions to
24
24
  different features of the IG, e.g., different notification content types. Because the
25
25
  choice of what topics and specific features to implement is left to individual servers,
26
26
  Inferno does not dictate specific Subscription features or topics. Instead, the
27
- tester will provide Subsription instances that Inferno will submit, modified lightly if needed,
27
+ tester will provide Subscription instances that Inferno will submit, modified lightly if needed,
28
28
  e.g., to point to Inferno's notification endpoint, to the server
29
29
  as a part of the tests. In order for the tests to pass, the provided Subscription instances
30
30
  must themselves be conformant and consistent with the notifications subsequently sent,
31
31
  but no other requirements are placed on them,
32
32
  except those related to Inferno limitations (see the Current Limitations section below).
33
33
 
34
+ At the end of the session, Inferno will update the Subscription created during the Subscription
35
+ workflow tests using `PUT` and JSON Patch `PATCH` requests and then delete it, verifying that the
36
+ server supports these optional interactions.
37
+
34
38
  The content of all interactions sent by the server (responses and notifications),
35
39
  as well as tester-provided requests, will be checked
36
40
  for conformance to the Subscriptions IG requirements individually and used in
@@ -91,8 +95,14 @@ To run the server tests against the client tests:
91
95
  1. Wait 10-15 seconds while the client session makes requests,
92
96
  then click the "Click here" link in the wait dialog to evaluate the notifications.
93
97
  1. Back in the client session, click the "click here to complete the test" link
94
- and respond to the attestation
95
- 1. Meanwhile, the server tests will have completed and will be ready for review.
98
+ and respond to the attestation based on the results in the server session.
99
+ 1. The client session will then wait for the Subscription to be updated and deleted. The server
100
+ session's *Subscription Update and Deletion Verification* tests have already made these requests,
101
+ but they will have failed as Inferno only accepts these interactions when made during this test.
102
+ Return to the server session and re-run the *Subscription Update and Deletion Verification*
103
+ group.
104
+ 1. Once the server run has completed, return to the client session and click the "click here to
105
+ complete the test" link. Return to the server session to review the results.
96
106
 
97
107
  NOTE: Inferno uses the `Bearer` token sent in the `Authorization` HTTP header
98
108
  to associate requests with sessions. If multiple concurrent sessions are configured
@@ -177,4 +187,6 @@ Specific limitations to highlight include
177
187
  header using the `Subscription.channel.payload` element. Testers may instruct
178
188
  Inferno to request that `application/json` be used instead by placing it in the
179
189
  `Subscription.channel.payload` element of the Subscription they provide for Inferno
180
- to use.
190
+ to use.
191
+ - When verifying Subscription modification using `PATCH`, Inferno only uses JSON Patch. XML Patch and
192
+ FHIRPath Patch are not tested.
@@ -6,23 +6,25 @@ module SubscriptionsTestKit
6
6
  class SubscriptionCreateEndpoint < Inferno::DSL::SuiteEndpoint
7
7
  include SubscriptionsR5BackportR4Client::SubscriptionSimulationUtils
8
8
 
9
+ error_response_format :operation_outcome
10
+
9
11
  def test_run_identifier
10
12
  request.headers['authorization']&.delete_prefix('Bearer ')
11
13
  end
12
14
 
13
15
  def make_response
14
16
  response.format = :json
15
- response.status = 400
16
17
 
17
18
  begin
18
19
  subscription = FHIR.from_contents(request.body.string)
19
20
  rescue StandardError
20
- response.body = operation_outcome('error', 'invalid', 'No recognized R4 Subscription in request body').to_json
21
+ fhir_error_response(400, 'invalid', 'No recognized R4 Subscription in request body')
21
22
  return
22
23
  end
23
24
 
24
25
  verification_outcome = verify_subscription(subscription)
25
26
  if verification_outcome.present?
27
+ response.status = 400
26
28
  response.body = verification_outcome.to_json
27
29
  return
28
30
  end
@@ -34,7 +36,7 @@ module SubscriptionsTestKit
34
36
  subscription_hash = JSON.parse(existing_subscription_request.response_body)
35
37
  error_text = 'Inferno only supports one subscription per test run. Subscription already created with ' \
36
38
  "ID #{subscription_hash['id']}"
37
- response.body = operation_outcome('error', 'business-rule', error_text).to_json
39
+ fhir_error_response(400, 'business-rule', error_text)
38
40
  return
39
41
  end
40
42
 
@@ -60,35 +62,5 @@ module SubscriptionsTestKit
60
62
  def tags
61
63
  [SUBSCRIPTION_CREATE_TAG]
62
64
  end
63
-
64
- def verify_subscription(subscription)
65
- unless subscription.is_a? FHIR::Subscription
66
- return operation_outcome('error', 'invalid', 'No recognized R4 Subscription in request body')
67
- end
68
-
69
- unless subscription.channel&.type == 'rest-hook'
70
- return operation_outcome('error', 'business-rule', 'channel.type must be rest-hook')
71
- end
72
-
73
- unless valid_url?(subscription.channel&.endpoint)
74
- return operation_outcome('error', 'value', 'channel.endpoint is not recognized as a conformant URL')
75
- end
76
-
77
- heartbeat_period = find_heartbeat_period(subscription)
78
- operation_outcome('error', 'not-supported', 'heartbeatPeriod is not supported') unless heartbeat_period.nil?
79
- end
80
-
81
- def find_heartbeat_period(subscription)
82
- subscription.channel&.extension&.find do |e|
83
- e.url == 'http://hl7.org/fhir/uv/subscriptions-backport/StructureDefinition/backport-heartbeat-period'
84
- end
85
- end
86
-
87
- def valid_url?(url)
88
- uri = URI.parse(url)
89
- %w[http https].include?(uri.scheme)
90
- rescue URI::InvalidURIError
91
- false
92
- end
93
65
  end
94
66
  end
@@ -0,0 +1,38 @@
1
+ require_relative '../tags'
2
+ require_relative '../suites/subscriptions_r5_backport_r4_client/common/subscription_simulation_utils'
3
+
4
+ module SubscriptionsTestKit
5
+ class SubscriptionDeleteEndpoint < Inferno::DSL::SuiteEndpoint
6
+ include SubscriptionsR5BackportR4Client::SubscriptionSimulationUtils
7
+
8
+ error_response_format :operation_outcome
9
+
10
+ def test_run_identifier
11
+ request.headers['authorization']&.delete_prefix('Bearer ')
12
+ end
13
+
14
+ def make_response
15
+ unless subscription_change_allowed?
16
+ reject_subscription_change
17
+ return
18
+ end
19
+
20
+ subscription_id = request.params[:id]
21
+ subscription = find_subscription(test_run.test_session_id)
22
+
23
+ if subscription.present? && subscription.id == subscription_id
24
+ # Deleting an already deleted resource has no effect, so respond the same way
25
+ response.status = 204
26
+ else
27
+ fhir_error_response(404, 'not-found', "No subscription exists with ID #{subscription_id}")
28
+ end
29
+ end
30
+
31
+ # Rejected changes are left untagged so that they are not treated as part of the update and delete tests
32
+ def tags
33
+ return [] unless subscription_change_allowed?
34
+
35
+ [SUBSCRIPTION_DELETE_TAG, request.params[:id]]
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,69 @@
1
+ require 'hana'
2
+ require_relative 'subscription_update_endpoint'
3
+
4
+ module SubscriptionsTestKit
5
+ # Supports updates via JSON Patch (https://hl7.org/fhir/R4/http.html#patch). Other patch formats
6
+ # (XML Patch and FHIRPath Patch) are not supported.
7
+ class SubscriptionPatchEndpoint < SubscriptionUpdateEndpoint
8
+ # Not inherited from SubscriptionUpdateEndpoint, so it must be declared here as well
9
+ error_response_format :operation_outcome
10
+
11
+ def requested_subscription_hash(current_subscription)
12
+ # Media types are case-insensitive
13
+ unless request.content_type&.downcase&.start_with?(JSON_PATCH_MIME_TYPE)
14
+ fhir_error_response(415, 'not-supported',
15
+ "Inferno only supports JSON Patch requests with Content-Type '#{JSON_PATCH_MIME_TYPE}'")
16
+ return
17
+ end
18
+
19
+ patch = parse_json_patch(request.body.string)
20
+ if patch.nil?
21
+ fhir_error_response(400, 'invalid', 'Request body is not a valid JSON Patch document')
22
+ return
23
+ end
24
+
25
+ # Apply the patch to the Subscription as the client would see it on a read
26
+ current_status = current_subscription_status_code(current_subscription)
27
+ current_hash = current_subscription.source_hash.deep_dup.merge('status' => current_status)
28
+ apply_json_patch(patch, current_hash)
29
+ end
30
+
31
+ # The Subscription hash with the patch applied, or nil if an error response has been set
32
+ def apply_json_patch(patch, subscription_hash)
33
+ patched_hash = Hana::Patch.new(patch).apply(subscription_hash)
34
+ return patched_hash if patched_hash.is_a?(Hash)
35
+
36
+ fhir_error_response(422, 'processing', 'Applying the JSON Patch did not produce a JSON object')
37
+ nil
38
+ # Besides its own exceptions, Hana raises plain Ruby errors for some patches it cannot apply, e.g., an
39
+ # IndexError for a copy from a missing array index or a TypeError for a path through a number
40
+ rescue StandardError => e
41
+ fhir_error_response(422, 'processing', "Unable to apply JSON Patch: #{e.message}")
42
+ nil
43
+ end
44
+
45
+ def parse_json_patch(body)
46
+ patch = JSON.parse(body)
47
+ return unless patch.is_a?(Array) && patch.all? { |operation| valid_json_patch_operation?(operation) }
48
+
49
+ patch
50
+ rescue JSON::ParserError
51
+ nil
52
+ end
53
+
54
+ # Checks the members each operation requires (https://datatracker.ietf.org/doc/html/rfc6902#section-4)
55
+ # so that malformed operations are rejected here rather than raising non-patch errors when applied
56
+ def valid_json_patch_operation?(operation)
57
+ return false unless operation.is_a?(Hash) && operation['path'].is_a?(String)
58
+
59
+ case operation['op']
60
+ when 'add', 'replace', 'test'
61
+ operation.key?('value')
62
+ when 'move', 'copy'
63
+ operation['from'].is_a?(String)
64
+ else
65
+ operation['op'] == 'remove'
66
+ end
67
+ end
68
+ end
69
+ end
@@ -5,6 +5,8 @@ module SubscriptionsTestKit
5
5
  class SubscriptionReadEndpoint < Inferno::DSL::SuiteEndpoint
6
6
  include SubscriptionsR5BackportR4Client::SubscriptionSimulationUtils
7
7
 
8
+ error_response_format :operation_outcome
9
+
8
10
  def test_run_identifier
9
11
  request.headers['authorization']&.delete_prefix('Bearer ')
10
12
  end
@@ -13,15 +15,13 @@ module SubscriptionsTestKit
13
15
  response.format = :json
14
16
  subscription_id = request.params[:id]
15
17
 
16
- subscription = find_subscription(test_run.test_session_id)
18
+ subscription = find_current_subscription(test_run.test_session_id)
17
19
 
18
20
  if subscription.present? && subscription.id == subscription_id
19
- status_code = determine_subscription_status_code(subscription_id)
21
+ status_code = current_subscription_status_code(subscription)
20
22
  response.body = subscription.source_hash.merge('status' => status_code).to_json
21
23
  else
22
- response.status = 404
23
- response.body = operation_outcome('error', 'not-found',
24
- "No subscription exists with ID #{subscription_id}").to_json
24
+ fhir_error_response(404, 'not-found', "No subscription exists with ID #{subscription_id}")
25
25
  end
26
26
  end
27
27
 
@@ -5,33 +5,35 @@ module SubscriptionsTestKit
5
5
  class SubscriptionStatusEndpoint < Inferno::DSL::SuiteEndpoint
6
6
  include SubscriptionsR5BackportR4Client::SubscriptionSimulationUtils
7
7
 
8
+ error_response_format :operation_outcome
9
+
8
10
  def test_run_identifier
9
11
  request.headers['authorization']&.delete_prefix('Bearer ')
10
12
  end
11
13
 
12
14
  def make_response
13
15
  response.format = :json
14
- subscription = find_subscription(test_run.test_session_id)
16
+ subscription = find_current_subscription(test_run.test_session_id)
15
17
 
16
- unless subscription.present?
17
- not_found
18
+ subscription_id = request.params[:id]
19
+ if subscription.blank? || (subscription_id.present? && subscription_id != subscription.id)
20
+ fhir_error_response(404, 'not-found')
18
21
  return
19
22
  end
20
23
 
21
- subscription_id = request.params[:id]
24
+ status_code = current_subscription_status_code(subscription)
22
25
 
23
26
  # Handle resource-level status params
24
27
  unless subscription_id.present?
25
28
  begin
26
29
  params = FHIR.from_contents(request.body.string)
27
30
  rescue StandardError
28
- response.status = 400
29
- response.body = operation_outcome('error', 'invalid', 'Invalid Parameters in request body').to_json
31
+ fhir_error_response(400, 'invalid', 'Invalid Parameters in request body')
30
32
  return
31
33
  end
32
34
 
33
- unless subscription_params_match?(params, subscription)
34
- not_found
35
+ unless subscription_params_match?(params, subscription, status_code)
36
+ fhir_error_response(404, 'not-found')
35
37
  return
36
38
  end
37
39
  end
@@ -39,32 +41,25 @@ module SubscriptionsTestKit
39
41
  notification_json = notification_bundle_input(result)
40
42
  subscription_url = "#{base_subscription_url}/#{subscription.id}"
41
43
  subscription_topic = subscription.criteria
42
- status_code = determine_subscription_status_code(subscription_id)
43
44
  event_count = determine_event_count(test_run.test_session_id)
44
45
  response.status = 200
45
46
  response.body = derive_status_bundle(notification_json, subscription_url, subscription_topic, status_code,
46
47
  event_count, request.url).to_json
47
48
  end
48
49
 
49
- def subscription_params_match?(params, subscription)
50
+ def subscription_params_match?(params, subscription, status_code)
50
51
  id_params = find_params(params, 'id')
51
52
 
52
53
  return false if id_params&.any? && id_params&.none? { |p| p.valueString == subscription.id }
53
54
 
54
55
  status_params = find_params(params, 'status')
55
- subscription_status = determine_subscription_status_code(subscription.id)
56
- status_params.blank? || status_params.any? { |p| p.valueString == subscription_status }
56
+ status_params.blank? || status_params.any? { |p| p.valueString == status_code }
57
57
  end
58
58
 
59
59
  def tags
60
60
  [SUBSCRIPTION_STATUS_TAG]
61
61
  end
62
62
 
63
- def not_found
64
- response.status = 404
65
- response.body = operation_outcome('error', 'not-found').to_json
66
- end
67
-
68
63
  def find_params(params, name)
69
64
  params&.parameter&.filter { |p| p.name == name }
70
65
  end
@@ -0,0 +1,81 @@
1
+ require_relative '../tags'
2
+ require_relative '../suites/subscriptions_r5_backport_r4_client/common/subscription_simulation_utils'
3
+
4
+ module SubscriptionsTestKit
5
+ class SubscriptionUpdateEndpoint < Inferno::DSL::SuiteEndpoint
6
+ include SubscriptionsR5BackportR4Client::SubscriptionSimulationUtils
7
+
8
+ error_response_format :operation_outcome
9
+
10
+ def test_run_identifier
11
+ request.headers['authorization']&.delete_prefix('Bearer ')
12
+ end
13
+
14
+ def make_response
15
+ if subscription_change_allowed?
16
+ update_subscription
17
+ else
18
+ reject_subscription_change
19
+ end
20
+ end
21
+
22
+ def update_subscription
23
+ response.format = :json
24
+ subscription_id = request.params[:id]
25
+
26
+ current_subscription = find_current_subscription(test_run.test_session_id)
27
+ unless current_subscription.present? && current_subscription.id == subscription_id
28
+ fhir_error_response(404, 'not-found', "No subscription exists with ID #{subscription_id}")
29
+ return
30
+ end
31
+
32
+ # We have to manipulate the raw hash so that we don't lose the _payload primitive extension
33
+ subscription_hash = requested_subscription_hash(current_subscription)
34
+ return if subscription_hash.nil?
35
+
36
+ begin
37
+ subscription = FHIR.from_contents(subscription_hash.to_json)
38
+ rescue StandardError
39
+ subscription = nil
40
+ end
41
+
42
+ verification_outcome = verify_subscription(subscription)
43
+ if verification_outcome.present?
44
+ response.status = 400
45
+ response.body = verification_outcome.to_json
46
+ return
47
+ end
48
+
49
+ unless subscription.id == subscription_id
50
+ fhir_error_response(400, 'invalid', "Updated Subscription id '#{subscription.id}' does not match the id " \
51
+ "'#{subscription_id}' in the request URL")
52
+ return
53
+ end
54
+
55
+ subscription_hash['status'] = current_subscription_status_code(subscription)
56
+ subscription_hash['channel']['payload'] = actual_mime_type(subscription)
57
+ response.status = 200
58
+ response.body = subscription_hash.to_json
59
+ end
60
+
61
+ # The requested state of the Subscription as a raw hash, or nil if an error response has been set
62
+ def requested_subscription_hash(_current_subscription)
63
+ subscription_hash = begin
64
+ JSON.parse(request.body.string)
65
+ rescue JSON::ParserError
66
+ nil
67
+ end
68
+ return subscription_hash if subscription_hash.is_a?(Hash)
69
+
70
+ fhir_error_response(400, 'invalid', 'No recognized R4 Subscription in request body')
71
+ nil
72
+ end
73
+
74
+ # Rejected changes are left untagged so that they are not treated as part of the update and delete tests
75
+ def tags
76
+ return [] unless subscription_change_allowed?
77
+
78
+ [SUBSCRIPTION_UPDATE_TAG, request.params[:id]]
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SubscriptionsTestKit
4
+ # Values shared by the FHIR validators that the client and server suites define
5
+ # Validator definitions kept in the suites rather than pulled out into this module
6
+ # to keep them local to the suites even though identical.
7
+ SUBSCRIPTIONS_BACKPORT_IG = 'hl7.fhir.uv.subscriptions-backport#1.1.0'
8
+ UNRESOLVED_URL_VALIDATION_MESSAGE = /\A\S+: \S+: URL value '.*' does not resolve/
9
+ end
@@ -103,8 +103,8 @@ hl7.fhir.uv.subscriptions_1.1.0,122,https://hl7.org/fhir/uv/subscriptions-backpo
103
103
  hl7.fhir.uv.subscriptions_1.1.0,123,https://hl7.org/fhir/uv/subscriptions-backport/STU1.1/CapabilityStatement-backport-subscription-server-r4.html#summary,MAY support the $get-ws-binding-token operation,MAY,Server,,Not Tested,Used for Websocket channels that Inferno does not currently support,NA,NA
104
104
  hl7.fhir.uv.subscriptions_1.1.0,124,https://hl7.org/fhir/uv/subscriptions-backport/STU1.1/CapabilityStatement-backport-subscription-server-r4.html#summary,A Server SHALL be capable of returning a Subscription resource using: GET [base]/Subscription/[id],SHALL,Server,,,,"",""
105
105
  hl7.fhir.uv.subscriptions_1.1.0,125,https://hl7.org/fhir/uv/subscriptions-backport/STU1.1/CapabilityStatement-backport-subscription-server-r4.html#summary,A Server SHOULD be capable of creating a Subscription resource using either: POST [base]/Subscription or PUT [base]/Subscription/[id],SHOULD,Server,,,,"",""
106
- hl7.fhir.uv.subscriptions_1.1.0,126,https://hl7.org/fhir/uv/subscriptions-backport/STU1.1/CapabilityStatement-backport-subscription-server-r4.html#summary,A Server SHOULD be capable of modifying a Subscription resource using either: PUT [base]/Subscription/[id] or PATCH [base]/Subscription/[id],SHOULD,Server,,,,"",""
107
- hl7.fhir.uv.subscriptions_1.1.0,127,https://hl7.org/fhir/uv/subscriptions-backport/STU1.1/CapabilityStatement-backport-subscription-server-r4.html#summary,A Server SHOULD be capable of deleting a Subscription resource using: DELETE [base]/Subscription/[id],SHOULD,Server,,,,"",""
106
+ hl7.fhir.uv.subscriptions_1.1.0,126,https://hl7.org/fhir/uv/subscriptions-backport/STU1.1/CapabilityStatement-backport-subscription-server-r4.html#summary,A Server SHOULD be capable of modifying a Subscription resource using either: PUT [base]/Subscription/[id] or PATCH [base]/Subscription/[id],SHOULD,Server,,,,"7.01, 7.02","subscriptions_r5_backport_r4_server-subscriptions_r4_server_update_delete-subscriptions_r4_server_update_put, subscriptions_r5_backport_r4_server-subscriptions_r4_server_update_delete-subscriptions_r4_server_update_patch"
107
+ hl7.fhir.uv.subscriptions_1.1.0,127,https://hl7.org/fhir/uv/subscriptions-backport/STU1.1/CapabilityStatement-backport-subscription-server-r4.html#summary,A Server SHOULD be capable of deleting a Subscription resource using: DELETE [base]/Subscription/[id],SHOULD,Server,,,,7.03,subscriptions_r5_backport_r4_server-subscriptions_r4_server_update_delete-subscriptions_r4_server_delete
108
108
  hl7.fhir.uv.subscriptions_1.1.0,128,https://hl7.org/fhir/uv/subscriptions-backport/STU1.1/CapabilityStatement-backport-subscription-server-r4.html#summary,A Server [MAY] be capable of searching for Subscription resources using: GET [base]/Subscription/?[parameters],MAY,Server,,,,"",""
109
109
  hl7.fhir.uv.subscriptions_1.1.0,129,https://hl7.org/fhir/uv/subscriptions-backport/STU1.1/CapabilityStatement-backport-subscription-server-r4.html#summary,Search Parameter Summary: SHALL [support] url,SHALL,Server,,,,"",""
110
110
  hl7.fhir.uv.subscriptions_1.1.0,130,https://hl7.org/fhir/uv/subscriptions-backport/STU1.1/CapabilityStatement-backport-subscription-server-r4.html#summary,Search Parameter Summary: SHOULD [support] status,SHOULD,Server,,,,"",""