Repository navigation
ID-278: support GET and typed inputs on the client suite $status operation - #51
Merged
Merged
Conversation
…ation The simulated server now accepts the Subscription $status operation via GET at the type level, reads POST inputs from valueId and valueCode, and rejects inputs sent with any other value type with a 400 OperationOutcome. The status input filter now applies to instance-level invocations too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The IG says the id and status inputs are ignored when $status is invoked on a Subscription instance, so only the type-level endpoint reads and validates them. Specs now check the returned status Bundle and error codes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reject GET query inputs without a value and body inputs whose only value is not a non-empty valueId/valueCode string, order the repeated-parameter spec so it guards against request.params, and clarify the OR semantics in the client suite description. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
karlnaden
requested changes
Oct 20, 2026
Changes to the ubuntu-latest image break CI for Ruby 3.3.6, so run the workflows on ubuntu-24.04 until the Ruby version is upgraded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hing matches A single SubscriptionStatusEndpoint now handles both the instance and type invocations, choosing by the request path. Type-level requests that match no Subscription return a 200 empty searchset Bundle instead of a 404, per the operation's search semantics, and input lookups use SubscriptionSimulationUtils#find_all_parameters. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
karlnaden
approved these changes
Oct 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Previously, the client suite's simulated
$statusoperation did not follow the OperationDefinition when invoked on the type (Subscription/$status):Subscription/[id]/$statushad a GET route)idandstatusinputs fromvalueStringinstead ofvalueIdandvalueCode, so a conformant client got a 404 for its own SubscriptionNow, type-level
$statussupports GET, with inputs as query parameters, and POST, with inputs in aParametersbody.SubscriptionStatusEndpointhandles both invocations, choosing by the request path. Type-level requests use search semantics: when no Subscription matches the inputs, Inferno returns a 200 empty searchset Bundle instead of a 404. Instance-level behavior is unchanged and still ignores the inputs, as the IG specifies. Malformed type-level inputs are rejected with a 400invalidOperationOutcome that names the problem, includingvalueIdorvalueCode(e.g.,valueString,"valueId": 123, or a secondvalue[x]), e.g., "Theidparameter must have a non-emptyvalueIdand no other value."?id=), e.g., "Theidquery parameter must have a value."Parametersresource, which previously caused a 500NOTE: clients that sent
valueStringinputs toSubscription/$statuspreviously got a 200 and will now get a 400, and type-level requests that match nothing now get a 200 empty Bundle instead of a 404.This PR also pins the CI workflows to
ubuntu-24.04, sinceubuntu-latestno longer supports Ruby 3.3.6.Testing Guidance
Beyond unit tests (
bundle exec rspec spec/subscriptions_test_kit/endpoints), these changes are easiest to see by calling$statusfrom Postman against the client suite once the server suite has created a Subscription.Setup
bundle exec inferno services start, thenbundle exec inferno start)Create a Subscription
Call
$statuswith Postmanaccess_token=SAMPLE_TOKENand sendsContent-Type: application/fhir+jsonon the POSTs.hostto your Inferno (http://localhost:4567forinferno start,http://localhostfor Docker).subscription_idis filled in by request 1.active, and storessubscription_idfrom it?id={{subscription_id}}: 200 searchset Bundle (demonstrates GET support)?id=other: 200 empty searchset Bundle?status=off: 200 empty searchset Bundle?id=: 400 "Theidquery parameter must have a value."{"name": "id", "valueId": "{{subscription_id}}"}: 200 searchset Bundle (demonstratesvalueId){"name": "id", "valueString": "{{subscription_id}}"}: 400 "Theidparameter must have a non-emptyvalueIdand no other value."{"name": "status", "valueString": "active"}: 400 "Thestatusparameter must have a non-emptyvalueCodeand no other value."{{subscription_id}}/$status?status=off: 200 searchset Bundle (inputs are ignored on the instance)NOTE: you can run the collection against
mainto compare. Request 1 can't find the Subscription there, so first setsubscription_idto the id named in the server's 1.1.02 dialog.Postman collection (ID-278 Subscription $status)
{ "info": { "name": "ID-278 Subscription $status", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", "description": "Calls $status on the Subscriptions client suite while it waits in test 1.1.01. Set `host` to your Inferno (http://localhost:4567 for `inferno start`, http://localhost for Docker)." }, "auth": { "type": "bearer", "bearer": [ { "key": "token", "value": "{{access_token}}", "type": "string" } ] }, "variable": [ { "key": "host", "value": "http://localhost:4567" }, { "key": "access_token", "value": "SAMPLE_TOKEN" }, { "key": "subscription_id", "value": "" } ], "item": [ { "name": "1. Type-level GET, no inputs (sets subscription_id)", "request": { "method": "GET", "header": [], "url": "{{host}}/custom/subscriptions_r5_backport_r4_client/fhir/Subscription/$status", "description": "Returns the status of the Subscription the server suite created and stores its id for the requests below." }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('200 searchset Bundle', () => {", " pm.response.to.have.status(200);", " pm.expect(pm.response.json().type).to.eql('searchset');", "});", "pm.test('SubscriptionStatus has status active', () => {", " const params = pm.response.json().entry[0].resource.parameter;", " pm.expect(params.find(p => p.name === 'status').valueCode).to.eql('active');", "});", "const ref = pm.response.json().entry[0].resource.parameter.find(p => p.name === 'subscription').valueReference.reference;", "pm.collectionVariables.set('subscription_id', ref.split('/').pop());" ] } } ] }, { "name": "2. Type-level GET, matching id", "request": { "method": "GET", "header": [], "url": "{{host}}/custom/subscriptions_r5_backport_r4_client/fhir/Subscription/$status?id={{subscription_id}}", "description": "" }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('200 searchset Bundle', () => {", " pm.response.to.have.status(200);", " pm.expect(pm.response.json().type).to.eql('searchset');", "});", "pm.test('SubscriptionStatus has status active', () => {", " const params = pm.response.json().entry[0].resource.parameter;", " pm.expect(params.find(p => p.name === 'status').valueCode).to.eql('active');", "});" ] } } ] }, { "name": "3. Type-level GET, other id", "request": { "method": "GET", "header": [], "url": "{{host}}/custom/subscriptions_r5_backport_r4_client/fhir/Subscription/$status?id=other", "description": "" }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('200 searchset Bundle', () => {", " pm.response.to.have.status(200);", " pm.expect(pm.response.json().type).to.eql('searchset');", "});", "pm.test('Bundle has no entries', () => {", " pm.expect(pm.response.json().total).to.eql(0);", " pm.expect(pm.response.json().entry || []).to.be.empty;", "});" ] } } ] }, { "name": "4. Type-level GET, non-matching status", "request": { "method": "GET", "header": [], "url": "{{host}}/custom/subscriptions_r5_backport_r4_client/fhir/Subscription/$status?status=off", "description": "" }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('200 searchset Bundle', () => {", " pm.response.to.have.status(200);", " pm.expect(pm.response.json().type).to.eql('searchset');", "});", "pm.test('Bundle has no entries', () => {", " pm.expect(pm.response.json().total).to.eql(0);", " pm.expect(pm.response.json().entry || []).to.be.empty;", "});" ] } } ] }, { "name": "5. Type-level GET, id without a value", "request": { "method": "GET", "header": [], "url": "{{host}}/custom/subscriptions_r5_backport_r4_client/fhir/Subscription/$status?id=", "description": "" }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('400 invalid', () => {", " pm.response.to.have.status(400);", " pm.expect(pm.response.json().issue[0].code).to.eql('invalid');", "});", "pm.test('names the problem', () => {", " pm.expect(pm.response.json().issue[0].details.text).to.eql(\"The `id` query parameter must have a value.\");", "});" ] } } ] }, { "name": "6. Type-level POST, valueId", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/fhir+json" } ], "url": "{{host}}/custom/subscriptions_r5_backport_r4_client/fhir/Subscription/$status", "description": "", "body": { "mode": "raw", "raw": "{\n \"resourceType\": \"Parameters\",\n \"parameter\": [\n {\n \"name\": \"id\",\n \"valueId\": \"{{subscription_id}}\"\n }\n ]\n}" } }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('200 searchset Bundle', () => {", " pm.response.to.have.status(200);", " pm.expect(pm.response.json().type).to.eql('searchset');", "});", "pm.test('SubscriptionStatus has status active', () => {", " const params = pm.response.json().entry[0].resource.parameter;", " pm.expect(params.find(p => p.name === 'status').valueCode).to.eql('active');", "});" ] } } ] }, { "name": "7. Type-level POST, id as valueString", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/fhir+json" } ], "url": "{{host}}/custom/subscriptions_r5_backport_r4_client/fhir/Subscription/$status", "description": "", "body": { "mode": "raw", "raw": "{\n \"resourceType\": \"Parameters\",\n \"parameter\": [\n {\n \"name\": \"id\",\n \"valueString\": \"{{subscription_id}}\"\n }\n ]\n}" } }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('400 invalid', () => {", " pm.response.to.have.status(400);", " pm.expect(pm.response.json().issue[0].code).to.eql('invalid');", "});", "pm.test('names the problem', () => {", " pm.expect(pm.response.json().issue[0].details.text).to.eql(\"The `id` parameter must have a non-empty `valueId` and no other value.\");", "});" ] } } ] }, { "name": "8. Type-level POST, status as valueString", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/fhir+json" } ], "url": "{{host}}/custom/subscriptions_r5_backport_r4_client/fhir/Subscription/$status", "description": "", "body": { "mode": "raw", "raw": "{\n \"resourceType\": \"Parameters\",\n \"parameter\": [\n {\n \"name\": \"status\",\n \"valueString\": \"active\"\n }\n ]\n}" } }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('400 invalid', () => {", " pm.response.to.have.status(400);", " pm.expect(pm.response.json().issue[0].code).to.eql('invalid');", "});", "pm.test('names the problem', () => {", " pm.expect(pm.response.json().issue[0].details.text).to.eql(\"The `status` parameter must have a non-empty `valueCode` and no other value.\");", "});" ] } } ] }, { "name": "9. Instance GET ignores status input", "request": { "method": "GET", "header": [], "url": "{{host}}/custom/subscriptions_r5_backport_r4_client/fhir/Subscription/{{subscription_id}}/$status?status=off", "description": "" }, "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "pm.test('200 searchset Bundle', () => {", " pm.response.to.have.status(200);", " pm.expect(pm.response.json().type).to.eql('searchset');", "});", "pm.test('SubscriptionStatus has status active', () => {", " const params = pm.response.json().entry[0].resource.parameter;", " pm.expect(params.find(p => p.name === 'status').valueCode).to.eql('active');", "});" ] } } ] } ] }🤖 Generated with Claude Code