From 199c7dbe6ea0c05948101c6873c8963a89a9a015 Mon Sep 17 00:00:00 2001 From: alexd-leaporbit Date: Sat, 17 Oct 2026 09:56:30 -0400 Subject: [PATCH 1/5] ID-279: replace a mismatched Authorization header on server Subscriptions The server suite's Subscription check only looked for an exact "Authorization: Bearer " entry in channel.header. When the tester's Subscription already carried an Authorization header with a different token, Inferno appended a second Authorization header instead of replacing the first. Notification verification then compared the notification against the stale header and failed, even though the server sent the token Inferno asked for. server_check_channel now finds Authorization entries by header name, case-insensitively, and leaves the header untouched only when there is exactly one entry and it matches "Bearer " in the same form notification verification parses. Otherwise it removes every Authorization entry, keeps all other headers, and appends the correct one, with a warning that says whether the header was added or replaced. The warning no longer repeats the old token. A header field that is not a list is replaced with a list instead of raising an error, and entries that are not strings are left alone. The Subscription conformance test description now says Inferno replaces any other Authorization entry. Fixes #48 Co-Authored-By: Claude Opus 5.5 --- .../subscription_conformance_verification.rb | 34 +++- .../subscription_conformance_test.rb | 2 +- ...scription_conformance_verification_spec.rb | 171 +++++++++++++++++- 3 files changed, 194 insertions(+), 13 deletions(-) diff --git a/lib/subscriptions_test_kit/common/subscription_conformance_verification.rb b/lib/subscriptions_test_kit/common/subscription_conformance_verification.rb index 6753039..2835661 100644 --- a/lib/subscriptions_test_kit/common/subscription_conformance_verification.rb +++ b/lib/subscriptions_test_kit/common/subscription_conformance_verification.rb @@ -93,17 +93,33 @@ def server_check_channel(subscription, access_token) )) end - unless subscription_channel['header'].present? && - subscription_channel['header'].include?("Authorization: Bearer #{access_token}") - add_message('warning', %( - Added the Authorization header field with a Bearer token set to #{access_token} to the `header` field on the - Subscription resource in order to connect successfully with the Inferno subscription channel. - )) - subscription_channel['header'] = [] unless subscription_channel['header'].present? - subscription_channel['header'].append("Authorization: Bearer #{access_token}") - end + server_check_channel_authorization_header(subscription_channel, access_token) subscription['channel'] = subscription_channel subscription end + + def server_check_channel_authorization_header(subscription_channel, access_token) + headers = subscription_channel['header'].is_a?(Array) ? subscription_channel['header'] : [] + authorization_headers = headers.select { |header| channel_authorization_header?(header) } + return if authorization_headers.length == 1 && + channel_authorization_header_matches?(authorization_headers.first, access_token) + + action = authorization_headers.empty? ? 'Added' : 'Replaced' + add_message('warning', %( + #{action} the Authorization header field with a Bearer token set to `#{access_token}` in the `header` field on + the Subscription resource in order to connect successfully with the Inferno subscription channel. + )) + subscription_channel['header'] = (headers - authorization_headers) + ["Authorization: Bearer #{access_token}"] + end + + def channel_authorization_header?(header) + header.is_a?(String) && header.split(':', 2).first.to_s.strip.casecmp?('authorization') + end + + # Must agree with how notification header verification parses requested headers. + def channel_authorization_header_matches?(header, access_token) + name, _separator, value = header.partition(': ') + name.casecmp?('authorization') && value == "Bearer #{access_token}" + end end end diff --git a/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/common/interaction/subscription_conformance_test.rb b/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/common/interaction/subscription_conformance_test.rb index 7e1fb7e..85f6108 100644 --- a/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/common/interaction/subscription_conformance_test.rb +++ b/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/common/interaction/subscription_conformance_test.rb @@ -27,7 +27,7 @@ class SubscriptionConformanceTest < Inferno::Test `content-type` HTTP header on notifications. - The `header` element must include the `Authorization` header with a Bearer token set to the inputted Inferno access token to direct the server to send that header to identify the notifications are for this session. - Inferno will add the entry if it is not present. + Inferno will add the entry if it is not present, replacing any other `Authorization` entry. ) simulation_verification diff --git a/spec/subscriptions_test_kit/common/subscription_conformance_verification_spec.rb b/spec/subscriptions_test_kit/common/subscription_conformance_verification_spec.rb index 858d859..36f5397 100644 --- a/spec/subscriptions_test_kit/common/subscription_conformance_verification_spec.rb +++ b/spec/subscriptions_test_kit/common/subscription_conformance_verification_spec.rb @@ -197,9 +197,11 @@ def entity_result_message(runnable) end input :subscription_resource, :access_token + output :checked_subscription run do - server_check_channel(JSON.parse(subscription_resource), access_token) + checked_subscription = server_check_channel(JSON.parse(subscription_resource), access_token) + output(checked_subscription: checked_subscription.to_json) no_error_verification('There were verification errors') end end @@ -209,6 +211,11 @@ def entity_result_message(runnable) Inferno::Repositories::Tests.new.insert(test) end + def checked_headers(result) + checked_subscription = JSON.parse(result.output_json).find { |o| o['name'] == 'checked_subscription' }['value'] + JSON.parse(checked_subscription)['channel']['header'] + end + it 'warns if subscription does not contain the correct channel endpoint' do allow(test).to receive(:suite).and_return(suite) @@ -254,7 +261,43 @@ def entity_result_message(runnable) ) end - it 'warns if subscription does not contain the correct channel header' do + it 'does not warn or change the header if it already has the correct Authorization header' do + allow(test).to receive(:suite).and_return(suite) + + result = run(test, subscription_resource: subscription_resource.to_json, + access_token:) + expect(result.result).to eq('pass') + expect(entity_result_message(test)).to_not match(/Authorization header field/) + expect(checked_headers(result)).to eq(['Authorization: Bearer SAMPLE_TOKEN']) + end + + it 'accepts a correct Authorization header whose name has different casing' do + allow(test).to receive(:suite).and_return(suite) + + subscription_resource['channel']['header'] = ['authorization: Bearer SAMPLE_TOKEN'] + + result = run(test, subscription_resource: subscription_resource.to_json, + access_token:) + expect(result.result).to eq('pass') + expect(entity_result_message(test)).to_not match(/Authorization header field/) + expect(checked_headers(result)).to eq(['authorization: Bearer SAMPLE_TOKEN']) + end + + it 'warns and adds the Authorization header if the subscription has no header' do + allow(test).to receive(:suite).and_return(suite) + + subscription_resource['channel'].delete('header') + + result = run(test, subscription_resource: subscription_resource.to_json, + access_token:) + expect(result.result).to eq('pass') + expect(entity_result_message(test)).to match( + /Added the Authorization header field with a Bearer token set to `SAMPLE_TOKEN` in the `header` field/ + ) + expect(checked_headers(result)).to eq(['Authorization: Bearer SAMPLE_TOKEN']) + end + + it 'warns and replaces an Authorization header with a different token' do allow(test).to receive(:suite).and_return(suite) subscription_resource['channel']['header'] = ['Authorization: Bearer INCORRECT_TOKEN'] @@ -263,8 +306,130 @@ def entity_result_message(runnable) access_token:) expect(result.result).to eq('pass') expect(entity_result_message(test)).to match( - /Added the Authorization header field with a Bearer token set to SAMPLE_TOKEN to the `header` field/ + /Replaced the Authorization header field with a Bearer token set to `SAMPLE_TOKEN` in the `header` field/ + ) + expect(entity_result_message(test)).to_not include('INCORRECT_TOKEN') + expect(checked_headers(result)).to eq(['Authorization: Bearer SAMPLE_TOKEN']) + end + + it 'replaces an Authorization header regardless of the casing of its name' do + allow(test).to receive(:suite).and_return(suite) + + subscription_resource['channel']['header'] = ['AUTHORIZATION : Bearer INCORRECT_TOKEN'] + + result = run(test, subscription_resource: subscription_resource.to_json, + access_token:) + expect(result.result).to eq('pass') + expect(entity_result_message(test)).to match(/Replaced the Authorization header field/) + expect(checked_headers(result)).to eq(['Authorization: Bearer SAMPLE_TOKEN']) + end + + it 'replaces every Authorization header and keeps other headers' do + allow(test).to receive(:suite).and_return(suite) + + subscription_resource['channel']['header'] = [ + 'Authorization: Bearer SAMPLE_TOKEN', + 'X-Custom-Header: value', + 'authorization: Bearer INCORRECT_TOKEN' + ] + + result = run(test, subscription_resource: subscription_resource.to_json, + access_token:) + expect(result.result).to eq('pass') + expect(entity_result_message(test)).to match(/Replaced the Authorization header field/) + expect(checked_headers(result)).to eq(['X-Custom-Header: value', 'Authorization: Bearer SAMPLE_TOKEN']) + end + + [ + 'Authorization:Bearer SAMPLE_TOKEN', + 'Authorization: Bearer SAMPLE_TOKEN ', + 'Authorization : Bearer SAMPLE_TOKEN', + ' Authorization: Bearer SAMPLE_TOKEN' + ].each do |malformed_header| + it "replaces a malformed Authorization header with the correct token: '#{malformed_header}'" do + allow(test).to receive(:suite).and_return(suite) + + subscription_resource['channel']['header'] = [malformed_header] + + result = run(test, subscription_resource: subscription_resource.to_json, + access_token:) + expect(result.result).to eq('pass') + expect(entity_result_message(test)).to match(/Replaced the Authorization header field/) + expect(checked_headers(result)).to eq(['Authorization: Bearer SAMPLE_TOKEN']) + end + end + + it 'replaces duplicate correct Authorization headers with a single entry' do + allow(test).to receive(:suite).and_return(suite) + + subscription_resource['channel']['header'] = ['Authorization: Bearer SAMPLE_TOKEN'] * 2 + + result = run(test, subscription_resource: subscription_resource.to_json, + access_token:) + expect(result.result).to eq('pass') + expect(entity_result_message(test)).to match(/Replaced the Authorization header field/) + expect(checked_headers(result)).to eq(['Authorization: Bearer SAMPLE_TOKEN']) + end + + it 'replaces an Authorization header whose scheme Inferno does not recognize' do + allow(test).to receive(:suite).and_return(suite) + + subscription_resource['channel']['header'] = ['Authorization: bearer SAMPLE_TOKEN'] + + result = run(test, subscription_resource: subscription_resource.to_json, + access_token:) + expect(result.result).to eq('pass') + expect(entity_result_message(test)).to match(/Replaced the Authorization header field/) + expect(checked_headers(result)).to eq(['Authorization: Bearer SAMPLE_TOKEN']) + end + + it 'ignores header entries that are not strings when looking for the Authorization header' do + allow(test).to receive(:suite).and_return(suite) + + subscription_resource['channel']['header'] = ['', 42, 'Authorization: Bearer SAMPLE_TOKEN'] + + result = run(test, subscription_resource: subscription_resource.to_json, + access_token:) + expect(result.result).to eq('pass') + expect(checked_headers(result)).to eq(['', 42, 'Authorization: Bearer SAMPLE_TOKEN']) + end + + it 'adds the Authorization header if the header field is not a list' do + allow(test).to receive(:suite).and_return(suite) + + subscription_resource['channel']['header'] = 'Authorization: Bearer INCORRECT_TOKEN' + + result = run(test, subscription_resource: subscription_resource.to_json, + access_token:) + expect(result.result).to eq('pass') + expect(entity_result_message(test)).to match(/Added the Authorization header field/) + expect(checked_headers(result)).to eq(['Authorization: Bearer SAMPLE_TOKEN']) + end + + it 'keeps headers whose names only contain Authorization' do + allow(test).to receive(:suite).and_return(suite) + + subscription_resource['channel']['header'] = ['X-Authorization: Bearer SAMPLE_TOKEN', 'Authorization-Info: value'] + + result = run(test, subscription_resource: subscription_resource.to_json, + access_token:) + expect(result.result).to eq('pass') + expect(entity_result_message(test)).to match(/Added the Authorization header field/) + expect(checked_headers(result)).to eq( + ['X-Authorization: Bearer SAMPLE_TOKEN', 'Authorization-Info: value', 'Authorization: Bearer SAMPLE_TOKEN'] ) end + + it 'accepts a correct Authorization header whose token contains a colon' do + allow(test).to receive(:suite).and_return(suite) + + subscription_resource['channel']['header'] = ['Authorization: Bearer abc:def'] + + result = run(test, subscription_resource: subscription_resource.to_json, + access_token: 'abc:def') + expect(result.result).to eq('pass') + expect(entity_result_message(test)).to_not match(/Authorization header field/) + expect(checked_headers(result)).to eq(['Authorization: Bearer abc:def']) + end end end From 0bf1f87420bf92a39582d4060fd0be36d69faf88 Mon Sep 17 00:00:00 2001 From: alexd-leaporbit Date: Tue, 20 Oct 2026 13:35:42 -0400 Subject: [PATCH 2/5] ID-279: pin GitHub Actions runners to ubuntu-24.04 The CI and execution-script workflows ran on ubuntu-latest, which moves to a new Ubuntu release whenever GitHub updates the label and was causing CI problems. Every job in ruby.yml and run_inferno_execution_scripts.yml now runs on ubuntu-24.04, which the maintainer asked all in-progress PRs to use. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ruby.yml | 8 ++++---- .github/workflows/run_inferno_execution_scripts.yml | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index afac92d..02fd760 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -8,7 +8,7 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 strategy: matrix: ruby-version: ['3.3.6'] @@ -23,7 +23,7 @@ jobs: test: needs: build - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 strategy: matrix: ruby-version: ['3.3.6'] @@ -39,7 +39,7 @@ jobs: lint: needs: build - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 strategy: matrix: ruby-version: ['3.3.6'] @@ -55,7 +55,7 @@ jobs: requirements: needs: build - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 strategy: matrix: ruby-version: ['3.3.6'] diff --git a/.github/workflows/run_inferno_execution_scripts.yml b/.github/workflows/run_inferno_execution_scripts.yml index 501b003..dee497a 100644 --- a/.github/workflows/run_inferno_execution_scripts.yml +++ b/.github/workflows/run_inferno_execution_scripts.yml @@ -12,7 +12,7 @@ on: jobs: find-execution-scripts: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 outputs: scripts: ${{ steps.list.outputs.scripts }} steps: @@ -24,7 +24,7 @@ jobs: test: needs: find-execution-scripts - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 strategy: fail-fast: false # let all files run even if one fails matrix: From 0b85f92e32199e8f3c287d668c48672cd559a4f5 Mon Sep 17 00:00:00 2001 From: alexd-leaporbit Date: Tue, 20 Oct 2026 13:40:27 -0400 Subject: [PATCH 3/5] ID-279: handle malformed channel and header on server Subscriptions server_check_channel treated a channel.header that was not a list as empty, so a tester who wrote the header as a single string lost it without being told. A header string is now converted into a list with a warning saying Inferno did so, and the usual Authorization check then runs on that list. Any other non-list value is left unchanged and recorded as an error, so the Subscription conformance test fails instead of quietly replacing what the tester provided. server_check_channel also read channel.endpoint without checking that channel exists, so a Subscription with no channel, or with a channel that is not an object, raised an error. It now returns such a Subscription unchanged, and subscription_verification fails it with the existing "must include a channel element" message. The warning about a changed channel endpoint is now a squiggly heredoc, so an endpoint value containing a newline no longer turns the message into a code block. The Subscription conformance test description now says that header must be a list of strings, that a single string is converted and any other value fails, and that a type other than rest-hook fails the test rather than being corrected. Co-Authored-By: Claude Opus 5.5 --- .../subscription_conformance_verification.rb | 37 +++++++-- .../subscription_conformance_test.rb | 8 +- ...scription_conformance_verification_spec.rb | 76 ++++++++++++++++++- .../subscription_conformance_test_spec.rb | 30 ++++++++ 4 files changed, 141 insertions(+), 10 deletions(-) diff --git a/lib/subscriptions_test_kit/common/subscription_conformance_verification.rb b/lib/subscriptions_test_kit/common/subscription_conformance_verification.rb index 2835661..0fb392a 100644 --- a/lib/subscriptions_test_kit/common/subscription_conformance_verification.rb +++ b/lib/subscriptions_test_kit/common/subscription_conformance_verification.rb @@ -72,10 +72,13 @@ def subscription_verification(subscription_resource, validator: :default) def server_check_channel(subscription, access_token) subscription_channel = subscription['channel'] + return subscription unless subscription_channel.is_a?(Hash) + unless channel_field_matches?(subscription_channel, 'endpoint', subscription_channel_url) - add_message('warning', %( - The subscription url was changed from #{subscription_channel['endpoint']} to - #{subscription_channel_url})) + add_message('warning', <<~MESSAGE) + The subscription url was changed from #{subscription_channel['endpoint']} to + #{subscription_channel_url} + MESSAGE subscription_channel['endpoint'] = subscription_channel_url end @@ -99,7 +102,9 @@ def server_check_channel(subscription, access_token) end def server_check_channel_authorization_header(subscription_channel, access_token) - headers = subscription_channel['header'].is_a?(Array) ? subscription_channel['header'] : [] + headers = server_channel_header_list(subscription_channel, access_token) + return if headers.nil? + authorization_headers = headers.select { |header| channel_authorization_header?(header) } return if authorization_headers.length == 1 && channel_authorization_header_matches?(authorization_headers.first, access_token) @@ -112,11 +117,33 @@ def server_check_channel_authorization_header(subscription_channel, access_token subscription_channel['header'] = (headers - authorization_headers) + ["Authorization: Bearer #{access_token}"] end + def server_channel_header_list(subscription_channel, access_token) + headers = subscription_channel['header'] + case headers + when nil + [] + when Array + headers + when String + add_message('warning', %( + The `header` field on the Subscription resource must be a list of strings. Inferno converted the single + string value into a list containing that value. + )) + subscription_channel['header'] = [headers] + else + add_message('error', %( + The `header` field on the Subscription resource must be a list of strings, so Inferno could not add the + Authorization header field with a Bearer token set to `#{access_token}`. + )) + nil + end + end + def channel_authorization_header?(header) header.is_a?(String) && header.split(':', 2).first.to_s.strip.casecmp?('authorization') end - # Must agree with how notification header verification parses requested headers. + # Accepts only the canonical form, which notification header verification parses to the same name and value def channel_authorization_header_matches?(header, access_token) name, _separator, value = header.partition(': ') name.casecmp?('authorization') && value == "Bearer #{access_token}" diff --git a/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/common/interaction/subscription_conformance_test.rb b/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/common/interaction/subscription_conformance_test.rb index 85f6108..fe36e29 100644 --- a/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/common/interaction/subscription_conformance_test.rb +++ b/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/common/interaction/subscription_conformance_test.rb @@ -20,14 +20,16 @@ class SubscriptionConformanceTest < Inferno::Test `#{Inferno::Application['base_url']}/custom/subscriptions_r5_backport_r4_server#{SUBSCRIPTION_CHANNEL_PATH}`. The test will add the correct url to this field if it is not properly set. - The `type` element must be set to `rest-hook`, as the Inferno subscription workflow tests use a `rest-hook` - subscription channel to receive incoming notifications. The test will add the correct type to this field if it - is not properly set. + subscription channel to receive incoming notifications. The test will fail if this field is not set to + `rest-hook`. - The `payload` element should be `application/fhir+json`. Inferno will update it to that value unless the provided value is `application/json`, which Inferno also supports receiving as the `content-type` HTTP header on notifications. - The `header` element must include the `Authorization` header with a Bearer token set to the inputted Inferno access token to direct the server to send that header to identify the notifications are for this session. - Inferno will add the entry if it is not present, replacing any other `Authorization` entry. + Inferno will add the entry if it is not present, replacing any other `Authorization` entry. The `header` + element must be a list of strings: Inferno converts a single string into a list, and the test fails for any + other value. ) simulation_verification diff --git a/spec/subscriptions_test_kit/common/subscription_conformance_verification_spec.rb b/spec/subscriptions_test_kit/common/subscription_conformance_verification_spec.rb index 36f5397..50600a0 100644 --- a/spec/subscriptions_test_kit/common/subscription_conformance_verification_spec.rb +++ b/spec/subscriptions_test_kit/common/subscription_conformance_verification_spec.rb @@ -229,6 +229,17 @@ def checked_headers(result) ) end + it 'renders the channel endpoint warning as prose if the endpoint contains a newline' do + allow(test).to receive(:suite).and_return(suite) + + subscription_resource['channel']['endpoint'] = "https://incorrect-url.com\nhttps://other-url.com" + + run(test, subscription_resource: subscription_resource.to_json, access_token:) + message = entity_result_message(test) + expect(message).to match('The subscription url was changed from https://incorrect-url.com') + expect(message.lines.grep(/\A {4,}\S/)).to be_empty + end + it 'warns if subscription does not contain the correct channel payload' do allow(test).to receive(:suite).and_return(suite) @@ -394,7 +405,22 @@ def checked_headers(result) expect(checked_headers(result)).to eq(['', 42, 'Authorization: Bearer SAMPLE_TOKEN']) end - it 'adds the Authorization header if the header field is not a list' do + it 'warns and converts a header string with the correct Authorization header into a list' do + allow(test).to receive(:suite).and_return(suite) + + subscription_resource['channel']['header'] = 'Authorization: Bearer SAMPLE_TOKEN' + + result = run(test, subscription_resource: subscription_resource.to_json, + access_token:) + expect(result.result).to eq('pass') + expect(entity_result_message(test)).to match( + /The `header` field on the Subscription resource must be a list of strings. Inferno converted the single/ + ) + expect(entity_result_message(test)).to_not match(/Authorization header field/) + expect(checked_headers(result)).to eq(['Authorization: Bearer SAMPLE_TOKEN']) + end + + it 'converts a header string into a list and replaces an Authorization header with a different token' do allow(test).to receive(:suite).and_return(suite) subscription_resource['channel']['header'] = 'Authorization: Bearer INCORRECT_TOKEN' @@ -402,10 +428,56 @@ def checked_headers(result) result = run(test, subscription_resource: subscription_resource.to_json, access_token:) expect(result.result).to eq('pass') - expect(entity_result_message(test)).to match(/Added the Authorization header field/) + expect(entity_result_message(test)).to match(/Inferno converted the single/) + expect(entity_result_message(test)).to match(/Replaced the Authorization header field/) expect(checked_headers(result)).to eq(['Authorization: Bearer SAMPLE_TOKEN']) end + it 'converts a header string into a list and keeps it when adding the Authorization header' do + allow(test).to receive(:suite).and_return(suite) + + subscription_resource['channel']['header'] = 'X-Custom-Header: value' + + result = run(test, subscription_resource: subscription_resource.to_json, + access_token:) + expect(result.result).to eq('pass') + expect(entity_result_message(test)).to match(/Inferno converted the single/) + expect(entity_result_message(test)).to match(/Added the Authorization header field/) + expect(checked_headers(result)).to eq(['X-Custom-Header: value', 'Authorization: Bearer SAMPLE_TOKEN']) + end + + [{ 'Authorization' => 'Bearer SAMPLE_TOKEN' }, 42].each do |malformed_header| + it "fails and leaves the header unchanged if it is neither a list nor a string: #{malformed_header.inspect}" do + allow(test).to receive(:suite).and_return(suite) + + subscription_resource['channel']['header'] = malformed_header + + result = run(test, subscription_resource: subscription_resource.to_json, + access_token:) + expect(result.result).to eq('fail') + expect(entity_result_message(test)).to match( + /The `header` field on the Subscription resource must be a list of strings, so Inferno could not add the/ + ) + expect(checked_headers(result)).to eq(malformed_header) + end + end + + [nil, 'rest-hook', []].each do |malformed_channel| + it "returns the subscription unchanged if the channel is not an object: #{malformed_channel.inspect}" do + allow(test).to receive(:suite).and_return(suite) + + subscription_resource['channel'] = malformed_channel + subscription_resource.compact! + + result = run(test, subscription_resource: subscription_resource.to_json, + access_token:) + expect(result.result).to eq('pass') + expect(entity_result_message(test)).to be_empty + checked_subscription = JSON.parse(result.output_json).find { |o| o['name'] == 'checked_subscription' }['value'] + expect(JSON.parse(checked_subscription)).to eq(subscription_resource) + end + end + it 'keeps headers whose names only contain Authorization' do allow(test).to receive(:suite).and_return(suite) diff --git a/spec/subscriptions_test_kit/subscription_r4_server/common/interaction/subscription_conformance_test_spec.rb b/spec/subscriptions_test_kit/subscription_r4_server/common/interaction/subscription_conformance_test_spec.rb index 219ee5c..bd50ef7 100644 --- a/spec/subscriptions_test_kit/subscription_r4_server/common/interaction/subscription_conformance_test_spec.rb +++ b/spec/subscriptions_test_kit/subscription_r4_server/common/interaction/subscription_conformance_test_spec.rb @@ -98,5 +98,35 @@ def no_custom_extensions_request?(request) expect(result.result).to eq('fail') expect(verification_request).to have_been_made end + + [nil, 'rest-hook', []].each do |malformed_channel| + it "fails if the channel is not an object: #{malformed_channel.inspect}" do + allow(test).to receive(:suite).and_return(suite) + + subscription_resource['channel'] = malformed_channel + subscription_resource.compact! + + result = run(test, subscription_resource: subscription_resource.to_json, + access_token:) + + expect(result.result).to eq('fail') + expect(result.result_message).to eq('The Subscription must include a `channel` element.') + end + end + + it 'fails if the channel header is neither a list nor a string' do + allow(test).to receive(:suite).and_return(suite) + + verification_request = stub_request(:post, validation_url) + .to_return(status: 200, body: operation_outcome_success.to_json) + subscription_resource['channel']['header'] = { 'Authorization' => 'Bearer SAMPLE_TOKEN' } + + result = run(test, subscription_resource: subscription_resource.to_json, + access_token:) + + expect(result.result).to eq('fail') + expect(result.result_message).to eq('Subscription resource is not conformant.') + expect(verification_request).to have_been_made + end end end From 4a58e7bfc5b95bd29b07ae992f861ddf89a0be37 Mon Sep 17 00:00:00 2001 From: alexd-leaporbit Date: Tue, 20 Oct 2026 13:46:28 -0400 Subject: [PATCH 4/5] ID-279: check notification headers against the submitted Subscription The event notification conformance test took the requested headers from the Subscription the server returned when it was created. The IG requires each channel.header value of the Subscription to be sent on notifications, but does not require a server to echo those headers back. A server that left header out of its response made the test raise an error. A server that returned different header values had the notification compared against those values instead of the ones Inferno asked for, including the Authorization header. notification_header_verification takes a new optional requested_subscription keyword, which defaults to the subscription argument so existing callers are unaffected. The notification conformance test passes the Subscription Inferno submitted (the creation request body), so the requested headers always come from what Inferno asked for, while the content type is still checked against the payload the server returned. The test now fails with a clear message when the returned Subscription is not a JSON object. Requested header strings are parsed the way HTTP defines header fields: the name ends at the first colon, whitespace around the value is ignored, and names are compared case-insensitively. Previously a header written without a space after the colon, such as "X-Custom:value", could never match the header the server sent. A missing or malformed channel or header, entries that are not strings, blank header names, and a payload that is not a string are skipped instead of raising an error. Co-Authored-By: Claude Opus 5.5 --- .../notification_conformance_verification.rb | 32 +++++-- .../notification_conformance_test.rb | 5 +- ...ification_conformance_verification_spec.rb | 77 +++++++++++++++++ .../notification_conformance_test_spec.rb | 83 ++++++++++++++++++- 4 files changed, 188 insertions(+), 9 deletions(-) diff --git a/lib/subscriptions_test_kit/common/notification_conformance_verification.rb b/lib/subscriptions_test_kit/common/notification_conformance_verification.rb index 34c8199..b149527 100644 --- a/lib/subscriptions_test_kit/common/notification_conformance_verification.rb +++ b/lib/subscriptions_test_kit/common/notification_conformance_verification.rb @@ -308,19 +308,34 @@ def id_only_event_notification_verification(notification_bundle, criteria_resour verify_id_only_notification_bundle_entries(bundle_entries) end - def notification_header_verification(request_headers, subscription) - mime_type = subscription&.dig('channel', 'payload') + # The requested headers come from requested_subscription, the Subscription as the client submitted it. + def notification_header_verification(request_headers, subscription, requested_subscription: subscription) + mime_type = notification_subscription_channel(subscription)['payload'] # Verification for hl7.fhir.uv.subscriptions_1.1.0@26 - if mime_type.present? + if mime_type.is_a?(String) && mime_type.present? content_type_header = request_headers.find { |header| header.name.downcase == 'content-type' } handle_content_type_header_errors(mime_type, content_type_header) end # Verification for hl7.fhir.uv.subscriptions_1.1.0@27 - subscription.dig('channel', 'header').each do |requested_header| + requested_channel = notification_subscription_channel(requested_subscription) + notification_requested_headers(requested_channel).each do |requested_header| handle_sent_header_errors(request_headers, requested_header) end end + def notification_subscription_channel(subscription) + subscription_channel = subscription['channel'] if subscription.is_a?(Hash) + subscription_channel.is_a?(Hash) ? subscription_channel : {} + end + + def notification_requested_headers(subscription_channel) + headers = subscription_channel['header'] + headers = [headers] if headers.is_a?(String) + return [] unless headers.is_a?(Array) + + headers.grep(String).select { |header| notification_header_name_and_value(header).first.present? } + end + def handle_content_type_header_errors(mime_type, content_type_header) if content_type_header.blank? add_message('error', "Content type header not sent, subscription requested #{mime_type}\n") @@ -331,9 +346,14 @@ def handle_content_type_header_errors(mime_type, content_type_header) end end + # HTTP allows optional whitespace after the colon and around the value + def notification_header_name_and_value(header) + name, _separator, value = header.partition(':') + [name.strip.downcase, value.strip] + end + def handle_sent_header_errors(request_headers, requested_header) - requested_name = requested_header.partition(': ').first.downcase - requested_value = requested_header.partition(': ').last + requested_name, requested_value = notification_header_name_and_value(requested_header) sent_header = request_headers.find { |header| header.name.downcase == requested_name } if sent_header.blank? add_message('error', "Requested header not sent: #{requested_header}\n") diff --git a/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/common/interaction_verification/notification_conformance_test.rb b/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/common/interaction_verification/notification_conformance_test.rb index 90f102f..539b708 100644 --- a/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/common/interaction_verification/notification_conformance_test.rb +++ b/lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/common/interaction_verification/notification_conformance_test.rb @@ -67,6 +67,8 @@ def subscription_type assert_valid_json(latest_subscription.response_body) subscription = JSON.parse(latest_subscription.response_body) + assert(subscription.is_a?(Hash), 'The Subscription the server returned is not a JSON object.') + requested_subscription = JSON.parse(latest_subscription.request_body) requests = load_tagged_requests('event-notification', subscription['id']) skip_if requests.empty?, %( @@ -84,7 +86,8 @@ def subscription_type ) notification_header_verification( request.request_headers, - subscription + subscription, + requested_subscription: ) end diff --git a/spec/subscriptions_test_kit/common/notification_conformance_verification_spec.rb b/spec/subscriptions_test_kit/common/notification_conformance_verification_spec.rb index 8db7c51..95c4a5a 100644 --- a/spec/subscriptions_test_kit/common/notification_conformance_verification_spec.rb +++ b/spec/subscriptions_test_kit/common/notification_conformance_verification_spec.rb @@ -761,6 +761,83 @@ def entity_result_message_type(runnable) 'Expected application/json, received application/fhir+json ' \ "Requested header not sent: accept: application/json\n") end + + it 'matches a requested header written without a space after the colon' do + subscription_resource['channel']['header'] = ['Authorization:Bearer SAMPLE_TOKEN'] + inputs = { subscription: subscription_resource.to_json } + result = run(test, inputs) + expect(result.result).to eq('pass') + end + + it 'ignores optional whitespace around a requested header value' do + subscription_resource['channel']['header'] = ['content-type: application/fhir+json '] + inputs = { subscription: subscription_resource.to_json } + result = run(test, inputs) + expect(result.result).to eq('pass') + end + + it 'reports the parsed name and value when a requested header written without a space does not match' do + subscription_resource['channel']['header'] = ['Authorization:Bearer OTHER_TOKEN'] + inputs = { subscription: subscription_resource.to_json } + result = run(test, inputs) + expect(result.result).to eq('fail') + expect(entity_result_message(test)).to eq( + 'Requested value for header authorization does not match the request sent. ' \ + 'Expected Bearer OTHER_TOKEN, received Bearer SAMPLE_TOKEN' + ) + end + + [5, {}, ['application/fhir+json']].each do |malformed_payload| + it "does not check the content type when the payload is not a string: #{malformed_payload.inspect}" do + subscription_resource['channel']['payload'] = malformed_payload + inputs = { subscription: subscription_resource.to_json } + result = run(test, inputs) + expect(result.result).to eq('pass') + end + end + + it 'skips requested header entries with a blank name' do + subscription_resource['channel']['header'] = ['', ': value', 'Authorization: Bearer SAMPLE_TOKEN'] + inputs = { subscription: subscription_resource.to_json } + result = run(test, inputs) + expect(result.result).to eq('pass') + end + + it 'passes when the Subscription has no header element' do + subscription_resource['channel'].delete('header') + inputs = { subscription: subscription_resource.to_json } + result = run(test, inputs) + expect(result.result).to eq('pass') + end + + it 'checks a header element that is a single string' do + subscription_resource['channel']['header'] = 'accept: application/fhir+json' + inputs = { subscription: subscription_resource.to_json } + result = run(test, inputs) + expect(result.result).to eq('fail') + expect(entity_result_message(test)).to eq("Requested header not sent: accept: application/fhir+json\n") + end + + it 'ignores header entries that are not strings' do + subscription_resource['channel']['header'] = [42, { 'name' => 'value' }, 'Authorization: Bearer SAMPLE_TOKEN'] + inputs = { subscription: subscription_resource.to_json } + result = run(test, inputs) + expect(result.result).to eq('pass') + end + + it 'ignores a header element that is neither a list nor a string' do + subscription_resource['channel']['header'] = { 'Authorization' => 'Bearer SAMPLE_TOKEN' } + inputs = { subscription: subscription_resource.to_json } + result = run(test, inputs) + expect(result.result).to eq('pass') + end + + it 'does not error when the channel is not an object' do + subscription_resource['channel'] = 'rest-hook' + inputs = { subscription: subscription_resource.to_json } + result = run(test, inputs) + expect(result.result).to eq('pass') + end end describe 'Notification Header Verification Error Handling' do diff --git a/spec/subscriptions_test_kit/subscription_r4_server/common/interaction_verification/notification_conformance_test_spec.rb b/spec/subscriptions_test_kit/subscription_r4_server/common/interaction_verification/notification_conformance_test_spec.rb index e1db2b7..5a64ab1 100644 --- a/spec/subscriptions_test_kit/subscription_r4_server/common/interaction_verification/notification_conformance_test_spec.rb +++ b/spec/subscriptions_test_kit/subscription_r4_server/common/interaction_verification/notification_conformance_test_spec.rb @@ -64,7 +64,8 @@ let(:access_token) { 'SAMPLE_TOKEN' } let(:subscription_id) { '123' } - def create_request(url: subscription_channel, direction: 'incoming', tags: nil, body: nil, status: 200, headers: nil) + def create_request(url: subscription_channel, direction: 'incoming', tags: nil, body: nil, status: 200, headers: nil, + response_body: body) headers ||= [ { type: 'request', @@ -84,7 +85,7 @@ def create_request(url: subscription_channel, direction: 'incoming', tags: nil, url:, result:, test_session_id: test_session.id, - response_body: body.is_a?(Hash) ? body.to_json : body, + response_body: response_body.is_a?(Hash) ? response_body.to_json : response_body, request_body: body.is_a?(Hash) ? body.to_json : body, status:, headers:, @@ -200,6 +201,84 @@ def create_request(url: subscription_channel, direction: 'incoming', tags: nil, expect(verification_request).to have_been_made end + it 'fails if a requested header is not sent and the server response has no header element' do + subscription_resource.dig('channel', 'header') << 'accept: application/fhir+json' + response_subscription = subscription_resource.deep_dup + response_subscription['channel'].delete('header') + verification_request = stub_request(:post, validation_url) + .to_return(status: 200, body: operation_outcome_success.to_json) + + create_request(url: server_endpoint, direction: 'outgoing', tags: ['subscription_creation', 'empty'], + body: subscription_resource, response_body: response_subscription, status: 201) + create_request(tags: ['event-notification', subscription_id], body: empty_notification_bundle) + result = run(test) + expect(result.result).to eq('fail') + messages = Inferno::Repositories::Messages.new.messages_for_result(result.id).map(&:message) + expect(messages).to include(a_string_including('Requested header not sent: accept: application/fhir+json')) + expect(verification_request).to have_been_made + end + + it 'does not check headers the server response adds that Inferno did not request' do + response_subscription = subscription_resource.deep_dup + response_subscription.dig('channel', 'header') << 'accept: application/fhir+json' + verification_request = stub_request(:post, validation_url) + .to_return(status: 200, body: operation_outcome_success.to_json) + + create_request(url: server_endpoint, direction: 'outgoing', tags: ['subscription_creation', 'empty'], + body: subscription_resource, response_body: response_subscription, status: 201) + create_request(tags: ['event-notification', subscription_id], body: empty_notification_bundle) + result = run(test) + expect(result.result).to eq('pass') + expect(verification_request).to have_been_made + end + + it 'passes if the notification sends the requested Authorization header the server response does not match' do + response_subscription = subscription_resource.deep_dup + response_subscription['channel']['header'] = ['Authorization: Bearer OTHER_TOKEN'] + verification_request = stub_request(:post, validation_url) + .to_return(status: 200, body: operation_outcome_success.to_json) + + create_request(url: server_endpoint, direction: 'outgoing', tags: ['subscription_creation', 'empty'], + body: subscription_resource, response_body: response_subscription, status: 201) + create_request(tags: ['event-notification', subscription_id], body: empty_notification_bundle) + result = run(test) + expect(result.result).to eq('pass') + expect(verification_request).to have_been_made + end + + it 'fails if the notification sends the Authorization header from the server response, not the requested one' do + response_subscription = subscription_resource.deep_dup + response_subscription['channel']['header'] = ['Authorization: Bearer OTHER_TOKEN'] + verification_request = stub_request(:post, validation_url) + .to_return(status: 200, body: operation_outcome_success.to_json) + notification_headers = [ + { type: 'request', name: 'Authorization', value: 'Bearer OTHER_TOKEN' }, + { type: 'request', name: 'content-type', value: 'application/fhir+json' } + ] + + create_request(url: server_endpoint, direction: 'outgoing', tags: ['subscription_creation', 'empty'], + body: subscription_resource, response_body: response_subscription, status: 201) + create_request(tags: ['event-notification', subscription_id], body: empty_notification_bundle, + headers: notification_headers) + result = run(test) + expect(result.result).to eq('fail') + messages = Inferno::Repositories::Messages.new.messages_for_result(result.id).map(&:message) + expect(messages).to include( + a_string_including('Requested value for header authorization does not match the request sent. ' \ + "Expected Bearer #{access_token}, received Bearer OTHER_TOKEN") + ) + expect(verification_request).to have_been_made + end + + it 'fails if the server returned a Subscription body that is not a JSON object' do + create_request(url: server_endpoint, direction: 'outgoing', tags: ['subscription_creation', 'empty'], + body: subscription_resource, response_body: '[1]', status: 201) + create_request(tags: ['event-notification', subscription_id], body: empty_notification_bundle) + result = run(test) + expect(result.result).to eq('fail') + expect(result.result_message).to match(/The Subscription the server returned is not a JSON object/) + end + it 'fails if wrong mime type is sent' do subscription_resource['channel']['payload'] = 'application/json' verification_request = stub_request(:post, validation_url) From 9d9c1beda420aea79fabee4ae40904c376ea044b Mon Sep 17 00:00:00 2001 From: alexd-leaporbit Date: Wed, 21 Oct 2026 10:55:21 -0400 Subject: [PATCH 5/5] ID-279: apply review wording to the channel header messages Reword the three messages server_check_channel adds when it changes or rejects the tester's channel.header, as the reviewer suggested. The Authorization warning now starts with "Tester-provided Subscription updated before use:" and says the Bearer token comes from the "Notification Access Token" input. It no longer says the change is needed to connect to the Inferno subscription channel. The warning for a single-string header and the error for any other non-list header use the same wording. The error for a non-list header no longer names the access token, so server_channel_header_list had an unused access_token argument. That argument is removed from the method and from its one caller. The module specs now match on the new wording, and each one also checks that the Authorization warning still names the token. Co-Authored-By: Claude Opus 5.5 --- .../subscription_conformance_verification.rb | 15 ++++--- ...scription_conformance_verification_spec.rb | 42 +++++++++++-------- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/lib/subscriptions_test_kit/common/subscription_conformance_verification.rb b/lib/subscriptions_test_kit/common/subscription_conformance_verification.rb index 0fb392a..f990434 100644 --- a/lib/subscriptions_test_kit/common/subscription_conformance_verification.rb +++ b/lib/subscriptions_test_kit/common/subscription_conformance_verification.rb @@ -102,7 +102,7 @@ def server_check_channel(subscription, access_token) end def server_check_channel_authorization_header(subscription_channel, access_token) - headers = server_channel_header_list(subscription_channel, access_token) + headers = server_channel_header_list(subscription_channel) return if headers.nil? authorization_headers = headers.select { |header| channel_authorization_header?(header) } @@ -111,13 +111,13 @@ def server_check_channel_authorization_header(subscription_channel, access_token action = authorization_headers.empty? ? 'Added' : 'Replaced' add_message('warning', %( - #{action} the Authorization header field with a Bearer token set to `#{access_token}` in the `header` field on - the Subscription resource in order to connect successfully with the Inferno subscription channel. + Tester-provided Subscription updated before use: #{action} the Authorization header entry in the `header` + field with the Bearer token `#{access_token}` provided in the "Notification Access Token" input. )) subscription_channel['header'] = (headers - authorization_headers) + ["Authorization: Bearer #{access_token}"] end - def server_channel_header_list(subscription_channel, access_token) + def server_channel_header_list(subscription_channel) headers = subscription_channel['header'] case headers when nil @@ -126,14 +126,13 @@ def server_channel_header_list(subscription_channel, access_token) headers when String add_message('warning', %( - The `header` field on the Subscription resource must be a list of strings. Inferno converted the single - string value into a list containing that value. + Tester-provided Subscription updated before use: incorrect string value for element `header` converted into + a list containing the provided string. )) subscription_channel['header'] = [headers] else add_message('error', %( - The `header` field on the Subscription resource must be a list of strings, so Inferno could not add the - Authorization header field with a Bearer token set to `#{access_token}`. + The `header` element on the tester-provided Subscription must be a list of strings. )) nil end diff --git a/spec/subscriptions_test_kit/common/subscription_conformance_verification_spec.rb b/spec/subscriptions_test_kit/common/subscription_conformance_verification_spec.rb index 50600a0..548212b 100644 --- a/spec/subscriptions_test_kit/common/subscription_conformance_verification_spec.rb +++ b/spec/subscriptions_test_kit/common/subscription_conformance_verification_spec.rb @@ -278,7 +278,7 @@ def checked_headers(result) result = run(test, subscription_resource: subscription_resource.to_json, access_token:) expect(result.result).to eq('pass') - expect(entity_result_message(test)).to_not match(/Authorization header field/) + expect(entity_result_message(test)).to_not match(/Authorization header entry/) expect(checked_headers(result)).to eq(['Authorization: Bearer SAMPLE_TOKEN']) end @@ -290,7 +290,7 @@ def checked_headers(result) result = run(test, subscription_resource: subscription_resource.to_json, access_token:) expect(result.result).to eq('pass') - expect(entity_result_message(test)).to_not match(/Authorization header field/) + expect(entity_result_message(test)).to_not match(/Authorization header entry/) expect(checked_headers(result)).to eq(['authorization: Bearer SAMPLE_TOKEN']) end @@ -303,7 +303,10 @@ def checked_headers(result) access_token:) expect(result.result).to eq('pass') expect(entity_result_message(test)).to match( - /Added the Authorization header field with a Bearer token set to `SAMPLE_TOKEN` in the `header` field/ + /Tester-provided Subscription updated before use: Added the Authorization header entry in the `header`/ + ) + expect(entity_result_message(test)).to match( + /field with the Bearer token `SAMPLE_TOKEN` provided in the "Notification Access Token" input/ ) expect(checked_headers(result)).to eq(['Authorization: Bearer SAMPLE_TOKEN']) end @@ -317,7 +320,10 @@ def checked_headers(result) access_token:) expect(result.result).to eq('pass') expect(entity_result_message(test)).to match( - /Replaced the Authorization header field with a Bearer token set to `SAMPLE_TOKEN` in the `header` field/ + /Tester-provided Subscription updated before use: Replaced the Authorization header entry in the `header`/ + ) + expect(entity_result_message(test)).to match( + /field with the Bearer token `SAMPLE_TOKEN` provided in the "Notification Access Token" input/ ) expect(entity_result_message(test)).to_not include('INCORRECT_TOKEN') expect(checked_headers(result)).to eq(['Authorization: Bearer SAMPLE_TOKEN']) @@ -331,7 +337,7 @@ def checked_headers(result) result = run(test, subscription_resource: subscription_resource.to_json, access_token:) expect(result.result).to eq('pass') - expect(entity_result_message(test)).to match(/Replaced the Authorization header field/) + expect(entity_result_message(test)).to match(/Replaced the Authorization header entry/) expect(checked_headers(result)).to eq(['Authorization: Bearer SAMPLE_TOKEN']) end @@ -347,7 +353,7 @@ def checked_headers(result) result = run(test, subscription_resource: subscription_resource.to_json, access_token:) expect(result.result).to eq('pass') - expect(entity_result_message(test)).to match(/Replaced the Authorization header field/) + expect(entity_result_message(test)).to match(/Replaced the Authorization header entry/) expect(checked_headers(result)).to eq(['X-Custom-Header: value', 'Authorization: Bearer SAMPLE_TOKEN']) end @@ -365,7 +371,7 @@ def checked_headers(result) result = run(test, subscription_resource: subscription_resource.to_json, access_token:) expect(result.result).to eq('pass') - expect(entity_result_message(test)).to match(/Replaced the Authorization header field/) + expect(entity_result_message(test)).to match(/Replaced the Authorization header entry/) expect(checked_headers(result)).to eq(['Authorization: Bearer SAMPLE_TOKEN']) end end @@ -378,7 +384,7 @@ def checked_headers(result) result = run(test, subscription_resource: subscription_resource.to_json, access_token:) expect(result.result).to eq('pass') - expect(entity_result_message(test)).to match(/Replaced the Authorization header field/) + expect(entity_result_message(test)).to match(/Replaced the Authorization header entry/) expect(checked_headers(result)).to eq(['Authorization: Bearer SAMPLE_TOKEN']) end @@ -390,7 +396,7 @@ def checked_headers(result) result = run(test, subscription_resource: subscription_resource.to_json, access_token:) expect(result.result).to eq('pass') - expect(entity_result_message(test)).to match(/Replaced the Authorization header field/) + expect(entity_result_message(test)).to match(/Replaced the Authorization header entry/) expect(checked_headers(result)).to eq(['Authorization: Bearer SAMPLE_TOKEN']) end @@ -414,9 +420,9 @@ def checked_headers(result) access_token:) expect(result.result).to eq('pass') expect(entity_result_message(test)).to match( - /The `header` field on the Subscription resource must be a list of strings. Inferno converted the single/ + /Tester-provided Subscription updated before use: incorrect string value for element `header` converted into/ ) - expect(entity_result_message(test)).to_not match(/Authorization header field/) + expect(entity_result_message(test)).to_not match(/Authorization header entry/) expect(checked_headers(result)).to eq(['Authorization: Bearer SAMPLE_TOKEN']) end @@ -428,8 +434,8 @@ def checked_headers(result) result = run(test, subscription_resource: subscription_resource.to_json, access_token:) expect(result.result).to eq('pass') - expect(entity_result_message(test)).to match(/Inferno converted the single/) - expect(entity_result_message(test)).to match(/Replaced the Authorization header field/) + expect(entity_result_message(test)).to match(/incorrect string value for element `header` converted into/) + expect(entity_result_message(test)).to match(/Replaced the Authorization header entry/) expect(checked_headers(result)).to eq(['Authorization: Bearer SAMPLE_TOKEN']) end @@ -441,8 +447,8 @@ def checked_headers(result) result = run(test, subscription_resource: subscription_resource.to_json, access_token:) expect(result.result).to eq('pass') - expect(entity_result_message(test)).to match(/Inferno converted the single/) - expect(entity_result_message(test)).to match(/Added the Authorization header field/) + expect(entity_result_message(test)).to match(/incorrect string value for element `header` converted into/) + expect(entity_result_message(test)).to match(/Added the Authorization header entry/) expect(checked_headers(result)).to eq(['X-Custom-Header: value', 'Authorization: Bearer SAMPLE_TOKEN']) end @@ -456,7 +462,7 @@ def checked_headers(result) access_token:) expect(result.result).to eq('fail') expect(entity_result_message(test)).to match( - /The `header` field on the Subscription resource must be a list of strings, so Inferno could not add the/ + /The `header` element on the tester-provided Subscription must be a list of strings\./ ) expect(checked_headers(result)).to eq(malformed_header) end @@ -486,7 +492,7 @@ def checked_headers(result) result = run(test, subscription_resource: subscription_resource.to_json, access_token:) expect(result.result).to eq('pass') - expect(entity_result_message(test)).to match(/Added the Authorization header field/) + expect(entity_result_message(test)).to match(/Added the Authorization header entry/) expect(checked_headers(result)).to eq( ['X-Authorization: Bearer SAMPLE_TOKEN', 'Authorization-Info: value', 'Authorization: Bearer SAMPLE_TOKEN'] ) @@ -500,7 +506,7 @@ def checked_headers(result) result = run(test, subscription_resource: subscription_resource.to_json, access_token: 'abc:def') expect(result.result).to eq('pass') - expect(entity_result_message(test)).to_not match(/Authorization header field/) + expect(entity_result_message(test)).to_not match(/Authorization header entry/) expect(checked_headers(result)).to eq(['Authorization: Bearer abc:def']) end end