diff --git a/.github/workflows/pr-pipeline.yml b/.github/workflows/pr-pipeline.yml index 77252f52..5f6be8ad 100644 --- a/.github/workflows/pr-pipeline.yml +++ b/.github/workflows/pr-pipeline.yml @@ -49,7 +49,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '24' cache: 'npm' - name: Install dependencies diff --git a/tests/cs/cs-snomed-ecl.test.js b/tests/cs/cs-snomed-ecl.test.js index 1ac0a8c9..85e47887 100644 --- a/tests/cs/cs-snomed-ecl.test.js +++ b/tests/cs/cs-snomed-ecl.test.js @@ -26,12 +26,101 @@ describe('ECL Validator Test Suite', () => { beforeAll(async () => { // Load test SNOMED data opContext = new OperationContext('en', await TestUtilities.loadTranslations()); - const factory = new SnomedServicesFactory(opContext.i18n, join(__dirname, '../../data/snomed-testing.cache')); + const factory = new SnomedServicesFactory(opContext.i18n, join(__dirname, '../../tx/data/snomed-testing.cache')); await factory.load(); snomedServices = factory.snomedServices; eclValidator = new ECLValidator(snomedServices); }); + describe('ECL cardinality counts distinct values (issue #230 bug 3)', () => { + const expandCodes = (ecl) => { + const ctx = snomedServices.filterECL(ecl, true, opContext); + return new Set((ctx.descendants || []).map(i => snomedServices.concepts.getConceptId(i).toString())); + }; + + // 192008 has two raw 116676008 (Associated morphology) relationship rows but + // only ONE distinct value — the local analog of the issue's 903008. It must + // satisfy [1..1] and [1..*], and must NOT satisfy [2..*]. Counting raw rows + // (the bug) gave the opposite. + test('a single distinct morphology satisfies [1..1]/[1..*], not [2..*]', () => { + const one = expandCodes('* : [1..1] 116676008 = *'); + const atLeastOne = expandCodes('* : [1..*] 116676008 = *'); + const two = expandCodes('* : [2..*] 116676008 = *'); + expect(one.has('192008')).toBe(true); + expect(atLeastOne.has('192008')).toBe(true); + expect(two.has('192008')).toBe(false); + }); + + test('[1..1] and [2..*] partition [1..*] (distinct-value counting is consistent)', () => { + const one = expandCodes('* : [1..1] 116676008 = *'); + const two = expandCodes('* : [2..*] 116676008 = *'); + const atLeastOne = expandCodes('* : [1..*] 116676008 = *'); + expect(one.size + two.size).toBe(atLeastOne.size); + for (const c of one) { + expect(two.has(c)).toBe(false); + } + }); + + // Value-constrained form, matching the shape of the external case + // `< 64572001 : [1..1] 363698007 = << 10200004` whose count rose once + // distinct counting was applied. 192008 has Associated morphology 442021009 + // in two raw rows but one distinct value, so it satisfies [1..1] (not [2..*]) + // for that specific value. + test('value-constrained [1..1] includes a concept with one distinct value stated across rows', () => { + const one = expandCodes('* : [1..1] 116676008 = 442021009'); + const atLeastOne = expandCodes('* : [1..*] 116676008 = 442021009'); + const two = expandCodes('* : [2..*] 116676008 = 442021009'); + expect(one.has('192008')).toBe(true); + expect(atLeastOne.has('192008')).toBe(true); + expect(two.has('192008')).toBe(false); + }); + }); + + describe('ECL memberOf (issue #230 bugs 1 & 2)', () => { + const expand = (ecl) => { + const ctx = snomedServices.filterECL(ecl, true, opContext); + return ctx.descendants || []; + }; + const codesOf = (ecl) => expand(ecl).map(i => snomedServices.concepts.getConceptId(i).toString()); + + // Bug 1: a bare ^ must be rejected with + // an error that names the concept (was: returned the fabricated 0xFFFFFFFF + // sentinel / the concept itself). + test('bare ^ is rejected with a value-identifying error', () => { + expect(() => snomedServices.filterECL('^404684003', true, opContext)) + .toThrow(/404684003 is not a reference set/); + }); + + // Bug 1: returns the refset's referenced-component concepts, including + // concepts that are themselves inactive (active refers to the membership row, + // not the referenced concept). 900000000000526001 (an association refset) + // has one member: the inactive concept 307530000. + test('memberOf returns referenced concepts, including inactive ones', () => { + const codes = codesOf('^900000000000526001'); + expect(codes).toContain('307530000'); + const idx = expand('^900000000000526001').find( + i => snomedServices.concepts.getConceptId(i).toString() === '307530000'); + expect(snomedServices.isActive(idx)).toBe(false); // inactive concept, still returned + }); + + // Bug 1: never fabricate the 0xFFFFFFFF (4294967295) sentinel, and never + // return the operand concept itself. + test('memberOf never emits the sentinel or the operand itself', () => { + const codes = codesOf('^900000000000526001'); + expect(codes).not.toContain('4294967295'); + expect(codes).not.toContain('900000000000526001'); + }); + + // Bug 2: a wrapped/computed operand must be evaluated, not rejected as + // "non-concept-reference refset is not yet supported". + test('wrapped memberOf operands are evaluated, not rejected as unsupported', () => { + expect(() => snomedServices.filterECL('^(<<192008)', true, opContext)).not.toThrow(); + expect(() => snomedServices.filterECL('^(404684003)', true, opContext)).not.toThrow(); + // computed operand is lenient: non-refset concepts in the set are skipped + expect(Array.isArray(codesOf('^(404684003)'))).toBe(true); + }); + }); + describe('ECL Lexer Tests', () => { test('should tokenize simple concept reference', () => { diff --git a/tests/cs/cs-snomed.test.js b/tests/cs/cs-snomed.test.js index 2e3c24c7..db784cbc 100644 --- a/tests/cs/cs-snomed.test.js +++ b/tests/cs/cs-snomed.test.js @@ -30,13 +30,19 @@ const {Designations} = require("../../tx/library/designations"); const {TestUtilities} = require("../test-utilities"); const folders = require('../../library/folder-setup'); -// Shared cache file paths and utilities +// Shared cache file paths and utilities. +// testCachePath is the scratch location the import test (below) writes/deletes. +// committedCachePath is the checked-in copy under tx/data (which IS in git), +// used by the consumer tests when no fresh import has been produced this run. const testCachePath = folders.ensureFilePath('snomed-testing.cache'); +const committedCachePath = path.resolve(__dirname, '../../tx/data/snomed-testing.cache'); const fallbackCachePath = folders.ensureFilePath('sct_intl_20250201.cache'); function findAvailableCacheFile() { if (fs.existsSync(testCachePath)) { return testCachePath; + } else if (fs.existsSync(committedCachePath)) { + return committedCachePath; } else if (fs.existsSync(fallbackCachePath)) { return fallbackCachePath; } diff --git a/tests/library/canonical-resource.test.js b/tests/library/canonical-resource.test.js new file mode 100644 index 00000000..a6eec5a7 --- /dev/null +++ b/tests/library/canonical-resource.test.js @@ -0,0 +1,57 @@ +const { CanonicalResource } = require('../../tx/library/canonical-resource'); + +/** + * isMoreRecent() is documented to return a boolean. The 'alpha' and (especially) + * the 'default' branch compare versions with localeCompare(), which yields + * -1/0/1 — and -1 is truthy. The default branch previously returned that raw + * number, so it both returned a non-boolean and reported "more recent" exactly + * when the version actually sorted EARLIER. These tests lock in the boolean + * contract and correct ordering for both branches. + */ + +function cr(version, algorithm) { + const jsonObj = { resourceType: 'CodeSystem', url: 'http://example.org/cs', version }; + if (algorithm !== undefined) { + jsonObj.versionAlgorithmString = algorithm; + } + return new CanonicalResource(jsonObj); +} + +describe('CanonicalResource.isMoreRecent — boolean contract', () => { + describe('alpha algorithm', () => { + test('returns a strict boolean', () => { + expect(typeof cr('b', 'alpha').isMoreRecent(cr('a', 'alpha'))).toBe('boolean'); + expect(typeof cr('a', 'alpha').isMoreRecent(cr('b', 'alpha'))).toBe('boolean'); + }); + + test('true when this version sorts after other', () => { + expect(cr('b', 'alpha').isMoreRecent(cr('a', 'alpha'))).toBe(true); + }); + + test('false when this version sorts before other', () => { + expect(cr('a', 'alpha').isMoreRecent(cr('b', 'alpha'))).toBe(false); + }); + }); + + describe('default (unrecognised) algorithm', () => { + test('returns a strict boolean even for the fallback branch', () => { + expect(typeof cr('b', 'totally-unknown').isMoreRecent(cr('a', 'totally-unknown'))).toBe('boolean'); + expect(typeof cr('a', 'totally-unknown').isMoreRecent(cr('b', 'totally-unknown'))).toBe('boolean'); + }); + + test('true when this version sorts after other', () => { + expect(cr('b', 'totally-unknown').isMoreRecent(cr('a', 'totally-unknown'))).toBe(true); + }); + + // The original bug: localeCompare('a','b') === -1 (truthy) was returned as-is, + // so this earlier-sorting version was wrongly reported as more recent. + test('false (not truthy -1) when this version sorts before other', () => { + const result = cr('a', 'totally-unknown').isMoreRecent(cr('b', 'totally-unknown')); + expect(result).toBe(false); + }); + }); + + test('returns false when versions are equal', () => { + expect(cr('a', 'totally-unknown').isMoreRecent(cr('a', 'totally-unknown'))).toBe(false); + }); +}); diff --git a/tests/library/codesystem.test.js b/tests/library/codesystem.test.js index 222822fa..a408a3ca 100644 --- a/tests/library/codesystem.test.js +++ b/tests/library/codesystem.test.js @@ -539,8 +539,9 @@ describe('CodeSystem', () => { expect(Array.isArray(parsed.identifier)).toBe(true); expect(parsed.identifier.length).toBe(2); - // R5-only filter operators should be removed - expect(parsed.filter[0].operator).not.toContain('generalizes'); + // 'generalizes' is a valid R4 operator (filter-operator value set, 4.0.1) + // and must be retained on R5 -> R4 conversion + expect(parsed.filter[0].operator).toContain('generalizes'); expect(parsed.filter[0].operator).toContain('='); expect(parsed.filter[0].operator).toContain('is-a'); }); @@ -654,7 +655,7 @@ describe('CodeSystem', () => { filter: [ { "code": "concept", - "operator": ["generalizes"], // Only R5 operator + "operator": ["child-of"], // Only an R5-only operator (stripped for R4) "value": "A string value" } ] @@ -922,7 +923,8 @@ describe('CodeSystem', () => { expect(xmlOutput).toContain(''); expect(xmlOutput).not.toContain('versionAlgorithmString'); - expect(xmlOutput).not.toContain(''); + // 'generalizes' is valid in R4 and must be retained + expect(xmlOutput).toContain(''); expect(xmlOutput).toContain(''); expect(xmlOutput).toContain(''); diff --git a/tests/library/ucum.test.js b/tests/library/ucum.test.js index 9640cdab..f4f22790 100644 --- a/tests/library/ucum.test.js +++ b/tests/library/ucum.test.js @@ -10,13 +10,12 @@ const { XMLParser } = require('fast-xml-parser'); const { - UcumException, Decimal, Pair, Registry, UcumVersionDetails + UcumException, Decimal, Pair, Registry } = require('../../tx/library/ucum-types.js'); const { UcumService} = require('../../tx/library/ucum-service'); describe('UCUM Library Tests', () => { let ucumService; - const jestConsole = console; beforeEach(() => { global.console = require('console'); @@ -44,6 +43,37 @@ describe('UCUM Library Tests', () => { }); }); + describe('Issue 252: special-unit handlers register under their real code (not undefined)', () => { + // CelsiusHandler/FahrenheitHandler override getCode() but never set a `.code` + // field; register() must key on getCode() so they don't both land under + // `undefined` (with Fahrenheit overwriting Celsius). + let registry; + beforeAll(() => { registry = new Registry(); }); + + test('Cel is registered under "Cel"', () => { + expect(registry.exists('Cel')).toBe(true); + expect(registry.get('Cel').getCode()).toBe('Cel'); + }); + + test('[degF] is registered under "[degF]"', () => { + expect(registry.exists('[degF]')).toBe(true); + expect(registry.get('[degF]').getCode()).toBe('[degF]'); + }); + + test('Celsius and Fahrenheit are distinct (no overwrite)', () => { + expect(registry.get('Cel')).not.toBe(registry.get('[degF]')); + }); + + test('nothing is registered under the undefined key', () => { + expect(registry.exists(undefined)).toBe(false); + }); + + test('offset-free special units (HoldingHandler) still register correctly', () => { + expect(registry.exists('[pH]')).toBe(true); + expect(registry.get('[pH]').getCode()).toBe('[pH]'); + }); + }); + describe('Decimal Class Tests', () => { describe('Integer Conversion', () => { test('should convert decimals to integers correctly', () => { @@ -290,7 +320,7 @@ describe('UCUM Library Tests', () => { // Test a smaller range for performance for (let i = 90.5; i < 91; i += 0.01) { const decimal = new Decimal(i.toString()); - const expected = i * 2.2046226218487758072297380134503; + const expected = i / 0.45359237; // pounds per kg = 1 / 0.45359237 (exact lb definition) const actual = ucumService.convert(decimal, 'kg', '[lb_av]'); const actualFloat = parseFloat(actual.asDecimal()); @@ -562,12 +592,6 @@ if (functionalTestCases.length > 0) { // Helper functions - function getTestCasesByType(type) { - return functionalTestCases - .filter(tc => tc.type === type) - .map(tc => [tc.id, tc]); - } - function runValidationCase(testCase) { const { id, unit, valid, reason } = testCase.attributes; const expectedValid = valid === 'true'; @@ -792,14 +816,6 @@ function parseXmlTestCases(xmlContent) { } } - // Log summary of loaded tests - const testCounts = testCases.reduce((counts, tc) => { - counts[tc.type] = (counts[tc.type] || 0) + 1; - return counts; - }, {}); - - // console.log('Loaded test cases by type:', testCounts); - return testCases; } diff --git a/tests/tx/cs-provider-list.test.js b/tests/tx/cs-provider-list.test.js new file mode 100644 index 00000000..4455f346 --- /dev/null +++ b/tests/tx/cs-provider-list.test.js @@ -0,0 +1,43 @@ +const { ListCodeSystemProvider } = require('../../tx/cs/cs-provider-list'); + +/** + * ListCodeSystemProvider.codeSystems is an ARRAY (despite the field's + * "Map" doc comment). Loaders must append with .push(). + * library.js#loadUrl previously called .set() on it (copied from loadNpm but + * rewritten Map-style), which threw because arrays have no .set — so any + * `url:`/`url/cs:` package source failed to load. These tests lock the contract. + */ + +describe('ListCodeSystemProvider.codeSystems contract', () => { + test('is an array, initially empty', () => { + const cp = new ListCodeSystemProvider(); + expect(Array.isArray(cp.codeSystems)).toBe(true); + expect(cp.codeSystems).toHaveLength(0); + }); + + test('has no .set method (loaders must use .push)', () => { + const cp = new ListCodeSystemProvider(); + expect(cp.codeSystems.set).toBeUndefined(); + expect(typeof cp.codeSystems.push).toBe('function'); + }); + + test('pushed code systems are returned by listCodeSystems', async () => { + const cp = new ListCodeSystemProvider(); + cp.codeSystems.push({ url: 'http://a', vurl: 'http://a|1', id: 'a' }); + cp.codeSystems.push({ url: 'http://b', vurl: 'http://b|1', id: 'b' }); + const list = await cp.listCodeSystems('R5', null); + expect(list).toHaveLength(2); + expect(list.map(c => c.url)).toEqual(['http://a', 'http://b']); + }); + + test('assignIds iterates the array and assigns unique ids', () => { + const cp = new ListCodeSystemProvider(); + cp.codeSystems.push({ url: 'http://a' }); // no id + cp.codeSystems.push({ url: 'http://b', id: 'a' }); // collides after first gets id + const ids = new Set(); + cp.assignIds(ids); + const assigned = cp.codeSystems.map(c => c.id); + expect(new Set(assigned).size).toBe(2); // all unique + expect(ids.size).toBe(2); + }); +}); diff --git a/tests/tx/designations.test.js b/tests/tx/designations.test.js index c459a354..b89ec59f 100644 --- a/tests/tx/designations.test.js +++ b/tests/tx/designations.test.js @@ -355,6 +355,13 @@ describe('Designations', () => { expect(passes).toBe(true); }); + test('empty filter passes() matches every value (was broken by this.null typo)', () => { + const emptyFilter = new SearchFilterText(''); + expect(emptyFilter.passes('anything at all')).toBe(true); + expect(emptyFilter.passes('')).toBe(true); + expect(emptyFilter.passes('Blood pressure measurement', true)).toEqual({ passes: true, rating: 0 }); + }); + test('should return rating for matches', () => { const result = searchFilter.passes('Blood pressure measurement', true); expect(result.passes).toBe(true); diff --git a/tests/tx/expansion-cache.test.js b/tests/tx/expansion-cache.test.js new file mode 100644 index 00000000..5c817dc3 --- /dev/null +++ b/tests/tx/expansion-cache.test.js @@ -0,0 +1,81 @@ +const { ExpansionCache } = require('../../tx/operation-context'); + +/** + * ExpansionCache caches an expansion only when it took at least + * MIN_CACHE_TIME_MS — unless forceCaching is enabled, in which case every + * expansion is cached regardless of duration. The test runner uses + * forceCaching to run the whole suite a third time with the cache fully active. + */ + +describe('ExpansionCache duration threshold', () => { + test('does not cache fast expansions by default', () => { + const c = new ExpansionCache(null); + expect(c.set('k', { v: 1 }, 10)).toBe(false); + expect(c.get('k')).toBeFalsy(); + }); + + test('caches slow expansions by default', () => { + const c = new ExpansionCache(null); + expect(c.set('k', { v: 1 }, ExpansionCache.MIN_CACHE_TIME_MS)).toBe(true); + expect(c.get('k')).toEqual({ v: 1 }); + }); +}); + +describe('ExpansionCache forceCaching', () => { + test('defaults to off', () => { + expect(new ExpansionCache(null).forceCaching).toBe(false); + }); + + test('caches fast (even zero-duration) expansions when on', () => { + const c = new ExpansionCache(null); + c.forceCaching = true; + expect(c.set('k', { v: 1 }, 0)).toBe(true); + expect(c.get('k')).toEqual({ v: 1 }); + }); + + test('still caches slow expansions when on', () => { + const c = new ExpansionCache(null); + c.forceCaching = true; + expect(c.set('k', { v: 2 }, 9999)).toBe(true); + expect(c.get('k')).toEqual({ v: 2 }); + }); + + test('clearAll empties the cache (used to reset between passes)', () => { + const c = new ExpansionCache(null); + c.forceCaching = true; + c.set('k', { v: 1 }, 0); + expect(c.size()).toBeGreaterThan(0); + c.clearAll(); + expect(c.size()).toBe(0); + expect(c.get('k')).toBeFalsy(); + }); +}); + +describe('ExpansionCache.forceSet removed', () => { + test('the dead forceSet method no longer exists', () => { + expect(new ExpansionCache(null).forceSet).toBeUndefined(); + }); +}); + +describe('ExpansionCache.getStats (not shadowed by the stats field)', () => { + test('the stats field holds the ServerStats arg, not a method', () => { + const serverStats = { task() {}, taskDone() {} }; + const c = new ExpansionCache(serverStats); + expect(c.stats).toBe(serverStats); // field, not a function + expect(typeof c.getStats).toBe('function'); + }); + + test('getStats reports size, maxSize and hit counts', () => { + const c = new ExpansionCache(null, 50); + c.forceCaching = true; + c.set('a', { v: 1 }, 0); + c.set('b', { v: 2 }, 0); + c.get('a'); // 1 hit + c.get('a'); // 2 hits + c.get('b'); // 1 hit + const s = c.getStats(); + expect(s.size).toBe(2); + expect(s.maxSize).toBe(50); + expect(s.totalHits).toBe(3); + }); +}); diff --git a/tests/tx/params-language-cachekey.test.js b/tests/tx/params-language-cachekey.test.js new file mode 100644 index 00000000..e8564646 --- /dev/null +++ b/tests/tx/params-language-cachekey.test.js @@ -0,0 +1,71 @@ +const { TxParameters } = require('../../tx/params'); +const { TestUtilities } = require('../test-utilities'); + +/** + * Language must enter the expansion cache key. hasHTTPLanguages/ + * hasDisplayLanguages used to read Languages.source (a field that never + * existed), so they were always falsy and the language-folding blocks in + * hashSource() were dead — a cached expansion could be served in the wrong + * language. These tests confirm the flags are set when a language is supplied + * and that the language now changes hashSource(). + */ + +let langDefs, i18n; + +beforeAll(async () => { + langDefs = await TestUtilities.loadLanguageDefinitions(); + i18n = await TestUtilities.loadTranslations(langDefs); +}); + +function paramsWith(extra = []) { + const p = new TxParameters(langDefs, i18n); + p.readParams({ + resourceType: 'Parameters', + parameter: [{ name: 'url', valueUri: 'http://example.org/vs' }, ...extra] + }); + return p; +} + +describe('TxParameters — language enters the cache key', () => { + test('no language: flags false and hash stable', () => { + const a = paramsWith(); + const b = paramsWith(); + expect(a.hasHTTPLanguages).toBe(false); + expect(a.hasDisplayLanguages).toBe(false); + expect(a.hashSource()).toBe(b.hashSource()); + }); + + test('Accept-Language sets hasHTTPLanguages', () => { + const p = paramsWith([{ name: '__Accept-Language', valueCode: 'fr' }]); + expect(p.hasHTTPLanguages).toBe(true); + }); + + test('displayLanguage sets hasDisplayLanguages', () => { + const p = paramsWith([{ name: 'displayLanguage', valueCode: 'de' }]); + expect(p.hasDisplayLanguages).toBe(true); + }); + + test('different Accept-Language values produce different hashes', () => { + const fr = paramsWith([{ name: '__Accept-Language', valueCode: 'fr' }]); + const de = paramsWith([{ name: '__Accept-Language', valueCode: 'de' }]); + expect(fr.hashSource()).not.toBe(de.hashSource()); + }); + + test('a requested language differs from no language in the hash', () => { + const none = paramsWith(); + const fr = paramsWith([{ name: '__Accept-Language', valueCode: 'fr' }]); + expect(fr.hashSource()).not.toBe(none.hashSource()); + }); + + test('different displayLanguage values produce different hashes', () => { + const de = paramsWith([{ name: 'displayLanguage', valueCode: 'de' }]); + const es = paramsWith([{ name: 'displayLanguage', valueCode: 'es' }]); + expect(de.hashSource()).not.toBe(es.hashSource()); + }); + + test('the same requested language hashes identically (cacheable)', () => { + const a = paramsWith([{ name: '__Accept-Language', valueCode: 'fr' }]); + const b = paramsWith([{ name: '__Accept-Language', valueCode: 'fr' }]); + expect(a.hashSource()).toBe(b.hashSource()); + }); +}); diff --git a/tests/tx/params-nocache.test.js b/tests/tx/params-nocache.test.js new file mode 100644 index 00000000..a13dcacd --- /dev/null +++ b/tests/tx/params-nocache.test.js @@ -0,0 +1,73 @@ +const { TxParameters } = require('../../tx/params'); +const { TestUtilities } = require('../test-utilities'); + +/** + * no-cache=true must bust the expansion cache. The cache key (hashSource) reads + * FUid; the no-cache handler previously wrote `this.uid` (a write-only stray + * property), so FUid stayed '' and the key was unchanged. These tests confirm + * no-cache now changes FUid and therefore the hash. + */ + +let langDefs, i18n; + +beforeAll(async () => { + langDefs = await TestUtilities.loadLanguageDefinitions(); + i18n = await TestUtilities.loadTranslations(langDefs); +}); + +function paramsWith(extra = []) { + const p = new TxParameters(langDefs, i18n); + p.readParams({ + resourceType: 'Parameters', + parameter: [{ name: 'url', valueUri: 'http://example.org/vs' }, ...extra] + }); + return p; +} + +describe('TxParameters no-cache cache busting', () => { + test('without no-cache, FUid is empty and the hash is stable across identical inputs', () => { + const a = paramsWith(); + const b = paramsWith(); + expect(a.FUid).toBe(''); + expect(a.hashSource()).toBe(b.hashSource()); + }); + + test('no-cache=true (string form) sets FUid to a random UUID', () => { + const p = paramsWith([{ name: 'no-cache', valueString: 'true' }]); + expect(p.FUid).not.toBe(''); + expect(p.FUid).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i); + }); + + test('no-cache=true (boolean form) also busts the cache', () => { + const cacheable = paramsWith(); + const p = paramsWith([{ name: 'no-cache', valueBoolean: true }]); + expect(p.FUid).not.toBe(''); + expect(p.hashSource()).not.toBe(cacheable.hashSource()); + }); + + test('no-cache=false (boolean form) does not bust the cache', () => { + const plain = paramsWith(); + const p = paramsWith([{ name: 'no-cache', valueBoolean: false }]); + expect(p.FUid).toBe(''); + expect(p.hashSource()).toBe(plain.hashSource()); + }); + + test('no-cache=true changes the hash relative to a cacheable request', () => { + const cacheable = paramsWith(); + const noCache = paramsWith([{ name: 'no-cache', valueString: 'true' }]); + expect(noCache.hashSource()).not.toBe(cacheable.hashSource()); + }); + + test('two no-cache=true requests hash differently (so each bypasses the cache)', () => { + const a = paramsWith([{ name: 'no-cache', valueString: 'true' }]); + const b = paramsWith([{ name: 'no-cache', valueString: 'true' }]); + expect(a.hashSource()).not.toBe(b.hashSource()); + }); + + test('no-cache=false does not bust the cache', () => { + const plain = paramsWith(); + const noCacheFalse = paramsWith([{ name: 'no-cache', valueString: 'false' }]); + expect(noCacheFalse.FUid).toBe(''); + expect(noCacheFalse.hashSource()).toBe(plain.hashSource()); + }); +}); diff --git a/tests/tx/params-result-cachekey.test.js b/tests/tx/params-result-cachekey.test.js new file mode 100644 index 00000000..4b35dc29 --- /dev/null +++ b/tests/tx/params-result-cachekey.test.js @@ -0,0 +1,73 @@ +const { TxParameters } = require('../../tx/params'); +const { TestUtilities } = require('../test-utilities'); + +/** + * Result-affecting expansion parameters that were previously omitted from the + * cache key (hashSource), so a cached expansion could be served for a request + * with a different text filter, abstract setting, etc. These confirm each now + * changes the key, and that the same value keeps it stable (still cacheable). + */ + +let langDefs, i18n; + +beforeAll(async () => { + langDefs = await TestUtilities.loadLanguageDefinitions(); + i18n = await TestUtilities.loadTranslations(langDefs); +}); + +function paramsWith(extra = []) { + const p = new TxParameters(langDefs, i18n); + p.readParams({ + resourceType: 'Parameters', + parameter: [{ name: 'url', valueUri: 'http://example.org/vs' }, ...extra] + }); + return p; +} + +const base = () => paramsWith().hashSource(); + +describe('TxParameters — text filter enters the cache key', () => { + test('a filter differs from no filter', () => { + expect(paramsWith([{ name: 'filter', valueString: 'heart' }]).hashSource()).not.toBe(base()); + }); + + test('different filters produce different keys', () => { + const a = paramsWith([{ name: 'filter', valueString: 'heart' }]).hashSource(); + const b = paramsWith([{ name: 'filter', valueString: 'lung' }]).hashSource(); + expect(a).not.toBe(b); + }); + + test('the same filter is stable (still cacheable)', () => { + const a = paramsWith([{ name: 'filter', valueString: 'heart' }]).hashSource(); + const b = paramsWith([{ name: 'filter', valueString: 'heart' }]).hashSource(); + expect(a).toBe(b); + }); + + test('filter delimiters do not collide (free text is escaped)', () => { + const a = paramsWith([{ name: 'filter', valueString: 'a|b' }]).hashSource(); + const b = paramsWith([{ name: 'filter', valueString: 'a' }, { name: 'sort', valueString: 'b' }]).hashSource(); + expect(a).not.toBe(b); + }); +}); + +describe('TxParameters — other result-affecting params enter the cache key', () => { + test('abstract changes the key (default is abstractOk=true, so test false)', () => { + expect(paramsWith([{ name: 'abstract', valueBoolean: false }]).hashSource()).not.toBe(base()); + }); + + test('limitedExpansion changes the key', () => { + expect(paramsWith([{ name: 'limitedExpansion', valueBoolean: true }]).hashSource()).not.toBe(base()); + }); + + test('incomplete-ok changes the key', () => { + expect(paramsWith([{ name: 'incomplete-ok', valueBoolean: true }]).hashSource()).not.toBe(base()); + }); + + test('diagnostics changes the key', () => { + expect(paramsWith([{ name: 'diagnostics', valueBoolean: true }]).hashSource()).not.toBe(base()); + }); + + test('a plain request (none of these) has a stable key', () => { + expect(base()).toBe(base()); + }); +}); diff --git a/tests/tx/params-supplement-cachekey.test.js b/tests/tx/params-supplement-cachekey.test.js new file mode 100644 index 00000000..4ce29ac6 --- /dev/null +++ b/tests/tx/params-supplement-cachekey.test.js @@ -0,0 +1,62 @@ +const { TxParameters } = require('../../tx/params'); +const { TestUtilities } = require('../test-utilities'); + +/** + * useSupplement changes the expansion result (and a bad supplement must produce + * an error), so it must be part of the expansion cache key. It was previously + * absent from hashSource(), so supplement-none/good/bad requests collided in the + * cache — the forced-caching test pass surfaced this when a bad-supplement + * request returned a cached 200 instead of a 4xx. + */ + +let langDefs, i18n; + +beforeAll(async () => { + langDefs = await TestUtilities.loadLanguageDefinitions(); + i18n = await TestUtilities.loadTranslations(langDefs); +}); + +function paramsWith(extra = []) { + const p = new TxParameters(langDefs, i18n); + p.readParams({ + resourceType: 'Parameters', + parameter: [{ name: 'url', valueUri: 'http://example.org/vs' }, ...extra] + }); + return p; +} + +describe('TxParameters — useSupplement enters the cache key', () => { + test('no supplement: hash stable', () => { + expect(paramsWith().hashSource()).toBe(paramsWith().hashSource()); + }); + + test('a supplement differs from no supplement', () => { + const none = paramsWith(); + const withSup = paramsWith([{ name: 'useSupplement', valueCanonical: 'http://example.org/supp' }]); + expect(withSup.hashSource()).not.toBe(none.hashSource()); + }); + + test('different supplements produce different hashes', () => { + const a = paramsWith([{ name: 'useSupplement', valueCanonical: 'http://example.org/supp-a' }]); + const b = paramsWith([{ name: 'useSupplement', valueCanonical: 'http://example.org/supp-b' }]); + expect(a.hashSource()).not.toBe(b.hashSource()); + }); + + test('the same supplement hashes identically (cacheable)', () => { + const a = paramsWith([{ name: 'useSupplement', valueCanonical: 'http://example.org/supp' }]); + const b = paramsWith([{ name: 'useSupplement', valueCanonical: 'http://example.org/supp' }]); + expect(a.hashSource()).toBe(b.hashSource()); + }); + + test('supplement set is order-independent', () => { + const ab = paramsWith([ + { name: 'useSupplement', valueCanonical: 'http://example.org/a' }, + { name: 'useSupplement', valueCanonical: 'http://example.org/b' } + ]); + const ba = paramsWith([ + { name: 'useSupplement', valueCanonical: 'http://example.org/b' }, + { name: 'useSupplement', valueCanonical: 'http://example.org/a' } + ]); + expect(ab.hashSource()).toBe(ba.hashSource()); + }); +}); diff --git a/tests/tx/provider-codesystem-list.test.js b/tests/tx/provider-codesystem-list.test.js new file mode 100644 index 00000000..8e1ad42a --- /dev/null +++ b/tests/tx/provider-codesystem-list.test.js @@ -0,0 +1,74 @@ +const { Provider } = require('../../tx/provider'); + +/** + * addCodeSystem/deleteCodeSystem maintain a Map keyed by both the unversioned + * url ([url] -> most-recent version) and the versioned url ([url|version] -> that + * version). deleteCodeSystem previously re-inserted the *deleted* CodeSystem + * under its url (and scanned all urls, not just the same one), so a delete + * reverted itself for the unversioned lookup whenever any other system existed. + */ + +function cs(url, version) { + return { + url, + version, + vurl: version ? `${url}|${version}` : url, + // integer-style "more recent" + isMoreRecent(other) { return Number(this.version) > Number(other.version); } + }; +} + +function newProvider(...systems) { + const p = Object.create(Provider.prototype); + p.codeSystems = new Map(); + for (const s of systems) p.addCodeSystem(s); + return p; +} + +describe('Provider.deleteCodeSystem', () => { + test('deleting the only version removes both the url and vurl entries', () => { + const p = newProvider(cs('http://a', '1')); + p.deleteCodeSystem(cs('http://a', '1')); + expect(p.codeSystems.has('http://a')).toBe(false); + expect(p.codeSystems.has('http://a|1')).toBe(false); + expect(p.codeSystems.size).toBe(0); + }); + + test('deleting does not revert itself when another (different) code system exists', () => { + const p = newProvider(cs('http://a', '1'), cs('http://b', '1')); + p.deleteCodeSystem(cs('http://a', '1')); + // a must be gone; b untouched + expect(p.codeSystems.has('http://a')).toBe(false); + expect(p.codeSystems.has('http://a|1')).toBe(false); + expect(p.codeSystems.get('http://b')).toMatchObject({ url: 'http://b' }); + }); + + test('deleting the current default promotes the most-recent surviving same-url version', () => { + const p = newProvider(cs('http://a', '1'), cs('http://a', '2'), cs('http://a', '3')); + // default [a] currently points at v3 + expect(p.codeSystems.get('http://a').version).toBe('3'); + + p.deleteCodeSystem(cs('http://a', '3')); + + expect(p.codeSystems.has('http://a|3')).toBe(false); + // [a] promoted to the most recent survivor (v2) + expect(p.codeSystems.get('http://a').version).toBe('2'); + // older version still individually addressable + expect(p.codeSystems.get('http://a|1').version).toBe('1'); + }); + + test('deleting a non-default version leaves the default in place and removes that version', () => { + const p = newProvider(cs('http://a', '1'), cs('http://a', '2')); + p.deleteCodeSystem(cs('http://a', '1')); + expect(p.codeSystems.has('http://a|1')).toBe(false); + expect(p.codeSystems.get('http://a').version).toBe('2'); + expect(p.codeSystems.get('http://a|2').version).toBe('2'); + }); + + test('the unversioned default is never re-pointed at a different-url system', () => { + const p = newProvider(cs('http://a', '1'), cs('http://b', '5')); + p.deleteCodeSystem(cs('http://a', '1')); + // [a] must not exist, and certainly must not resolve to b + expect(p.codeSystems.get('http://a')).toBeUndefined(); + }); +}); diff --git a/tests/tx/renderer-resources.test.js b/tests/tx/renderer-resources.test.js new file mode 100644 index 00000000..c549d00a --- /dev/null +++ b/tests/tx/renderer-resources.test.js @@ -0,0 +1,238 @@ +const { Renderer } = require('../../tx/library/renderer'); +const { TxHtmlRenderer } = require('../../tx/tx-html'); +const { OperationContext } = require('../../tx/operation-context'); +const { Languages } = require('../../library/languages'); +const { TestUtilities } = require('../test-utilities'); + +/** + * These tests cover every resource type the renderer supports, plus the + * tx-html dispatch layer. They exercise valid rendering AND illegal-input + * corner cases (bad dates, codes, enums, filter ops, relationships, malformed + * structure). For illegal input the key assertion is that the error IDENTIFIES + * THE OFFENDING VALUE — not just that it throws. + */ + +let renderer; +let txHtml; + +beforeAll(async () => { + const langDefs = await TestUtilities.loadLanguageDefinitions(); + const i18n = await TestUtilities.loadTranslations(langDefs); + const opContext = new OperationContext(Languages.fromAcceptLanguage('en-US', langDefs), i18n); + renderer = new Renderer(opContext); + txHtml = new TxHtmlRenderer(renderer, null, Languages.fromAcceptLanguage('en-US', langDefs), i18n, null); +}); + +/** + * Assert that calling `fn` rejects with an InvalidError whose message contains + * every one of `expected` substrings (used to confirm the offending value and + * its location are named). + */ +async function expectInvalid(fn, expected) { + let err; + try { + await fn(); + } catch (e) { + err = e; + } + expect(err).toBeDefined(); + expect(err.name).toBe('InvalidError'); + for (const s of expected) { + expect(err.message).toContain(s); + } + return err; +} + +// ─── Valid rendering smoke tests for every supported type ──────────────────── + +describe('Renderer — valid rendering of all supported resource types', () => { + test('CodeSystem renders to non-empty HTML', async () => { + const html = await renderer.renderCodeSystem({ + resourceType: 'CodeSystem', url: 'http://example.org/cs', status: 'active', + content: 'complete', name: 'Example', + concept: [{ code: 'a', display: 'Alpha' }, { code: 'b', display: 'Beta' }] + }); + expect(typeof html).toBe('string'); + expect(html).toContain('Alpha'); + expect(html).toContain('a'); + }); + + test('ValueSet (compose) renders to non-empty HTML', async () => { + const html = await renderer.renderValueSet({ + resourceType: 'ValueSet', url: 'http://example.org/vs', status: 'active', + compose: { include: [{ system: 'http://example.org/cs', concept: [{ code: 'a', display: 'Alpha' }] }] } + }); + expect(html).toContain('Alpha'); + }); + + test('ConceptMap renders to non-empty HTML', async () => { + const html = await renderer.renderConceptMap({ + resourceType: 'ConceptMap', url: 'http://example.org/cm', status: 'active', + group: [{ source: 'http://s', target: 'http://t', + element: [{ code: 'a', target: [{ code: 'b', relationship: 'equivalent' }] }] }] + }); + expect(typeof html).toBe('string'); + expect(html.length).toBeGreaterThan(0); + }); + + test('CapabilityStatement renders to non-empty HTML', async () => { + const html = await renderer.renderCapabilityStatement({ + resourceType: 'CapabilityStatement', status: 'active' + }); + expect(typeof html).toBe('string'); + expect(html.length).toBeGreaterThan(0); + }); + + test('TerminologyCapabilities renders to non-empty HTML', async () => { + const html = await renderer.renderTerminologyCapabilities({ + resourceType: 'TerminologyCapabilities', status: 'active' + }); + expect(typeof html).toBe('string'); + expect(html.length).toBeGreaterThan(0); + }); + + test('lastUpdated is formatted and the {0} placeholder is substituted', async () => { + const html = await renderer.renderCodeSystem({ + resourceType: 'CodeSystem', url: 'http://x', status: 'active', content: 'complete', + meta: { lastUpdated: '2024-03-15T10:30:00Z' } + }); + expect(html).not.toContain('{0}'); // the bug we fixed + expect(html).toContain('March'); // localised date actually rendered + }); + + test('wrapper objects exposing .json are unwrapped', async () => { + const html = await renderer.renderCodeSystem({ + json: { resourceType: 'CodeSystem', url: 'http://x', status: 'active', content: 'complete' } + }); + expect(typeof html).toBe('string'); + }); +}); + +// ─── CodeSystem corner cases ───────────────────────────────────────────────── + +describe('Renderer.renderCodeSystem — illegal input', () => { + test('null resource names the problem', () => + expectInvalid(() => renderer.renderCodeSystem(null), ['renderCodeSystem', 'null'])); + + test('undefined resource names the problem', () => + expectInvalid(() => renderer.renderCodeSystem(undefined), ['renderCodeSystem', 'undefined'])); + + test('wrong resourceType names the actual type', () => + expectInvalid(() => renderer.renderCodeSystem({ resourceType: 'ValueSet' }), + ['CodeSystem', 'ValueSet'])); + + test('illegal status names the bad value', () => + expectInvalid(() => renderer.renderCodeSystem({ resourceType: 'CodeSystem', status: 'wat', content: 'complete' }), + ['status', 'wat'])); + + test('illegal content names the bad value', () => + expectInvalid(() => renderer.renderCodeSystem({ resourceType: 'CodeSystem', content: 'BOGUS' }), + ['content', 'BOGUS'])); + + test('non-string concept code names the bad value and path', () => + expectInvalid(() => renderer.renderCodeSystem({ + resourceType: 'CodeSystem', status: 'active', content: 'complete', + concept: [{ code: { nope: 1 }, display: 'A' }] + }), ['CodeSystem.concept.code', 'nope'])); + + test('illegal lastUpdated date names the bad value', () => + expectInvalid(() => renderer.renderCodeSystem({ + resourceType: 'CodeSystem', status: 'active', content: 'complete', + meta: { lastUpdated: '2024-13-45' } + }), ['date', 'meta.lastUpdated', '2024-13-45'])); +}); + +// ─── ValueSet corner cases ─────────────────────────────────────────────────── + +describe('Renderer.renderValueSet — illegal input', () => { + test('null resource names the problem', () => + expectInvalid(() => renderer.renderValueSet(null), ['renderValueSet', 'null'])); + + test('illegal status names the bad value', () => + expectInvalid(() => renderer.renderValueSet({ resourceType: 'ValueSet', status: 'nope' }), + ['status', 'nope'])); + + test('illegal filter operator names the bad op and path', () => + expectInvalid(() => renderer.renderValueSet({ + resourceType: 'ValueSet', status: 'active', + compose: { include: [{ system: 'http://y', filter: [{ property: 'p', op: 'BOGUS', value: 'v' }] }] } + }), ['filter.op', 'BOGUS'])); + + test('empty include (no system, no valueSet) names the include', () => + expectInvalid(() => renderer.renderValueSet({ + resourceType: 'ValueSet', status: 'active', compose: { include: [{}] } + }), ['ValueSet.compose.include'])); + + test('non-string concept code in include names the bad value', () => + expectInvalid(() => renderer.renderValueSet({ + resourceType: 'ValueSet', status: 'active', + compose: { include: [{ system: 'http://y', concept: [{ code: 42 }] }] } + }), ['concept.code', '42'])); +}); + +// ─── ConceptMap corner cases ───────────────────────────────────────────────── + +describe('Renderer.renderConceptMap — illegal input', () => { + test('null resource names the problem', () => + expectInvalid(() => renderer.renderConceptMap(null), ['renderConceptMap', 'null'])); + + test('illegal relationship names the bad value', () => + expectInvalid(() => renderer.renderConceptMap({ + resourceType: 'ConceptMap', status: 'active', + group: [{ source: 'http://s', target: 'http://t', + element: [{ code: 'a', target: [{ code: 'b', relationship: 'BOGUS' }] }] }] + }), ['relationship', 'BOGUS'])); + + test('illegal (legacy) equivalence names the bad value', () => + expectInvalid(() => renderer.renderConceptMap({ + resourceType: 'ConceptMap', status: 'active', + group: [{ source: 'http://s', target: 'http://t', + element: [{ code: 'a', target: [{ code: 'b', equivalence: 'BOGUS' }] }] }] + }), ['equivalence', 'BOGUS'])); +}); + +// ─── CapabilityStatement / TerminologyCapabilities corner cases ────────────── + +describe('Renderer — CapabilityStatement / TerminologyCapabilities illegal input', () => { + test('CapabilityStatement null names the problem', () => + expectInvalid(() => renderer.renderCapabilityStatement(null), + ['renderCapabilityStatement', 'null'])); + + test('CapabilityStatement illegal status names the bad value', () => + expectInvalid(() => renderer.renderCapabilityStatement({ resourceType: 'CapabilityStatement', status: 'zzz' }), + ['status', 'zzz'])); + + test('TerminologyCapabilities null names the problem', () => + expectInvalid(() => renderer.renderTerminologyCapabilities(null), + ['renderTerminologyCapabilities', 'null'])); + + test('TerminologyCapabilities illegal status names the bad value', () => + expectInvalid(() => renderer.renderTerminologyCapabilities({ resourceType: 'TerminologyCapabilities', status: 'zzz' }), + ['status', 'zzz'])); +}); + +// ─── tx-html dispatch layer ────────────────────────────────────────────────── + +describe('TxHtmlRenderer.render — dispatch guards', () => { + const req = { path: '/CodeSystem/x', query: {} }; + + test('null resource is reported, not a generic crash', () => + expectInvalid(() => txHtml.render(null, req), ['Cannot render', 'null'])); + + test('non-object resource is reported', () => + expectInvalid(() => txHtml.render('a string', req), ['Cannot render', 'string'])); + + test('missing resourceType is reported', () => + expectInvalid(() => txHtml.render({ url: 'http://x' }, req), ['resourceType'])); + + test('empty resourceType is reported', () => + expectInvalid(() => txHtml.render({ resourceType: '' }, req), ['resourceType'])); +}); + +// ─── displayDate is unaffected (still lenient for low-level formatting) ─────── + +describe('displayDate remains lenient (separate from resource validation)', () => { + test('illegal date returned unchanged by the low-level formatter', () => { + expect(renderer.displayDate('2024-13-45')).toBe('2024-13-45'); + }); +}); diff --git a/tests/tx/renderer.test.js b/tests/tx/renderer.test.js new file mode 100644 index 00000000..bbe12203 --- /dev/null +++ b/tests/tx/renderer.test.js @@ -0,0 +1,191 @@ +const { Renderer } = require('../../tx/library/renderer'); +const { Languages } = require('../../library/languages'); + +/** + * Build a Renderer whose opContext exposes the languages parsed from an + * Accept-Language header. Language definitions are passed as null so the + * BCP-47 parser skips registry validation (we only care about how the parsed + * language/script/region drive formatting, not whether the subtags are + * registered). Pass `undefined` to get a context with no languages at all. + */ +function rendererFor(acceptLanguage) { + let langs; + if (acceptLanguage === undefined) { + langs = new Languages(null); // no languages added at all + } else { + langs = Languages.fromAcceptLanguage(acceptLanguage, null, false); + } + return new Renderer({ langs }); +} + +describe('Renderer._formatLocale (locale selection)', () => { + test('returns language-region for a simple tag', () => { + expect(rendererFor('en-US')._formatLocale()).toBe('en-US'); + expect(rendererFor('en-GB')._formatLocale()).toBe('en-GB'); + expect(rendererFor('fr-FR')._formatLocale()).toBe('fr-FR'); + }); + + test('returns language only when no region is present', () => { + expect(rendererFor('fr')._formatLocale()).toBe('fr'); + }); + + test('includes script and region when present', () => { + expect(rendererFor('zh-Hant-TW')._formatLocale()).toBe('zh-Hant-TW'); + }); + + test('honours quality-ordered preference (highest q first)', () => { + // de-DE has higher quality and should win over fr-FR + expect(rendererFor('fr-FR;q=0.5,de-DE;q=0.9')._formatLocale()).toBe('de-DE'); + }); + + test('skips the wildcard language and falls through to a real one', () => { + // wildcard has the highest quality but must be ignored for formatting + expect(rendererFor('*;q=1.0,en-GB;q=0.9')._formatLocale()).toBe('en-GB'); + }); + + test('falls back to en-US when only a wildcard is supplied', () => { + expect(rendererFor('*')._formatLocale()).toBe('en-US'); + }); + + test('falls back to en-US when there are no languages', () => { + expect(rendererFor(undefined)._formatLocale()).toBe('en-US'); + }); + + test('does not throw and falls back when opContext has no langs', () => { + expect(new Renderer({})._formatLocale()).toBe('en-US'); + expect(new Renderer({ langs: null })._formatLocale()).toBe('en-US'); + }); +}); + +describe('Renderer.displayDate (precision handling)', () => { + const r = rendererFor('en-US'); + + test('year only is returned unchanged', () => { + expect(r.displayDate('2024')).toBe('2024'); + expect(r.displayDate('1900')).toBe('1900'); + }); + + test('year-month becomes "Month Year"', () => { + expect(r.displayDate('2024-03')).toBe('March 2024'); + expect(r.displayDate('2024-12')).toBe('December 2024'); + }); + + test('full date is localised with month name', () => { + const out = r.displayDate('2024-03-15'); + expect(out).toContain('March'); + expect(out).toContain('15'); + expect(out).toContain('2024'); + }); + + test('dateTime with Z renders date and time in UTC', () => { + const out = r.displayDate('2024-03-15T10:30:00Z'); + expect(out).toContain('March'); + expect(out).toContain('2024'); + expect(out).toContain('10:30'); + expect(out).toContain('UTC'); + }); + + test('instant with milliseconds renders to the second', () => { + const out = r.displayDate('2024-03-15T10:30:00.123Z'); + expect(out).toContain('10:30'); + expect(out).toContain('UTC'); + }); + + test('dateTime with a numeric offset is normalised to UTC', () => { + // 10:30 at +10:00 is 00:30 UTC, on the same calendar day + const out = r.displayDate('2024-03-15T10:30:00+10:00'); + expect(out).toContain('UTC'); + expect(out).toContain('March 15, 2024'); + expect(out).toContain('12:30'); // 00:30 shown as 12:30 AM in en-US + expect(out).toContain('AM'); + }); + + test('dateTime offset can roll the date back across midnight', () => { + // 05:00 at +10:00 is 19:00 UTC on the PREVIOUS day + const out = r.displayDate('2024-03-15T05:00:00+10:00'); + expect(out).toContain('March 14, 2024'); + expect(out).toContain('UTC'); + }); + + test('dateTime without a timezone is not labelled UTC', () => { + const out = r.displayDate('2024-03-15T10:30:00'); + expect(out).toContain('March'); + expect(out).toContain('2024'); + expect(out).not.toContain('UTC'); + }); +}); + +describe('Renderer.displayDate (locale-specific formatting)', () => { + test('US English orders month before day', () => { + const out = rendererFor('en-US').displayDate('2024-03-15'); + expect(out.indexOf('March')).toBeLessThan(out.indexOf('15')); + }); + + test('UK English orders day before month', () => { + const out = rendererFor('en-GB').displayDate('2024-03-15'); + expect(out.indexOf('15')).toBeLessThan(out.indexOf('March')); + }); + + test('French uses localised month names', () => { + expect(rendererFor('fr-FR').displayDate('2024-03')).toBe('mars 2024'); + }); + + test('German uses localised month names', () => { + expect(rendererFor('de-DE').displayDate('2024-03')).toBe('März 2024'); + }); + + test('the same instant formats differently per locale but means the same time', () => { + const us = rendererFor('en-US').displayDate('2024-03-15T10:30:00+10:00'); + const gb = rendererFor('en-GB').displayDate('2024-03-15T10:30:00+10:00'); + expect(us).not.toBe(gb); + expect(us).toContain('12:30'); // 24h 00:30 -> 12:30 AM + expect(gb).toContain('00:30'); // GB uses 24h clock + }); +}); + +describe('Renderer.displayDate (robustness / edge cases)', () => { + const r = rendererFor('en-US'); + + test('empty, null, and undefined yield an empty string', () => { + expect(r.displayDate('')).toBe(''); + expect(r.displayDate(null)).toBe(''); + expect(r.displayDate(undefined)).toBe(''); + }); + + test('non-string input is coerced safely', () => { + expect(r.displayDate(2024)).toBe('2024'); + }); + + test('non-date strings are returned unchanged', () => { + expect(r.displayDate('not-a-date')).toBe('not-a-date'); + expect(r.displayDate('hello world')).toBe('hello world'); + }); + + test('malformed month/day values are returned unchanged (no silent rollover)', () => { + expect(r.displayDate('2024-13')).toBe('2024-13'); // month 13 + expect(r.displayDate('2024-00')).toBe('2024-00'); // month 0 + expect(r.displayDate('2024-13-45')).toBe('2024-13-45'); // month & day invalid + expect(r.displayDate('2024-02-30')).toBe('2024-02-30'); // Feb 30 doesn't exist + }); + + test('invalid time components are returned unchanged', () => { + expect(r.displayDate('2024-03-15T25:99:00Z')).toBe('2024-03-15T25:99:00Z'); + }); + + test('leap-day formats correctly', () => { + const out = r.displayDate('2024-02-29'); + expect(out).toContain('February'); + expect(out).toContain('29'); + // and a non-leap year Feb 29 is rejected + expect(r.displayDate('2023-02-29')).toBe('2023-02-29'); + }); + + test('never throws regardless of input', () => { + const inputs = ['2024', '2024-03', '2024-03-15', '2024-03-15T10:30:00Z', + '2024-13-99', 'garbage', '', null, undefined, 42, {}, []]; + for (const v of inputs) { + expect(() => r.displayDate(v)).not.toThrow(); + expect(typeof r.displayDate(v)).toBe('string'); + } + }); +}); diff --git a/tests/tx/test-cases.test.js b/tests/tx/test-cases.test.js index b1855681..9156cc75 100644 --- a/tests/tx/test-cases.test.js +++ b/tests/tx/test-cases.test.js @@ -2,7 +2,7 @@ // Generated from test-cases.json // Regenerate with: node generate-tests.js -const { runTest, startTxTests, finishTxTests } = require('../../tx/tests/test-runner'); +const { runTest, startTxTests, finishTxTests, setForcedCaching } = require('../../tx/tests/test-runner'); describe('Tx Tests', () => { @@ -6769,5 +6769,3481 @@ describe('related2', () => { }); + +describe('cached (forced caching)', () => { + beforeAll(() => { setForcedCaching(true); }); + afterAll(() => { setForcedCaching(false); }); + +describe('metadata', () => { + // tests for minimal requirements for metadata statements + + it('metadataR5-cached', async () => { + await runTest({"suite":"metadata","test":"metadata"}, "5.0"); + }); + + it('term-capsR5-cached', async () => { + await runTest({"suite":"metadata","test":"term-caps"}, "5.0"); + }); + +}); + +describe('simple-cases', () => { + // basic tests, setting up for the API tests to come + + it('simple-expand-allR5-cached', async () => { + await runTest({"suite":"simple-cases","test":"simple-expand-all"}, "5.0"); + }); + + it('simple-expand-activeR5-cached', async () => { + await runTest({"suite":"simple-cases","test":"simple-expand-active"}, "5.0"); + }); + + it('simple-expand-inactiveR5-cached', async () => { + await runTest({"suite":"simple-cases","test":"simple-expand-inactive"}, "5.0"); + }); + + it('simple-expand-enumR5-cached', async () => { + await runTest({"suite":"simple-cases","test":"simple-expand-enum"}, "5.0"); + }); + + it('simple-expand-enum-badR5-cached', async () => { + await runTest({"suite":"simple-cases","test":"simple-expand-enum-bad"}, "5.0"); + }); + + it('simple-expand-isaR5-cached', async () => { + await runTest({"suite":"simple-cases","test":"simple-expand-isa"}, "5.0"); + }); + + it('simple-expand-child-ofR5-cached', async () => { + await runTest({"suite":"simple-cases","test":"simple-expand-child-of"}, "5.0"); + }); + + it('simple-expand-isa-o2R5-cached', async () => { + await runTest({"suite":"simple-cases","test":"simple-expand-isa-o2"}, "5.0"); + }); + + it('simple-expand-isa-c2R5-cached', async () => { + await runTest({"suite":"simple-cases","test":"simple-expand-isa-c2"}, "5.0"); + }); + + it('simple-expand-isa-o2c2R5-cached', async () => { + await runTest({"suite":"simple-cases","test":"simple-expand-isa-o2c2"}, "5.0"); + }); + + it('simple-expand-propR5-cached', async () => { + await runTest({"suite":"simple-cases","test":"simple-expand-prop"}, "5.0"); + }); + + it('simple-expand-regexR5-cached', async () => { + await runTest({"suite":"simple-cases","test":"simple-expand-regex"}, "5.0"); + }); + + it('simple-expand-regex2R5-cached', async () => { + await runTest({"suite":"simple-cases","test":"simple-expand-regex2"}, "5.0"); + }); + + it('simple-expand-regexp-propR5-cached', async () => { + await runTest({"suite":"simple-cases","test":"simple-expand-regexp-prop"}, "5.0"); + }); + + it('simple-lookup-1R5-cached', async () => { + await runTest({"suite":"simple-cases","test":"simple-lookup-1"}, "5.0"); + }); + + it('simple-lookup-2R5-cached', async () => { + await runTest({"suite":"simple-cases","test":"simple-lookup-2"}, "5.0"); + }); + + it('simple-expand-all-countR5-cached', async () => { + await runTest({"suite":"simple-cases","test":"simple-expand-all-count"}, "5.0"); + }); + + it('simple-expand-containedR5-cached', async () => { + await runTest({"suite":"simple-cases","test":"simple-expand-contained"}, "5.0"); + }); + +}); + +describe('parameters', () => { + // Testing out the various expansion parameters that the IG publisher makes use of + + it('parameters-expand-all-hierarchyR5-cached', async () => { + await runTest({"suite":"parameters","test":"parameters-expand-all-hierarchy"}, "5.0"); + }); + + it('parameters-expand-enum-hierarchyR5-cached', async () => { + await runTest({"suite":"parameters","test":"parameters-expand-enum-hierarchy"}, "5.0"); + }); + + it('parameters-expand-isa-hierarchyR5-cached', async () => { + await runTest({"suite":"parameters","test":"parameters-expand-isa-hierarchy"}, "5.0"); + }); + + it('parameters-expand-all-activeR5-cached', async () => { + await runTest({"suite":"parameters","test":"parameters-expand-all-active"}, "5.0"); + }); + + it('parameters-expand-active-activeR5-cached', async () => { + await runTest({"suite":"parameters","test":"parameters-expand-active-active"}, "5.0"); + }); + + it('parameters-expand-inactive-activeR5-cached', async () => { + await runTest({"suite":"parameters","test":"parameters-expand-inactive-active"}, "5.0"); + }); + + it('parameters-expand-enum-activeR5-cached', async () => { + await runTest({"suite":"parameters","test":"parameters-expand-enum-active"}, "5.0"); + }); + + it('parameters-expand-isa-activeR5-cached', async () => { + await runTest({"suite":"parameters","test":"parameters-expand-isa-active"}, "5.0"); + }); + + it('parameters-expand-all-inactiveR5-cached', async () => { + await runTest({"suite":"parameters","test":"parameters-expand-all-inactive"}, "5.0"); + }); + + it('parameters-expand-active-inactiveR5-cached', async () => { + await runTest({"suite":"parameters","test":"parameters-expand-active-inactive"}, "5.0"); + }); + + it('parameters-expand-inactive-inactiveR5-cached', async () => { + await runTest({"suite":"parameters","test":"parameters-expand-inactive-inactive"}, "5.0"); + }); + + it('parameters-expand-enum-inactiveR5-cached', async () => { + await runTest({"suite":"parameters","test":"parameters-expand-enum-inactive"}, "5.0"); + }); + + it('parameters-expand-isa-inactiveR5-cached', async () => { + await runTest({"suite":"parameters","test":"parameters-expand-isa-inactive"}, "5.0"); + }); + + it('parameters-expand-all-designationsR5-cached', async () => { + await runTest({"suite":"parameters","test":"parameters-expand-all-designations"}, "5.0"); + }); + + it('parameters-expand-enum-designationsR5-cached', async () => { + await runTest({"suite":"parameters","test":"parameters-expand-enum-designations"}, "5.0"); + }); + + it('parameters-expand-isa-designationsR5-cached', async () => { + await runTest({"suite":"parameters","test":"parameters-expand-isa-designations"}, "5.0"); + }); + + it('parameters-expand-all-definitionsR5-cached', async () => { + await runTest({"suite":"parameters","test":"parameters-expand-all-definitions"}, "5.0"); + }); + + it('parameters-expand-enum-definitionsR5-cached', async () => { + await runTest({"suite":"parameters","test":"parameters-expand-enum-definitions"}, "5.0"); + }); + + it('parameters-expand-isa-definitionsR5-cached', async () => { + await runTest({"suite":"parameters","test":"parameters-expand-isa-definitions"}, "5.0"); + }); + + it('parameters-expand-all-definitions2R5-cached', async () => { + await runTest({"suite":"parameters","test":"parameters-expand-all-definitions2"}, "5.0"); + }); + + it('parameters-expand-enum-definitions2R5-cached', async () => { + await runTest({"suite":"parameters","test":"parameters-expand-enum-definitions2"}, "5.0"); + }); + + it('parameters-expand-enum-definitions3R5-cached', async () => { + await runTest({"suite":"parameters","test":"parameters-expand-enum-definitions3"}, "5.0"); + }); + + it('parameters-expand-isa-definitions2R5-cached', async () => { + await runTest({"suite":"parameters","test":"parameters-expand-isa-definitions2"}, "5.0"); + }); + + it('parameters-expand-all-propertyR5-cached', async () => { + await runTest({"suite":"parameters","test":"parameters-expand-all-property"}, "5.0"); + }); + + it('parameters-expand-enum-propertyR5-cached', async () => { + await runTest({"suite":"parameters","test":"parameters-expand-enum-property"}, "5.0"); + }); + + it('parameters-expand-isa-propertyR5-cached', async () => { + await runTest({"suite":"parameters","test":"parameters-expand-isa-property"}, "5.0"); + }); + + it('parameters-expand-supplement-noneR5-cached', async () => { + await runTest({"suite":"parameters","test":"parameters-expand-supplement-none"}, "5.0"); + }); + + it('parameters-expand-supplement-goodR5-cached', async () => { + await runTest({"suite":"parameters","test":"parameters-expand-supplement-good"}, "5.0"); + }); + + it('parameters-expand-supplement-badR5-cached', async () => { + await runTest({"suite":"parameters","test":"parameters-expand-supplement-bad"}, "5.0"); + }); + + it('parameters-validate-supplement-noneR5-cached', async () => { + await runTest({"suite":"parameters","test":"parameters-validate-supplement-none"}, "5.0"); + }); + + it('parameters-validate-supplement-goodR5-cached', async () => { + await runTest({"suite":"parameters","test":"parameters-validate-supplement-good"}, "5.0"); + }); + + it('parameters-validate-supplement-badR5-cached', async () => { + await runTest({"suite":"parameters","test":"parameters-validate-supplement-bad"}, "5.0"); + }); + + it('parameters-lookup-supplement-noneR5-cached', async () => { + await runTest({"suite":"parameters","test":"parameters-lookup-supplement-none"}, "5.0"); + }); + + it('parameters-lookup-supplement-goodR5-cached', async () => { + await runTest({"suite":"parameters","test":"parameters-lookup-supplement-good"}, "5.0"); + }); + + it('parameters-lookup-supplement-badR5-cached', async () => { + await runTest({"suite":"parameters","test":"parameters-lookup-supplement-bad"}, "5.0"); + }); + +}); + +describe('language', () => { + // Testing returning language by request, getting the right designation + + it('language-echo-en-noneR5-cached', async () => { + await runTest({"suite":"language","test":"language-echo-en-none"}, "5.0"); + }); + + it('language-echo-de-noneR5-cached', async () => { + await runTest({"suite":"language","test":"language-echo-de-none"}, "5.0"); + }); + + it('language-echo-en-multi-noneR5-cached', async () => { + await runTest({"suite":"language","test":"language-echo-en-multi-none"}, "5.0"); + }); + + it('language-echo-de-multi-noneR5-cached', async () => { + await runTest({"suite":"language","test":"language-echo-de-multi-none"}, "5.0"); + }); + + it('language-echo-en-en-paramR5-cached', async () => { + await runTest({"suite":"language","test":"language-echo-en-en-param"}, "5.0"); + }); + + it('language-echo-en-en-vsR5-cached', async () => { + await runTest({"suite":"language","test":"language-echo-en-en-vs"}, "5.0"); + }); + + it('language-echo-en-en-headerR5-cached', async () => { + await runTest({"suite":"language","test":"language-echo-en-en-header"}, "5.0"); + }); + + it('language-echo-en-en-vslangR5-cached', async () => { + await runTest({"suite":"language","test":"language-echo-en-en-vslang"}, "5.0"); + }); + + it('language-echo-en-en-mixedR5-cached', async () => { + await runTest({"suite":"language","test":"language-echo-en-en-mixed"}, "5.0"); + }); + + it('language-echo-de-de-paramR5-cached', async () => { + await runTest({"suite":"language","test":"language-echo-de-de-param"}, "5.0"); + }); + + it('language-echo-de-de-vsR5-cached', async () => { + await runTest({"suite":"language","test":"language-echo-de-de-vs"}, "5.0"); + }); + + it('language-echo-de-de-headerR5-cached', async () => { + await runTest({"suite":"language","test":"language-echo-de-de-header"}, "5.0"); + }); + + it('language-echo-en-multi-en-paramR5-cached', async () => { + await runTest({"suite":"language","test":"language-echo-en-multi-en-param"}, "5.0"); + }); + + it('language-echo-en-multi-en-vsR5-cached', async () => { + await runTest({"suite":"language","test":"language-echo-en-multi-en-vs"}, "5.0"); + }); + + it('language-echo-en-multi-en-headerR5-cached', async () => { + await runTest({"suite":"language","test":"language-echo-en-multi-en-header"}, "5.0"); + }); + + it('language-echo-de-multi-de-paramR5-cached', async () => { + await runTest({"suite":"language","test":"language-echo-de-multi-de-param"}, "5.0"); + }); + + it('language-echo-de-multi-de-vsR5-cached', async () => { + await runTest({"suite":"language","test":"language-echo-de-multi-de-vs"}, "5.0"); + }); + + it('language-echo-de-multi-de-headerR5-cached', async () => { + await runTest({"suite":"language","test":"language-echo-de-multi-de-header"}, "5.0"); + }); + + it('language-xform-en-multi-de-softR5-cached', async () => { + await runTest({"suite":"language","test":"language-xform-en-multi-de-soft"}, "5.0"); + }); + + it('language-xform-en-multi-de-hardR5-cached', async () => { + await runTest({"suite":"language","test":"language-xform-en-multi-de-hard"}, "5.0"); + }); + + it('language-xform-en-multi-de-defaultR5-cached', async () => { + await runTest({"suite":"language","test":"language-xform-en-multi-de-default"}, "5.0"); + }); + + it('language-xform-de-multi-en-softR5-cached', async () => { + await runTest({"suite":"language","test":"language-xform-de-multi-en-soft"}, "5.0"); + }); + + it('language-xform-de-multi-en-hardR5-cached', async () => { + await runTest({"suite":"language","test":"language-xform-de-multi-en-hard"}, "5.0"); + }); + + it('language-xform-de-multi-en-defaultR5-cached', async () => { + await runTest({"suite":"language","test":"language-xform-de-multi-en-default"}, "5.0"); + }); + + it('language-echo-en-designationR5-cached', async () => { + await runTest({"suite":"language","test":"language-echo-en-designation"}, "5.0"); + }); + + it('language-echo-en-designationsR5-cached', async () => { + await runTest({"suite":"language","test":"language-echo-en-designations"}, "5.0"); + }); + +}); + +describe('language2', () => { + // A series of tests that test display name validation for various permutations of languages + + it('validation-right-de-enR5-cached', async () => { + await runTest({"suite":"language2","test":"validation-right-de-en"}, "5.0"); + }); + + it('validation-right-de-ende-NR5-cached', async () => { + await runTest({"suite":"language2","test":"validation-right-de-ende-N"}, "5.0"); + }); + + it('validation-right-de-endeR5-cached', async () => { + await runTest({"suite":"language2","test":"validation-right-de-ende"}, "5.0"); + }); + + it('validation-right-de-noneR5-cached', async () => { + await runTest({"suite":"language2","test":"validation-right-de-none"}, "5.0"); + }); + + it('validation-right-en-enR5-cached', async () => { + await runTest({"suite":"language2","test":"validation-right-en-en"}, "5.0"); + }); + + it('validation-right-en-ende-NR5-cached', async () => { + await runTest({"suite":"language2","test":"validation-right-en-ende-N"}, "5.0"); + }); + + it('validation-right-en-endeR5-cached', async () => { + await runTest({"suite":"language2","test":"validation-right-en-ende"}, "5.0"); + }); + + it('validation-right-en-noneR5-cached', async () => { + await runTest({"suite":"language2","test":"validation-right-en-none"}, "5.0"); + }); + + it('validation-right-none-enR5-cached', async () => { + await runTest({"suite":"language2","test":"validation-right-none-en"}, "5.0"); + }); + + it('validation-right-none-ende-NR5-cached', async () => { + await runTest({"suite":"language2","test":"validation-right-none-ende-N"}, "5.0"); + }); + + it('validation-right-none-endeR5-cached', async () => { + await runTest({"suite":"language2","test":"validation-right-none-ende"}, "5.0"); + }); + + it('validation-right-none-noneR5-cached', async () => { + await runTest({"suite":"language2","test":"validation-right-none-none"}, "5.0"); + }); + + it('validation-wrong-de-enR5-cached', async () => { + await runTest({"suite":"language2","test":"validation-wrong-de-en"}, "5.0"); + }); + + it('validation-wrong-de-en-badR5-cached', async () => { + await runTest({"suite":"language2","test":"validation-wrong-de-en-bad"}, "5.0"); + }); + + it('validation-wrong-de-ende-NR5-cached', async () => { + await runTest({"suite":"language2","test":"validation-wrong-de-ende-N"}, "5.0"); + }); + + it('validation-wrong-de-endeR5-cached', async () => { + await runTest({"suite":"language2","test":"validation-wrong-de-ende"}, "5.0"); + }); + + it('validation-wrong-de-noneR5-cached', async () => { + await runTest({"suite":"language2","test":"validation-wrong-de-none"}, "5.0"); + }); + + it('validation-wrong-en-enR5-cached', async () => { + await runTest({"suite":"language2","test":"validation-wrong-en-en"}, "5.0"); + }); + + it('validation-wrong-en-ende-NR5-cached', async () => { + await runTest({"suite":"language2","test":"validation-wrong-en-ende-N"}, "5.0"); + }); + + it('validation-wrong-en-endeR5-cached', async () => { + await runTest({"suite":"language2","test":"validation-wrong-en-ende"}, "5.0"); + }); + + it('validation-wrong-en-noneR5-cached', async () => { + await runTest({"suite":"language2","test":"validation-wrong-en-none"}, "5.0"); + }); + + it('validation-wrong-none-enR5-cached', async () => { + await runTest({"suite":"language2","test":"validation-wrong-none-en"}, "5.0"); + }); + + it('validation-wrong-none-ende-NR5-cached', async () => { + await runTest({"suite":"language2","test":"validation-wrong-none-ende-N"}, "5.0"); + }); + + it('validation-wrong-none-endeR5-cached', async () => { + await runTest({"suite":"language2","test":"validation-wrong-none-ende"}, "5.0"); + }); + + it('validation-wrong-none-noneR5-cached', async () => { + await runTest({"suite":"language2","test":"validation-wrong-none-none"}, "5.0"); + }); + +}); + +describe('extensions', () => { + // Testing proper handling of extensions, which depends on the extension + + it('extensions-echo-allR5-cached', async () => { + await runTest({"suite":"extensions","test":"extensions-echo-all"}, "5.0"); + }); + + it('extensions-echo-enumeratedR5-cached', async () => { + await runTest({"suite":"extensions","test":"extensions-echo-enumerated"}, "5.0"); + }); + + it('extensions-echo-bad-supplementR5-cached', async () => { + await runTest({"suite":"extensions","test":"extensions-echo-bad-supplement"}, "5.0"); + }); + + it('validate-code-bad-supplementR5-cached', async () => { + await runTest({"suite":"extensions","test":"validate-code-bad-supplement"}, "5.0"); + }); + + it('validate-coding-bad-supplementR5-cached', async () => { + await runTest({"suite":"extensions","test":"validate-coding-bad-supplement"}, "5.0"); + }); + + it('validate-coding-bad-supplement-urlR5-cached', async () => { + await runTest({"suite":"extensions","test":"validate-coding-bad-supplement-url"}, "5.0"); + }); + + it('validate-codeableconcept-bad-supplementR5-cached', async () => { + await runTest({"suite":"extensions","test":"validate-codeableconcept-bad-supplement"}, "5.0"); + }); + + it('validate-coding-good-supplementR5-cached', async () => { + await runTest({"suite":"extensions","test":"validate-coding-good-supplement"}, "5.0"); + }); + + it('validate-coding-good2-supplementR5-cached', async () => { + await runTest({"suite":"extensions","test":"validate-coding-good2-supplement"}, "5.0"); + }); + + it('validate-code-inactive-displayR5-cached', async () => { + await runTest({"suite":"extensions","test":"validate-code-inactive-display"}, "5.0"); + }); + + it('validate-code-inactiveR5-cached', async () => { + await runTest({"suite":"extensions","test":"validate-code-inactive"}, "5.0"); + }); + +}); + +describe('validation', () => { + // Testing various validation parameter combinations + + it('validation-simple-code-goodR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-simple-code-good"}, "5.0"); + }); + + it('validation-simple-code-implied-goodR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-simple-code-implied-good"}, "5.0"); + }); + + it('validation-simple-coding-goodR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-simple-coding-good"}, "5.0"); + }); + + it('validation-simple-codeableconcept-goodR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-simple-codeableconcept-good"}, "5.0"); + }); + + it('validation-simple-code-bad-codeR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-simple-code-bad-code"}, "5.0"); + }); + + it('validation-simple-code-implied-bad-codeR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-simple-code-implied-bad-code"}, "5.0"); + }); + + it('validation-simple-coding-bad-codeR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-simple-coding-bad-code"}, "5.0"); + }); + + it('validation-simple-coding-bad-code-inactiveR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-simple-coding-bad-code-inactive"}, "5.0"); + }); + + it('validation-simple-codeableconcept-bad-codeR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-simple-codeableconcept-bad-code"}, "5.0"); + }); + + it('validation-simple-code-bad-valueSetR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-simple-code-bad-valueSet"}, "5.0"); + }); + + it('validation-simple-coding-bad-valueSetR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-simple-coding-bad-valueSet"}, "5.0"); + }); + + it('validation-simple-codeableconcept-bad-valueSetR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-simple-codeableconcept-bad-valueSet"}, "5.0"); + }); + + it('validation-simple-code-bad-importR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-simple-code-bad-import"}, "5.0"); + }); + + it('validation-simple-coding-bad-importR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-simple-coding-bad-import"}, "5.0"); + }); + + it('validation-simple-codeableconcept-bad-importR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-simple-codeableconcept-bad-import"}, "5.0"); + }); + + it('validation-simple-code-bad-systemR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-simple-code-bad-system"}, "5.0"); + }); + + it('validation-simple-coding-bad-systemR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-simple-coding-bad-system"}, "5.0"); + }); + + it('validation-simple-coding-bad-system2R5-cached', async () => { + await runTest({"suite":"validation","test":"validation-simple-coding-bad-system2"}, "5.0"); + }); + + it('validation-simple-coding-bad-system-localR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-simple-coding-bad-system-local"}, "5.0"); + }); + + it('validation-simple-coding-no-systemR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-simple-coding-no-system"}, "5.0"); + }); + + it('validation-simple-codeableconcept-bad-systemR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-simple-codeableconcept-bad-system"}, "5.0"); + }); + + it('validation-simple-code-good-displayR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-simple-code-good-display"}, "5.0"); + }); + + it('validation-simple-coding-good-displayR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-simple-coding-good-display"}, "5.0"); + }); + + it('validation-simple-codeableconcept-good-displayR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-simple-codeableconcept-good-display"}, "5.0"); + }); + + it('validation-simple-code-bad-displayR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-simple-code-bad-display"}, "5.0"); + }); + + it('validation-simple-code-bad-display-wsR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-simple-code-bad-display-ws"}, "5.0"); + }); + + it('validation-simple-coding-bad-displayR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-simple-coding-bad-display"}, "5.0"); + }); + + it('validation-simple-codeableconcept-bad-displayR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-simple-codeableconcept-bad-display"}, "5.0"); + }); + + it('validation-simple-code-bad-display-warningR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-simple-code-bad-display-warning"}, "5.0"); + }); + + it('validation-simple-coding-bad-display-warningR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-simple-coding-bad-display-warning"}, "5.0"); + }); + + it('validation-simple-codeableconcept-bad-display-warningR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-simple-codeableconcept-bad-display-warning"}, "5.0"); + }); + + it('validation-simple-code-good-languageR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-simple-code-good-language"}, "5.0"); + }); + + it('validation-simple-coding-good-languageR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-simple-coding-good-language"}, "5.0"); + }); + + it('validation-simple-codeableconcept-good-languageR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-simple-codeableconcept-good-language"}, "5.0"); + }); + + it('validation-simple-code-bad-languageR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-simple-code-bad-language"}, "5.0"); + }); + + it('validation-simple-code-good-regexR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-simple-code-good-regex"}, "5.0"); + }); + + it('validation-simple-code-bad-regexR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-simple-code-bad-regex"}, "5.0"); + }); + + it('validation-simple-coding-bad-languageR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-simple-coding-bad-language"}, "5.0"); + }); + + it('validation-simple-coding-bad-language-headerR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-simple-coding-bad-language-header"}, "5.0"); + }); + + it('validation-simple-coding-bad-language-vsR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-simple-coding-bad-language-vs"}, "5.0"); + }); + + it('validation-simple-coding-bad-language-vslangR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-simple-coding-bad-language-vslang"}, "5.0"); + }); + + it('validation-simple-codeableconcept-bad-languageR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-simple-codeableconcept-bad-language"}, "5.0"); + }); + + it('validation-complex-codeableconcept-fullR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-complex-codeableconcept-full"}, "5.0"); + }); + + it('validation-complex-codeableconcept-vsonlyR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-complex-codeableconcept-vsonly"}, "5.0"); + }); + + it('validation-cs-code-goodR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-cs-code-good"}, "5.0"); + }); + + it('validation-cs-code-bad-codeR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-cs-code-bad-code"}, "5.0"); + }); + + it('validation-contained-badR5-cached', async () => { + await runTest({"suite":"validation","test":"validation-contained-bad"}, "5.0"); + }); + +}); + +describe('version', () => { + // Testing various version issues. There's two versions of a code system, and three value sets that select different versions + + it('version-simple-code-bad-version1R5-cached', async () => { + await runTest({"suite":"version","test":"version-simple-code-bad-version1"}, "5.0"); + }); + + it('version-simple-coding-bad-version1R5-cached', async () => { + await runTest({"suite":"version","test":"version-simple-coding-bad-version1"}, "5.0"); + }); + + it('version-simple-codeableconcept-bad-version1R5-cached', async () => { + await runTest({"suite":"version","test":"version-simple-codeableconcept-bad-version1"}, "5.0"); + }); + + it('version-simple-codeableconcept-bad-version2R5-cached', async () => { + await runTest({"suite":"version","test":"version-simple-codeableconcept-bad-version2"}, "5.0"); + }); + + it('version-simple-code-good-versionR5-cached', async () => { + await runTest({"suite":"version","test":"version-simple-code-good-version"}, "5.0"); + }); + + it('version-simple-coding-good-versionR5-cached', async () => { + await runTest({"suite":"version","test":"version-simple-coding-good-version"}, "5.0"); + }); + + it('version-simple-codeableconcept-good-versionR5-cached', async () => { + await runTest({"suite":"version","test":"version-simple-codeableconcept-good-version"}, "5.0"); + }); + + it('version-version-profile-noneR5-cached', async () => { + await runTest({"suite":"version","test":"version-version-profile-none"}, "5.0"); + }); + + it('version-version-profile-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"version-version-profile-default"}, "5.0"); + }); + + it('validation-version-profile-codingR5-cached', async () => { + await runTest({"suite":"version","test":"validation-version-profile-coding"}, "5.0"); + }); + + it('coding-vnn-vsnnR5-cached', async () => { + await runTest({"suite":"version","test":"coding-vnn-vsnn"}, "5.0"); + }); + + it('coding-v10-vs1wR5-cached', async () => { + await runTest({"suite":"version","test":"coding-v10-vs1w"}, "5.0"); + }); + + it('coding-v10-vs1wbR5-cached', async () => { + await runTest({"suite":"version","test":"coding-v10-vs1wb"}, "5.0"); + }); + + it('coding-v10-vs10R5-cached', async () => { + await runTest({"suite":"version","test":"coding-v10-vs10"}, "5.0"); + }); + + it('coding-v10-vs20R5-cached', async () => { + await runTest({"suite":"version","test":"coding-v10-vs20"}, "5.0"); + }); + + it('coding-v10-vsbbR5-cached', async () => { + await runTest({"suite":"version","test":"coding-v10-vsbb"}, "5.0"); + }); + + it('coding-v10-vsbbR5-cached', async () => { + await runTest({"suite":"version","test":"coding-v10-vsbb"}, "5.0"); + }); + + it('coding-v10-vsnnR5-cached', async () => { + await runTest({"suite":"version","test":"coding-v10-vsnn"}, "5.0"); + }); + + it('coding-vbb-vs10R5-cached', async () => { + await runTest({"suite":"version","test":"coding-vbb-vs10"}, "5.0"); + }); + + it('coding-vbb-vsnnR5-cached', async () => { + await runTest({"suite":"version","test":"coding-vbb-vsnn"}, "5.0"); + }); + + it('coding-vnn-vs1wR5-cached', async () => { + await runTest({"suite":"version","test":"coding-vnn-vs1w"}, "5.0"); + }); + + it('coding-vnn-vs1wbR5-cached', async () => { + await runTest({"suite":"version","test":"coding-vnn-vs1wb"}, "5.0"); + }); + + it('coding-vnn-vs10R5-cached', async () => { + await runTest({"suite":"version","test":"coding-vnn-vs10"}, "5.0"); + }); + + it('coding-vnn-vsbbR5-cached', async () => { + await runTest({"suite":"version","test":"coding-vnn-vsbb"}, "5.0"); + }); + + it('coding-vnn-vsnn-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"coding-vnn-vsnn-default"}, "5.0"); + }); + + it('coding-v10-vs1w-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"coding-v10-vs1w-default"}, "5.0"); + }); + + it('coding-v10-vs1wb-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"coding-v10-vs1wb-default"}, "5.0"); + }); + + it('coding-v10-vs10-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"coding-v10-vs10-default"}, "5.0"); + }); + + it('coding-v10-vs20-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"coding-v10-vs20-default"}, "5.0"); + }); + + it('coding-v10-vsbb-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"coding-v10-vsbb-default"}, "5.0"); + }); + + it('coding-v10-vsnn-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"coding-v10-vsnn-default"}, "5.0"); + }); + + it('coding-vbb-vs10-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"coding-vbb-vs10-default"}, "5.0"); + }); + + it('coding-vbb-vsnn-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"coding-vbb-vsnn-default"}, "5.0"); + }); + + it('coding-vnn-vs1w-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"coding-vnn-vs1w-default"}, "5.0"); + }); + + it('coding-vnn-vs1wb-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"coding-vnn-vs1wb-default"}, "5.0"); + }); + + it('coding-vnn-vs10-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"coding-vnn-vs10-default"}, "5.0"); + }); + + it('coding-vnn-vsbb-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"coding-vnn-vsbb-default"}, "5.0"); + }); + + it('coding-vnn-vsnn-checkR5-cached', async () => { + await runTest({"suite":"version","test":"coding-vnn-vsnn-check"}, "5.0"); + }); + + it('coding-v10-vs1w-checkR5-cached', async () => { + await runTest({"suite":"version","test":"coding-v10-vs1w-check"}, "5.0"); + }); + + it('coding-v10-vs1wb-checkR5-cached', async () => { + await runTest({"suite":"version","test":"coding-v10-vs1wb-check"}, "5.0"); + }); + + it('coding-v10-vs10-checkR5-cached', async () => { + await runTest({"suite":"version","test":"coding-v10-vs10-check"}, "5.0"); + }); + + it('coding-v10-vs20-checkR5-cached', async () => { + await runTest({"suite":"version","test":"coding-v10-vs20-check"}, "5.0"); + }); + + it('coding-v10-vsbb-checkR5-cached', async () => { + await runTest({"suite":"version","test":"coding-v10-vsbb-check"}, "5.0"); + }); + + it('coding-v10-vsnn-checkR5-cached', async () => { + await runTest({"suite":"version","test":"coding-v10-vsnn-check"}, "5.0"); + }); + + it('coding-vbb-vs10-checkR5-cached', async () => { + await runTest({"suite":"version","test":"coding-vbb-vs10-check"}, "5.0"); + }); + + it('coding-vbb-vsnn-checkR5-cached', async () => { + await runTest({"suite":"version","test":"coding-vbb-vsnn-check"}, "5.0"); + }); + + it('coding-vnn-vs1w-checkR5-cached', async () => { + await runTest({"suite":"version","test":"coding-vnn-vs1w-check"}, "5.0"); + }); + + it('coding-vnn-vs1wb-checkR5-cached', async () => { + await runTest({"suite":"version","test":"coding-vnn-vs1wb-check"}, "5.0"); + }); + + it('coding-vnn-vs10-checkR5-cached', async () => { + await runTest({"suite":"version","test":"coding-vnn-vs10-check"}, "5.0"); + }); + + it('coding-vnn-vsbb-checkR5-cached', async () => { + await runTest({"suite":"version","test":"coding-vnn-vsbb-check"}, "5.0"); + }); + + it('coding-vnn-vsnn-forceR5-cached', async () => { + await runTest({"suite":"version","test":"coding-vnn-vsnn-force"}, "5.0"); + }); + + it('coding-v10-vs1w-forceR5-cached', async () => { + await runTest({"suite":"version","test":"coding-v10-vs1w-force"}, "5.0"); + }); + + it('coding-v10-vs1wb-forceR5-cached', async () => { + await runTest({"suite":"version","test":"coding-v10-vs1wb-force"}, "5.0"); + }); + + it('coding-v10-vs10-forceR5-cached', async () => { + await runTest({"suite":"version","test":"coding-v10-vs10-force"}, "5.0"); + }); + + it('coding-v10-vs20-forceR5-cached', async () => { + await runTest({"suite":"version","test":"coding-v10-vs20-force"}, "5.0"); + }); + + it('coding-v10-vsbb-forceR5-cached', async () => { + await runTest({"suite":"version","test":"coding-v10-vsbb-force"}, "5.0"); + }); + + it('coding-v10-vsnn-forceR5-cached', async () => { + await runTest({"suite":"version","test":"coding-v10-vsnn-force"}, "5.0"); + }); + + it('coding-vbb-vs10-forceR5-cached', async () => { + await runTest({"suite":"version","test":"coding-vbb-vs10-force"}, "5.0"); + }); + + it('coding-vbb-vsnn-forceR5-cached', async () => { + await runTest({"suite":"version","test":"coding-vbb-vsnn-force"}, "5.0"); + }); + + it('coding-vnn-vs1w-forceR5-cached', async () => { + await runTest({"suite":"version","test":"coding-vnn-vs1w-force"}, "5.0"); + }); + + it('coding-vnn-vs1wb-forceR5-cached', async () => { + await runTest({"suite":"version","test":"coding-vnn-vs1wb-force"}, "5.0"); + }); + + it('coding-vnn-vs10-forceR5-cached', async () => { + await runTest({"suite":"version","test":"coding-vnn-vs10-force"}, "5.0"); + }); + + it('coding-vnn-vsbb-forceR5-cached', async () => { + await runTest({"suite":"version","test":"coding-vnn-vsbb-force"}, "5.0"); + }); + + it('codeableconcept-vnn-vsnnR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-vnn-vsnn"}, "5.0"); + }); + + it('codeableconcept-v10-vs1wR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-v10-vs1w"}, "5.0"); + }); + + it('codeableconcept-v10-vs1wbR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-v10-vs1wb"}, "5.0"); + }); + + it('codeableconcept-v10-vs10R5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-v10-vs10"}, "5.0"); + }); + + it('codeableconcept-v10-vs20R5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-v10-vs20"}, "5.0"); + }); + + it('codeableconcept-v10-vsbbR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-v10-vsbb"}, "5.0"); + }); + + it('codeableconcept-v10-vsbbR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-v10-vsbb"}, "5.0"); + }); + + it('codeableconcept-v10-vsnnR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-v10-vsnn"}, "5.0"); + }); + + it('codeableconcept-vbb-vs10R5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-vbb-vs10"}, "5.0"); + }); + + it('codeableconcept-vbb-vsnnR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-vbb-vsnn"}, "5.0"); + }); + + it('codeableconcept-vnn-vs1wR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-vnn-vs1w"}, "5.0"); + }); + + it('codeableconcept-vnn-vs1wbR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-vnn-vs1wb"}, "5.0"); + }); + + it('codeableconcept-vnn-vs10R5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-vnn-vs10"}, "5.0"); + }); + + it('codeableconcept-vnn-vsbbR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-vnn-vsbb"}, "5.0"); + }); + + it('codeableconcept-vnn-vsnn-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-vnn-vsnn-default"}, "5.0"); + }); + + it('codeableconcept-v10-vs1w-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-v10-vs1w-default"}, "5.0"); + }); + + it('codeableconcept-v10-vs1wb-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-v10-vs1wb-default"}, "5.0"); + }); + + it('codeableconcept-v10-vs10-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-v10-vs10-default"}, "5.0"); + }); + + it('codeableconcept-v10-vs20-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-v10-vs20-default"}, "5.0"); + }); + + it('codeableconcept-v10-vsbb-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-v10-vsbb-default"}, "5.0"); + }); + + it('codeableconcept-v10-vsnn-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-v10-vsnn-default"}, "5.0"); + }); + + it('codeableconcept-vbb-vs10-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-vbb-vs10-default"}, "5.0"); + }); + + it('codeableconcept-vbb-vsnn-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-vbb-vsnn-default"}, "5.0"); + }); + + it('codeableconcept-vnn-vs1w-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-vnn-vs1w-default"}, "5.0"); + }); + + it('codeableconcept-vnn-vs1wb-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-vnn-vs1wb-default"}, "5.0"); + }); + + it('codeableconcept-vnn-vs10-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-vnn-vs10-default"}, "5.0"); + }); + + it('codeableconcept-vnn-vsbb-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-vnn-vsbb-default"}, "5.0"); + }); + + it('codeableconcept-vnn-vsnn-checkR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-vnn-vsnn-check"}, "5.0"); + }); + + it('codeableconcept-v10-vs1w-checkR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-v10-vs1w-check"}, "5.0"); + }); + + it('codeableconcept-v10-vs1wb-checkR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-v10-vs1wb-check"}, "5.0"); + }); + + it('codeableconcept-v10-vs10-checkR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-v10-vs10-check"}, "5.0"); + }); + + it('codeableconcept-v10-vs20-checkR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-v10-vs20-check"}, "5.0"); + }); + + it('codeableconcept-v10-vsbb-checkR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-v10-vsbb-check"}, "5.0"); + }); + + it('codeableconcept-v10-vsnn-checkR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-v10-vsnn-check"}, "5.0"); + }); + + it('codeableconcept-vbb-vs10-checkR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-vbb-vs10-check"}, "5.0"); + }); + + it('codeableconcept-vbb-vsnn-checkR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-vbb-vsnn-check"}, "5.0"); + }); + + it('codeableconcept-vnn-vs1w-checkR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-vnn-vs1w-check"}, "5.0"); + }); + + it('codeableconcept-vnn-vs1wb-checkR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-vnn-vs1wb-check"}, "5.0"); + }); + + it('codeableconcept-vnn-vs10-checkR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-vnn-vs10-check"}, "5.0"); + }); + + it('codeableconcept-vnn-vsbb-checkR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-vnn-vsbb-check"}, "5.0"); + }); + + it('codeableconcept-vnn-vsnn-forceR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-vnn-vsnn-force"}, "5.0"); + }); + + it('codeableconcept-v10-vs1w-forceR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-v10-vs1w-force"}, "5.0"); + }); + + it('codeableconcept-v10-vs1wb-forceR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-v10-vs1wb-force"}, "5.0"); + }); + + it('codeableconcept-v10-vs10-forceR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-v10-vs10-force"}, "5.0"); + }); + + it('codeableconcept-v10-vs20-forceR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-v10-vs20-force"}, "5.0"); + }); + + it('codeableconcept-v10-vsbb-forceR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-v10-vsbb-force"}, "5.0"); + }); + + it('codeableconcept-v10-vsnn-forceR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-v10-vsnn-force"}, "5.0"); + }); + + it('codeableconcept-vbb-vs10-forceR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-vbb-vs10-force"}, "5.0"); + }); + + it('codeableconcept-vbb-vsnn-forceR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-vbb-vsnn-force"}, "5.0"); + }); + + it('codeableconcept-vnn-vs1w-forceR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-vnn-vs1w-force"}, "5.0"); + }); + + it('codeableconcept-vnn-vs1wb-forceR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-vnn-vs1wb-force"}, "5.0"); + }); + + it('codeableconcept-vnn-vs10-forceR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-vnn-vs10-force"}, "5.0"); + }); + + it('codeableconcept-vnn-vsbb-forceR5-cached', async () => { + await runTest({"suite":"version","test":"codeableconcept-vnn-vsbb-force"}, "5.0"); + }); + + it('code-vnn-vsnnR5-cached', async () => { + await runTest({"suite":"version","test":"code-vnn-vsnn"}, "5.0"); + }); + + it('code-v10-vs1wR5-cached', async () => { + await runTest({"suite":"version","test":"code-v10-vs1w"}, "5.0"); + }); + + it('code-v10-vs1wbR5-cached', async () => { + await runTest({"suite":"version","test":"code-v10-vs1wb"}, "5.0"); + }); + + it('code-v10-vs10R5-cached', async () => { + await runTest({"suite":"version","test":"code-v10-vs10"}, "5.0"); + }); + + it('code-v10-vs20R5-cached', async () => { + await runTest({"suite":"version","test":"code-v10-vs20"}, "5.0"); + }); + + it('code-v10-vsbbR5-cached', async () => { + await runTest({"suite":"version","test":"code-v10-vsbb"}, "5.0"); + }); + + it('code-v10-vsnnR5-cached', async () => { + await runTest({"suite":"version","test":"code-v10-vsnn"}, "5.0"); + }); + + it('code-vbb-vs10R5-cached', async () => { + await runTest({"suite":"version","test":"code-vbb-vs10"}, "5.0"); + }); + + it('code-vbb-vsnnR5-cached', async () => { + await runTest({"suite":"version","test":"code-vbb-vsnn"}, "5.0"); + }); + + it('code-vnn-vs1wR5-cached', async () => { + await runTest({"suite":"version","test":"code-vnn-vs1w"}, "5.0"); + }); + + it('code-vnn-vs1wbR5-cached', async () => { + await runTest({"suite":"version","test":"code-vnn-vs1wb"}, "5.0"); + }); + + it('code-vnn-vs10R5-cached', async () => { + await runTest({"suite":"version","test":"code-vnn-vs10"}, "5.0"); + }); + + it('code-vnn-vsbbR5-cached', async () => { + await runTest({"suite":"version","test":"code-vnn-vsbb"}, "5.0"); + }); + + it('code-vnn-vsnn-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"code-vnn-vsnn-default"}, "5.0"); + }); + + it('code-v10-vs1w-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"code-v10-vs1w-default"}, "5.0"); + }); + + it('code-v10-vs1wb-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"code-v10-vs1wb-default"}, "5.0"); + }); + + it('code-v10-vs10-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"code-v10-vs10-default"}, "5.0"); + }); + + it('code-v10-vs20-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"code-v10-vs20-default"}, "5.0"); + }); + + it('code-v10-vsbb-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"code-v10-vsbb-default"}, "5.0"); + }); + + it('code-v10-vsnn-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"code-v10-vsnn-default"}, "5.0"); + }); + + it('code-vbb-vs10-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"code-vbb-vs10-default"}, "5.0"); + }); + + it('code-vbb-vsnn-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"code-vbb-vsnn-default"}, "5.0"); + }); + + it('code-vnn-vs1wb-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"code-vnn-vs1wb-default"}, "5.0"); + }); + + it('code-vnn-vs10-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"code-vnn-vs10-default"}, "5.0"); + }); + + it('code-vnn-vsbb-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"code-vnn-vsbb-default"}, "5.0"); + }); + + it('code-vnn-vsnn-checkR5-cached', async () => { + await runTest({"suite":"version","test":"code-vnn-vsnn-check"}, "5.0"); + }); + + it('code-v10-vs1w-checkR5-cached', async () => { + await runTest({"suite":"version","test":"code-v10-vs1w-check"}, "5.0"); + }); + + it('code-v10-vs1wb-checkR5-cached', async () => { + await runTest({"suite":"version","test":"code-v10-vs1wb-check"}, "5.0"); + }); + + it('code-v10-vs10-checkR5-cached', async () => { + await runTest({"suite":"version","test":"code-v10-vs10-check"}, "5.0"); + }); + + it('code-v10-vs20-checkR5-cached', async () => { + await runTest({"suite":"version","test":"code-v10-vs20-check"}, "5.0"); + }); + + it('code-v10-vsbb-checkR5-cached', async () => { + await runTest({"suite":"version","test":"code-v10-vsbb-check"}, "5.0"); + }); + + it('code-v10-vsnn-checkR5-cached', async () => { + await runTest({"suite":"version","test":"code-v10-vsnn-check"}, "5.0"); + }); + + it('code-vbb-vs10-checkR5-cached', async () => { + await runTest({"suite":"version","test":"code-vbb-vs10-check"}, "5.0"); + }); + + it('code-vbb-vsnn-checkR5-cached', async () => { + await runTest({"suite":"version","test":"code-vbb-vsnn-check"}, "5.0"); + }); + + it('code-vnn-vs1w-checkR5-cached', async () => { + await runTest({"suite":"version","test":"code-vnn-vs1w-check"}, "5.0"); + }); + + it('code-vnn-vs1wb-checkR5-cached', async () => { + await runTest({"suite":"version","test":"code-vnn-vs1wb-check"}, "5.0"); + }); + + it('code-vnn-vs10-checkR5-cached', async () => { + await runTest({"suite":"version","test":"code-vnn-vs10-check"}, "5.0"); + }); + + it('code-vnn-vsbb-checkR5-cached', async () => { + await runTest({"suite":"version","test":"code-vnn-vsbb-check"}, "5.0"); + }); + + it('code-vnn-vsnn-forceR5-cached', async () => { + await runTest({"suite":"version","test":"code-vnn-vsnn-force"}, "5.0"); + }); + + it('code-v10-vs1w-forceR5-cached', async () => { + await runTest({"suite":"version","test":"code-v10-vs1w-force"}, "5.0"); + }); + + it('code-v10-vs1wb-forceR5-cached', async () => { + await runTest({"suite":"version","test":"code-v10-vs1wb-force"}, "5.0"); + }); + + it('code-v10-vs10-forceR5-cached', async () => { + await runTest({"suite":"version","test":"code-v10-vs10-force"}, "5.0"); + }); + + it('code-v10-vs20-forceR5-cached', async () => { + await runTest({"suite":"version","test":"code-v10-vs20-force"}, "5.0"); + }); + + it('code-v10-vsbb-forceR5-cached', async () => { + await runTest({"suite":"version","test":"code-v10-vsbb-force"}, "5.0"); + }); + + it('code-v10-vsnn-forceR5-cached', async () => { + await runTest({"suite":"version","test":"code-v10-vsnn-force"}, "5.0"); + }); + + it('code-vbb-vs10-forceR5-cached', async () => { + await runTest({"suite":"version","test":"code-vbb-vs10-force"}, "5.0"); + }); + + it('code-vbb-vsnn-forceR5-cached', async () => { + await runTest({"suite":"version","test":"code-vbb-vsnn-force"}, "5.0"); + }); + + it('code-vnn-vs1w-forceR5-cached', async () => { + await runTest({"suite":"version","test":"code-vnn-vs1w-force"}, "5.0"); + }); + + it('code-vnn-vs1wb-forceR5-cached', async () => { + await runTest({"suite":"version","test":"code-vnn-vs1wb-force"}, "5.0"); + }); + + it('code-vnn-vs10-forceR5-cached', async () => { + await runTest({"suite":"version","test":"code-vnn-vs10-force"}, "5.0"); + }); + + it('code-vnn-vsbb-forceR5-cached', async () => { + await runTest({"suite":"version","test":"code-vnn-vsbb-force"}, "5.0"); + }); + + it('code-vnn-vsmix-1R5-cached', async () => { + await runTest({"suite":"version","test":"code-vnn-vsmix-1"}, "5.0"); + }); + + it('code-vnn-vsmix-2R5-cached', async () => { + await runTest({"suite":"version","test":"code-vnn-vsmix-2"}, "5.0"); + }); + + it('vs-expand-all-vR5-cached', async () => { + await runTest({"suite":"version","test":"vs-expand-all-v"}, "5.0"); + }); + + it('vs-expand-all-v1R5-cached', async () => { + await runTest({"suite":"version","test":"vs-expand-all-v1"}, "5.0"); + }); + + it('vs-expand-all-v2R5-cached', async () => { + await runTest({"suite":"version","test":"vs-expand-all-v2"}, "5.0"); + }); + + it('vs-expand-v-mixedR5-cached', async () => { + await runTest({"suite":"version","test":"vs-expand-v-mixed"}, "5.0"); + }); + + it('vs-expand-v-n-requestR5-cached', async () => { + await runTest({"suite":"version","test":"vs-expand-v-n-request"}, "5.0"); + }); + + it('vs-expand-v-wR5-cached', async () => { + await runTest({"suite":"version","test":"vs-expand-v-w"}, "5.0"); + }); + + it('vs-expand-v-wbR5-cached', async () => { + await runTest({"suite":"version","test":"vs-expand-v-wb"}, "5.0"); + }); + + it('vs-expand-v1R5-cached', async () => { + await runTest({"suite":"version","test":"vs-expand-v1"}, "5.0"); + }); + + it('vs-expand-v2R5-cached', async () => { + await runTest({"suite":"version","test":"vs-expand-v2"}, "5.0"); + }); + + it('vs-expand-all-v-forceR5-cached', async () => { + await runTest({"suite":"version","test":"vs-expand-all-v-force"}, "5.0"); + }); + + it('vs-expand-all-v1-forceR5-cached', async () => { + await runTest({"suite":"version","test":"vs-expand-all-v1-force"}, "5.0"); + }); + + it('vs-expand-all-v2-forceR5-cached', async () => { + await runTest({"suite":"version","test":"vs-expand-all-v2-force"}, "5.0"); + }); + + it('vs-expand-v-mixed-forceR5-cached', async () => { + await runTest({"suite":"version","test":"vs-expand-v-mixed-force"}, "5.0"); + }); + + it('vs-expand-v-n-force-requestR5-cached', async () => { + await runTest({"suite":"version","test":"vs-expand-v-n-force-request"}, "5.0"); + }); + + it('vs-expand-v-w-forceR5-cached', async () => { + await runTest({"suite":"version","test":"vs-expand-v-w-force"}, "5.0"); + }); + + it('vs-expand-v-wb-forceR5-cached', async () => { + await runTest({"suite":"version","test":"vs-expand-v-wb-force"}, "5.0"); + }); + + it('vs-expand-v1-forceR5-cached', async () => { + await runTest({"suite":"version","test":"vs-expand-v1-force"}, "5.0"); + }); + + it('vs-expand-v2-forceR5-cached', async () => { + await runTest({"suite":"version","test":"vs-expand-v2-force"}, "5.0"); + }); + + it('vs-expand-all-v-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"vs-expand-all-v-default"}, "5.0"); + }); + + it('vs-expand-all-v1-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"vs-expand-all-v1-default"}, "5.0"); + }); + + it('vs-expand-all-v2-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"vs-expand-all-v2-default"}, "5.0"); + }); + + it('vs-expand-v-mixed-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"vs-expand-v-mixed-default"}, "5.0"); + }); + + it('vs-expand-v-n-default-requestR5-cached', async () => { + await runTest({"suite":"version","test":"vs-expand-v-n-default-request"}, "5.0"); + }); + + it('vs-expand-v-w-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"vs-expand-v-w-default"}, "5.0"); + }); + + it('vs-expand-v-wb-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"vs-expand-v-wb-default"}, "5.0"); + }); + + it('vs-expand-v1-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"vs-expand-v1-default"}, "5.0"); + }); + + it('vs-expand-v2-defaultR5-cached', async () => { + await runTest({"suite":"version","test":"vs-expand-v2-default"}, "5.0"); + }); + + it('vs-expand-all-v-checkR5-cached', async () => { + await runTest({"suite":"version","test":"vs-expand-all-v-check"}, "5.0"); + }); + + it('vs-expand-all-v1-checkR5-cached', async () => { + await runTest({"suite":"version","test":"vs-expand-all-v1-check"}, "5.0"); + }); + + it('vs-expand-all-v2-checkR5-cached', async () => { + await runTest({"suite":"version","test":"vs-expand-all-v2-check"}, "5.0"); + }); + + it('vs-expand-v-mixed-checkR5-cached', async () => { + await runTest({"suite":"version","test":"vs-expand-v-mixed-check"}, "5.0"); + }); + + it('vs-expand-v-n-check-requestR5-cached', async () => { + await runTest({"suite":"version","test":"vs-expand-v-n-check-request"}, "5.0"); + }); + + it('vs-expand-v-w-checkR5-cached', async () => { + await runTest({"suite":"version","test":"vs-expand-v-w-check"}, "5.0"); + }); + + it('vs-expand-v-wb-checkR5-cached', async () => { + await runTest({"suite":"version","test":"vs-expand-v-wb-check"}, "5.0"); + }); + + it('vs-expand-v1-checkR5-cached', async () => { + await runTest({"suite":"version","test":"vs-expand-v1-check"}, "5.0"); + }); + + it('vs-expand-v2-checkR5-cached', async () => { + await runTest({"suite":"version","test":"vs-expand-v2-check"}, "5.0"); + }); + +}); + +describe('overload', () => { + // A set of tests that test out handling of value sets that cross versions of the same code system + + it('expand-allR5-cached', async () => { + await runTest({"suite":"overload","test":"expand-all"}, "5.0"); + }); + + it('expand-all-versionedR5-cached', async () => { + await runTest({"suite":"overload","test":"expand-all-versioned"}, "5.0"); + }); + + it('expand-all-mergedR5-cached', async () => { + await runTest({"suite":"overload","test":"expand-all-merged"}, "5.0"); + }); + + it('expand-enum-goodR5-cached', async () => { + await runTest({"suite":"overload","test":"expand-enum-good"}, "5.0"); + }); + + it('expand-enum-badR5-cached', async () => { + await runTest({"suite":"overload","test":"expand-enum-bad"}, "5.0"); + }); + + it('expand-excludeR5-cached', async () => { + await runTest({"suite":"overload","test":"expand-exclude"}, "5.0"); + }); + + it('expand-exclude-versionedR5-cached', async () => { + await runTest({"suite":"overload","test":"expand-exclude-versioned"}, "5.0"); + }); + + it('expand-exclude-mergedR5-cached', async () => { + await runTest({"suite":"overload","test":"expand-exclude-merged"}, "5.0"); + }); + + it('validate-all-goodR5-cached', async () => { + await runTest({"suite":"overload","test":"validate-all-good"}, "5.0"); + }); + + it('validate-all-good2R5-cached', async () => { + await runTest({"suite":"overload","test":"validate-all-good2"}, "5.0"); + }); + + it('validate-all-good3R5-cached', async () => { + await runTest({"suite":"overload","test":"validate-all-good3"}, "5.0"); + }); + + it('validate-all-good4R5-cached', async () => { + await runTest({"suite":"overload","test":"validate-all-good4"}, "5.0"); + }); + + it('validate-all-bad2R5-cached', async () => { + await runTest({"suite":"overload","test":"validate-all-bad2"}, "5.0"); + }); + + it('validate-all-bad2vR5-cached', async () => { + await runTest({"suite":"overload","test":"validate-all-bad2v"}, "5.0"); + }); + + it('expand-all-sysverR5-cached', async () => { + await runTest({"suite":"overload","test":"expand-all-sysver"}, "5.0"); + }); + + it('expand-exclude-enumR5-cached', async () => { + await runTest({"suite":"overload","test":"expand-exclude-enum"}, "5.0"); + }); + + it('expand-mixedR5-cached', async () => { + await runTest({"suite":"overload","test":"expand-mixed"}, "5.0"); + }); + + it('validate-bad-enum-code1R5-cached', async () => { + await runTest({"suite":"overload","test":"validate-bad-enum-code1"}, "5.0"); + }); + + it('validate-bad-exclude-code1R5-cached', async () => { + await runTest({"suite":"overload","test":"validate-bad-exclude-code1"}, "5.0"); + }); + + it('validate-bad-unknownR5-cached', async () => { + await runTest({"suite":"overload","test":"validate-bad-unknown"}, "5.0"); + }); + + it('validate-v1code2-wrongdisplayR5-cached', async () => { + await runTest({"suite":"overload","test":"validate-v1code2-wrongdisplay"}, "5.0"); + }); + + it('validate-bad-v1code4R5-cached', async () => { + await runTest({"suite":"overload","test":"validate-bad-v1code4"}, "5.0"); + }); + + it('validate-bad-v2code3R5-cached', async () => { + await runTest({"suite":"overload","test":"validate-bad-v2code3"}, "5.0"); + }); + + it('validate-good-code2-v1displayR5-cached', async () => { + await runTest({"suite":"overload","test":"validate-good-code2-v1display"}, "5.0"); + }); + + it('validate-good-enum-code3R5-cached', async () => { + await runTest({"suite":"overload","test":"validate-good-enum-code3"}, "5.0"); + }); + + it('validate-good-exclude-code4R5-cached', async () => { + await runTest({"suite":"overload","test":"validate-good-exclude-code4"}, "5.0"); + }); + + it('validate-good-v1code1R5-cached', async () => { + await runTest({"suite":"overload","test":"validate-good-v1code1"}, "5.0"); + }); + + it('validate-good-v1code2-displayR5-cached', async () => { + await runTest({"suite":"overload","test":"validate-good-v1code2-display"}, "5.0"); + }); + + it('validate-good2aR5-cached', async () => { + await runTest({"suite":"overload","test":"validate-good2a"}, "5.0"); + }); + +}); + +describe('fragment', () => { + // Testing handling a code system fragment + + it('fragment-expansionR5-cached', async () => { + await runTest({"suite":"fragment","test":"fragment-expansion"}, "5.0"); + }); + + it('validation-fragment-code-goodR5-cached', async () => { + await runTest({"suite":"fragment","test":"validation-fragment-code-good"}, "5.0"); + }); + + it('validation-fragment-coding-goodR5-cached', async () => { + await runTest({"suite":"fragment","test":"validation-fragment-coding-good"}, "5.0"); + }); + + it('validation-fragment-codeableconcept-goodR5-cached', async () => { + await runTest({"suite":"fragment","test":"validation-fragment-codeableconcept-good"}, "5.0"); + }); + + it('validation-fragment-code-bad-codeR5-cached', async () => { + await runTest({"suite":"fragment","test":"validation-fragment-code-bad-code"}, "5.0"); + }); + + it('validation-fragment-coding-bad-codeR5-cached', async () => { + await runTest({"suite":"fragment","test":"validation-fragment-coding-bad-code"}, "5.0"); + }); + + it('validation-fragment-codeableconcept-bad-codeR5-cached', async () => { + await runTest({"suite":"fragment","test":"validation-fragment-codeableconcept-bad-code"}, "5.0"); + }); + +}); + +describe('big', () => { + // Testing handling a big code system + + + it('big-echo-zero-fifty-limitR5-cached', async () => { + await runTest({"suite":"big","test":"big-echo-zero-fifty-limit"}, "5.0"); + }); + + it('big-echo-fifty-fifty-limitR5-cached', async () => { + await runTest({"suite":"big","test":"big-echo-fifty-fifty-limit"}, "5.0"); + }); + + it('big-circle-bangR5-cached', async () => { + await runTest({"suite":"big","test":"big-circle-bang"}, "5.0"); + }); + + it('big-circle-validateR5-cached', async () => { + await runTest({"suite":"big","test":"big-circle-validate"}, "5.0"); + }); + +}); + +describe('other', () => { + // Misc tests based on issues submitted by users + + it('dual-filterR5-cached', async () => { + await runTest({"suite":"other","test":"dual-filter"}, "5.0"); + }); + + it('validation-dual-filter-inR5-cached', async () => { + await runTest({"suite":"other","test":"validation-dual-filter-in"}, "5.0"); + }); + + it('validation-dual-filter-outR5-cached', async () => { + await runTest({"suite":"other","test":"validation-dual-filter-out"}, "5.0"); + }); + +}); + +describe('errors', () => { + // Testing Various Error Conditions + + it('unknown-system1R5-cached', async () => { + await runTest({"suite":"errors","test":"unknown-system1"}, "5.0"); + }); + + it('unknown-system2R5-cached', async () => { + await runTest({"suite":"errors","test":"unknown-system2"}, "5.0"); + }); + + it('broken-filter-validateR5-cached', async () => { + await runTest({"suite":"errors","test":"broken-filter-validate"}, "5.0"); + }); + + it('broken-filter2-validateR5-cached', async () => { + await runTest({"suite":"errors","test":"broken-filter2-validate"}, "5.0"); + }); + + it('broken-filter-expandR5-cached', async () => { + await runTest({"suite":"errors","test":"broken-filter-expand"}, "5.0"); + }); + + it('combination-okR5-cached', async () => { + await runTest({"suite":"errors","test":"combination-ok"}, "5.0"); + }); + + it('combination-badR5-cached', async () => { + await runTest({"suite":"errors","test":"combination-bad"}, "5.0"); + }); + +}); + +describe('deprecated', () => { + // Testing Deprecated+Withdrawn warnings + + it('withdrawnR5-cached', async () => { + await runTest({"suite":"deprecated","test":"withdrawn"}, "5.0"); + }); + + it('not-withdrawnR5-cached', async () => { + await runTest({"suite":"deprecated","test":"not-withdrawn"}, "5.0"); + }); + + it('withdrawn-validateR5-cached', async () => { + await runTest({"suite":"deprecated","test":"withdrawn-validate"}, "5.0"); + }); + + it('not-withdrawn-validateR5-cached', async () => { + await runTest({"suite":"deprecated","test":"not-withdrawn-validate"}, "5.0"); + }); + + it('experimentalR5-cached', async () => { + await runTest({"suite":"deprecated","test":"experimental"}, "5.0"); + }); + + it('experimental-validateR5-cached', async () => { + await runTest({"suite":"deprecated","test":"experimental-validate"}, "5.0"); + }); + + it('draftR5-cached', async () => { + await runTest({"suite":"deprecated","test":"draft"}, "5.0"); + }); + + it('draft-validateR5-cached', async () => { + await runTest({"suite":"deprecated","test":"draft-validate"}, "5.0"); + }); + + it('vs-deprecationR5-cached', async () => { + await runTest({"suite":"deprecated","test":"vs-deprecation"}, "5.0"); + }); + + it('deprecating-validateR5-cached', async () => { + await runTest({"suite":"deprecated","test":"deprecating-validate"}, "5.0"); + }); + + it('deprecating-validate-2R5-cached', async () => { + await runTest({"suite":"deprecated","test":"deprecating-validate-2"}, "5.0"); + }); + +}); + +describe('notSelectable', () => { + // Testing notSelectable + + it('notSelectable-prop-allR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-prop-all"}, "5.0"); + }); + + it('notSelectable-noprop-allR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-noprop-all"}, "5.0"); + }); + + it('notSelectable-reprop-allR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-reprop-all"}, "5.0"); + }); + + it('notSelectable-unprop-allR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-unprop-all"}, "5.0"); + }); + + it('notSelectable-prop-trueR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-prop-true"}, "5.0"); + }); + + it('notSelectable-prop-trueUCR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-prop-trueUC"}, "5.0"); + }); + + it('notSelectable-noprop-trueR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-noprop-true"}, "5.0"); + }); + + it('notSelectable-reprop-trueR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-reprop-true"}, "5.0"); + }); + + it('notSelectable-unprop-trueR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-unprop-true"}, "5.0"); + }); + + it('notSelectable-prop-falseR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-prop-false"}, "5.0"); + }); + + it('notSelectable-noprop-falseR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-noprop-false"}, "5.0"); + }); + + it('notSelectable-reprop-falseR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-reprop-false"}, "5.0"); + }); + + it('notSelectable-unprop-falseR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-unprop-false"}, "5.0"); + }); + + it('notSelectable-prop-inR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-prop-in"}, "5.0"); + }); + + it('notSelectable-prop-outR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-prop-out"}, "5.0"); + }); + + it('notSelectable-prop-true-trueR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-prop-true-true"}, "5.0"); + }); + + it('notSelectable-prop-trueUC-trueR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-prop-trueUC-true"}, "5.0"); + }); + + it('notSelectable-prop-in-trueR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-prop-in-true"}, "5.0"); + }); + + it('notSelectable-prop-out-trueR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-prop-out-true"}, "5.0"); + }); + + it('notSelectable-noprop-true-trueR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-noprop-true-true"}, "5.0"); + }); + + it('notSelectable-reprop-true-trueR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-reprop-true-true"}, "5.0"); + }); + + it('notSelectable-unprop-true-trueR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-unprop-true-true"}, "5.0"); + }); + + it('notSelectable-prop-true-falseR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-prop-true-false"}, "5.0"); + }); + + it('notSelectable-prop-in-falseR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-prop-in-false"}, "5.0"); + }); + + it('notSelectable-prop-in-unknownR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-prop-in-unknown"}, "5.0"); + }); + + it('notSelectable-prop-out-unknownR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-prop-out-unknown"}, "5.0"); + }); + + it('notSelectable-prop-out-falseR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-prop-out-false"}, "5.0"); + }); + + it('notSelectable-noprop-true-falseR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-noprop-true-false"}, "5.0"); + }); + + it('notSelectable-reprop-true-falseR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-reprop-true-false"}, "5.0"); + }); + + it('notSelectable-unprop-true-falseR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-unprop-true-false"}, "5.0"); + }); + + it('notSelectable-prop-false-trueR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-prop-false-true"}, "5.0"); + }); + + it('notSelectable-noprop-false-trueR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-noprop-false-true"}, "5.0"); + }); + + it('notSelectable-reprop-false-trueR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-reprop-false-true"}, "5.0"); + }); + + it('notSelectable-unprop-false-trueR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-unprop-false-true"}, "5.0"); + }); + + it('notSelectable-prop-false-falseR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-prop-false-false"}, "5.0"); + }); + + it('notSelectable-noprop-false-falseR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-noprop-false-false"}, "5.0"); + }); + + it('notSelectable-reprop-false-falseR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-reprop-false-false"}, "5.0"); + }); + + it('notSelectable-unprop-false-falseR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-unprop-false-false"}, "5.0"); + }); + + it('notSelectable-noprop-true-unknownR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-noprop-true-unknown"}, "5.0"); + }); + + it('notSelectable-reprop-true-unknownR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-reprop-true-unknown"}, "5.0"); + }); + + it('notSelectable-unprop-true-unknownR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-unprop-true-unknown"}, "5.0"); + }); + + it('notSelectable-prop-true-unknownR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-prop-true-unknown"}, "5.0"); + }); + + it('notSelectable-prop-false-unknownR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-prop-false-unknown"}, "5.0"); + }); + + it('notSelectable-noprop-false-unknownR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-noprop-false-unknown"}, "5.0"); + }); + + it('notSelectable-reprop-false-unknownR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-reprop-false-unknown"}, "5.0"); + }); + + it('notSelectable-unprop-false-unknownR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-unprop-false-unknown"}, "5.0"); + }); + + it('notSelectable-prop-true-true-param-trueR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-prop-true-true-param-true"}, "5.0"); + }); + + it('notSelectable-prop-true-true-param-falseR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-prop-true-true-param-false"}, "5.0"); + }); + + it('notSelectable-prop-false-false-param-trueR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-prop-false-false-param-true"}, "5.0"); + }); + + it('notSelectable-prop-false-false-param-falseR5-cached', async () => { + await runTest({"suite":"notSelectable","test":"notSelectable-prop-false-false-param-false"}, "5.0"); + }); + +}); + +describe('inactive', () => { + // Testing Inactive codes + + it('inactive-expandR5-cached', async () => { + await runTest({"suite":"inactive","test":"inactive-expand"}, "5.0"); + }); + + it('inactive-inactive-expandR5-cached', async () => { + await runTest({"suite":"inactive","test":"inactive-inactive-expand"}, "5.0"); + }); + + it('inactive-active-expandR5-cached', async () => { + await runTest({"suite":"inactive","test":"inactive-active-expand"}, "5.0"); + }); + + it('inactive-1-validateR5-cached', async () => { + await runTest({"suite":"inactive","test":"inactive-1-validate"}, "5.0"); + }); + + it('inactive-2-validateR5-cached', async () => { + await runTest({"suite":"inactive","test":"inactive-2-validate"}, "5.0"); + }); + + it('inactive-3-validateR5-cached', async () => { + await runTest({"suite":"inactive","test":"inactive-3-validate"}, "5.0"); + }); + + it('inactive-1a-validateR5-cached', async () => { + await runTest({"suite":"inactive","test":"inactive-1a-validate"}, "5.0"); + }); + + it('inactive-2a-validateR5-cached', async () => { + await runTest({"suite":"inactive","test":"inactive-2a-validate"}, "5.0"); + }); + + it('inactive-3a-validateR5-cached', async () => { + await runTest({"suite":"inactive","test":"inactive-3a-validate"}, "5.0"); + }); + + it('inactive-1b-validateR5-cached', async () => { + await runTest({"suite":"inactive","test":"inactive-1b-validate"}, "5.0"); + }); + + it('inactive-2b-validateR5-cached', async () => { + await runTest({"suite":"inactive","test":"inactive-2b-validate"}, "5.0"); + }); + + it('inactive-3b-validateR5-cached', async () => { + await runTest({"suite":"inactive","test":"inactive-3b-validate"}, "5.0"); + }); + +}); + +describe('case', () => { + // Test Case Sensitivity handling + + it('case-insensitive-code1-1R5-cached', async () => { + await runTest({"suite":"case","test":"case-insensitive-code1-1"}, "5.0"); + }); + + it('case-insensitive-code1-2R5-cached', async () => { + await runTest({"suite":"case","test":"case-insensitive-code1-2"}, "5.0"); + }); + + it('case-insensitive-code1-3R5-cached', async () => { + await runTest({"suite":"case","test":"case-insensitive-code1-3"}, "5.0"); + }); + + it('case-sensitive-code1-1R5-cached', async () => { + await runTest({"suite":"case","test":"case-sensitive-code1-1"}, "5.0"); + }); + + it('case-sensitive-code1-2R5-cached', async () => { + await runTest({"suite":"case","test":"case-sensitive-code1-2"}, "5.0"); + }); + + it('case-sensitive-code1-3R5-cached', async () => { + await runTest({"suite":"case","test":"case-sensitive-code1-3"}, "5.0"); + }); + +}); + +describe('translate', () => { + // Tests for ConceptMap.$translate + + it('translate-1R5-cached', async () => { + await runTest({"suite":"translate","test":"translate-1"}, "5.0"); + }); + + it('translate-reverseR5-cached', async () => { + await runTest({"suite":"translate","test":"translate-reverse"}, "5.0"); + }); + +}); + +describe('tho', () => { + // Misc assorted test cases from tho + + it('act-classR5-cached', async () => { + await runTest({"suite":"tho","test":"act-class"}, "5.0"); + }); + + it('act-class-activeonlyR5-cached', async () => { + await runTest({"suite":"tho","test":"act-class-activeonly"}, "5.0"); + }); + +}); + +describe('exclude', () => { + // Tests for proper functioning of exclude + + it('exclude-1R5-cached', async () => { + await runTest({"suite":"exclude","test":"exclude-1"}, "5.0"); + }); + + it('exclude-2R5-cached', async () => { + await runTest({"suite":"exclude","test":"exclude-2"}, "5.0"); + }); + + it('exclude-zeroR5-cached', async () => { + await runTest({"suite":"exclude","test":"exclude-zero"}, "5.0"); + }); + + it('exclude-allR5-cached', async () => { + await runTest({"suite":"exclude","test":"exclude-all"}, "5.0"); + }); + + it('exclude-comboR5-cached', async () => { + await runTest({"suite":"exclude","test":"exclude-combo"}, "5.0"); + }); + + it('include-comboR5-cached', async () => { + await runTest({"suite":"exclude","test":"include-combo"}, "5.0"); + }); + + it('exclude-genderR5-cached', async () => { + await runTest({"suite":"exclude","test":"exclude-gender"}, "5.0"); + }); + + it('exclude-gender2R5-cached', async () => { + await runTest({"suite":"exclude","test":"exclude-gender2"}, "5.0"); + }); + +}); + +describe('search', () => { + // Tests for proper functioning of text search. Note what we're not interested in the implementation of the text search itself, so we only test very obvious results. We're just interested in testing support for the parameter + + it('search-all-yesR5-cached', async () => { + await runTest({"suite":"search","test":"search-all-yes"}, "5.0"); + }); + + it('search-all-noR5-cached', async () => { + await runTest({"suite":"search","test":"search-all-no"}, "5.0"); + }); + + it('search-filter-yesR5-cached', async () => { + await runTest({"suite":"search","test":"search-filter-yes"}, "5.0"); + }); + + it('search-filter-noR5-cached', async () => { + await runTest({"suite":"search","test":"search-filter-no"}, "5.0"); + }); + + it('search-enum-yesR5-cached', async () => { + await runTest({"suite":"search","test":"search-enum-yes"}, "5.0"); + }); + + it('search-enum-noR5-cached', async () => { + await runTest({"suite":"search","test":"search-enum-no"}, "5.0"); + }); + +}); + +describe('default-valueset-version', () => { + // Test the default-valueset-version parameter + + it('direct-expand-oneR5-cached', async () => { + await runTest({"suite":"default-valueset-version","test":"direct-expand-one"}, "5.0"); + }); + + it('direct-expand-twoR5-cached', async () => { + await runTest({"suite":"default-valueset-version","test":"direct-expand-two"}, "5.0"); + }); + + it('indirect-expand-oneR5-cached', async () => { + await runTest({"suite":"default-valueset-version","test":"indirect-expand-one"}, "5.0"); + }); + + it('indirect-expand-twoR5-cached', async () => { + await runTest({"suite":"default-valueset-version","test":"indirect-expand-two"}, "5.0"); + }); + + it('indirect-expand-zeroR5-cached', async () => { + await runTest({"suite":"default-valueset-version","test":"indirect-expand-zero"}, "5.0"); + }); + + it('indirect-expand-zero-pinnedR5-cached', async () => { + await runTest({"suite":"default-valueset-version","test":"indirect-expand-zero-pinned"}, "5.0"); + }); + + it('indirect-expand-zero-pinned-wrongR5-cached', async () => { + await runTest({"suite":"default-valueset-version","test":"indirect-expand-zero-pinned-wrong"}, "5.0"); + }); + + it('indirect-validation-oneR5-cached', async () => { + await runTest({"suite":"default-valueset-version","test":"indirect-validation-one"}, "5.0"); + }); + + it('indirect-validation-twoR5-cached', async () => { + await runTest({"suite":"default-valueset-version","test":"indirect-validation-two"}, "5.0"); + }); + + it('indirect-validation-zeroR5-cached', async () => { + await runTest({"suite":"default-valueset-version","test":"indirect-validation-zero"}, "5.0"); + }); + + it('indirect-validation-zero-pinnedR5-cached', async () => { + await runTest({"suite":"default-valueset-version","test":"indirect-validation-zero-pinned"}, "5.0"); + }); + + it('indirect-validation-zero-pinned-wrongR5-cached', async () => { + await runTest({"suite":"default-valueset-version","test":"indirect-validation-zero-pinned-wrong"}, "5.0"); + }); + +}); + +describe('tx.fhir.org', () => { + // These are tx.fhir.org specific tests. There's no expectation that other servers will pass these tests, and they are not executed by default. (other servers can, but they depend on other set up not controlled by the tests + + it('snomed-validation-1R5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"snomed-validation-1"}, "5.0"); + }); + + it('loinc-lookup-codeR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-lookup-code"}, "5.0"); + }); + + it('loinc-lookup-partR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-lookup-part"}, "5.0"); + }); + + it('loinc-lookup-listR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-lookup-list"}, "5.0"); + }); + + it('loinc-lookup-answerR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-lookup-answer"}, "5.0"); + }); + + it('loinc-validate-codeR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-validate-code"}, "5.0"); + }); + + it('loinc-validate-code-uzR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-validate-code-uz"}, "5.0"); + }); + + it('loinc-validate-discouraged-codeR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-validate-discouraged-code"}, "5.0"); + }); + + it('loinc-validate-code-supp1R5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-validate-code-supp1"}, "5.0"); + }); + + it('loinc-validate-code-supp2R5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-validate-code-supp2"}, "5.0"); + }); + + it('loinc-validate-partR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-validate-part"}, "5.0"); + }); + + it('loinc-validate-listR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-validate-list"}, "5.0"); + }); + + it('loinc-validate-answerR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-validate-answer"}, "5.0"); + }); + + it('loinc-validate-invalidR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-validate-invalid"}, "5.0"); + }); + + it('loinc-expand-enumR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-expand-enum"}, "5.0"); + }); + + it('loinc-expand-allR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-expand-all"}, "5.0"); + }); + + it('loinc-expand-all-limitedR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-expand-all-limited"}, "5.0"); + }); + + it('loinc-expand-enum-badR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-expand-enum-bad"}, "5.0"); + }); + + it('loinc-expand-statusR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-expand-status"}, "5.0"); + }); + + it('loinc-expand-parentR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-expand-parent"}, "5.0"); + }); + + it('loinc-expand-class-regexR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-expand-class-regex"}, "5.0"); + }); + + it('loinc-expand-prop-componentR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-expand-prop-component"}, "5.0"); + }); + + it('loinc-expand-prop-methodR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-expand-prop-method"}, "5.0"); + }); + + it('loinc-expand-prop-component-strR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-expand-prop-component-str"}, "5.0"); + }); + + it('loinc-expand-prop-order-obsR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-expand-prop-order-obs"}, "5.0"); + }); + + it('loinc-expand-concept-is-aR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-expand-concept-is-a"}, "5.0"); + }); + + it('loinc-expand-copyrightR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-expand-copyright"}, "5.0"); + }); + + it('loinc-expand-scale-typeR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-expand-scale-type"}, "5.0"); + }); + + it('loinc-validate-enum-goodR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-validate-enum-good"}, "5.0"); + }); + + it('loinc-validate-enum-badR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-validate-enum-bad"}, "5.0"); + }); + + it('loinc-validate-filter-prop-component-goodR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-validate-filter-prop-component-good"}, "5.0"); + }); + + it('loinc-validate-filter-prop-component-badR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-validate-filter-prop-component-bad"}, "5.0"); + }); + + it('loinc-validate-filter-status-goodR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-validate-filter-status-good"}, "5.0"); + }); + + it('loinc-validate-filter-status-badR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-validate-filter-status-bad"}, "5.0"); + }); + + it('loinc-validate-filter-class-regex-goodR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-validate-filter-class-regex-good"}, "5.0"); + }); + + it('loinc-validate-filter-class-regex-badR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-validate-filter-class-regex-bad"}, "5.0"); + }); + + it('loinc-validate-filter-scale-type-goodR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-validate-filter-scale-type-good"}, "5.0"); + }); + + it('loinc-validate-filter-scale-type-badR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-validate-filter-scale-type-bad"}, "5.0"); + }); + + it('loinc-expand-list-request-parametersR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-expand-list-request-parameters"}, "5.0"); + }); + + it('loinc-validate-list-goodR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-validate-list-good"}, "5.0"); + }); + + it('loinc-validate-list-badR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-validate-list-bad"}, "5.0"); + }); + + it('loinc-expand-filter-list-request-parametersR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-expand-filter-list-request-parameters"}, "5.0"); + }); + + it('loinc-validate-filter-list-type-goodR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-validate-filter-list-type-good"}, "5.0"); + }); + + it('loinc-validate-filter-list-badR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-validate-filter-list-bad"}, "5.0"); + }); + + it('loinc-expand-filter-dockind-request-parametersR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-expand-filter-dockind-request-parameters"}, "5.0"); + }); + + it('loinc-validate-filter-dockind-type-goodR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-validate-filter-dockind-type-good"}, "5.0"); + }); + + it('loinc-validate-filter-dockind-badR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-validate-filter-dockind-bad"}, "5.0"); + }); + + it('loinc-validate-filter-classtype-goodR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-validate-filter-classtype-good"}, "5.0"); + }); + + it('loinc-validate-filter-classtype-badR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-validate-filter-classtype-bad"}, "5.0"); + }); + + it('loinc-expand-filter-answers-for1R5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-expand-filter-answers-for1"}, "5.0"); + }); + + it('loinc-expand-filter-answers-for2R5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-expand-filter-answers-for2"}, "5.0"); + }); + + it('loinc-expand-filter-answer-listR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-expand-filter-answer-list"}, "5.0"); + }); + + it('snomed-expand-activeR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"snomed-expand-active"}, "5.0"); + }); + + it('snomed-expand-inactiveR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"snomed-expand-inactive"}, "5.0"); + }); + + it('snomed-expand-inactive2R5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"snomed-expand-inactive2"}, "5.0"); + }); + + it('snomed-expand-moduleid-1R5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"snomed-expand-moduleid-1"}, "5.0"); + }); + + it('snomed-expand-moduleid-2R5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"snomed-expand-moduleid-2"}, "5.0"); + }); + + it('snomed-expand-moduleid-3R5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"snomed-expand-moduleid-3"}, "5.0"); + }); + + it('snomed-expand-moduleid-4R5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"snomed-expand-moduleid-4"}, "5.0"); + }); + + it('snomed-expand-property-1R5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"snomed-expand-property-1"}, "5.0"); + }); + + it('snomed-expand-property-2R5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"snomed-expand-property-2"}, "5.0"); + }); + + it('snomed-validate-active-badR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"snomed-validate-active-bad"}, "5.0"); + }); + + it('snomed-validate-active-goodR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"snomed-validate-active-good"}, "5.0"); + }); + + it('snomed-validate-inactive-badR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"snomed-validate-inactive-bad"}, "5.0"); + }); + + it('snomed-validate-inactive-goodR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"snomed-validate-inactive-good"}, "5.0"); + }); + + it('snomed-validate-moduleid-badR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"snomed-validate-moduleid-bad"}, "5.0"); + }); + + it('snomed-validate-moduleid-goodR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"snomed-validate-moduleid-good"}, "5.0"); + }); + + it('snomed-validate-property-badR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"snomed-validate-property-bad"}, "5.0"); + }); + + it('snomed-validate-property-goodR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"snomed-validate-property-good"}, "5.0"); + }); + + it('snomed-translateR5-cached', async () => { + await runTest({"suite":"tx.fhir.org","test":"snomed-translate"}, "5.0"); + }); + +}); + +describe('snomed', () => { + // This snomed tests are based on the subset distributed with the tx-ecosystem IG + + it('snomed-inactive-displayR5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-inactive-display"}, "5.0"); + }); + + it('snomed-isa-in-displayR5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-isa-in-display"}, "5.0"); + }); + + it('snomed-isa-out-displayR5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-isa-out-display"}, "5.0"); + }); + + it('snomed-expand-inactiveR5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-inactive"}, "5.0"); + }); + + it('snomed-expand-isaR5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-isa"}, "5.0"); + }); + + it('snomed-expand-ecl-descOrSelfR5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-ecl-descOrSelf"}, "5.0"); + }); + + it('snomed-expand-ecl-descendentsR5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-ecl-descendents"}, "5.0"); + }); + + it('snomed-expand-ecl-ancestorsR5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-ecl-ancestors"}, "5.0"); + }); + + it('snomed-expand-ecl-ancOrSelfR5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-ecl-ancOrSelf"}, "5.0"); + }); + + it('snomed-expand-ecl-childrenOrSelfR5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-ecl-childrenOrSelf"}, "5.0"); + }); + + it('snomed-expand-ecl-childrenR5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-ecl-children"}, "5.0"); + }); + + it('snomed-expand-ecl-parentsR5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-ecl-parents"}, "5.0"); + }); + + it('snomed-expand-ecl-parentsOrSelfR5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-ecl-parentsOrSelf"}, "5.0"); + }); + + it('snomed-expand-ecl-wildcardR5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-ecl-wildcard"}, "5.0"); + }); + + it('snomed-expand-ecl-memberOf-refsetR5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-ecl-memberOf-refset"}, "5.0"); + }); + + it('snomed-expand-ecl-memberOf-nonRefsetR5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-ecl-memberOf-nonRefset"}, "5.0"); + }); + + it('snomed-expand-ecl-orR5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-ecl-or"}, "5.0"); + }); + + it('snomed-expand-ecl-andR5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-ecl-and"}, "5.0"); + }); + + it('snomed-expand-ecl-minusR5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-ecl-minus"}, "5.0"); + }); + + it('snomed-expand-ecl-minus-emptyR5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-ecl-minus-empty"}, "5.0"); + }); + + it('snomed-expand-ecl-wildcard-minusR5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-ecl-wildcard-minus"}, "5.0"); + }); + + it('snomed-expand-ecl-parens-precedenceR5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-ecl-parens-precedence"}, "5.0"); + }); + + it('snomed-expand-ecl-left-associativeR5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-ecl-left-associative"}, "5.0"); + }); + + it('snomed-expand-ecl-term-matchR5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-ecl-term-match"}, "5.0"); + }); + + it('snomed-expand-ecl-term-mismatchR5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-ecl-term-mismatch"}, "5.0"); + }); + + it('snomed-expand-ecl-term-with-operatorR5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-ecl-term-with-operator"}, "5.0"); + }); + + it('snomed-expand-ecl-unknown-conceptR5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-ecl-unknown-concept"}, "5.0"); + }); + + it('snomed-expand-ecl-invalid-sctidR5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-ecl-invalid-sctid"}, "5.0"); + }); + + it('snomed-expand-ecl-missing-focusR5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-ecl-missing-focus"}, "5.0"); + }); + + it('snomed-expand-ecl-trailing-tokensR5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-ecl-trailing-tokens"}, "5.0"); + }); + + it('snomed-expand-ecl-nested-parensR5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-ecl-nested-parens"}, "5.0"); + }); + + it('snomed-expand-ecl-refinement-simpleR5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-ecl-refinement-simple"}, "5.0"); + }); + + it('snomed-expand-ecl-refinement-morphologyR5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-ecl-refinement-morphology"}, "5.0"); + }); + + it('snomed-expand-ecl-refinement-wildcardR5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-ecl-refinement-wildcard"}, "5.0"); + }); + + it('snomed-expand-ecl-refinement-groupR5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-ecl-refinement-group"}, "5.0"); + }); + + it('snomed-expand-ecl-refinement-cardinalityR5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-ecl-refinement-cardinality"}, "5.0"); + }); + + it('snomed-expand-ecl-dottedR5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-ecl-dotted"}, "5.0"); + }); + + it('snomed-expand-too-bigR5-cached', async () => { + await runTest({"suite":"snomed","test":"snomed-expand-too-big"}, "5.0"); + }); + + it('lookupR5-cached', async () => { + await runTest({"suite":"snomed","test":"lookup"}, "5.0"); + }); + + it('lookup-pcR5-cached', async () => { + await runTest({"suite":"snomed","test":"lookup-pc"}, "5.0"); + }); + + it('validate-code-pc-goodR5-cached', async () => { + await runTest({"suite":"snomed","test":"validate-code-pc-good"}, "5.0"); + }); + + it('validate-code-pc-bad1R5-cached', async () => { + await runTest({"suite":"snomed","test":"validate-code-pc-bad1"}, "5.0"); + }); + + it('validate-code-pc-bad2R5-cached', async () => { + await runTest({"suite":"snomed","test":"validate-code-pc-bad2"}, "5.0"); + }); + + it('validate-code-pc-noneR5-cached', async () => { + await runTest({"suite":"snomed","test":"validate-code-pc-none"}, "5.0"); + }); + + it('validate-code-pc-listR5-cached', async () => { + await runTest({"suite":"snomed","test":"validate-code-pc-list"}, "5.0"); + }); + + it('validate-code-pc-list-no-pcR5-cached', async () => { + await runTest({"suite":"snomed","test":"validate-code-pc-list-no-pc"}, "5.0"); + }); + + it('validate-code-pc-filterR5-cached', async () => { + await runTest({"suite":"snomed","test":"validate-code-pc-filter"}, "5.0"); + }); + + it('expand-pc-noneR5-cached', async () => { + await runTest({"suite":"snomed","test":"expand-pc-none"}, "5.0"); + }); + + it('expand-pc-listR5-cached', async () => { + await runTest({"suite":"snomed","test":"expand-pc-list"}, "5.0"); + }); + + it('expand-pc-filterR5-cached', async () => { + await runTest({"suite":"snomed","test":"expand-pc-filter"}, "5.0"); + }); + + it('validate-code-implied-1R5-cached', async () => { + await runTest({"suite":"snomed","test":"validate-code-implied-1"}, "5.0"); + }); + + it('validate-code-implied-1bR5-cached', async () => { + await runTest({"suite":"snomed","test":"validate-code-implied-1b"}, "5.0"); + }); + + it('validate-code-implied-2R5-cached', async () => { + await runTest({"suite":"snomed","test":"validate-code-implied-2"}, "5.0"); + }); + + it('validate-code-implied-2bR5-cached', async () => { + await runTest({"suite":"snomed","test":"validate-code-implied-2b"}, "5.0"); + }); + +}); + +describe('batch', () => { + // Test Batch Validation + + it('batch-validateR5-cached', async () => { + await runTest({"suite":"batch","test":"batch-validate"}, "5.0"); + }); + + it('batch-validate-badR5-cached', async () => { + await runTest({"suite":"batch","test":"batch-validate-bad"}, "5.0"); + }); + +}); + +describe('omop', () => { + // Tests for OMOP implementations. Note that some servers only do OMOP (and some don't). The tests are based on a stable subset of OMOP maintained by Davera Gabriel + + it('omop-basic-validation-code-goodR5-cached', async () => { + await runTest({"suite":"omop","test":"omop-basic-validation-code-good"}, "5.0"); + }); + + it('omop-basic-validation-coding-goodR5-cached', async () => { + await runTest({"suite":"omop","test":"omop-basic-validation-coding-good"}, "5.0"); + }); + + it('omop-basic-validation-codeableconcept-goodR5-cached', async () => { + await runTest({"suite":"omop","test":"omop-basic-validation-codeableconcept-good"}, "5.0"); + }); + + it('omop-basic-validation-code-badR5-cached', async () => { + await runTest({"suite":"omop","test":"omop-basic-validation-code-bad"}, "5.0"); + }); + + it('omop-basic-validation-coding-badR5-cached', async () => { + await runTest({"suite":"omop","test":"omop-basic-validation-coding-bad"}, "5.0"); + }); + + it('omop-basic-validation-codeableconcept-badR5-cached', async () => { + await runTest({"suite":"omop","test":"omop-basic-validation-codeableconcept-bad"}, "5.0"); + }); + + it('omop-basic-validation-code-bad-displayR5-cached', async () => { + await runTest({"suite":"omop","test":"omop-basic-validation-code-bad-display"}, "5.0"); + }); + + it('omop-basic-validation-coding-bad-displayR5-cached', async () => { + await runTest({"suite":"omop","test":"omop-basic-validation-coding-bad-display"}, "5.0"); + }); + + it('omop-basic-validation-codeableconcept-bad-displayR5-cached', async () => { + await runTest({"suite":"omop","test":"omop-basic-validation-codeableconcept-bad-display"}, "5.0"); + }); + + it('omop-basic-validation-code-bad-versionR5-cached', async () => { + await runTest({"suite":"omop","test":"omop-basic-validation-code-bad-version"}, "5.0"); + }); + + it('omop-basic-validation-coding-bad-versionR5-cached', async () => { + await runTest({"suite":"omop","test":"omop-basic-validation-coding-bad-version"}, "5.0"); + }); + + it('omop-basic-validation-codeableconcept-bad-versionR5-cached', async () => { + await runTest({"suite":"omop","test":"omop-basic-validation-codeableconcept-bad-version"}, "5.0"); + }); + + it('omop-basic-validation-code-good-vsR5-cached', async () => { + await runTest({"suite":"omop","test":"omop-basic-validation-code-good-vs"}, "5.0"); + }); + + it('omop-basic-validation-coding-good-vsR5-cached', async () => { + await runTest({"suite":"omop","test":"omop-basic-validation-coding-good-vs"}, "5.0"); + }); + + it('omop-basic-validation-codeableconcept-good-vsR5-cached', async () => { + await runTest({"suite":"omop","test":"omop-basic-validation-codeableconcept-good-vs"}, "5.0"); + }); + + it('omop-basic-validation-code-bad-vsR5-cached', async () => { + await runTest({"suite":"omop","test":"omop-basic-validation-code-bad-vs"}, "5.0"); + }); + + it('omop-basic-validation-coding-bad-vsR5-cached', async () => { + await runTest({"suite":"omop","test":"omop-basic-validation-coding-bad-vs"}, "5.0"); + }); + + it('omop-basic-validation-codeableconcept-bad-vsR5-cached', async () => { + await runTest({"suite":"omop","test":"omop-basic-validation-codeableconcept-bad-vs"}, "5.0"); + }); + + it('omop-lookup-codeR5-cached', async () => { + await runTest({"suite":"omop","test":"omop-lookup-code"}, "5.0"); + }); + + it('omop-lookup-code2R5-cached', async () => { + await runTest({"suite":"omop","test":"omop-lookup-code2"}, "5.0"); + }); + + it('omop-lookup-code3R5-cached', async () => { + await runTest({"suite":"omop","test":"omop-lookup-code3"}, "5.0"); + }); + + it('omop-basic-validation-code-good-vs-urlR5-cached', async () => { + await runTest({"suite":"omop","test":"omop-basic-validation-code-good-vs-url"}, "5.0"); + }); + + it('omop-basic-validation-code-bad-vs-urlR5-cached', async () => { + await runTest({"suite":"omop","test":"omop-basic-validation-code-bad-vs-url"}, "5.0"); + }); + + it('omop-expand-explicitR5-cached', async () => { + await runTest({"suite":"omop","test":"omop-expand-explicit"}, "5.0"); + }); + + it('translate-loinc-implicitR5-cached', async () => { + await runTest({"suite":"omop","test":"translate-loinc-implicit"}, "5.0"); + }); + + it('translate-loinc-implicit-badR5-cached', async () => { + await runTest({"suite":"omop","test":"translate-loinc-implicit-bad"}, "5.0"); + }); + +}); + +describe('UCUM', () => { + // UCUM Test Cases + + it('lookupR5-cached', async () => { + await runTest({"suite":"UCUM","test":"lookup"}, "5.0"); + }); + + it('lookup-with-annotationR5-cached', async () => { + await runTest({"suite":"UCUM","test":"lookup-with-annotation"}, "5.0"); + }); + + it('expand-ucum-all-4R4-cached', async () => { + await runTest({"suite":"UCUM","test":"expand-ucum-all-4"}, "4.0"); + }); + + it('expand-ucum-all-5R5-cached', async () => { + await runTest({"suite":"UCUM","test":"expand-ucum-all-5"}, "5.0"); + }); + + it('expand-ucum-canonicalR5-cached', async () => { + await runTest({"suite":"UCUM","test":"expand-ucum-canonical"}, "5.0"); + }); + + it('validate-ucum-canonical-goodR5-cached', async () => { + await runTest({"suite":"UCUM","test":"validate-ucum-canonical-good"}, "5.0"); + }); + + it('validate-ucum-canonical-badR5-cached', async () => { + await runTest({"suite":"UCUM","test":"validate-ucum-canonical-bad"}, "5.0"); + }); + + it('validate-all-canonical-goodR5-cached', async () => { + await runTest({"suite":"UCUM","test":"validate-all-canonical-good"}, "5.0"); + }); + + it('validate-ucum-all-badR5-cached', async () => { + await runTest({"suite":"UCUM","test":"validate-ucum-all-bad"}, "5.0"); + }); + +}); + +describe('related', () => { + // Tests for candidate new 'related' operation + + it('related-allR5-cached', async () => { + await runTest({"suite":"related","test":"related-all"}, "5.0"); + }); + + it('related-activeR5-cached', async () => { + await runTest({"suite":"related","test":"related-active"}, "5.0"); + }); + + it('related-inactiveR5-cached', async () => { + await runTest({"suite":"related","test":"related-inactive"}, "5.0"); + }); + + it('related-enumeratedR5-cached', async () => { + await runTest({"suite":"related","test":"related-enumerated"}, "5.0"); + }); + + it('related-is-aR5-cached', async () => { + await runTest({"suite":"related","test":"related-is-a"}, "5.0"); + }); + + it('related-regex-1R5-cached', async () => { + await runTest({"suite":"related","test":"related-regex-1"}, "5.0"); + }); + + it('related-regex-2R5-cached', async () => { + await runTest({"suite":"related","test":"related-regex-2"}, "5.0"); + }); + + it('related-listsR5-cached', async () => { + await runTest({"suite":"related","test":"related-lists"}, "5.0"); + }); + + it('related-lists-moreR5-cached', async () => { + await runTest({"suite":"related","test":"related-lists-more"}, "5.0"); + }); + + it('related-lists-lessR5-cached', async () => { + await runTest({"suite":"related","test":"related-lists-less"}, "5.0"); + }); + + it('related-lists-overR5-cached', async () => { + await runTest({"suite":"related","test":"related-lists-over"}, "5.0"); + }); + + it('related-lists-disjR5-cached', async () => { + await runTest({"suite":"related","test":"related-lists-disj"}, "5.0"); + }); + + it('related-systemsR5-cached', async () => { + await runTest({"suite":"related","test":"related-systems"}, "5.0"); + }); + + it('related-systemsR5-cached', async () => { + await runTest({"suite":"related","test":"related-systems"}, "5.0"); + }); + + it('related-systems-lessR5-cached', async () => { + await runTest({"suite":"related","test":"related-systems-less"}, "5.0"); + }); + + it('related-systems-moreR5-cached', async () => { + await runTest({"suite":"related","test":"related-systems-more"}, "5.0"); + }); + + it('related-system-disjR5-cached', async () => { + await runTest({"suite":"related","test":"related-system-disj"}, "5.0"); + }); + + it('related-system-overR5-cached', async () => { + await runTest({"suite":"related","test":"related-system-over"}, "5.0"); + }); + + it('related-filters-1R5-cached', async () => { + await runTest({"suite":"related","test":"related-filters-1"}, "5.0"); + }); + + it('related-filters-2R5-cached', async () => { + await runTest({"suite":"related","test":"related-filters-2"}, "5.0"); + }); + + it('related-filters-3R5-cached', async () => { + await runTest({"suite":"related","test":"related-filters-3"}, "5.0"); + }); + + it('related-mixed-1R5-cached', async () => { + await runTest({"suite":"related","test":"related-mixed-1"}, "5.0"); + }); + + it('related-mixed-1-lessR5-cached', async () => { + await runTest({"suite":"related","test":"related-mixed-1-less"}, "5.0"); + }); + + it('related-mixed-1-moreR5-cached', async () => { + await runTest({"suite":"related","test":"related-mixed-1-more"}, "5.0"); + }); + + it('related-mixed-1-disjR5-cached', async () => { + await runTest({"suite":"related","test":"related-mixed-1-disj"}, "5.0"); + }); + + it('related-mixed-1-overR5-cached', async () => { + await runTest({"suite":"related","test":"related-mixed-1-over"}, "5.0"); + }); + + it('related-filters-lessR5-cached', async () => { + await runTest({"suite":"related","test":"related-filters-less"}, "5.0"); + }); + + it('related-filters-moreR5-cached', async () => { + await runTest({"suite":"related","test":"related-filters-more"}, "5.0"); + }); + +}); + +describe('bugs', () => { + // A series of tests that deal with discovered bugs in FHIRsmith. These tests are specific to FHIRsmith - internal QA + + it('country-codesR5-cached', async () => { + await runTest({"suite":"bugs","test":"country-codes"}, "5.0"); + }); + + it('no-systemR5-cached', async () => { + await runTest({"suite":"bugs","test":"no-system"}, "5.0"); + }); + + it('sct-parseR5-cached', async () => { + await runTest({"suite":"bugs","test":"sct-parse"}, "5.0"); + }); + + it('sct-parse-pcR5-cached', async () => { + await runTest({"suite":"bugs","test":"sct-parse-pc"}, "5.0"); + }); + + it('lang-caseR5-cached', async () => { + await runTest({"suite":"bugs","test":"lang-case"}, "5.0"); + }); + + it('lang-case2R5-cached', async () => { + await runTest({"suite":"bugs","test":"lang-case2"}, "5.0"); + }); + + it('provenanceR5-cached', async () => { + await runTest({"suite":"bugs","test":"provenance"}, "5.0"); + }); + + it('country-codeR5-cached', async () => { + await runTest({"suite":"bugs","test":"country-code"}, "5.0"); + }); + + it('sct-msg-4R4-cached', async () => { + await runTest({"suite":"bugs","test":"sct-msg-4"}, "4.0"); + }); + + it('sct-msg-5R5-cached', async () => { + await runTest({"suite":"bugs","test":"sct-msg-5"}, "5.0"); + }); + + it('sct-display-1R5-cached', async () => { + await runTest({"suite":"bugs","test":"sct-display-1"}, "5.0"); + }); + + it('sct-display-2R5-cached', async () => { + await runTest({"suite":"bugs","test":"sct-display-2"}, "5.0"); + }); + + it('x12-badR5-cached', async () => { + await runTest({"suite":"bugs","test":"x12-bad"}, "5.0"); + }); + + it('3166-aR5-cached', async () => { + await runTest({"suite":"bugs","test":"3166-a"}, "5.0"); + }); + + it('3166-bR5-cached', async () => { + await runTest({"suite":"bugs","test":"3166-b"}, "5.0"); + }); + + it('3166-cR5-cached', async () => { + await runTest({"suite":"bugs","test":"3166-c"}, "5.0"); + }); + + it('3166-dR5-cached', async () => { + await runTest({"suite":"bugs","test":"3166-d"}, "5.0"); + }); + +}); + +describe('permutations', () => { + // A set of permutations generated by Claude with the goal of increasing test coverage. + + it('bad-cc1-all-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"bad-cc1-all-request"}, "5.0"); + }); + + it('bad-cc1-enumerated-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"bad-cc1-enumerated-request"}, "5.0"); + }); + + it('bad-cc1-exclude-filter-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"bad-cc1-exclude-filter-request"}, "5.0"); + }); + + it('bad-cc1-exclude-import-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"bad-cc1-exclude-import-request"}, "5.0"); + }); + + it('bad-cc1-exclude-list-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"bad-cc1-exclude-list-request"}, "5.0"); + }); + + it('bad-cc1-import-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"bad-cc1-import-request"}, "5.0"); + }); + + it('bad-cc1-isa-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"bad-cc1-isa-request"}, "5.0"); + }); + + it('bad-cc2-all-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"bad-cc2-all-request"}, "5.0"); + }); + + it('bad-cc2-enumerated-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"bad-cc2-enumerated-request"}, "5.0"); + }); + + it('bad-cc2-exclude-filter-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"bad-cc2-exclude-filter-request"}, "5.0"); + }); + + it('bad-cc2-exclude-import-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"bad-cc2-exclude-import-request"}, "5.0"); + }); + + it('bad-cc2-exclude-list-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"bad-cc2-exclude-list-request"}, "5.0"); + }); + + it('bad-cc2-import-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"bad-cc2-import-request"}, "5.0"); + }); + + it('bad-cc2-isa-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"bad-cc2-isa-request"}, "5.0"); + }); + + it('bad-coding-all-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"bad-coding-all-request"}, "5.0"); + }); + + it('bad-coding-enumerated-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"bad-coding-enumerated-request"}, "5.0"); + }); + + it('bad-coding-exclude-filter-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"bad-coding-exclude-filter-request"}, "5.0"); + }); + + it('bad-coding-exclude-import-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"bad-coding-exclude-import-request"}, "5.0"); + }); + + it('bad-coding-exclude-list-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"bad-coding-exclude-list-request"}, "5.0"); + }); + + it('bad-coding-import-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"bad-coding-import-request"}, "5.0"); + }); + + it('bad-coding-isa-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"bad-coding-isa-request"}, "5.0"); + }); + + it('bad-scd-all-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"bad-scd-all-request"}, "5.0"); + }); + + it('bad-scd-enumerated-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"bad-scd-enumerated-request"}, "5.0"); + }); + + it('bad-scd-exclude-filter-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"bad-scd-exclude-filter-request"}, "5.0"); + }); + + it('bad-scd-exclude-import-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"bad-scd-exclude-import-request"}, "5.0"); + }); + + it('bad-scd-exclude-list-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"bad-scd-exclude-list-request"}, "5.0"); + }); + + it('bad-scd-import-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"bad-scd-import-request"}, "5.0"); + }); + + it('bad-scd-isa-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"bad-scd-isa-request"}, "5.0"); + }); + + it('good-cc1-all-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"good-cc1-all-request"}, "5.0"); + }); + + it('good-cc1-enumerated-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"good-cc1-enumerated-request"}, "5.0"); + }); + + it('good-cc1-exclude-filter-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"good-cc1-exclude-filter-request"}, "5.0"); + }); + + it('good-cc1-exclude-import-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"good-cc1-exclude-import-request"}, "5.0"); + }); + + it('good-cc1-exclude-list-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"good-cc1-exclude-list-request"}, "5.0"); + }); + + it('good-cc1-import-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"good-cc1-import-request"}, "5.0"); + }); + + it('good-cc1-isa-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"good-cc1-isa-request"}, "5.0"); + }); + + it('good-cc2-all-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"good-cc2-all-request"}, "5.0"); + }); + + it('good-cc2-enumerated-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"good-cc2-enumerated-request"}, "5.0"); + }); + + it('good-cc2-exclude-filter-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"good-cc2-exclude-filter-request"}, "5.0"); + }); + + it('good-cc2-exclude-import-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"good-cc2-exclude-import-request"}, "5.0"); + }); + + it('good-cc2-exclude-list-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"good-cc2-exclude-list-request"}, "5.0"); + }); + + it('good-cc2-import-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"good-cc2-import-request"}, "5.0"); + }); + + it('good-cc2-isa-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"good-cc2-isa-request"}, "5.0"); + }); + + it('good-coding-all-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"good-coding-all-request"}, "5.0"); + }); + + it('good-coding-enumerated-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"good-coding-enumerated-request"}, "5.0"); + }); + + it('good-coding-exclude-filter-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"good-coding-exclude-filter-request"}, "5.0"); + }); + + it('good-coding-exclude-import-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"good-coding-exclude-import-request"}, "5.0"); + }); + + it('good-coding-exclude-list-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"good-coding-exclude-list-request"}, "5.0"); + }); + + it('good-coding-import-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"good-coding-import-request"}, "5.0"); + }); + + it('good-coding-isa-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"good-coding-isa-request"}, "5.0"); + }); + + it('good-scd-all-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"good-scd-all-request"}, "5.0"); + }); + + it('good-scd-enumerated-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"good-scd-enumerated-request"}, "5.0"); + }); + + it('good-scd-exclude-filter-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"good-scd-exclude-filter-request"}, "5.0"); + }); + + it('good-scd-exclude-import-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"good-scd-exclude-import-request"}, "5.0"); + }); + + it('good-scd-exclude-list-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"good-scd-exclude-list-request"}, "5.0"); + }); + + it('good-scd-import-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"good-scd-import-request"}, "5.0"); + }); + + it('good-scd-isa-requestR5-cached', async () => { + await runTest({"suite":"permutations","test":"good-scd-isa-request"}, "5.0"); + }); + +}); + +describe('regex-bad', () => { + // Bad Regex - checking defences against denial of service attack. These are unusual because servers have the option to succeed, or to refuse the request + + it('expand-regex-badR5-cached', async () => { + await runTest({"suite":"regex-bad","test":"expand-regex-bad"}, "5.0"); + }); + + it('validate-regex-badR5-cached', async () => { + await runTest({"suite":"regex-bad","test":"validate-regex-bad"}, "5.0"); + }); + + it('expand-regex-bad-2R5-cached', async () => { + await runTest({"suite":"regex-bad","test":"expand-regex-bad-2"}, "5.0"); + }); + + it('validate-regex-bad-2R5-cached', async () => { + await runTest({"suite":"regex-bad","test":"validate-regex-bad-2"}, "5.0"); + }); + +}); + +describe('related2', () => { + // Tests for $compare operation - comparing two value sets to determine their relationship (equivalent, subset, superset, overlap, disjoint, unknown) + + it('related-eq-identical-defR5-cached', async () => { + await runTest({"suite":"related2","test":"related-eq-identical-def"}, "5.0"); + }); + + it('related-eq-enum-reorderR5-cached', async () => { + await runTest({"suite":"related2","test":"related-eq-enum-reorder"}, "5.0"); + }); + + it('related-eq-multi-include-reorderR5-cached', async () => { + await runTest({"suite":"related2","test":"related-eq-multi-include-reorder"}, "5.0"); + }); + + it('related-eq-filter-vs-enumR5-cached', async () => { + await runTest({"suite":"related2","test":"related-eq-filter-vs-enum"}, "5.0"); + }); + + it('related-eq-import-vs-inlineR5-cached', async () => { + await runTest({"suite":"related2","test":"related-eq-import-vs-inline"}, "5.0"); + }); + + it('related-eq-import-reorderR5-cached', async () => { + await runTest({"suite":"related2","test":"related-eq-import-reorder"}, "5.0"); + }); + + it('related-expeq-exclude-vs-enumR5-cached', async () => { + await runTest({"suite":"related2","test":"related-expeq-exclude-vs-enum"}, "5.0"); + }); + + it('related-expeq-exclude-partialR5-cached', async () => { + await runTest({"suite":"related2","test":"related-expeq-exclude-partial"}, "5.0"); + }); + + it('related-sub-branch-vs-rootR5-cached', async () => { + await runTest({"suite":"related2","test":"related-sub-branch-vs-root"}, "5.0"); + }); + + it('related-sub-enum-vs-filterR5-cached', async () => { + await runTest({"suite":"related2","test":"related-sub-enum-vs-filter"}, "5.0"); + }); + + it('related-sub-base-vs-import-plusR5-cached', async () => { + await runTest({"suite":"related2","test":"related-sub-base-vs-import-plus"}, "5.0"); + }); + + it('related-sub-leaf-vs-subtreeR5-cached', async () => { + await runTest({"suite":"related2","test":"related-sub-leaf-vs-subtree"}, "5.0"); + }); + + it('related-super-root-vs-branchR5-cached', async () => { + await runTest({"suite":"related2","test":"related-super-root-vs-branch"}, "5.0"); + }); + + it('related-expsub-exclude-narrowerR5-cached', async () => { + await runTest({"suite":"related2","test":"related-expsub-exclude-narrower"}, "5.0"); + }); + + it('related-disj-diff-systemsR5-cached', async () => { + await runTest({"suite":"related2","test":"related-disj-diff-systems"}, "5.0"); + }); + + it('related-disj-diff-branchesR5-cached', async () => { + await runTest({"suite":"related2","test":"related-disj-diff-branches"}, "5.0"); + }); + + it('related-disj-enum-no-intersectionR5-cached', async () => { + await runTest({"suite":"related2","test":"related-disj-enum-no-intersection"}, "5.0"); + }); + + it('related-disj-multi-systemR5-cached', async () => { + await runTest({"suite":"related2","test":"related-disj-multi-system"}, "5.0"); + }); + + it('related-ov-enum-partialR5-cached', async () => { + await runTest({"suite":"related2","test":"related-ov-enum-partial"}, "5.0"); + }); + + it('related-ov-filter-vs-enumR5-cached', async () => { + await runTest({"suite":"related2","test":"related-ov-filter-vs-enum"}, "5.0"); + }); + + it('related-ov-multi-include-partialR5-cached', async () => { + await runTest({"suite":"related2","test":"related-ov-multi-include-partial"}, "5.0"); + }); + + it('related-ov-import-partialR5-cached', async () => { + await runTest({"suite":"related2","test":"related-ov-import-partial"}, "5.0"); + }); + + it('related-ov-cross-systemR5-cached', async () => { + await runTest({"suite":"related2","test":"related-ov-cross-system"}, "5.0"); + }); + + it('related-ov-exclude-partialR5-cached', async () => { + await runTest({"suite":"related2","test":"related-ov-exclude-partial"}, "5.0"); + }); + + it('related-unk-snomed-both-filterR5-cached', async () => { + await runTest({"suite":"related2","test":"related-unk-snomed-both-filter"}, "5.0"); + }); + + it('related-unk-snomed-filter-vs-enumR5-cached', async () => { + await runTest({"suite":"related2","test":"related-unk-snomed-filter-vs-enum"}, "5.0"); + }); + + it('related-unk-unknown-systemR5-cached', async () => { + await runTest({"suite":"related2","test":"related-unk-unknown-system"}, "5.0"); + }); + + it('related-ver-same-def-diff-cs-versionR5-cached', async () => { + await runTest({"suite":"related2","test":"related-ver-same-def-diff-cs-version"}, "5.0"); + }); + + it('related-ver-all-diff-cs-versionR5-cached', async () => { + await runTest({"suite":"related2","test":"related-ver-all-diff-cs-version"}, "5.0"); + }); + + it('related-ver-branch-diff-cs-versionR5-cached', async () => { + await runTest({"suite":"related2","test":"related-ver-branch-diff-cs-version"}, "5.0"); + }); + + it('related-ver-unversioned-vs-pinnedR5-cached', async () => { + await runTest({"suite":"related2","test":"related-ver-unversioned-vs-pinned"}, "5.0"); + }); + + it('related-ver-same-vs-diff-versionR5-cached', async () => { + await runTest({"suite":"related2","test":"related-ver-same-vs-diff-version"}, "5.0"); + }); + + it('related-ver-import-version-cascadeR5-cached', async () => { + await runTest({"suite":"related2","test":"related-ver-import-version-cascade"}, "5.0"); + }); + +}); + +}); + }); diff --git a/tests/tx/translate-originmap.test.js b/tests/tx/translate-originmap.test.js new file mode 100644 index 00000000..00cac7c0 --- /dev/null +++ b/tests/tx/translate-originmap.test.js @@ -0,0 +1,69 @@ +const TranslateWorker = require('../../tx/workers/translate'); + +/** + * Issue #247: $translate emitted `originMap` keyed to *direction* rather than + * explicitness — a dropped 8th argument meant `reverse` landed in the `explicit` + * slot, so forward always emitted it and reverse never did. The chosen policy is + * "always emit": originMap (the versioned canonical of the contributing map) + * should be present for every match, in both directions. + * + * These call the helpers directly via the prototype (no provider/sqlite needed); + * they use only cm.listTranslations(...)/listTranslationsReverse(...), cm.vurl + * and this.hasMatch. + */ + +const CM_VURL = 'http://hl7.org/fhir/ConceptMap/example|1.0.0'; + +function stubCm() { + return { + vurl: CM_VURL, + listTranslations() { + return [{ + group: { target: 'http://example.org/target' }, + match: { target: [{ code: 'T', relationship: 'equivalent', equivalence: 'equal' }] } + }]; + }, + listTranslationsReverse() { + return [{ + group: { source: 'http://example.org/source', target: 'http://example.org/target' }, + match: { code: 'S' }, + target: { code: 'T', relationship: 'equivalent', equivalence: 'equal' } + }]; + } + }; +} + +function worker() { + return Object.create(TranslateWorker.prototype); +} + +function originMapOf(output) { + const match = output.find(o => o.name === 'match'); + if (!match) return undefined; + const om = match.part.find(p => p.name === 'originMap'); + return om && om.valueCanonical; +} + +describe('$translate originMap is emitted in both directions (issue #247)', () => { + test('forward translate emits originMap (versioned canonical)', () => { + const output = []; + const added = worker().translateUsingGroupsForwards( + stubCm(), { system: 'http://example.org/source', code: 'S' }, null, null, null, output); + expect(added).toBe(true); + expect(originMapOf(output)).toBe(CM_VURL); + }); + + test('reverse translate also emits originMap (previously never did)', () => { + const output = []; + const added = worker().translateUsingGroupsReverse( + stubCm(), { system: 'http://example.org/target', code: 'T' }, null, null, null, output); + expect(added).toBe(true); + expect(originMapOf(output)).toBe(CM_VURL); + }); + + test('the helpers no longer take an explicit/reverse trailing arg', () => { + // 6-arity: cm, coding, targetScope, targetSystem, params, output + expect(TranslateWorker.prototype.translateUsingGroupsForwards.length).toBe(6); + expect(TranslateWorker.prototype.translateUsingGroupsReverse.length).toBe(6); + }); +}); diff --git a/tests/tx/validate-issues.test.js b/tests/tx/validate-issues.test.js new file mode 100644 index 00000000..b47743da --- /dev/null +++ b/tests/tx/validate-issues.test.js @@ -0,0 +1,200 @@ +/** + * Regression tests for the validate.js issues that were previously emitted via a + * non-existent `op.addIssueNoId(...)` helper (now `op.addIssue(new Issue(...))` + * with a null msgId). Those calls would have thrown `op.addIssueNoId is not a + * function` whenever their branch fired. + * + * There are two layers here: + * 1. API-contract tests on Issue / OperationOutcome that lock in the shape the + * fix relies on (and would catch reintroduction of the bug). + * 2. Integration tests that drive the worker into two of the fixed branches + * that are reachable through the public validation paths: + * - example-content code system -> "vs-invalid" warning (validate.js ~1125) + * - incomplete-validation message -> "process-note" (validate.js ~566) + * The other three fixed branches (~827, ~1525, ~1532) are fallback paths that + * are preempted by more specific translated messages in normal flows; the + * "does not crash" tests below exercise those surrounding paths so a fallback, + * if it ever fires, won't crash. + * + * All providers are in-memory FhirCodeSystemProvider instances, so no native + * database is required. + */ + +const { ValidateWorker } = require('../../tx/workers/validate'); +const { CodeSystem } = require('../../tx/library/codesystem'); +const { FhirCodeSystemProvider } = require('../../tx/cs/cs-cs'); +const { OperationOutcome, Issue } = require('../../tx/library/operation-outcome'); +const ValueSet = require('../../tx/library/valueset'); +const { Languages } = require('../../library/languages'); +const { OperationContext } = require('../../tx/operation-context'); +const { TestUtilities } = require('../test-utilities'); +const { TxParameters } = require('../../tx/params'); + +const mockLog = { debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn() }; +const MESSAGE_ID_EXT = 'http://hl7.org/fhir/StructureDefinition/operationoutcome-message-id'; + +function adminGenderCS() { + return new CodeSystem({ + resourceType: 'CodeSystem', url: 'http://hl7.org/fhir/administrative-gender', + version: '5.0.0', name: 'AdministrativeGender', status: 'active', + caseSensitive: true, content: 'complete', + concept: [{ code: 'male', display: 'Male' }, { code: 'female', display: 'Female' }, { code: 'unknown', display: 'Unknown' }] + }); +} + +// content: 'example' drives contentMode to 'example' during VS validation. +function exampleCS() { + return new CodeSystem({ + resourceType: 'CodeSystem', url: 'http://example.org/example-cs', + version: '1.0.0', name: 'ExampleCS', status: 'active', + caseSensitive: true, content: 'example', + concept: [{ code: 'a', display: 'A' }, { code: 'b', display: 'B' }] + }); +} + +// Provider whose incompleteValidationMessage returns a message, so CS validation +// of a valid code emits a process-note. +class IncompleteProvider extends FhirCodeSystemProvider { + async incompleteValidationMessage() { + return 'Validation is incomplete: the concept model has not been checked'; + } +} + +function provider(ctx, { incomplete = false } = {}) { + const makeAdmin = (c, supps) => incomplete + ? new IncompleteProvider(c, adminGenderCS(), supps || []) + : new FhirCodeSystemProvider(c, adminGenderCS(), supps || []); + return { + getCodeSystem: (c, url) => + url === 'http://hl7.org/fhir/administrative-gender' ? adminGenderCS().jsonObj + : url === 'http://example.org/example-cs' ? exampleCS().jsonObj : null, + getCodeSystemProvider: (c, url, version, supps) => + url === 'http://hl7.org/fhir/administrative-gender' ? makeAdmin(c, supps) + : url === 'http://example.org/example-cs' ? new FhirCodeSystemProvider(c, exampleCS(), supps || []) : null, + getCodeSystemById: () => null, + findValueSet: () => null, + getValueSetById: () => null, + loadSupplements: () => [] + }; +} + +function getIssues(result) { + const p = (result.parameter || []).find(x => x.name === 'issues'); + return p && p.resource && p.resource.issue ? p.resource.issue : []; +} +const hasMsgIdExt = issue => (issue.extension || []).some(e => e.url === MESSAGE_ID_EXT); + +describe('Issue / OperationOutcome API contract (locks in the addIssueNoId fix)', () => { + test('OperationOutcome has addIssue but NOT addIssueNoId', () => { + const op = new OperationOutcome(); + expect(typeof op.addIssue).toBe('function'); + expect(op.addIssueNoId).toBeUndefined(); + }); + + test('an Issue with a null msgId serialises WITHOUT a message-id extension', () => { + const op = new OperationOutcome(); + op.addIssue(new Issue('warning', 'not-found', 'code', null, 'a plain message', 'vs-invalid')); + const issue = op.jsonObj.issue[0]; + expect(issue.severity).toBe('warning'); + expect(issue.details.text).toBe('a plain message'); + expect(issue.details.coding[0].code).toBe('vs-invalid'); + expect(hasMsgIdExt(issue)).toBe(false); + }); + + test('an Issue WITH a msgId serialises a message-id extension (contrast)', () => { + const op = new OperationOutcome(); + op.addIssue(new Issue('error', 'invalid', 'code', 'SOME_MSG_ID', 'translated message', 'vs-invalid')); + expect(hasMsgIdExt(op.jsonObj.issue[0])).toBe(true); + }); +}); + +describe('validate.js — reachable fixed branches', () => { + let worker, opContext, txp; + + beforeEach(async () => { + jest.clearAllMocks(); + const langDefs = await TestUtilities.loadLanguageDefinitions(); + const i18n = await TestUtilities.loadTranslations(langDefs); + opContext = new OperationContext(new Languages(), i18n); + worker = new ValidateWorker(opContext, mockLog, provider(opContext), langDefs, i18n); + txp = new TxParameters(opContext.i18n.languageDefinitions, opContext.i18n); + txp.readParams({ resourceType: 'Parameters', parameter: [{ name: '__Accept-Language', valueCode: 'en' }] }); + }); + + test('example-content code system → "vs-invalid" warning with no message-id extension', async () => { + const coded = { coding: [{ system: 'http://example.org/example-cs', code: 'a' }] }; + const valueSet = new ValueSet({ + resourceType: 'ValueSet', url: 'http://example.org/ValueSet/example', + compose: { include: [{ system: 'http://example.org/example-cs' }] } + }); + + const result = await worker.doValidationVS(coded, valueSet, txp, 'coded', 'Coding'); + + const issue = getIssues(result).find(i => i.details && i.details.text && + i.details.text.includes('did not contain enough information')); + expect(issue).toBeDefined(); + expect(issue.severity).toBe('warning'); + expect(issue.details.coding.some(c => c.code === 'vs-invalid')).toBe(true); + expect(hasMsgIdExt(issue)).toBe(false); + }); + + test('incomplete-validation message → "process-note" info with no message-id extension', async () => { + const langDefs = await TestUtilities.loadLanguageDefinitions(); + const i18n = await TestUtilities.loadTranslations(langDefs); + // worker whose provider resolves the code system to the incomplete provider + const w = new ValidateWorker(opContext, mockLog, provider(opContext, { incomplete: true }), langDefs, i18n); + + const coded = { coding: [{ system: 'http://hl7.org/fhir/administrative-gender', code: 'male' }] }; + const csp = new IncompleteProvider(opContext, adminGenderCS(), []); + const result = await w.doValidationCS(coded, csp, txp, { issuePath: 'Coding', mode: 'coding' }); + + expect(result.parameter.find(p => p.name === 'result').valueBoolean).toBe(true); + const issue = getIssues(result).find(i => i.details && i.details.coding && + i.details.coding.some(c => c.code === 'process-note')); + expect(issue).toBeDefined(); + expect(issue.severity).toBe('information'); + expect(issue.details.text).toContain('Validation is incomplete'); + expect(hasMsgIdExt(issue)).toBe(false); + }); +}); + +describe('validate.js — invalid input does not crash (covers the fallback branches)', () => { + let worker, opContext, txp; + + beforeEach(async () => { + jest.clearAllMocks(); + const langDefs = await TestUtilities.loadLanguageDefinitions(); + const i18n = await TestUtilities.loadTranslations(langDefs); + opContext = new OperationContext(new Languages(), i18n); + worker = new ValidateWorker(opContext, mockLog, provider(opContext), langDefs, i18n); + txp = new TxParameters(opContext.i18n.languageDefinitions, opContext.i18n); + txp.readParams({ resourceType: 'Parameters', parameter: [{ name: '__Accept-Language', valueCode: 'en' }] }); + }); + + test('unknown system validates to false with an error issue and does not throw', async () => { + const coded = { coding: [{ system: 'http://unknown.example.org', code: 'x' }] }; + const valueSet = new ValueSet({ + resourceType: 'ValueSet', url: 'http://hl7.org/fhir/ValueSet/administrative-gender', + compose: { include: [{ system: 'http://hl7.org/fhir/administrative-gender' }] } + }); + + let result; + await expect((async () => { result = await worker.doValidationVS(coded, valueSet, txp, 'coded', 'Coding'); })()).resolves.toBeUndefined(); + expect(result.parameter.find(p => p.name === 'result').valueBoolean).toBe(false); + expect(getIssues(result).some(i => i.severity === 'error')).toBe(true); + }); + + test('known code absent from the value set validates to false with an error issue', async () => { + const coded = { coding: [{ system: 'http://hl7.org/fhir/administrative-gender', code: 'female' }] }; + const valueSet = new ValueSet({ + resourceType: 'ValueSet', url: 'http://example.org/ValueSet/just-male', + compose: { include: [{ system: 'http://hl7.org/fhir/administrative-gender', concept: [{ code: 'male' }] }] } + }); + + const result = await worker.doValidationVS(coded, valueSet, txp, 'coded', 'Coding'); + expect(result.parameter.find(p => p.name === 'result').valueBoolean).toBe(false); + const issue = getIssues(result).find(i => i.details && i.details.coding && + i.details.coding.some(c => c.code === 'not-in-vs')); + expect(issue).toBeDefined(); + }); +}); diff --git a/tests/tx/vsac-resync.test.js b/tests/tx/vsac-resync.test.js new file mode 100644 index 00000000..b5a18c2e --- /dev/null +++ b/tests/tx/vsac-resync.test.js @@ -0,0 +1,137 @@ +const { VSACValueSetProvider } = require('../../tx/vs/vs-vsac'); + +/** + * Tests for the operator "resync a ValueSet" action on the VSAC /info page. + * The provider is built with Object.create to skip the real constructor (which + * opens the sqlite database); we only exercise the form/password/dispatch logic. + */ + +function provider({ password = null, refreshing = false, resync } = {}) { + const p = Object.create(VSACValueSetProvider.prototype); + p.resyncPassword = password; + p.isRefreshing = refreshing; + p.stats = { task() {} }; + if (resync) p.resyncValueSet = resync; + return p; +} + +const postReq = body => ({ method: 'POST', body }); + +describe('VSAC resync — feature gating', () => { + test('disabled when no password configured', () => { + const p = provider({ password: null }); + expect(p._resyncEnabled()).toBe(false); + expect(p._resyncFormHtml()).toBe(''); + }); + + test('enabled when a password is configured; form has url + password inputs, no value echoed', () => { + const p = provider({ password: 'sekret' }); + expect(p._resyncEnabled()).toBe(true); + const form = p._resyncFormHtml(); + expect(form).toContain('method="post"'); + expect(form).toContain('name="url"'); + expect(form).toContain('type="password"'); + expect(form).not.toContain('sekret'); // never reflect the password + }); +}); + +describe('VSAC resync — password check (timing-safe)', () => { + test('matches only the exact configured password', () => { + const p = provider({ password: 's3cret' }); + expect(p._passwordMatches('s3cret')).toBe(true); + expect(p._passwordMatches('wrong')).toBe(false); + expect(p._passwordMatches('')).toBe(false); + expect(p._passwordMatches(null)).toBe(false); + expect(p._passwordMatches('s3cretX')).toBe(false); // length mismatch + }); + + test('never matches when no password is configured', () => { + expect(provider({ password: null })._passwordMatches('anything')).toBe(false); + }); +}); + +describe('VSAC resync — request handling', () => { + test('does nothing when the feature is disabled', async () => { + let called = false; + const p = provider({ password: null, resync: async () => { called = true; return 1; } }); + const note = await p._handleResyncRequest(postReq({ url: 'http://x', password: 'whatever' })); + expect(note).toBe(''); + expect(called).toBe(false); + }); + + test('wrong password: reports it and does not resync', async () => { + let called = false; + const p = provider({ password: 'pw', resync: async () => { called = true; return 1; } }); + const note = await p._handleResyncRequest(postReq({ url: 'http://x', password: 'nope' })); + expect(note).toMatch(/Incorrect password/i); + expect(called).toBe(false); + }); + + test('correct password but empty url: asks for a url, does not resync', async () => { + let called = false; + const p = provider({ password: 'pw', resync: async () => { called = true; return 1; } }); + const note = await p._handleResyncRequest(postReq({ url: ' ', password: 'pw' })); + expect(note).toMatch(/Enter a ValueSet URL/i); + expect(called).toBe(false); + }); + + test('correct password while a full sync is running: defers, does not resync', async () => { + let called = false; + const p = provider({ password: 'pw', refreshing: true, resync: async () => { called = true; return 1; } }); + const note = await p._handleResyncRequest(postReq({ url: 'http://x', password: 'pw' })); + expect(note).toMatch(/full sync is currently running/i); + expect(called).toBe(false); + }); + + test('correct password, idle: resyncs and reports the version count', async () => { + let withUrl = null; + const p = provider({ password: 'pw', resync: async (u) => { withUrl = u; return 4; } }); + const note = await p._handleResyncRequest(postReq({ url: 'http://example.org/vs', password: 'pw' })); + expect(withUrl).toBe('http://example.org/vs'); + expect(note).toMatch(/Resynced/); + expect(note).toContain('http://example.org/vs'); + expect(note).toContain('4'); + }); + + test('a bare OID is expanded to the VSAC canonical URL before resyncing', async () => { + let withUrl = null; + const p = provider({ password: 'pw', resync: async (u) => { withUrl = u; return 1; } }); + await p._handleResyncRequest(postReq({ url: '2.16.840.1.113883.3.526.3.1240', password: 'pw' })); + expect(withUrl).toBe('http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1240'); + }); +}); + +describe('VSAC resync — OID/URL expansion', () => { + const p = provider({ password: 'pw' }); + + test('bare OID expands', () => { + expect(p._expandOidOrUrl('2.16.840.1.113883.3.526.3.1240')) + .toBe('http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1240'); + }); + + test('urn:oid: prefix is stripped then expanded', () => { + expect(p._expandOidOrUrl('urn:oid:2.16.840.1.113883.3.526.3.1240')) + .toBe('http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.526.3.1240'); + }); + + test('a full URL is left unchanged', () => { + expect(p._expandOidOrUrl('http://hl7.org/fhir/ValueSet/x')) + .toBe('http://hl7.org/fhir/ValueSet/x'); + }); + + test('whitespace is trimmed', () => { + expect(p._expandOidOrUrl(' 2.16.840.1 ')).toBe('http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1'); + }); + + test('empty input stays empty', () => { + expect(p._expandOidOrUrl('')).toBe(''); + expect(p._expandOidOrUrl(null)).toBe(''); + }); + + test('resync failure is reported, not thrown', async () => { + const p = provider({ password: 'pw', resync: async () => { throw new Error('VSAC down'); } }); + const note = await p._handleResyncRequest(postReq({ url: 'http://x', password: 'pw' })); + expect(note).toMatch(/failed/i); + expect(note).toContain('VSAC down'); + }); +}); diff --git a/tests/tx/xversion.test.js b/tests/tx/xversion.test.js new file mode 100644 index 00000000..3bb66092 --- /dev/null +++ b/tests/tx/xversion.test.js @@ -0,0 +1,252 @@ +const { convertResourceToR5, convertResourceFromR5 } = require('../../tx/xversion/xv-resource'); + +/** + * Cross-version conversion tests, focused on resources that omit optional + * properties the converters previously assumed were present: + * - ValueSet without `compose` (expansion-only) + * - Parameters without a `parameter` array (empty Parameters) + * - Bundle entries without a `resource` (request/response/search-only) + * Plus "still works" cases to ensure the added guards didn't change normal + * conversion behaviour. + */ + +describe('xversion — ValueSet without compose (the reported bug)', () => { + test('expansion-only ValueSet converts R4 -> R5 without throwing', () => { + const vs = { resourceType: 'ValueSet', url: 'http://x', expansion: { contains: [{ system: 's', code: 'a' }] } }; + let out; + expect(() => { out = convertResourceToR5(vs, '4.0'); }).not.toThrow(); + expect(out.resourceType).toBe('ValueSet'); + expect(out.expansion.contains).toHaveLength(1); + }); + + test('expansion-only ValueSet converts R3 -> R5 without throwing', () => { + const vs = { resourceType: 'ValueSet', url: 'http://x', expansion: { contains: [] } }; + expect(() => convertResourceToR5(vs, '3.0')).not.toThrow(); + }); + + test('ValueSet with neither compose nor expansion converts without throwing', () => { + expect(() => convertResourceToR5({ resourceType: 'ValueSet', url: 'http://x' }, '4.0')).not.toThrow(); + }); + + test('ValueSet WITH compose still converts (guard did not skip real work)', () => { + const vs = { + resourceType: 'ValueSet', + compose: { include: [{ system: 's', filter: [{ property: 'p', op: 'is-a', value: 'v' }] }] } + }; + const out = convertResourceToR5(vs, '4.0'); + expect(out.compose.include[0].filter[0].op).toBe('is-a'); + }); + + test('R5-only filter operator is moved to an extension when targeting R4', () => { + const vs = { + resourceType: 'ValueSet', + compose: { include: [{ system: 's', filter: [{ property: 'p', op: 'child-of', value: 'v' }] }] } + }; + const out = convertResourceFromR5(vs, '4.0'); + const filter = out.compose.include[0].filter[0]; + expect(filter.op).toBeUndefined(); + expect(filter._op).toBeDefined(); + // _op is a FHIR primitive-element extension: { extension: [ { url, valueCode } ] } + expect(Array.isArray(filter._op.extension)).toBe(true); + expect(filter._op.extension[0].url).toBe('http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.compose.include.filter.op'); + expect(filter._op.extension[0].valueCode).toBe('child-of'); + }); +}); + +describe('xversion — filter op _op extension is a well-formed primitive extension', () => { + const OP_EXT_URL = 'http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.compose.include.filter.op'; + + // Assert the FHIR-correct shape: { extension: [ { url, valueCode } ] } + function expectWellFormedOp(filter, expectedCode) { + expect(filter.op).toBeUndefined(); + expect(filter._op).toBeDefined(); + expect(Array.isArray(filter._op.extension)).toBe(true); + expect(filter._op.extension).toHaveLength(1); + expect(filter._op.extension[0].url).toBe(OP_EXT_URL); + expect(filter._op.extension[0].valueCode).toBe(expectedCode); + // the URL must NOT be sitting directly on _op (the original bug) + expect(typeof filter._op.extension).not.toBe('string'); + } + + test('R5 -> R4 include filter (R5-only op)', () => { + const vs = { resourceType: 'ValueSet', compose: { include: [{ system: 's', filter: [{ property: 'p', op: 'child-of', value: 'v' }] }] } }; + const out = convertResourceFromR5(vs, '4.0'); + expectWellFormedOp(out.compose.include[0].filter[0], 'child-of'); + }); + + test('R5 -> R4 exclude filter (R5-only op)', () => { + const vs = { resourceType: 'ValueSet', compose: { exclude: [{ system: 's', filter: [{ property: 'p', op: 'child-of', value: 'v' }] }] } }; + const out = convertResourceFromR5(vs, '4.0'); + expectWellFormedOp(out.compose.exclude[0].filter[0], 'child-of'); + }); + + test('R5 -> R3 include filter (op not R3-compatible)', () => { + const vs = { resourceType: 'ValueSet', compose: { include: [{ system: 's', filter: [{ property: 'p', op: 'generalizes', value: 'v' }] }] } }; + const out = convertResourceFromR5(vs, '3.0'); + expectWellFormedOp(out.compose.include[0].filter[0], 'generalizes'); + }); + + test('R5 -> R3 exclude filter (op not R3-compatible)', () => { + const vs = { resourceType: 'ValueSet', compose: { exclude: [{ system: 's', filter: [{ property: 'p', op: 'generalizes', value: 'v' }] }] } }; + const out = convertResourceFromR5(vs, '3.0'); + expectWellFormedOp(out.compose.exclude[0].filter[0], 'generalizes'); + }); +}); + +describe('xversion — Parameters without a parameter array', () => { + test('empty Parameters converts R4 -> R5 without throwing', () => { + expect(() => convertResourceToR5({ resourceType: 'Parameters' }, '4.0')).not.toThrow(); + }); + + test('empty Parameters converts R5 -> R4 without throwing', () => { + expect(() => convertResourceFromR5({ resourceType: 'Parameters' }, '4.0')).not.toThrow(); + }); + + test('empty Parameters converts R5 -> R3 without throwing', () => { + expect(() => convertResourceFromR5({ resourceType: 'Parameters' }, '3.0')).not.toThrow(); + }); + + test('R5 Parameters with a match param but no parameter array does not throw', () => { + // Exercises the R5->R5 match-fixing clone path + expect(() => convertResourceToR5({ resourceType: 'Parameters' }, '5.0')).not.toThrow(); + }); + + test('Parameters WITH parameters still converts a nested resource', () => { + const params = { + resourceType: 'Parameters', + parameter: [{ name: 'tx-resource', resource: { resourceType: 'ValueSet', expansion: { contains: [] } } }] + }; + const out = convertResourceToR5(params, '4.0'); + expect(out.parameter[0].resource.resourceType).toBe('ValueSet'); + }); + + test('R5 -> R4 converts a match relationship to a legacy equivalence', () => { + const params = { + resourceType: 'Parameters', + parameter: [{ name: 'match', part: [{ name: 'relationship', valueCode: 'source-is-narrower-than-target' }] }] + }; + const out = convertResourceFromR5(params, '4.0'); + const equ = out.parameter[0].part.find(p => p.name === 'equivalence'); + expect(equ).toBeDefined(); + expect(equ.valueCode).toBe('wider'); + // relationship is removed in R4 + expect(out.parameter[0].part.find(p => p.name === 'relationship')).toBeUndefined(); + }); +}); + +describe('xversion — Bundle entries without a resource', () => { + test('request-only entry converts R4 -> R5 without throwing', () => { + const bundle = { resourceType: 'Bundle', type: 'batch', entry: [{ request: { method: 'GET', url: 'ValueSet' } }] }; + expect(() => convertResourceToR5(bundle, '4.0')).not.toThrow(); + }); + + test('request-only entry converts R5 -> R4 and is preserved', () => { + const bundle = { resourceType: 'Bundle', type: 'batch', entry: [{ request: { method: 'GET', url: 'ValueSet' } }] }; + const out = convertResourceFromR5(bundle, '4.0'); + expect(out.entry).toHaveLength(1); + expect(out.entry[0].request.url).toBe('ValueSet'); + expect(out.entry[0].resource).toBeUndefined(); + }); + + test('Bundle without an entry array converts without throwing', () => { + expect(() => convertResourceToR5({ resourceType: 'Bundle', type: 'searchset' }, '4.0')).not.toThrow(); + }); + + test('entry WITH a resource still has that resource converted', () => { + const bundle = { + resourceType: 'Bundle', type: 'collection', + entry: [{ resource: { resourceType: 'ValueSet', compose: { include: [{ system: 's' }] } } }] + }; + const out = convertResourceFromR5(bundle, '4.0'); + expect(out.entry[0].resource.resourceType).toBe('ValueSet'); + expect(out.entry[0].resource.compose.include[0].system).toBe('s'); + }); +}); + +describe('xversion — dispatcher passthrough', () => { + test('an undefined resource passes through both directions without throwing', () => { + expect(() => convertResourceToR5(undefined, '4.0')).not.toThrow(); + expect(() => convertResourceFromR5(undefined, '4.0')).not.toThrow(); + expect(convertResourceToR5(undefined, '4.0')).toBeUndefined(); + }); + + test('a resource with no resourceType is returned unchanged', () => { + const obj = { foo: 'bar' }; + expect(convertResourceToR5(obj, '4.0')).toBe(obj); + }); + + test('R5 source/target is a no-op passthrough', () => { + const vs = { resourceType: 'ValueSet', url: 'http://x' }; + expect(convertResourceToR5(vs, '5.0')).toBe(vs); + expect(convertResourceFromR5(vs, '5.0')).toBe(vs); + }); +}); + +describe('xversion — CodeSystem filter operator down-conversion (issue #251 part 1)', () => { + function cs(operators) { + return { resourceType: 'CodeSystem', url: 'http://x', status: 'active', + content: 'complete', filter: [{ code: 'concept', description: 'd', operator: operators, value: 'v' }] }; + } + + test('R5 -> R4 keeps generalizes (valid R4) and strips child-of / descendent-leaf (R5-only)', () => { + const out = convertResourceFromR5(cs(['is-a', 'generalizes', 'child-of', 'descendent-leaf']), '4.0'); + expect(out.filter[0].operator).toEqual(['is-a', 'generalizes']); + }); + + test('R5 -> R4 leaves an all-valid-R4 filter untouched', () => { + const out = convertResourceFromR5(cs(['=', 'is-a', 'descendent-of', 'is-not-a', 'regex', 'in', 'not-in', 'generalizes', 'exists']), '4.0'); + expect(out.filter[0].operator).toEqual(['=', 'is-a', 'descendent-of', 'is-not-a', 'regex', 'in', 'not-in', 'generalizes', 'exists']); + }); + + test('R5 -> R4 drops a filter whose operators are all R5-only', () => { + const out = convertResourceFromR5(cs(['child-of', 'descendent-leaf']), '4.0'); + expect(out.filter || []).toHaveLength(0); + }); + + test('R5 -> R3 keeps only R3-compatible operators (generalizes is R4+, so dropped)', () => { + const out = convertResourceFromR5(cs(['is-a', 'regex', 'generalizes', 'child-of']), '3.0'); + expect(out.filter[0].operator).toEqual(['is-a', 'regex']); + }); +}); + +describe('xversion — TerminologyCapabilities codeSystem.content (issue #251 part 2)', () => { + const CONTENT_EXT = 'http://hl7.org/fhir/5.0/StructureDefinition/extension-TerminologyCapabilities.codeSystem.content'; + + test('R5 -> R4 moves content into the content extension', () => { + const r5 = { resourceType: 'TerminologyCapabilities', status: 'active', codeSystem: [{ uri: 'http://x', content: 'complete' }] }; + const out = convertResourceFromR5(r5, '4.0'); + const c0 = out.codeSystem[0]; + expect(c0.content).toBeUndefined(); + expect(c0.extension.find(e => e.url === CONTENT_EXT).valueCode).toBe('complete'); + }); + + test('R4 -> R5 lifts content from the extension and removes that extension', () => { + const r4 = { resourceType: 'TerminologyCapabilities', status: 'active', + codeSystem: [{ uri: 'http://x', extension: [{ url: CONTENT_EXT, valueCode: 'complete' }] }] }; + const out = convertResourceToR5(r4, '4.0'); + const c0 = out.codeSystem[0]; + expect(c0.content).toBe('complete'); + // the redundant content extension must be gone (the delete previously no-op'd) + expect((c0.extension || []).some(e => e.url === CONTENT_EXT)).toBe(false); + }); + + test('R4 -> R5 preserves unrelated extensions while removing only the content one', () => { + const r4 = { resourceType: 'TerminologyCapabilities', status: 'active', + codeSystem: [{ uri: 'http://x', extension: [ + { url: 'http://other/ext', valueString: 'keep-me' }, + { url: CONTENT_EXT, valueCode: 'fragment' } + ] }] }; + const out = convertResourceToR5(r4, '4.0'); + const c0 = out.codeSystem[0]; + expect(c0.content).toBe('fragment'); + expect(c0.extension).toEqual([{ url: 'http://other/ext', valueString: 'keep-me' }]); + }); + + test('round-trip R5 -> R4 -> R5 preserves codeSystem.content', () => { + const r5 = { resourceType: 'TerminologyCapabilities', status: 'active', codeSystem: [{ uri: 'http://x', content: 'complete' }] }; + const r4 = convertResourceFromR5(JSON.parse(JSON.stringify(r5)), '4.0'); + const back = convertResourceToR5(r4, '4.0'); + expect(back.codeSystem[0].content).toBe('complete'); + expect((back.codeSystem[0].extension || []).some(e => e.url === CONTENT_EXT)).toBe(false); + }); +}); diff --git a/tx/cs/cs-provider-list.js b/tx/cs/cs-provider-list.js index 019c457f..7dedccc5 100644 --- a/tx/cs/cs-provider-list.js +++ b/tx/cs/cs-provider-list.js @@ -5,7 +5,8 @@ const { AbstractCodeSystemProvider } = require('./cs-provider-api'); */ class ListCodeSystemProvider extends AbstractCodeSystemProvider { /** - * {Map} A list of code system factories that contains all the preloaded native code systems + * {CodeSystem[]} The preloaded FHIR code systems in this list. This is an + * array — append with .push(), not Map-style .set(). */ codeSystems = []; diff --git a/tx/cs/cs-snomed.js b/tx/cs/cs-snomed.js index 2065f73f..8a78209a 100644 --- a/tx/cs/cs-snomed.js +++ b/tx/cs/cs-snomed.js @@ -326,6 +326,20 @@ class SnomedServices { return 0; } + // Like getConceptRefSet, but distinguishes "this concept is not a reference + // set" (returns null) from "it is a reference set that happens to have no + // members" (returns a numeric membersByRef, which may be 0 or the + // MAGIC_NO_CHILDREN sentinel). Used by memberOf to reject non-refset operands. + _findRefSetMembersRef(conceptIndex) { + for (let i = 0; i < this.refSetIndex.count(); i++) { + const refSet = this.refSetIndex.getReferenceSet(i); + if (refSet.definition === conceptIndex) { + return refSet.membersByRef; + } + } + return null; + } + // Filter support methods filterEquals(id) { const result = new SnomedFilterContext(); @@ -593,7 +607,7 @@ class SnomedServices { if (operator) { throw new Error('ECL hierarchy operators combined with ^ (member-of) are not yet supported'); } - return this._evalMemberOf(focus); + return this._evalMemberOf(focus, opContext); } // Plain concept reference @@ -682,18 +696,54 @@ class SnomedServices { }; /** - * Evaluate a MEMBER_OF node. Only plain concept-reference refsets are - * supported; complex expressions inside ^ are not yet supported. + * Evaluate a MEMBER_OF (^) node. The operand may be any ECL expression: it is + * resolved to a set of candidate reference-set concepts, and the result is the + * union of their active concept members' referenced components. + * + * When the operand is a bare, explicitly-named concept that is not a reference + * set, this throws (the caller asked for an error in that case). When the + * operand is a computed expression (e.g. ^(<<900000000000455006)), concepts in + * the resolved set that are not reference sets are simply skipped — the closure + * of "Reference set" necessarily includes the non-refset parent itself. + * * @param {object} memberOfNode + * @param {OperationContext} [opContext] * @returns {SnomedFilterContext} */ - _evalMemberOf = function (memberOfNode) { - const refSet = memberOfNode.refSet; - if (refSet.type !== ECLNodeType.CONCEPT_REFERENCE) { - throw new Error('ECL ^ (member-of) with a non-concept-reference refset is not yet supported'); + _evalMemberOf = function (memberOfNode, opContext) { + const operandIsBareRef = memberOfNode.refSet.type === ECLNodeType.CONCEPT_REFERENCE; + const refsetConcepts = this._eclResolveSet(this._evalECLNode(memberOfNode.refSet, opContext), opContext); + + const members = new Set(); + for (const refsetIdx of refsetConcepts) { + if (opContext) opContext.deadCheck('ecl:memberOf'); + const membersRef = this._findRefSetMembersRef(refsetIdx); + if (membersRef === null) { + if (operandIsBareRef) { + const code = this.concepts.getConceptId(refsetIdx).toString(); + throw new Error(`The SNOMED CT Concept ${code} is not a reference set`); + } + continue; // computed operand: ignore concepts that aren't reference sets + } + if (membersRef === 0 || membersRef === 0xFFFFFFFF) { + continue; // a reference set with no members + } + const memberList = this.refSetMembers.getMembers(membersRef); + for (const m of memberList || []) { + // Concept referenced components only (kind 0). Description/other members + // are excluded, which also prevents the no-component sentinel (0xFFFFFFFF) + // from leaking in. The referenced concept may itself be INACTIVE and is + // still returned — "active" in the spec qualifies the membership row (and + // inactive rows are already dropped at import), not the referenced + // concept. The expansion marks/handles inactivity (e.g. activeOnly). + if (m.kind === 0) { + members.add(m.ref); + } + } } - // filterIn accepts a comma-separated string; a single ID works fine - return this.filterIn(refSet.conceptId); + const result = new SnomedFilterContext(); + result.descendants = [...members]; + return result; }; /** @@ -844,25 +894,43 @@ class SnomedServices { * @returns {number} */ _countAttributeMatches = function (conceptIdx, attr, groupFilter, opContext) { - const attrResult = this.concepts.findConcept(attr.name.conceptId); - if (!attrResult.found) { - throw new Error(`The SNOMED CT Concept ${attr.name.conceptId} is not known`); + // The attribute type and the value set depend only on the (static) AST node, + // not on the concept being tested, so resolve them once per refinement + // attribute and reuse across the whole base set. Without this memo the value + // expression is re-evaluated for every concept — and for a wildcard value + // (`= *`) the entire active-concept set is re-enumerated per concept — + // making refinement evaluation O(baseSet × valueExpr). The AST is parsed + // fresh per request, so memoising on the node is safe within a request. + let resolved = attr._eclResolved; + if (!resolved) { + const attrResult = this.concepts.findConcept(attr.name.conceptId); + if (!attrResult.found) { + throw new Error(`The SNOMED CT Concept ${attr.name.conceptId} is not known`); + } + resolved = { + attrTypeIdx: attrResult.index, + valueSet: new Set(this._eclResolveSet(this._evalECLNode(attr.comparison.value, opContext), opContext)) + }; + attr._eclResolved = resolved; } - const attrTypeIdx = attrResult.index; - - const valueSet = new Set(this._eclResolveSet(this._evalECLNode(attr.comparison.value, opContext), opContext)); + const attrTypeIdx = resolved.attrTypeIdx; + const valueSet = resolved.valueSet; const relIdxs = this.getConceptRelationships(conceptIdx); - let count = 0; + // ECL cardinality counts non-redundant matching attributes — i.e. distinct + // matching values — not raw relationship rows. The same (type, value) can + // appear in multiple role groups; counting rows over-counts and makes e.g. + // [1..1] fail and [2..*] succeed for a concept with a single morphology. + const matchedTargets = new Set(); for (const relIdx of relIdxs) { if (opContext) opContext.deadCheck('ecl:countAttributeMatches'); const rel = this.relationships.getRelationship(relIdx); if (!rel.active) continue; if (rel.relType !== attrTypeIdx) continue; if (groupFilter !== null && rel.group !== groupFilter) continue; - if (valueSet.has(rel.target)) count++; + if (valueSet.has(rel.target)) matchedTargets.add(rel.target); } - return count; + return matchedTargets.size; }; /** diff --git a/tx/data/snomed-testing.cache b/tx/data/snomed-testing.cache new file mode 100644 index 00000000..0c3b5242 Binary files /dev/null and b/tx/data/snomed-testing.cache differ diff --git a/tx/library.js b/tx/library.js index 6b022402..c3325d06 100644 --- a/tx/library.js +++ b/tx/library.js @@ -604,44 +604,23 @@ class Library { version = parts[1]; } const packagePath = await packageManager.fetch(packageId, version); - if (mode === "fetch" || mode === "cs") { - return; - } - const fullPackagePath = path.join(this.cacheFolder, packagePath); - const contentLoader = new PackageContentLoader(fullPackagePath); - await contentLoader.initialize(); - - this.packageSources.push(contentLoader.id()+"#"+contentLoader.version()); - - let cp = new ListCodeSystemProvider(); - const resources = await contentLoader.getResourcesByType("CodeSystem"); - let csc = 0; - for (const resource of resources) { - const cs = new CodeSystem(await contentLoader.loadFile(resource, contentLoader.fhirVersion())); - if (this.#isIgnored(cs.url, cs.version)) { - this.log.info(`Ignoring CodeSystem ${cs.url}${cs.version ? '#' + cs.version : ''} (excluded by config)`); - continue; - } - cs.sourcePackage = contentLoader.pid(); - cp.codeSystems.push(cs); - csc++; - } - this.codeSystemProviders.push(cp); - let vs = null; - if (!csOnly) { - vs = new PackageValueSetProvider(contentLoader); - await vs.initialize(); - this.valueSetProviders.push(vs); - const cm = new PackageConceptMapProvider(contentLoader); - await cm.initialize(); - this.conceptMapProviders.push(cm); - } - - this.#logPackage(contentLoader.id(), contentLoader.version(), csc, vs ? vs.valueSetMap.size : 0); + await this.#loadPackageFromPath(packagePath, mode, csOnly); } async loadUrl(packageManager, url, isDefault, mode, csOnly) { const packagePath = await packageManager.fetchUrl(url); + await this.#loadPackageFromPath(packagePath, mode, csOnly); + } + + /** + * Shared loader for npm- and url-sourced packages. Given a fetched package + * path, loads its CodeSystems (and, unless csOnly, its ValueSets and + * ConceptMaps) into this library. Factored out of loadNpm/loadUrl so the two + * cannot drift apart (a past divergence here used Map-style .set() on the + * array-backed codeSystems list, breaking url sources). + * @private + */ + async #loadPackageFromPath(packagePath, mode, csOnly) { if (mode === "fetch" || mode === "cs") { return; } @@ -661,8 +640,7 @@ class Library { continue; } cs.sourcePackage = contentLoader.pid(); - cp.codeSystems.set(cs.url, cs); - cp.codeSystems.set(cs.vurl, cs); + cp.codeSystems.push(cs); csc++; } this.codeSystemProviders.push(cp); diff --git a/tx/library/canonical-resource.js b/tx/library/canonical-resource.js index d1da7a4f..e7c30632 100644 --- a/tx/library/canonical-resource.js +++ b/tx/library/canonical-resource.js @@ -129,8 +129,10 @@ class CanonicalResource { return this.dateIsMoreRecent(this.version, other.version); case 'integer': return parseInt(this.version, 10) > parseInt(other.version, 10); - case 'alpha': return this.version.localeCompare(other.version) > 0; - default: return this.version.localeCompare(other.version); + case 'alpha': + default: + // Return a boolean: true only when this.version sorts after other.version. + return this.version.localeCompare(other.version) > 0; } } if (this.date && other.date && this.date != other.date) { diff --git a/tx/library/designations.js b/tx/library/designations.js index 8d4bbb41..897d4232 100644 --- a/tx/library/designations.js +++ b/tx/library/designations.js @@ -46,7 +46,7 @@ class SearchFilterText { validateParameter(value, 'value', String); validateOptionalParameter(returnRating, 'returnRating', Boolean); - if (this.null) { + if (this.isNull) { return returnRating ? {passes: true, rating: 0} : true; } diff --git a/tx/library/renderer.js b/tx/library/renderer.js index f7512995..36e7c160 100644 --- a/tx/library/renderer.js +++ b/tx/library/renderer.js @@ -3,6 +3,27 @@ const {Extensions} = require("./extensions"); const {div} = require("../../library/html"); const {getValuePrimitive} = require("../../library/utilities"); const {getValueName} = require("../../library/utilities"); +const {InvalidError} = require("./errors"); + +// Valid FHIR value sets used when validating values before rendering, so that +// illegal input produces an error that names the offending value rather than +// being silently rendered or causing a generic crash downstream. +const VALID_FILTER_OPS = new Set([ + '=', 'is-a', 'descendent-of', 'is-not-a', 'regex', 'in', 'not-in', + 'generalizes', 'child-of', 'descendent-leaf', 'exists' +]); +const VALID_CONCEPTMAP_RELATIONSHIPS = new Set([ + 'related-to', 'equivalent', 'source-is-narrower-than-target', + 'source-is-broader-than-target', 'not-related-to' +]); +const VALID_CONCEPTMAP_EQUIVALENCES = new Set([ + 'relatedto', 'equivalent', 'equal', 'wider', 'subsumes', 'narrower', + 'specializes', 'inexact', 'unmatched', 'disjoint' +]); +const VALID_CODESYSTEM_CONTENT = new Set([ + 'not-present', 'example', 'fragment', 'complete', 'supplement' +]); +const VALID_PUBLICATION_STATUS = new Set(['draft', 'active', 'retired', 'unknown']); /** * @typedef {Object} TerminologyLinkResolver @@ -239,10 +260,12 @@ class Renderer { } renderMetadataLastUpdated(res, tbl) { - if (res.meta?.version) { + if (res.meta?.lastUpdated) { + this._requireValidDate(res.meta.lastUpdated, 'meta.lastUpdated'); let tr = tbl.tr(); - tr.td().b().tx(this.translate('RES_REND_UPDATED')); - tr.td().tx(this.displayDate(res.meta.version)); + // RES_REND_UPDATED is "Last updated: {0}" — supply the formatted date as + // the {0} parameter so the placeholder is substituted. + tr.td().b().tx(this.translate('RES_REND_UPDATED', [this.displayDate(res.meta.lastUpdated)])); } } @@ -318,7 +341,7 @@ class Renderer { } renderLinkComma(x, uri) { - let {desc, url} = this.linkResolver ? this.linkResolver.resolveURL(this.opContext, uri) : null; + let {desc, url} = (this.linkResolver ? this.linkResolver.resolveURL(this.opContext, uri) : null) || {}; if (url) { x.commaItem(desc, url); } else { @@ -328,10 +351,17 @@ class Renderer { async renderCoding(x, coding) { + if (coding === null || coding === undefined || typeof coding !== 'object') { + this._invalid('Coding', coding, 'invalid Coding'); + } + if (coding.code !== undefined && coding.code !== null && + (typeof coding.code !== 'string' || coding.code.trim() === '')) { + this._invalid('Coding.code', coding.code, 'invalid code'); + } let { desc, url - } = this.linkResolver ? await this.linkResolver.resolveCode(this.opContext, coding.system, coding.version, coding.code) : null; + } = (this.linkResolver ? await this.linkResolver.resolveCode(this.opContext, coding.system, coding.version, coding.code) : null) || {}; if (url) { x.ah(url).tx(desc); } else { @@ -347,10 +377,254 @@ class Renderer { return this.opContext.i18n.formatPhrasePlural(msgId, this.opContext.langs, num,[]); } - async renderValueSet(vs) { - if (vs.json) { - vs = vs.json; + /** + * Determine the BCP-47 locale to use for formatting, derived from the user's + * requested languages (parsed from the Accept-Language header). The region + * subtag (e.g. US vs GB) is what selects day/month ordering and month names. + * Falls back to 'en-US' when no usable language is available. + * @returns {string} a BCP-47 locale tag accepted by Intl + * @private + */ + _formatLocale() { + const langs = this.opContext && this.opContext.langs; + if (langs) { + for (const lang of langs) { + if (lang.language && lang.language !== '*') { + let tag = lang.language; + if (lang.script) tag += '-' + lang.script; + if (lang.region) tag += '-' + lang.region; + try { + if (Intl.DateTimeFormat.supportedLocalesOf(tag).length > 0) { + return tag; + } + // Locale is structurally valid but not supported by the runtime; + // Intl will still resolve it to a sensible fallback, so use it. + return tag; + } catch (_) { + // Structurally invalid tag — skip and try the next language. + } + } + } + } + return 'en-US'; + } + + // ── Validation helpers ────────────────────────────────────────────────────── + // These exist so that illegal input is reported with an error that names the + // offending field and value, rather than being silently rendered or causing a + // generic "Cannot read properties of null" style crash further down. + + /** + * Render a value for inclusion in an error message, distinguishing null, + * undefined, objects, and primitives, and truncating long values. + * @private + */ + _showValue(value) { + if (value === undefined) return 'undefined'; + if (value === null) return 'null'; + if (typeof value === 'object') { + let s; + try { s = JSON.stringify(value); } catch (_) { s = Object.prototype.toString.call(value); } + return s.length > 80 ? s.slice(0, 77) + '...' : s; + } + const s = String(value); + return `'${s.length > 80 ? s.slice(0, 77) + '...' : s}'`; + } + + /** + * Throw an InvalidError that identifies the offending value and its location. + * @param {string} path - dotted path to the offending element (e.g. "ConceptMap.group[0].target[0].relationship") + * @param {*} value - the offending value + * @param {string} what - short description of what is wrong (e.g. "invalid ConceptMap relationship") + * @private + */ + _invalid(path, value, what) { + throw new InvalidError(`${what} at ${path}: ${this._showValue(value)}`); + } + + /** + * Validate, unwrap, and type-check a resource before rendering. Accepts either + * a raw resource object or a wrapper exposing `.json`. Throws an InvalidError + * naming the problem when the input is missing, not an object, or of the wrong + * resourceType. + * @param {*} res - the resource (or wrapper) to render + * @param {string} expectedType - the FHIR resourceType this method renders + * @param {string} method - the calling method name, for the error message + * @returns {object} the unwrapped resource object + * @private + */ + _resolveResource(res, expectedType, method) { + if (res === null || res === undefined) { + throw new InvalidError(`${method}: no resource supplied (got ${res === null ? 'null' : 'undefined'})`); + } + if (typeof res !== 'object' || Array.isArray(res)) { + throw new InvalidError(`${method}: expected a ${expectedType} resource object but got ${Array.isArray(res) ? 'an array' : typeof res}`); + } + const r = (res.json !== undefined && res.json !== null) ? res.json : res; + if (r === null || typeof r !== 'object' || Array.isArray(r)) { + throw new InvalidError(`${method}: expected a ${expectedType} resource object but got ${this._showValue(r)}`); } + if (r.resourceType !== undefined && r.resourceType !== expectedType) { + throw new InvalidError(`${method}: expected resourceType '${expectedType}' but found ${this._showValue(r.resourceType)}`); + } + return r; + } + + /** + * True when `value` is a syntactically valid FHIR date/dateTime/instant. + * Used both to validate (via _requireValidDate) and to drive display. + * @private + */ + _isValidFhirDate(value) { + if (typeof value !== 'string') return false; + if (/^\d{4}$/.test(value)) return true; + + let m = /^(\d{4})-(\d{2})$/.exec(value); + if (m) { + const y = Number(m[1]), mo = Number(m[2]); + const d = new Date(Date.UTC(y, mo - 1, 1)); + return !isNaN(d.getTime()) && d.getUTCFullYear() === y && d.getUTCMonth() === mo - 1; + } + + m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value); + if (m) { + const y = Number(m[1]), mo = Number(m[2]), da = Number(m[3]); + const d = new Date(Date.UTC(y, mo - 1, da)); + return !isNaN(d.getTime()) && d.getUTCFullYear() === y && + d.getUTCMonth() === mo - 1 && d.getUTCDate() === da; + } + + if (value.includes('T')) { + const dt = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(\.\d+)?(Z|[+-]\d{2}:\d{2})$/.exec(value); + if (!dt) return false; + if (isNaN(new Date(value).getTime())) return false; + const mo = Number(dt[2]), da = Number(dt[3]), hh = Number(dt[4]), mi = Number(dt[5]), ss = Number(dt[6]); + return mo >= 1 && mo <= 12 && da >= 1 && da <= 31 && hh <= 23 && mi <= 59 && ss <= 60; + } + return false; + } + + /** + * Require a value (when present) to be a valid FHIR date; otherwise throw an + * error naming the field and value. Empty/absent values are allowed and + * returned unchanged. + * @private + */ + _requireValidDate(value, path) { + if (value === null || value === undefined || value === '') return value; + if (!this._isValidFhirDate(value)) { + this._invalid(path, value, 'invalid date'); + } + return value; + } + + /** + * Require a value to be a non-empty string code; otherwise throw an error + * naming the field and value. + * @private + */ + _requireValidCode(value, path) { + if (typeof value !== 'string' || value.trim() === '') { + this._invalid(path, value, 'invalid code'); + } + return value; + } + + /** + * Require a value (when present) to be a member of an allowed set; otherwise + * throw an error naming the field and value. + * @private + */ + _requireAllowed(value, allowed, path, what) { + if (value === null || value === undefined) return value; + if (!allowed.has(value)) { + this._invalid(path, value, what); + } + return value; + } + + /** + * Format a FHIR date / dateTime / instant value into a human-readable string + * using the user's locale. The output precision follows the input precision: + * a year stays a year, a year-month becomes "Month Year", a full date becomes + * a localised date, and a dateTime/instant becomes a localised date and time. + * + * Unparseable values are returned unchanged so the renderer never throws on + * unexpected input. + * + * @param {string} value - a FHIR date, dateTime, or instant (e.g. "2024", + * "2024-03", "2024-03-15", "2024-03-15T10:30:00Z") + * @returns {string} the localised representation, or '' for empty input + */ + displayDate(value) { + if (value === null || value === undefined || value === '') { + return ''; + } + if (typeof value !== 'string') { + value = String(value); + } + + const locale = this._formatLocale(); + + // Year only — nothing to localise. + if (/^\d{4}$/.test(value)) { + return value; + } + + // Year-month → "Month Year". + let m = /^(\d{4})-(\d{2})$/.exec(value); + if (m) { + const year = Number(m[1]), month = Number(m[2]); + const d = new Date(Date.UTC(year, month - 1, 1)); + // Reject values that Date silently rolled over (e.g. month 13). + if (isNaN(d.getTime()) || d.getUTCFullYear() !== year || d.getUTCMonth() !== month - 1) { + return value; + } + return new Intl.DateTimeFormat(locale, { + year: 'numeric', month: 'long', timeZone: 'UTC' + }).format(d); + } + + // Full date → localised date. + m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value); + if (m) { + const year = Number(m[1]), month = Number(m[2]), day = Number(m[3]); + const d = new Date(Date.UTC(year, month - 1, day)); + // Reject values that Date silently rolled over (e.g. 2024-02-30). + if (isNaN(d.getTime()) || d.getUTCFullYear() !== year || + d.getUTCMonth() !== month - 1 || d.getUTCDate() !== day) { + return value; + } + return new Intl.DateTimeFormat(locale, { + year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC' + }).format(d); + } + + // dateTime / instant → localised date and time. + if (value.includes('T')) { + const d = new Date(value); + if (isNaN(d.getTime())) return value; + const hasTimezone = /(Z|[+-]\d{2}:\d{2})$/.test(value); + const opts = { + year: 'numeric', month: 'long', day: 'numeric', + hour: '2-digit', minute: '2-digit', second: '2-digit' + }; + if (hasTimezone) { + // We don't know the user's timezone, so render the absolute instant in + // UTC and label it, rather than silently using the server's zone. + opts.timeZone = 'UTC'; + opts.timeZoneName = 'short'; + } + return new Intl.DateTimeFormat(locale, opts).format(d); + } + + // Unrecognised format — return unchanged. + return value; + } + + async renderValueSet(vs) { + vs = this._resolveResource(vs, 'ValueSet', 'renderValueSet'); + this._requireAllowed(vs.status, VALID_PUBLICATION_STATUS, 'ValueSet.status', 'invalid status'); let div_ = div(); div_.h2().tx("Properties"); @@ -369,9 +643,9 @@ class Renderer { } async renderCodeSystem(cs, sourcePackage) { - if (cs.json) { - cs = cs.json; - } + cs = this._resolveResource(cs, 'CodeSystem', 'renderCodeSystem'); + this._requireAllowed(cs.status, VALID_PUBLICATION_STATUS, 'CodeSystem.status', 'invalid status'); + this._requireAllowed(cs.content, VALID_CODESYSTEM_CONTENT, 'CodeSystem.content', 'invalid CodeSystem content'); let div_ = div(); @@ -399,7 +673,7 @@ class Renderer { p.tx(" "); p.startCommaList("and"); for (let ext of supplements) { - this.renderLinkComma(p, ext); + this.renderLinkComma(p, getValuePrimitive(ext)); } p.stopCommaList(); p.tx("."); @@ -447,6 +721,7 @@ class Renderer { li.tx(":"); const ul = li.ul(); for (let c of inc.concept) { + this._requireValidCode(c.code, 'ValueSet.compose.include.concept.code'); const li = ul.li(); const link = this.linkResolver ? await this.linkResolver.resolveCode(this.opContext, inc.system, inc.version, c.code) : null; if (link) { @@ -467,6 +742,7 @@ class Renderer { li.startCommaList("and"); for (let f of inc.filter) { let op = this.readFilterOp(f); + this._requireAllowed(op, VALID_FILTER_OPS, 'ValueSet.compose.include.filter.op', 'invalid filter operator'); if (op == 'exists') { if (f.value == "true") { li.commaItem(f.property+" "+ this.translate('VALUE_SET_EXISTS')); @@ -485,13 +761,15 @@ class Renderer { } li.stopCommaList(); } - } else { + } else if (inc.valueSet && inc.valueSet.length > 0) { li.tx(this.translatePlural(inc.valueSet.length, 'VALUE_SET_RULES_INC')); li.startCommaList("and"); for (let vs of inc.valueSet) { this.renderLinkComma(li, vs); } li.stopCommaList(); + } else { + this._invalid('ValueSet.compose.include', inc, 'invalid ValueSet include (must specify a system or at least one valueSet)'); } } @@ -713,6 +991,7 @@ class Renderer { } async addConceptRow(tbl, concept, level, cs, columnInfo) { + this._requireValidCode(concept.code, 'CodeSystem.concept.code'); const tr = tbl.tr(); // Apply styling for deprecated concepts @@ -1304,9 +1583,8 @@ class Renderer { } async renderCapabilityStatement(cs) { - if (cs.json) { - cs = cs.json; - } + cs = this._resolveResource(cs, 'CapabilityStatement', 'renderCapabilityStatement'); + this._requireAllowed(cs.status, VALID_PUBLICATION_STATUS, 'CapabilityStatement.status', 'invalid status'); let div_ = div(); @@ -1575,9 +1853,8 @@ class Renderer { } async renderTerminologyCapabilities(tc) { - if (tc.json) { - tc = tc.json; - } + tc = this._resolveResource(tc, 'TerminologyCapabilities', 'renderTerminologyCapabilities'); + this._requireAllowed(tc.status, VALID_PUBLICATION_STATUS, 'TerminologyCapabilities.status', 'invalid status'); let div_ = div(); @@ -1675,9 +1952,8 @@ class Renderer { * metadata table (reusing renderMetadataTable), then group-by-group rendering. */ async renderConceptMap(cm) { - if (cm.json) { - cm = cm.json; - } + cm = this._resolveResource(cm, 'ConceptMap', 'renderConceptMap'); + this._requireAllowed(cm.status, VALID_PUBLICATION_STATUS, 'ConceptMap.status', 'invalid status'); let div_ = div(); @@ -2080,8 +2356,12 @@ class Renderer { */ renderConceptMapRelationship(tr, tgt) { if (tgt.relationship) { + this._requireAllowed(tgt.relationship, VALID_CONCEPTMAP_RELATIONSHIPS, + 'ConceptMap.group.element.target.relationship', 'invalid ConceptMap relationship'); tr.td().tx(this.presentRelationshipCode(tgt.relationship)); } else if (tgt.equivalence) { + this._requireAllowed(tgt.equivalence, VALID_CONCEPTMAP_EQUIVALENCES, + 'ConceptMap.group.element.target.equivalence', 'invalid ConceptMap equivalence'); tr.td().tx(this.presentEquivalenceCode(tgt.equivalence)); } else { tr.td().tx("(" + "equivalent" + ")"); @@ -2252,6 +2532,7 @@ class Renderer { return f.op; } } + } module.exports = { Renderer }; diff --git a/tx/library/ucum-types.js b/tx/library/ucum-types.js index 07011b9d..08d75770 100644 --- a/tx/library/ucum-types.js +++ b/tx/library/ucum-types.js @@ -939,7 +939,10 @@ class Registry { } register(handler) { - this.handlers.set(handler.code, handler); + // Key on getCode() — the interface every handler implements. Some handlers + // (CelsiusHandler/FahrenheitHandler) override getCode() without setting a + // `.code` field, so keying on `.code` would register them under `undefined`. + this.handlers.set(handler.getCode(), handler); } } diff --git a/tx/operation-context.js b/tx/operation-context.js index b37f4f14..cccce84d 100644 --- a/tx/operation-context.js +++ b/tx/operation-context.js @@ -204,6 +204,10 @@ class ExpansionCache { this.cache = new Map(); this.maxSize = maxSize; this.memoryThresholdBytes = memoryThresholdMB * 1024 * 1024; + // When true, every expansion is cached regardless of how long it took + // (bypasses MIN_CACHE_TIME_MS). Used by the test runner to force the cache + // path so cache correctness (e.g. language in the key) is exercised. + this.forceCaching = false; } /** @@ -284,8 +288,9 @@ class ExpansionCache { * @returns {boolean} True if cached, false if duration too short */ set(key, expansion, durationMs) { - // Only cache if expansion took significant time - if (durationMs < ExpansionCache.MIN_CACHE_TIME_MS) { + // Only cache if expansion took significant time, unless forceCaching is on + // (in which case everything is cached regardless of duration). + if (!this.forceCaching && durationMs < ExpansionCache.MIN_CACHE_TIME_MS) { return false; } @@ -357,21 +362,6 @@ class ExpansionCache { return false; } - /** - * Force-store an expansion regardless of duration (for testing) - * @param {string} key - Hash key - * @param {Object} expansion - The expanded ValueSet - */ - forceSet(key, expansion) { - this.cache.set(key, { - expansion: expansion, - createdAt: Date.now(), - lastUsed: Date.now(), - durationMs: 0, - hitCount: 0 - }); - } - /** * Clear a specific entry * @param {string} key - Hash key @@ -388,22 +378,22 @@ class ExpansionCache { } /** - * Get cache statistics + * Get cache statistics. + * NB: named getStats(), not stats() — the `stats` field (the ServerStats + * passed to the constructor) would shadow a method called `stats`, making it + * unreachable. * @returns {Object} Stats object */ - stats() { + getStats() { let totalHits = 0; - let totalDuration = 0; for (const entry of this.cache.values()) { totalHits += entry.hitCount; - totalDuration += entry.durationMs; } return { size: this.cache.size, maxSize: this.maxSize, memoryThresholdMB: this.memoryThresholdBytes > 0 ? this.memoryThresholdBytes / (1024 * 1024) : 0, - totalHits, - totalDurationSaved: totalHits > 0 ? totalDuration * totalHits : 0 + totalHits }; } diff --git a/tx/params.js b/tx/params.js index 1703b881..669494c3 100644 --- a/tx/params.js +++ b/tx/params.js @@ -53,6 +53,11 @@ class TxParameters { this.FHTTPLanguages = null; this.FDisplayLanguages = null; + // Whether languages were explicitly supplied by the request (vs a + // synthesised default). Consumed by hasHTTPLanguages/hasDisplayLanguages so + // the requested language folds into the expansion cache key. + this.FHasHTTPLanguages = false; + this.FHasDisplayLanguages = false; this.FValueSetVersionRules = null; this.FUid = ''; @@ -87,11 +92,15 @@ class TxParameters { if (!params.parameter) { return; } - if (!this.hasHTTPLanguages && this.hasParam(params, "__Content-Language")) { - this.HTTPLanguages = Languages.fromAcceptLanguage(this.paramstr(params, "__Content-Language"), this.languageDefinitions, !this.validating); + if (this.hasParam(params, "__Content-Language")) { + const lang = this.paramstr(params, "__Content-Language"); + this.HTTPLanguages = Languages.fromAcceptLanguage(lang, this.languageDefinitions, !this.validating); + if (lang) this.FHasHTTPLanguages = true; } - if (!this.hasHTTPLanguages && this.hasParam(params, "__Accept-Language")) { - this.HTTPLanguages = Languages.fromAcceptLanguage(this.paramstr(params, "__Accept-Language"), this.languageDefinitions, !this.validating); + if (this.hasParam(params, "__Accept-Language")) { + const lang = this.paramstr(params, "__Accept-Language"); + this.HTTPLanguages = Languages.fromAcceptLanguage(lang, this.languageDefinitions, !this.validating); + if (lang) this.FHasHTTPLanguages = true; } for (let p of params.parameter) { @@ -124,7 +133,9 @@ class TxParameters { case 'displayLanguage': { try { - this.DisplayLanguages = Languages.fromAcceptLanguage(getValuePrimitive(p), this.languageDefinitions, !this.validating); + const lang = getValuePrimitive(p); + this.DisplayLanguages = Languages.fromAcceptLanguage(lang, this.languageDefinitions, !this.validating); + if (lang) this.FHasDisplayLanguages = true; } catch (error) { throw new Issue("error", "processing", null, 'INVALID_DISPLAY_NAME', this.i18n.translate('INVALID_DISPLAY_NAME', this.HTTPLanguages, [getValuePrimitive(p)]), "invalid-display").handleAsOO(400); } @@ -139,7 +150,10 @@ class TxParameters { break; } case 'no-cache': { - if (getValuePrimitive(p) === 'true') this.uid = crypto.randomUUID(); + // Write FUid (the field the cache key reads via hashSource); writing + // `this.uid` was a no-op so no-cache=true never busted the cache. + // Accept both the string ('true') and boolean (valueBoolean) forms. + if (strToBool(getValuePrimitive(p), false)) this.FUid = crypto.randomUUID(); break; } case '_incomplete': @@ -292,11 +306,11 @@ class TxParameters { } get hasHTTPLanguages() { - return this.FHTTPLanguages && this.FHTTPLanguages.source; + return this.FHasHTTPLanguages; } get hasDisplayLanguages() { - return this.FDisplayLanguages && this.FDisplayLanguages.source; + return this.FHasDisplayLanguages; } get hasDesignations() { @@ -422,6 +436,7 @@ e if (value) { if (name === 'displayLanguage' && (!this.FDisplayLanguages || overwrite)) { this.DisplayLanguages = Languages.fromAcceptLanguage(getValuePrimitive(value), this.languageDefinitions, !this.validating) + if (getValuePrimitive(value)) this.FHasDisplayLanguages = true; } if (name === 'designation') { @@ -561,6 +576,17 @@ e if (this.hasDesignations) { s = s + this.FDesignations.join(',') + '|'; } + if (this.supplements && this.supplements.size > 0) { + // useSupplement changes the expansion result (and a bad supplement must + // error), so it must be part of the cache key. Sort for determinism. + s = s + '$' + [...this.supplements].sort().join(',') + '|'; + } + // Further result-affecting parameters that were previously omitted from the + // key: the text filter (changes which codes expand), limited/incomplete + // expansion handling, whether abstract codes are included, and diagnostics. + // filter is free text, so JSON.stringify it to avoid delimiter collisions. + s = s + 'f:' + JSON.stringify(this.filter || '') + '|' + + b(this.limitedExpansion) + b(this.incompleteOK) + b(this.abstractOk) + b(this.diagnostics); for (let t of this.FVersionRules) { s = s + t.asString() + '|'; } @@ -623,9 +649,11 @@ e if (other.FHTTPLanguages) { this.FHTTPLanguages = other.FHTTPLanguages; + this.FHasHTTPLanguages = this.FHasHTTPLanguages || other.FHasHTTPLanguages; } if (other.FDisplayLanguages) { this.FDisplayLanguages = other.FDisplayLanguages; + this.FHasDisplayLanguages = this.FHasDisplayLanguages || other.FHasDisplayLanguages; } } diff --git a/tx/provider.js b/tx/provider.js index 162cabc5..2fe85a6d 100644 --- a/tx/provider.js +++ b/tx/provider.js @@ -1,7 +1,8 @@ const { CodeSystem } = require("./library/codesystem"); const {VersionUtilities} = require("../library/version-utilities"); const { FhirCodeSystemProvider} = require("./cs/cs-cs"); -const {OperationContext, TerminologyError} = require("./operation-context"); +const {OperationContext} = require("./operation-context"); +const {TerminologyError} = require("./library/errors"); const {validateParameter, validateOptionalParameter, validateArrayParameter} = require("../library/utilities"); const path = require("path"); const {PackageContentLoader} = require("../library/package-manager"); @@ -498,14 +499,16 @@ class Provider { deleteCodeSystem(cs) { this.codeSystems.delete(cs.vurl); this.codeSystems.delete(cs.url); + // If other versions of the SAME url survive, re-point the unversioned [url] + // entry at the most-recent surviving version. Otherwise leave it deleted. let existing = null; for (let t of this.codeSystems.values()) { - if (!existing || t.isMoreRecent(existing)) { + if (t.url === cs.url && (!existing || t.isMoreRecent(existing))) { existing = t; } } if (existing) { - this.codeSystems.set(cs.url, cs); + this.codeSystems.set(existing.url, existing); } } diff --git a/tx/tests/test-runner.js b/tx/tests/test-runner.js index efbd6221..46101564 100644 --- a/tx/tests/test-runner.js +++ b/tx/tests/test-runner.js @@ -22,6 +22,25 @@ async function startTxTests() { await loadValidator(); } +/** + * Force (or stop forcing) the expansion cache to cache every expansion + * regardless of duration, across all endpoints. Clears the caches so the run + * starts clean. Used to run the whole test suite a third time with caching + * fully active, which exercises cache correctness (e.g. that language is part + * of the cache key) that the fast, normally-uncached runs cannot. + */ +function setForcedCaching(enabled) { + if (!txModule || !Array.isArray(txModule.endpoints)) { + return; + } + for (const ep of txModule.endpoints) { + if (ep.expansionCache) { + ep.expansionCache.clearAll(); + ep.expansionCache.forceCaching = enabled; + } + } +} + async function finishTxTests() { console.log(txTestSummary()); let textfilename = path.join(__dirname, '../../test-cases-summary.txt'); @@ -153,4 +172,4 @@ async function unloadValidator() { } } -module.exports = { startTxTests, finishTxTests, runTest, txTestModeSet }; \ No newline at end of file +module.exports = { startTxTests, finishTxTests, runTest, txTestModeSet, setForcedCaching }; \ No newline at end of file diff --git a/tx/tests/testcases-generator.js b/tx/tests/testcases-generator.js index 93d253f8..b39fca92 100644 --- a/tx/tests/testcases-generator.js +++ b/tx/tests/testcases-generator.js @@ -38,11 +38,61 @@ function txTestVersion() { module.exports = { txTestVersion }; `; + // Emit the per-suite describe blocks. `suffix` is appended to each it() + // name so the same tests can be emitted more than once (e.g. a cached pass) + // with distinct, reportable names. + // When oneVersion is true, each test is emitted at a single version + // (caching behaviour is version-independent, so the forced-caching pass + // doesn't need both R4 and R5): prefer R5, fall back to R4 for R4-only tests. + const emitSuites = (suffix, oneVersion = false) => { + let s = ''; + for (const suite of testCases.suites) { + if (!suite.mode || modes.has(suite.mode)) { + s += `describe('${suite.name}', () => {\n`; + + if (suite.description) { + s += ` // ${suite.description}\n\n`; + } + + for (const test of suite.tests) { + if ((!test.mode || modes.has(test.mode)) && (!test["full-set"])) { + let testDetails = { + suite: suite.name, + test: test.name + } + + const escapedNameLiteral = JSON.stringify(test.name); + const hasR5 = (!test.version || test.version.startsWith("5.0")); + const hasR4 = (!test.version || test.version.startsWith("4.0")); + const emitR5 = () => { + s += ` it(${escapedNameLiteral} + 'R5${suffix}', async () => {\n`; + s += ` await runTest(${JSON.stringify(testDetails)}, "5.0");\n`; + s += ` });\n\n`; + }; + const emitR4 = () => { + s += ` it(${escapedNameLiteral} + 'R4${suffix}', async () => {\n`; + s += ` await runTest(${JSON.stringify(testDetails)}, "4.0");\n`; + s += ` });\n\n`; + }; + if (oneVersion) { + if (hasR5) emitR5(); else if (hasR4) emitR4(); + } else { + if (hasR5) emitR5(); + if (hasR4) emitR4(); + } + } + } + s += `});\n\n`; + } + } + return s; + }; + let output = `// AUTO-GENERATED FILE - DO NOT EDIT // Generated from test-cases.json // Regenerate with: node generate-tests.js -const { runTest, startTxTests, finishTxTests } = require('../../tx/tests/test-runner'); +const { runTest, startTxTests, finishTxTests, setForcedCaching } = require('../../tx/tests/test-runner'); describe('Tx Tests', () => { @@ -54,37 +104,20 @@ describe('Tx Tests', () => { }); `; - for (const suite of testCases.suites) { - if (!suite.mode || modes.has(suite.mode)) { - output += `describe('${suite.name}', () => {\n`; - - if (suite.description) { - output += ` // ${suite.description}\n\n`; - } - - for (const test of suite.tests) { - if ((!test.mode || modes.has(test.mode)) && (!test["full-set"])) { - let testDetails = { - suite: suite.name, - test: test.name - } + // First two passes: every test at R5 and R4, with no effective caching + // (expansions complete well under the cache threshold). + output += emitSuites(''); + + // Third pass: run everything again with the expansion cache forced on, + // regardless of how long each expansion takes. This exercises cache + // correctness (e.g. that language settings are part of the cache key) that + // the fast, normally-uncached runs above cannot. + output += `describe('cached (forced caching)', () => {\n`; + output += ` beforeAll(() => { setForcedCaching(true); });\n`; + output += ` afterAll(() => { setForcedCaching(false); });\n\n`; + output += emitSuites('-cached', true); + output += `});\n\n`; - const escapedName = test.name.replace(/'/g, "\\'"); - if (!test.version || test.version.startsWith("5.0")) { - output += ` it('${escapedName}R5', async () => {\n`; - output += ` await runTest(${JSON.stringify(testDetails)}, "5.0");\n`; - output += ` });\n\n`; - } - if (!test.version || test.version.startsWith("4.0")) { - output += ` it('${escapedName}R4', async () => {\n`; - output += ` await runTest(${JSON.stringify(testDetails)}, "4.0");\n`; - output += ` });\n\n`; - } - } - } - output += `});\n\n`; - } - } output += `});\n\n`; fs.writeFileSync(OUTPUT_FILE1, output); console.log(`Generated ${OUTPUT_FILE1}`); diff --git a/tx/tx-html.js b/tx/tx-html.js index be62b6f1..f477d19c 100644 --- a/tx/tx-html.js +++ b/tx/tx-html.js @@ -19,6 +19,7 @@ const {TerminologyCapabilitiesXML} = require("./xml/terminologycapabilities-xml" const {ParametersXML} = require("./xml/parameters-xml"); const {OperationOutcomeXML} = require("./xml/operationoutcome-xml"); const {debugLog} = require("./operation-context"); +const {InvalidError} = require("./library/errors"); const txHtmlLog = Logger.getInstance().child({ module: 'tx-html' }); @@ -311,6 +312,12 @@ class TxHtmlRenderer { return await this.buildHomePage(req); } else { try { + if (json === null || json === undefined || typeof json !== 'object' || Array.isArray(json)) { + throw new InvalidError(`Cannot render: expected a FHIR resource object but got ${json === null ? 'null' : (Array.isArray(json) ? 'an array' : typeof json)}`); + } + if (json.resourceType === undefined || json.resourceType === null || typeof json.resourceType !== 'string' || json.resourceType === '') { + throw new InvalidError(`Cannot render: resource has no resourceType (got ${json.resourceType === undefined ? 'undefined' : JSON.stringify(json.resourceType)})`); + } const _fmt = req?.query?._format || req?.query?.format || req?.body?._format; const op = req ? req.path.includes("$") : false; const resourceType = json.resourceType; diff --git a/tx/tx.js b/tx/tx.js index 89b30947..c30057f2 100644 --- a/tx/tx.js +++ b/tx/tx.js @@ -980,7 +980,9 @@ class TXModule { }); // External source info pages - router.get('/info/:id', async (req, res) => { + // GET renders the info page; POST lets a source's info() handle a form + // submission (e.g. VSAC on-demand resync) and re-renders the same page. + const infoHandler = async (req, res) => { const start = Date.now(); try { const source = req.txEndpoint.provider.externalSources.find(s => s.id() === req.params.id); @@ -1000,7 +1002,9 @@ class TXModule { } finally { this.countRequest('info', Date.now() - start); } - }); + }; + router.get('/info/:id', infoHandler); + router.post('/info/:id', infoHandler); } /** diff --git a/tx/vs/vs-vsac.js b/tx/vs/vs-vsac.js index 7d24ae2e..41470ae7 100644 --- a/tx/vs/vs-vsac.js +++ b/tx/vs/vs-vsac.js @@ -7,6 +7,12 @@ const { VersionUtilities } = require('../../library/version-utilities'); const folders = require('../../library/folder-setup'); const {debugLog} = require("../operation-context"); +// Persisted watermark for the phase-1b _lastUpdated scan. +const VSAC_LAST_UPDATED_KEY = 'vsac_last_updated_date'; + +// Canonical URL prefix for VSAC value sets, so operators can enter a bare OID. +const VSAC_VALUESET_URL_PREFIX = 'http://cts.nlm.nih.gov/fhir/ValueSet/'; + /** * VSAC (Value Set Authority Center) ValueSet provider * Fetches and caches ValueSets from the NLM VSAC FHIR server @@ -21,6 +27,8 @@ class VSACValueSetProvider extends AbstractValueSetProvider { * @param {number} [config.refreshIntervalHours=24] - Hours between refresh scans * @param {string} [config.baseUrl='http://cts.nlm.nih.gov/fhir'] - Base URL for VSAC FHIR server * @param {number} [config.timeoutMs=120000] - HTTP request timeout in milliseconds + * @param {string} [config.resyncPassword] - If set, enables the operator "resync a ValueSet" + * form on the /info page, gated by this password. If unset, the form is not offered. */ constructor(config, stats) { super(); @@ -31,6 +39,8 @@ class VSACValueSetProvider extends AbstractValueSetProvider { } this.apiKey = config.apiKey; + // Optional operator password for the on-demand resync form (see info()). + this.resyncPassword = config.resyncPassword || null; this.cacheFolder = folders.ensureFilePath("terminology-cache/vsac"); this.baseUrl = config.baseUrl || 'http://cts.nlm.nih.gov/fhir'; this.refreshIntervalHours = config.refreshIntervalHours || 24; @@ -127,6 +137,7 @@ class VSACValueSetProvider extends AbstractValueSetProvider { return; } this.queue = []; + this._pendingLastUpdated = null; this.isRefreshing = true; const runId = await this.database.startRun(); @@ -185,6 +196,9 @@ class VSACValueSetProvider extends AbstractValueSetProvider { this.queue = [...new Set(this.queue)]; let tracking = { totalFetched: 0, totalNew: 0, totalUpdated: 0, count: 0, newCount : 0 }; + // URLs that fail even after a requeue are permanently dropped this run; we + // hold the watermark back when this is non-zero so they get re-scanned. + let permanentFailures = 0; // phase 2: query for history & content this.requeue = []; for (let q of this.queue) { @@ -204,6 +218,7 @@ class VSACValueSetProvider extends AbstractValueSetProvider { try { await this.processContentAndHistory(q, tracking, this.requeue.length); } catch (error) { + permanentFailures++; debugLog(error); this.stats.task('VSAC Sync', error.message); } @@ -212,6 +227,20 @@ class VSACValueSetProvider extends AbstractValueSetProvider { // Reload map with fresh data await this._reloadMap(); + + // Commit the _lastUpdated watermark only now that phase 2 has durably + // written everything. Hold it back if any URL was permanently dropped, so + // those URLs are re-scanned next run (never advance past un-applied + // changes). If the run threw or the process restarted before this point, + // the watermark is left untouched and the window is re-scanned. + if (this._pendingLastUpdated && permanentFailures === 0) { + await this.database.setSetting(VSAC_LAST_UPDATED_KEY, this._pendingLastUpdated); + } else if (permanentFailures > 0) { + const holdMsg = `Holding _lastUpdated watermark: ${permanentFailures} URL(s) failed after retry; will re-scan next run`; + console.log(holdMsg); + this.stats.task('VSAC Sync', holdMsg); + } + let msg = `VSAC refresh completed. Total: ${tracking.totalFetched} ValueSets, New: ${tracking.totalNew}, Updated: ${tracking.totalUpdated}`; this.stats.taskDone('VSAC Sync', msg); console.log(msg); @@ -579,9 +608,7 @@ class VSACValueSetProvider extends AbstractValueSetProvider { * @private */ async _scanLastUpdated() { - const SETTING_KEY = 'vsac_last_updated_date'; - - let sinceDate = await this.database.getSetting(SETTING_KEY); + let sinceDate = await this.database.getSetting(VSAC_LAST_UPDATED_KEY); if (!sinceDate) { // No stored date — default to 10 days ago const d = new Date(); @@ -615,10 +642,13 @@ class VSACValueSetProvider extends AbstractValueSetProvider { url = this._getNextUrl(bundle); } - // Store the server date for next run - if (serverDate) { - await this.database.setSetting(SETTING_KEY, serverDate); - } + // Capture the server's date but do NOT commit the watermark here. It must + // only advance once phase 2 has durably upserted the queued URLs; committing + // it now (before phase 2) means a phase-2 failure or a process restart would + // move the watermark past URLs that were never written, stranding them + // permanently. refreshValueSets() commits this._pendingLastUpdated after + // phase 2 completes with no dropped URLs. + this._pendingLastUpdated = serverDate; return count; } @@ -631,8 +661,123 @@ class VSACValueSetProvider extends AbstractValueSetProvider { return "history"; } - async info() { + /** + * Whether the on-demand resync form is enabled (a resyncPassword is configured). + */ + _resyncEnabled() { + return !!this.resyncPassword; + } + + /** + * Timing-safe comparison of a provided password against the configured one. + * @private + */ + _passwordMatches(provided) { + if (!this.resyncPassword) { + return false; + } + const a = Buffer.from(String(provided == null ? '' : provided)); + const b = Buffer.from(String(this.resyncPassword)); + if (a.length !== b.length) { + return false; + } + return crypto.timingSafeEqual(a, b); + } + + /** + * Immediately resync a single ValueSet URL (all versions): fetch from VSAC, + * upsert, and reload the in-memory map. + * @param {string} url - the ValueSet canonical url + * @returns {Promise} number of versions fetched + */ + async resyncValueSet(url) { + const tracking = { totalFetched: 0, totalNew: 0, totalUpdated: 0, count: 0, newCount: 0 }; + await this.processContentAndHistory(url, tracking, 1); + await this._reloadMap(); + return tracking.totalFetched; + } + + /** + * Handle a POST from the resync form. Validates the password (timing-safe) and, + * if a full sync isn't already running, resyncs the requested URL. Returns an + * HTML notice. Never echoes or logs the password. + * @private + */ + async _handleResyncRequest(req) { + const escape = require('escape-html'); + if (!this._resyncEnabled()) { + return ''; + } + const body = req.body || {}; + const url = this._expandOidOrUrl(body.url); + + if (!this._passwordMatches(body.password)) { + // Small delay to blunt brute-forcing; reveal nothing else. + await new Promise(r => setTimeout(r, 1000)); + return '

Incorrect password — no action taken.

'; + } + if (!url) { + return '

Enter a ValueSet URL to resync.

'; + } + if (this.isRefreshing) { + return '

A full sync is currently running; please retry in a few minutes.

'; + } + try { + const n = await this.resyncValueSet(url); + console.log(`Manual resync of ${url}: ${n} version(s)`); + this.stats.task('VSAC Sync', `Manual resync of ${url}: ${n} version(s)`); + return `

Resynced ${escape(url)}: ${n} version(s).

`; + } catch (error) { + debugLog(error); + return `

Resync of ${escape(url)} failed: ${escape(error.message)}

`; + } + } + + /** + * Accept either a full canonical URL or a bare VSAC OID. A dotted-decimal OID + * (optionally with a urn:oid: prefix) is expanded to the VSAC ValueSet URL. + * @private + */ + _expandOidOrUrl(input) { + let s = (input == null ? '' : String(input)).trim(); + if (s.toLowerCase().startsWith('urn:oid:')) { + s = s.substring('urn:oid:'.length); + } + if (/^[0-9]+(\.[0-9]+)+$/.test(s)) { + return VSAC_VALUESET_URL_PREFIX + s; + } + return s; + } + + /** + * The resync form HTML, or '' when the feature is disabled (no password set). + * @private + */ + _resyncFormHtml() { + if (!this._resyncEnabled()) { + return ''; + } + return `
+ Resync a ValueSet +
+ +
+
+ + +
+
`; + } + + async info(req) { const escape = require('escape-html'); + + // Operator action: resync a specific ValueSet (POST from the form below). + let resyncNotice = ''; + if (req && req.method === 'POST') { + resyncNotice = await this._handleResyncRequest(req); + } + const db = await this.database._getReadConnection(); const rows = await new Promise((resolve, reject) => { @@ -689,7 +834,10 @@ class VSACValueSetProvider extends AbstractValueSetProvider { ? new Date(ts * 1000).toISOString().replace('T', ' ').substring(0, 19) + ' UTC' : '—'; - let html = '

VSAC Sync History

'; + let html = ''; + html += this._resyncFormHtml(); + html += resyncNotice; + html += '

VSAC Sync History

'; html += ''; html += ''; html += ''; diff --git a/tx/workers/translate.js b/tx/workers/translate.js index d7547a3c..87f2b228 100644 --- a/tx/workers/translate.js +++ b/tx/workers/translate.js @@ -205,10 +205,8 @@ class TranslateWorker extends TerminologyWorker { targetSystem = params.get('targetSystem'); } } - let explicit = true; // If no explicit concept map, we need to find one based on source/target if (conceptMaps.length == 0) { - explicit = false; if (reverse) { await this.findConceptMapsInAdditionalResources(conceptMaps,targetSystem, targetScope, sourceScope, coding.system); await this.provider.findConceptMapForTranslation(this.opContext, conceptMaps, targetSystem, targetScope, sourceScope, coding.system, coding.code); @@ -222,7 +220,7 @@ class TranslateWorker extends TerminologyWorker { } // Perform the translation - const result = await this.doTranslate(conceptMaps, coding, targetScope, targetSystem, txp, reverse, explicit); + const result = await this.doTranslate(conceptMaps, coding, targetScope, targetSystem, txp, reverse); return res.status(200).json(result); } @@ -322,7 +320,7 @@ class TranslateWorker extends TerminologyWorker { return result; } - translateUsingGroupsForwards(cm, coding, targetScope, targetSystem, params, output, explicit) { + translateUsingGroupsForwards(cm, coding, targetScope, targetSystem, params, output) { let result = false; const matches = cm.listTranslations(coding, targetScope, targetSystem); if (matches.length > 0) { @@ -385,12 +383,10 @@ class TranslateWorker extends TerminologyWorker { part: productParts }); } - if (!explicit) { - matchParts.push({ - name: 'originMap', - valueCanonical: cm.vurl - }); - } + matchParts.push({ + name: 'originMap', + valueCanonical: cm.vurl + }); output.push({ name: 'match', part: matchParts @@ -403,7 +399,7 @@ class TranslateWorker extends TerminologyWorker { return result; } - translateUsingGroupsReverse(cm, coding, targetScope, targetSystem, params, output, explicit) { + translateUsingGroupsReverse(cm, coding, targetScope, targetSystem, params, output) { let result = false; const matches = cm.listTranslationsReverse(coding, targetScope, targetSystem); if (matches.length > 0) { @@ -474,12 +470,10 @@ class TranslateWorker extends TerminologyWorker { part: productParts }); } - if (!explicit) { - matchParts.push({ - name: 'originMap', - valueCanonical: cm.vurl - }); - } + matchParts.push({ + name: 'originMap', + valueCanonical: cm.vurl + }); output.push({ name: 'match', part: matchParts @@ -491,7 +485,7 @@ class TranslateWorker extends TerminologyWorker { return result; } - async translateUsingCodeSystem(cm, coding, target, params, output, reverse, explicit) { + async translateUsingCodeSystem(cm, coding, target, params, output, reverse) { let result = false; const factory = cm.jsonObj.internalSource; let prov = await factory.build(this.opContext, []); @@ -537,12 +531,10 @@ class TranslateWorker extends TerminologyWorker { valueString: t.message }); } - if (!explicit) { - matchParts.push({ - name: 'originMap', - valueCanonical: cm.vurl - }); - } + matchParts.push({ + name: 'originMap', + valueCanonical: cm.vurl + }); output.push({ name: 'match', part: matchParts @@ -560,10 +552,9 @@ class TranslateWorker extends TerminologyWorker { * @param {string} targetSystem - Target code system (optional) * @param {Parameters} params - Full parameters object * @param {boolean} reverse - Full parameters object* - * @param {boolean} explicit - If the concept map was named explicitly * @returns {Object} Parameters resource with translate result */ - async doTranslate(conceptMaps, coding, targetScope, targetSystem, params, reverse, explicit) { + async doTranslate(conceptMaps, coding, targetScope, targetSystem, params, reverse) { this.deadCheck('doTranslate'); const result = []; @@ -572,11 +563,11 @@ class TranslateWorker extends TerminologyWorker { let added = false; for (const cm of conceptMaps) { if (cm.jsonObj.internalSource) { - added = await this.translateUsingCodeSystem(cm, coding, targetSystem, params, result, reverse, explicit) || added; + added = await this.translateUsingCodeSystem(cm, coding, targetSystem, params, result, reverse) || added; } else if (reverse) { - added = this.translateUsingGroupsReverse(cm, coding, targetScope, targetSystem, params, result, reverse, explicit) || added; + added = this.translateUsingGroupsReverse(cm, coding, targetScope, targetSystem, params, result) || added; } else{ - added = this.translateUsingGroupsForwards(cm, coding, targetScope, targetSystem, params, result, reverse, explicit) || added; + added = this.translateUsingGroupsForwards(cm, coding, targetScope, targetSystem, params, result) || added; } } result.push({ diff --git a/tx/workers/validate.js b/tx/workers/validate.js index 966b75b1..364ccaa9 100644 --- a/tx/workers/validate.js +++ b/tx/workers/validate.js @@ -467,7 +467,7 @@ class ValueSetChecker { return false; } let cs = await this.worker.findCodeSystem(system, version, this.params, ['complete', 'fragment'], op,true, false, false, this.worker.requiredSupplements); - this.seeSourceProvider(cs, system); + this.worker.seeSourceProvider(cs, system); if (cs === null) { this.worker.opContext.addNote(this.valueSet, 'Didn\'t find CodeSystem "' + this.worker.renderer.displayCoded(system, version) + '"', this.indentCount); result = null; @@ -563,7 +563,7 @@ class ValueSetChecker { } let msg = await cs.incompleteValidationMessage(ctxt.context, this.params.HTTPLanguages); if (msg) { - op.addIssueNoId('information', 'informational', addToPath(path, 'code'), msg, 'process-note'); + op.addIssue(new Issue('information', 'informational', addToPath(path, 'code'), null, msg, 'process-note')); } inactive.value = await cs.isInactive(ctxt.context); inactive.path = path; @@ -824,7 +824,7 @@ class ValueSetChecker { } else { let msg = 'The code system "' + ccc.system + '" version "' + ccc.version + '" in the ValueSet expansion is different to the one in the value ("' + version + '")'; messages.push(msg); - op.addIssueNoId('error', 'not-found', addToPath(path, 'version'), msg, 'vs-invalid'); + op.addIssue(new Issue('error', 'not-found', addToPath(path, 'version'), null, msg, 'vs-invalid')); return false; } let cs = await this.worker.findCodeSystem(system, v, this.params, ['complete', 'fragment'], op, true, true, false, this.worker.requiredSupplements); @@ -1122,7 +1122,7 @@ class ValueSetChecker { if ((cause.value === 'not-found' && contentMode.value !== 'complete') || contentMode.value === 'example') { let m = 'The system ' + c.system + ' was found but did not contain enough information to properly validate the code "' + c.code + '" ("' + c.display + '") (mode = ' + contentMode.value + ')'; msg(m); - op.addIssueNoId('warning', 'not-found', path, m, 'vs-invalid'); + op.addIssue(new Issue('warning', 'not-found', path, null, m, 'vs-invalid')); } else if (c.display && list.designations.length > 0) { await this.checkDisplays(list, defLang, c, msg, op, path); } @@ -1522,14 +1522,14 @@ class ValueSetChecker { } else if (ok === null) { result.AddParamBool('result', false); result.addParamStr('message', 'The system "' + system + '" is unknown so the /"' + code + '" cannot be confirmed to be in the value set ' + this.valueSet.name); - op.addIssueNoId('error', cause.value, 'code', 'The system "' + system + '" is unknown so the /"' + code + '" cannot be confirmed to be in the value set ' + this.valueSet.name, 'not-found'); + op.addIssue(new Issue('error', cause.value, 'code', null, 'The system "' + system + '" is unknown so the /"' + code + '" cannot be confirmed to be in the value set ' + this.valueSet.name, 'not-found')); for (let us of unknownSystems) { result.addParamCanonical('x-caused-by-unknown-system', us); } } else { result.AddParamBool('result', false); result.addParamStr('message', 'The system/code "' + system + '"/"' + code + '" is not in the value set ' + this.valueSet.name); - op.addIssueNoId('error', cause.value, 'code', 'The system/code "' + system + '"/"' + code + '" is not in the value set ' + this.valueSet.name, 'not-in-vs'); + op.addIssue(new Issue('error', cause.value, 'code', null, 'The system/code "' + system + '"/"' + code + '" is not in the value set ' + this.valueSet.name, 'not-in-vs')); if (cause.value) { result.AddParamCode('cause', cause.value); } diff --git a/tx/workers/worker.js b/tx/workers/worker.js index 5df09aaa..f9aa1bf5 100644 --- a/tx/workers/worker.js +++ b/tx/workers/worker.js @@ -1,4 +1,4 @@ -const { TerminologyError} = require('../operation-context'); +const { TerminologyError} = require('../library/errors'); const { CodeSystem } = require('../library/codesystem'); const ValueSet = require('../library/valueset'); const {VersionUtilities} = require("../../library/version-utilities"); diff --git a/tx/xversion/xv-bundle.js b/tx/xversion/xv-bundle.js index ebc6d881..3b3aedea 100644 --- a/tx/xversion/xv-bundle.js +++ b/tx/xversion/xv-bundle.js @@ -18,8 +18,7 @@ function bundleToR5(jsonObj, sourceVersion) { for (let be of jsonObj.entry || []) { convertResourceToR5(be.resource, sourceVersion); } - - throw new Error(`Unsupported FHIR version: ${sourceVersion}`); + return jsonObj; } /** diff --git a/tx/xversion/xv-codesystem.js b/tx/xversion/xv-codesystem.js index d7dcdbc4..f1c73bee 100644 --- a/tx/xversion/xv-codesystem.js +++ b/tx/xversion/xv-codesystem.js @@ -139,9 +139,12 @@ function codeSystemR5ToR3(r5Obj) { * @private */ function isR5OnlyFilterOperator(operator) { + // Operators added in R5 that are not valid in R4 or earlier. + // NOTE: 'generalizes' is NOT R5-only — it is a valid R4 operator + // (filter-operator value set, 4.0.1), so it must not be stripped here. const r5OnlyOperators = [ - 'generalizes', // Added in R5 - // Add other R5-only operators as they're identified + 'child-of', // Added in R5 + 'descendent-leaf', // Added in R5 ]; return r5OnlyOperators.includes(operator); } diff --git a/tx/xversion/xv-parameters.js b/tx/xversion/xv-parameters.js index cc2a084c..edb1f0eb 100644 --- a/tx/xversion/xv-parameters.js +++ b/tx/xversion/xv-parameters.js @@ -18,7 +18,7 @@ function parametersToR5(jsonObj, sourceVersion) { } const {convertResourceToR5} = require("./xv-resource"); - for (let p of jsonObj.parameter) { + for (let p of jsonObj.parameter || []) { if (p.resource) { p.resource = convertResourceToR5(p.resource, sourceVersion); } @@ -59,7 +59,7 @@ function parametersFromR5(r5Obj, targetVersion) { function parametersR5ToR4(r5Obj) { const {convertResourceFromR5} = require("./xv-resource"); - for (let p of r5Obj.parameter) { + for (let p of r5Obj.parameter || []) { if (p.resource) { p.resource = convertResourceFromR5(p.resource, "R4"); } @@ -71,7 +71,7 @@ function parametersR5ToR4(r5Obj) { } function convertResourceWithinR5(r5Obj) { - for (let p of r5Obj.parameter) { + for (let p of r5Obj.parameter || []) { if (p.name == 'match') { fixMatchParameterfor5(p); } @@ -135,7 +135,7 @@ function convertParameterR5ToR3(p) { function parametersR5ToR3(r5Obj) { const {convertResourceFromR5} = require("./xv-resource"); - for (let p of r5Obj.parameter) { + for (let p of r5Obj.parameter || []) { if (p.resource) { p.resource = convertResourceFromR5(p.resource, "R3"); } diff --git a/tx/xversion/xv-resource.js b/tx/xversion/xv-resource.js index 409adba9..5c73ea56 100644 --- a/tx/xversion/xv-resource.js +++ b/tx/xversion/xv-resource.js @@ -9,7 +9,7 @@ const {bundleFromR5, bundleToR5} = require("./xv-bundle"); function convertResourceToR5(data, sourceVersion) { - if (sourceVersion == "5.0" || !data.resourceType) { + if (!data || sourceVersion == "5.0" || !data.resourceType) { return data; } switch (data.resourceType) { @@ -26,7 +26,7 @@ function convertResourceToR5(data, sourceVersion) { } function convertResourceFromR5(data, targetVersion) { - if (targetVersion == "5.0" || !data.resourceType) { + if (!data || targetVersion == "5.0" || !data.resourceType) { return data; } switch (data.resourceType) { diff --git a/tx/xversion/xv-terminologyCapabilities.js b/tx/xversion/xv-terminologyCapabilities.js index 542d0c21..1df4a1ce 100644 --- a/tx/xversion/xv-terminologyCapabilities.js +++ b/tx/xversion/xv-terminologyCapabilities.js @@ -15,17 +15,22 @@ function terminologyCapabilitiesToR5(jsonObj, sourceVersion) { } if (VersionUtilities.isR4Ver(sourceVersion)) { + const contentExtUrl = "http://hl7.org/fhir/5.0/StructureDefinition/extension-TerminologyCapabilities.codeSystem.content"; for (const cs of jsonObj.codeSystem || []) { - if (cs.content) { - let cnt = Extensions.readString(cs, "http://hl7.org/fhir/5.0/StructureDefinition/extension-TerminologyCapabilities.codeSystem.content"); - if (cnt) { - delete cs.extensions; - cs.content = cnt; + // In R4 the content is carried only by the content extension (there is no + // native codeSystem.content), so read it from the extension directly. + const cnt = Extensions.readString(cs, contentExtUrl); + if (cnt) { + cs.content = cnt; + // Remove the now-redundant source extension (the field is `extension`, + // singular). Filter so any unrelated extensions are preserved. + cs.extension = (cs.extension || []).filter(e => e.url !== contentExtUrl); + if (cs.extension.length === 0) { + delete cs.extension; } } } - return jsonObj; } diff --git a/tx/xversion/xv-valueset.js b/tx/xversion/xv-valueset.js index 7d6b8d1c..1f14fb3b 100644 --- a/tx/xversion/xv-valueset.js +++ b/tx/xversion/xv-valueset.js @@ -14,10 +14,10 @@ function valueSetToR5(jsonObj, sourceVersion) { if (VersionUtilities.isR5Ver(sourceVersion)) { return jsonObj; // No conversion needed } - for (const inc of jsonObj.compose.include || []) { + for (const inc of jsonObj.compose?.include || []) { valueSetIncludeToR5(inc); } - for (const inc of jsonObj.compose.exclude || []) { + for (const inc of jsonObj.compose?.exclude || []) { valueSetIncludeToR5(inc); } if (VersionUtilities.isR4Ver(sourceVersion)) { @@ -90,7 +90,7 @@ function valueSetR5ToR4(r5Obj) { if (include.filter && Array.isArray(include.filter)) { include.filter = include.filter.map(filter => { if (filter.op && isR5OnlyFilterOperator(filter.op)) { - filter._op = { "extension": "http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.compose.include.filter.op", "valueCode": filter.op} + filter._op = { "extension": [{ url: "http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.compose.include.filter.op", "valueCode": filter.op}]}; delete filter.op; } return filter; @@ -105,7 +105,7 @@ function valueSetR5ToR4(r5Obj) { if (exclude.filter && Array.isArray(exclude.filter)) { exclude.filter = exclude.filter.map(filter => { if (filter.op && isR5OnlyFilterOperator(filter.op)) { - filter._op = { "extension": "http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.compose.include.filter.op", "valueCode": filter.op} + filter._op = { "extension": [{ url: "http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.compose.include.filter.op", "valueCode": filter.op}]}; delete filter.op; } return filter; @@ -155,7 +155,7 @@ function valueSetR5ToR3(r5Obj) { if (include.filter && Array.isArray(include.filter)) { include.filter = include.filter.map(filter => { if (filter.op && !isR3CompatibleFilterOperator(filter.op)) { - filter._op = { "extension": "http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.compose.include.filter.op", "valueCode": filter.op} + filter._op = { "extension": [{ url: "http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.compose.include.filter.op", "valueCode": filter.op}]}; delete filter.op; } return filter; @@ -170,7 +170,7 @@ function valueSetR5ToR3(r5Obj) { if (exclude.filter && Array.isArray(exclude.filter)) { exclude.filter = exclude.filter.map(filter => { if (filter.op && !isR3CompatibleFilterOperator(filter.op)) { - filter._op = { "extension": "http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.compose.include.filter.op", "valueCode": filter.op} + filter._op = { "extension": [{ url: "http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.compose.include.filter.op", "valueCode": filter.op}]}; delete filter.op; } return filter; @@ -223,7 +223,7 @@ function convertContainsPropertyR5ToR4(containsList) { */ function isR5OnlyFilterOperator(operator) { const r5OnlyOperators = [ - 'child-of', ' descendent-leaf' // Added in R5 + 'child-of', 'descendent-leaf' // Added in R5 ]; return r5OnlyOperators.includes(operator); }
TimeEventDetail