From 23f48b8e7d365bdd6ece38d510cce4c7f3698673 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Tue, 29 Sep 2026 09:42:45 +1000 Subject: [PATCH 1/2] set up release --- CHANGELOG.md | 29 ++++ package-lock.json | 4 +- package.json | 2 +- tests/tx/test-cases.test.js | 276 +++++++++++++++++++++++++++++++++ tx/tests/test-cases-version.js | 2 +- 5 files changed, 309 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e663177e..8fcd93b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,35 @@ All notable changes to Health Intersections FHIRsmith will be documented in this The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.14.1] - 2026-09-29 + +### Added + +- TestReport module: user administration - the administrator (`adminPassword`) manages users at /testing/admin/users, with rights to edit named links and/or delete reports +- TestReport module: named links - test script and participant URLs shown by name, maintained at /testing/admin/links +- TestReport module: the report list shows the test engine, client and server, the number of tests and the run length; a report page shows setup, each test with its operations and assertions, and teardown +- Test reports from the FHIRsmith build name the test modes they ran with + +### Changed + +- Security: the publisher and testing web pages require a CSRF token on every form post (lusca), a SameSite=Lax session cookie, and a same-origin `Origin` on posts; the session id is regenerated on login +- Security: the publisher rate limits task actions (create, approve, delete, retry) and task output reads, as well as logins +- `server.trustProxy` now defaults to trusting only loopback, link-local and private-network proxies (was `true`, which let anyone reaching the port directly set their own client address and get round the rate limits). See nginx.md - a proxy elsewhere needs to be configured, and the proxy must pass on `Host` or `X-Forwarded-Host`, or form posts are refused +- $expand: the `displayLanguage` expansion parameter is written without spaces (`en,*;q=0`, not `en, *; q=0`) +- $translate: `result` is only true if at least one match is a translation - a `not-related-to` (R4: `disjoint`/`unmatched`) or `noMap` match is still returned, but `result` is false +- SNOMED CT edition names are held in one table (tx/sct/editions.js), used by the provider and the unknown code messages + +### Fixed + +- $validate-code: when the code is not in the value set, it is looked up in the code system version the value set selects (`compose.include.version`), not the server's default version - so the version and display reported come from the right edition +- $validate-code: the unknown code message named the SNOMED CT test edition as "unknown"; it is now "Test Edition" +- Registry: the SNOMED CT test edition (`http://snomed.info/xsct/31000003106`) is labelled, rather than shown as `??` + +### Tx Conformance Statement + +FHIRsmith passed all 3585 HL7 terminology service tests (modes tx.fhir.org+omop+general+snomed+mimetypes+icd-11+closure, tests v1.9.6, runner v6.10.4) + ## [0.14.0] - 2026-09-27 ### Added diff --git a/package-lock.json b/package-lock.json index 10ab9bc6..b6d97d28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "fhirsmith", - "version": "0.14.0", + "version": "0.14.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "fhirsmith", - "version": "0.14.0", + "version": "0.14.1", "license": "BSD-3", "dependencies": { "axios": "^1.13.4", diff --git a/package.json b/package.json index a5dd5fd7..4ab1bdbb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fhirsmith", - "version": "0.14.0", + "version": "0.14.1", "txVersion": "1.9.5-SNAPSHOT", "description": "A Node.js server that provides a collection of tools to serve the FHIR ecosystem", "main": "server.js", diff --git a/tests/tx/test-cases.test.js b/tests/tx/test-cases.test.js index 91be7535..fbdde65e 100644 --- a/tests/tx/test-cases.test.js +++ b/tests/tx/test-cases.test.js @@ -345,6 +345,22 @@ describe('simple-cases', () => { await runTest({"suite":"simple-cases","test":"simple-subsumes-unknown-code-coding"}, "4.0"); }); + it("simple-subsumes-no-system" + 'R5', async () => { + await runTest({"suite":"simple-cases","test":"simple-subsumes-no-system"}, "5.0"); + }); + + it("simple-subsumes-no-system" + 'R4', async () => { + await runTest({"suite":"simple-cases","test":"simple-subsumes-no-system"}, "4.0"); + }); + + it("simple-subsumes-no-system-coding" + 'R5', async () => { + await runTest({"suite":"simple-cases","test":"simple-subsumes-no-system-coding"}, "5.0"); + }); + + it("simple-subsumes-no-system-coding" + 'R4', async () => { + await runTest({"suite":"simple-cases","test":"simple-subsumes-no-system-coding"}, "4.0"); + }); + }); describe('parameters', () => { @@ -1626,6 +1642,150 @@ describe('validation', () => { await runTest({"suite":"validation","test":"validation-contained-bad"}, "4.0"); }); + it("validation-missing-vs-code-no-system" + 'R5', async () => { + await runTest({"suite":"validation","test":"validation-missing-vs-code-no-system"}, "5.0"); + }); + + it("validation-missing-vs-code-no-system" + 'R4', async () => { + await runTest({"suite":"validation","test":"validation-missing-vs-code-no-system"}, "4.0"); + }); + + it("validation-missing-vs-system-no-code" + 'R5', async () => { + await runTest({"suite":"validation","test":"validation-missing-vs-system-no-code"}, "5.0"); + }); + + it("validation-missing-vs-system-no-code" + 'R4', async () => { + await runTest({"suite":"validation","test":"validation-missing-vs-system-no-code"}, "4.0"); + }); + + it("validation-missing-vs-nothing" + 'R5', async () => { + await runTest({"suite":"validation","test":"validation-missing-vs-nothing"}, "5.0"); + }); + + it("validation-missing-vs-nothing" + 'R4', async () => { + await runTest({"suite":"validation","test":"validation-missing-vs-nothing"}, "4.0"); + }); + + it("validation-missing-vs-coding-no-code" + 'R5', async () => { + await runTest({"suite":"validation","test":"validation-missing-vs-coding-no-code"}, "5.0"); + }); + + it("validation-missing-vs-coding-no-code" + 'R4', async () => { + await runTest({"suite":"validation","test":"validation-missing-vs-coding-no-code"}, "4.0"); + }); + + it("validation-missing-vs-coding-display-only" + 'R5', async () => { + await runTest({"suite":"validation","test":"validation-missing-vs-coding-display-only"}, "5.0"); + }); + + it("validation-missing-vs-coding-display-only" + 'R4', async () => { + await runTest({"suite":"validation","test":"validation-missing-vs-coding-display-only"}, "4.0"); + }); + + it("validation-missing-vs-cc-no-system" + 'R5', async () => { + await runTest({"suite":"validation","test":"validation-missing-vs-cc-no-system"}, "5.0"); + }); + + it("validation-missing-vs-cc-no-system" + 'R4', async () => { + await runTest({"suite":"validation","test":"validation-missing-vs-cc-no-system"}, "4.0"); + }); + + it("validation-missing-vs-cc-no-code" + 'R5', async () => { + await runTest({"suite":"validation","test":"validation-missing-vs-cc-no-code"}, "5.0"); + }); + + it("validation-missing-vs-cc-no-code" + 'R4', async () => { + await runTest({"suite":"validation","test":"validation-missing-vs-cc-no-code"}, "4.0"); + }); + + it("validation-missing-vs-cc-text-only" + 'R5', async () => { + await runTest({"suite":"validation","test":"validation-missing-vs-cc-text-only"}, "5.0"); + }); + + it("validation-missing-vs-cc-text-only" + 'R4', async () => { + await runTest({"suite":"validation","test":"validation-missing-vs-cc-text-only"}, "4.0"); + }); + + it("validation-missing-vs-cc-mixed" + 'R5', async () => { + await runTest({"suite":"validation","test":"validation-missing-vs-cc-mixed"}, "5.0"); + }); + + it("validation-missing-vs-cc-mixed" + 'R4', async () => { + await runTest({"suite":"validation","test":"validation-missing-vs-cc-mixed"}, "4.0"); + }); + + it("validation-missing-cs-code-no-url" + 'R5', async () => { + await runTest({"suite":"validation","test":"validation-missing-cs-code-no-url"}, "5.0"); + }); + + it("validation-missing-cs-code-no-url" + 'R4', async () => { + await runTest({"suite":"validation","test":"validation-missing-cs-code-no-url"}, "4.0"); + }); + + it("validation-missing-cs-url-no-code" + 'R5', async () => { + await runTest({"suite":"validation","test":"validation-missing-cs-url-no-code"}, "5.0"); + }); + + it("validation-missing-cs-url-no-code" + 'R4', async () => { + await runTest({"suite":"validation","test":"validation-missing-cs-url-no-code"}, "4.0"); + }); + + it("validation-missing-cs-nothing" + 'R5', async () => { + await runTest({"suite":"validation","test":"validation-missing-cs-nothing"}, "5.0"); + }); + + it("validation-missing-cs-nothing" + 'R4', async () => { + await runTest({"suite":"validation","test":"validation-missing-cs-nothing"}, "4.0"); + }); + + it("validation-missing-cs-coding-no-code" + 'R5', async () => { + await runTest({"suite":"validation","test":"validation-missing-cs-coding-no-code"}, "5.0"); + }); + + it("validation-missing-cs-coding-no-code" + 'R4', async () => { + await runTest({"suite":"validation","test":"validation-missing-cs-coding-no-code"}, "4.0"); + }); + + it("validation-missing-cs-coding-no-system" + 'R5', async () => { + await runTest({"suite":"validation","test":"validation-missing-cs-coding-no-system"}, "5.0"); + }); + + it("validation-missing-cs-coding-no-system" + 'R4', async () => { + await runTest({"suite":"validation","test":"validation-missing-cs-coding-no-system"}, "4.0"); + }); + + it("validation-missing-cs-cc-no-system" + 'R5', async () => { + await runTest({"suite":"validation","test":"validation-missing-cs-cc-no-system"}, "5.0"); + }); + + it("validation-missing-cs-cc-no-system" + 'R4', async () => { + await runTest({"suite":"validation","test":"validation-missing-cs-cc-no-system"}, "4.0"); + }); + + it("validation-missing-cs-cc-no-code" + 'R5', async () => { + await runTest({"suite":"validation","test":"validation-missing-cs-cc-no-code"}, "5.0"); + }); + + it("validation-missing-cs-cc-no-code" + 'R4', async () => { + await runTest({"suite":"validation","test":"validation-missing-cs-cc-no-code"}, "4.0"); + }); + + it("validation-missing-vs-coding-no-system-infer" + 'R5', async () => { + await runTest({"suite":"validation","test":"validation-missing-vs-coding-no-system-infer"}, "5.0"); + }); + + it("validation-missing-vs-coding-no-system-infer" + 'R4', async () => { + await runTest({"suite":"validation","test":"validation-missing-vs-coding-no-system-infer"}, "4.0"); + }); + + it("validation-missing-vs-cc-no-system-infer" + 'R5', async () => { + await runTest({"suite":"validation","test":"validation-missing-vs-cc-no-system-infer"}, "5.0"); + }); + + it("validation-missing-vs-cc-no-system-infer" + 'R4', async () => { + await runTest({"suite":"validation","test":"validation-missing-vs-cc-no-system-infer"}, "4.0"); + }); + }); describe('version', () => { @@ -4474,6 +4634,14 @@ describe('translate2', () => { await runTest({"suite":"translate2","test":"translate-6a"}, "4.0"); }); + it("translate-6b" + 'R5', async () => { + await runTest({"suite":"translate2","test":"translate-6b"}, "5.0"); + }); + + it("translate-6b" + 'R4', async () => { + await runTest({"suite":"translate2","test":"translate-6b"}, "4.0"); + }); + it("translate-reverse-r4" + 'R4', async () => { await runTest({"suite":"translate2","test":"translate-reverse-r4"}, "4.0"); }); @@ -5668,6 +5836,14 @@ describe('snomed', () => { await runTest({"suite":"snomed","test":"lookup-pc"}, "4.0"); }); + it("lookup-pc-unbracketed" + 'R5', async () => { + await runTest({"suite":"snomed","test":"lookup-pc-unbracketed"}, "5.0"); + }); + + it("lookup-pc-unbracketed" + 'R4', async () => { + await runTest({"suite":"snomed","test":"lookup-pc-unbracketed"}, "4.0"); + }); + it("validate-code-pc-good" + 'R5', async () => { await runTest({"suite":"snomed","test":"validate-code-pc-good"}, "5.0"); }); @@ -5700,6 +5876,14 @@ describe('snomed', () => { await runTest({"suite":"snomed","test":"validate-code-pc-nested-good"}, "4.0"); }); + it("validate-code-pc-unbracketed" + 'R5', async () => { + await runTest({"suite":"snomed","test":"validate-code-pc-unbracketed"}, "5.0"); + }); + + it("validate-code-pc-unbracketed" + 'R4', async () => { + await runTest({"suite":"snomed","test":"validate-code-pc-unbracketed"}, "4.0"); + }); + it("validate-code-pc-mrcm-lateralizable" + 'R5', async () => { await runTest({"suite":"snomed","test":"validate-code-pc-mrcm-lateralizable"}, "5.0"); }); @@ -9743,6 +9927,14 @@ describe('simple-cases', () => { await runTest({"suite":"simple-cases","test":"simple-subsumes-unknown-code-coding"}, "5.0"); }); + it("simple-subsumes-no-system" + 'R5-cached', async () => { + await runTest({"suite":"simple-cases","test":"simple-subsumes-no-system"}, "5.0"); + }); + + it("simple-subsumes-no-system-coding" + 'R5-cached', async () => { + await runTest({"suite":"simple-cases","test":"simple-subsumes-no-system-coding"}, "5.0"); + }); + }); describe('parameters', () => { @@ -10396,6 +10588,78 @@ describe('validation', () => { await runTest({"suite":"validation","test":"validation-contained-bad"}, "5.0"); }); + it("validation-missing-vs-code-no-system" + 'R5-cached', async () => { + await runTest({"suite":"validation","test":"validation-missing-vs-code-no-system"}, "5.0"); + }); + + it("validation-missing-vs-system-no-code" + 'R5-cached', async () => { + await runTest({"suite":"validation","test":"validation-missing-vs-system-no-code"}, "5.0"); + }); + + it("validation-missing-vs-nothing" + 'R5-cached', async () => { + await runTest({"suite":"validation","test":"validation-missing-vs-nothing"}, "5.0"); + }); + + it("validation-missing-vs-coding-no-code" + 'R5-cached', async () => { + await runTest({"suite":"validation","test":"validation-missing-vs-coding-no-code"}, "5.0"); + }); + + it("validation-missing-vs-coding-display-only" + 'R5-cached', async () => { + await runTest({"suite":"validation","test":"validation-missing-vs-coding-display-only"}, "5.0"); + }); + + it("validation-missing-vs-cc-no-system" + 'R5-cached', async () => { + await runTest({"suite":"validation","test":"validation-missing-vs-cc-no-system"}, "5.0"); + }); + + it("validation-missing-vs-cc-no-code" + 'R5-cached', async () => { + await runTest({"suite":"validation","test":"validation-missing-vs-cc-no-code"}, "5.0"); + }); + + it("validation-missing-vs-cc-text-only" + 'R5-cached', async () => { + await runTest({"suite":"validation","test":"validation-missing-vs-cc-text-only"}, "5.0"); + }); + + it("validation-missing-vs-cc-mixed" + 'R5-cached', async () => { + await runTest({"suite":"validation","test":"validation-missing-vs-cc-mixed"}, "5.0"); + }); + + it("validation-missing-cs-code-no-url" + 'R5-cached', async () => { + await runTest({"suite":"validation","test":"validation-missing-cs-code-no-url"}, "5.0"); + }); + + it("validation-missing-cs-url-no-code" + 'R5-cached', async () => { + await runTest({"suite":"validation","test":"validation-missing-cs-url-no-code"}, "5.0"); + }); + + it("validation-missing-cs-nothing" + 'R5-cached', async () => { + await runTest({"suite":"validation","test":"validation-missing-cs-nothing"}, "5.0"); + }); + + it("validation-missing-cs-coding-no-code" + 'R5-cached', async () => { + await runTest({"suite":"validation","test":"validation-missing-cs-coding-no-code"}, "5.0"); + }); + + it("validation-missing-cs-coding-no-system" + 'R5-cached', async () => { + await runTest({"suite":"validation","test":"validation-missing-cs-coding-no-system"}, "5.0"); + }); + + it("validation-missing-cs-cc-no-system" + 'R5-cached', async () => { + await runTest({"suite":"validation","test":"validation-missing-cs-cc-no-system"}, "5.0"); + }); + + it("validation-missing-cs-cc-no-code" + 'R5-cached', async () => { + await runTest({"suite":"validation","test":"validation-missing-cs-cc-no-code"}, "5.0"); + }); + + it("validation-missing-vs-coding-no-system-infer" + 'R5-cached', async () => { + await runTest({"suite":"validation","test":"validation-missing-vs-coding-no-system-infer"}, "5.0"); + }); + + it("validation-missing-vs-cc-no-system-infer" + 'R5-cached', async () => { + await runTest({"suite":"validation","test":"validation-missing-vs-cc-no-system-infer"}, "5.0"); + }); + }); describe('version', () => { @@ -11856,6 +12120,10 @@ describe('translate2', () => { await runTest({"suite":"translate2","test":"translate-6a"}, "5.0"); }); + it("translate-6b" + 'R5-cached', async () => { + await runTest({"suite":"translate2","test":"translate-6b"}, "5.0"); + }); + it("translate-reverse-r4" + 'R4-cached', async () => { await runTest({"suite":"translate2","test":"translate-reverse-r4"}, "4.0"); }); @@ -12474,6 +12742,10 @@ describe('snomed', () => { await runTest({"suite":"snomed","test":"lookup-pc"}, "5.0"); }); + it("lookup-pc-unbracketed" + 'R5-cached', async () => { + await runTest({"suite":"snomed","test":"lookup-pc-unbracketed"}, "5.0"); + }); + it("validate-code-pc-good" + 'R5-cached', async () => { await runTest({"suite":"snomed","test":"validate-code-pc-good"}, "5.0"); }); @@ -12490,6 +12762,10 @@ describe('snomed', () => { await runTest({"suite":"snomed","test":"validate-code-pc-nested-good"}, "5.0"); }); + it("validate-code-pc-unbracketed" + 'R5-cached', async () => { + await runTest({"suite":"snomed","test":"validate-code-pc-unbracketed"}, "5.0"); + }); + it("validate-code-pc-mrcm-lateralizable" + 'R5-cached', async () => { await runTest({"suite":"snomed","test":"validate-code-pc-mrcm-lateralizable"}, "5.0"); }); diff --git a/tx/tests/test-cases-version.js b/tx/tests/test-cases-version.js index 01d032b6..938cddb6 100644 --- a/tx/tests/test-cases-version.js +++ b/tx/tests/test-cases-version.js @@ -3,6 +3,6 @@ // Regenerate with: node generate-tests.js function txTestVersion() { - return '1.9.5-SNAPSHOT'; + return '1.9.6'; } module.exports = { txTestVersion }; From 0ecadbdeabbf278a45e7e1eb4b1ce15e555de61f Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Tue, 29 Sep 2026 10:06:40 +1000 Subject: [PATCH 2/2] hack around git api restriction --- tx/tests/test-runner.js | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/tx/tests/test-runner.js b/tx/tests/test-runner.js index 481b2337..3164ffcb 100644 --- a/tx/tests/test-runner.js +++ b/tx/tests/test-runner.js @@ -10,6 +10,8 @@ const {txTestVersion} = require("./test-cases-version"); const folders = require('../../library/folder-setup'); const {VersionUtilities} = require("../../library/version-utilities"); const packageJson = require('../../package.json'); +const axios = require('axios'); +const { pipeline } = require('stream/promises'); let count = 0; let error = 0; @@ -216,8 +218,30 @@ async function stopServer() { } } +// fhir-validator-wrapper checks for a newer validator_cli.jar by asking the GitHub REST API, +// unauthenticated. On a CI runner that shares a limit of 60 requests an hour with every other +// job on the same IP address; once it is used up, the validator never starts and every tx test +// fails. The release download URL is a plain redirect, not the API, so in CI the jar is fetched +// from there (if it isn't already present) and the wrapper is told not to check. A CI runner +// starts empty, so this always gets the current release anyway. Locally nothing changes. +const LATEST_VALIDATOR_URL = 'https://github.com/hapifhir/org.hl7.fhir.core/releases/latest/download/validator_cli.jar'; + +async function ensureValidatorJar(jarPath) { + if (fs.existsSync(jarPath)) { + return; + } + const tmp = jarPath + '.download'; + const res = await axios.get(LATEST_VALIDATOR_URL, { responseType: 'stream', maxRedirects: 10, timeout: 300000 }); + await pipeline(res.data, fs.createWriteStream(tmp)); + fs.renameSync(tmp, jarPath); +} + async function loadValidator() { const validatorJarPath = folders.ensureFilePath('bin/validator_cli.jar'); + const inCI = !!process.env.CI; + if (inCI) { + await ensureValidatorJar(validatorJarPath); + } log = Logger.getInstance().child({ module: 'test-runner' }); validator = new FhirValidator(validatorJarPath, log); const validatorConfig = { @@ -231,7 +255,9 @@ async function loadValidator() { // 'server' parameter passed to runTxTest() - is our own express server on localhost, and all // content is our own fixtures, so there is nothing untrusted that could redirect the validator // anywhere. Protection has to be off for these tests to connect at all. - ssrfProtection: false + ssrfProtection: false, + // see ensureValidatorJar: no GitHub API call in CI + skipUpdateCheck: inCI } await validator.start(validatorConfig); await validator.loadIG("hl7.fhir.uv.tx-ecosystem", "current");