зеркало из
1
0
Форкнуть 0

Add HL7 FHIR validator support

This commit is contained in:
ljoy913 2022-01-07 14:32:21 -08:00
Родитель 09ae95f0dc
Коммит 94924221be
16 изменённых файлов: 446 добавлений и 45 удалений

8
.gitignore поставляемый
Просмотреть файл

@ -130,4 +130,10 @@ testdata/missing_kid_key.json
tmp/*
# full fhir schema
schema/fhir-schema-full.json
schema/fhir-schema-full.json
# temp fhir bundle file
~temp.fhirbundle.json
# downloaded HL7 validator for JRE deployment
validator_cli.jar

16
.vscode/settings.json поставляемый
Просмотреть файл

@ -2,10 +2,18 @@
"eslint.alwaysShowStatus": true,
"eslint.format.enable": true,
"cSpell.words": [
"badcrl",
"clearline",
"Codeable",
"Excludable",
"execa",
"exitcode",
"fhir",
"fhirbundle",
"fhirhealthcard",
"fhirout",
"fhirvalidator",
"fidm",
"grayscale",
"healthcard",
"istextorbinary",
@ -13,8 +21,12 @@
"jwks",
"jwkset",
"jwspayload",
"libressl",
"logfile",
"loglevel",
"loinc",
"LOINC",
"myfhirbundle",
"npmpackage",
"outdir",
"pako",
@ -22,6 +34,8 @@
"qrcode",
"qrnumeric",
"repos",
"testdata"
"testdata",
"testif",
"uuidv"
]
}

Просмотреть файл

@ -97,6 +97,7 @@ To validate health card artifacts, use the `shc-validator.ts` script, or simply
(choices: "fhirbundle", "jwspayload", "jws", "healthcard", "fhirhealthcard", "qrnumeric", "qr", "jwkset")
-l, --loglevel <loglevel> set the minimum log level (choices: "debug", "info", "warning", "error", "fatal", default: "warning")
-P, --profile <profile> vaccination profile to validate (choices: "any", "usa-covid19-immunization", default: "any")
-V, --validator <validator> FHIR bundle validator (choices: "default", "fhirvalidator" (requires Java runtime or Docker))
-d, --directory <directory> trusted issuer directory to validate against
-o, --logout <path> output path for log (if not specified log will be printed on console)
-f, --fhirout <path> output path for the extracted FHIR bundle
@ -160,9 +161,15 @@ const results = validate.jws(jwsString);
results.then(console.log)
```
## Notes
Validation of the FHIR bundle is currently limited. The tool validates a subset of the full FHIR schema; the behavior can be scoped by using the profile option, or changed by modifying the `src/prune-fhir-schema.ts` script. Extensive tests and conformance to the [Vaccination & Testing Implementation Guide](http://build.fhir.org/ig/dvci/vaccine-credential-ig/branches/main/) can be performed using the [FHIR validator](https://wiki.hl7.org/Using_the_FHIR_Validator) tool.
## FHIR Validation
Validation of the FHIR bundle is currently not comprehensive. The tool validates a subset of the full FHIR schema; the behavior can be scoped by using the profile option, or changed by modifying the `src/prune-fhir-schema.ts` script. Extensive tests and conformance to the [Vaccination & Testing Implementation Guide](http://build.fhir.org/ig/dvci/vaccine-credential-ig/branches/main/) can be performed using the [FHIR validator](https://wiki.hl7.org/Using_the_FHIR_Validator) tool.
This tool can now apply the HL7 FHIR Validator, in place of the limited default validator, with the use of the `--validator fhirvalidator` option. The HL7 FHIR Validator is a Java application and so requires a Java runtime (JRE), or alternatively, Docker to be installed on your system.
This tool will attempt to run it with an installed JRE first, if available. If not, it will attempt to instantiate a Docker image (with a JRE). If neither method is succeeds an error will be returned.
__Note__: Docker may require elevated permissions to execute docker commands, requiring this tool to also run with elevated permissions when attempting to use a Docker image. For example:
```
# Run shc-validator as sudo ('-E env "PATH=$PATH"' preserves the environment of the current user)
sudo -E env "PATH=$PATH" shc-validator --path myfhirbundle.json --type fhirbundle --validator fhirvalidator
```

Просмотреть файл

@ -0,0 +1,9 @@
FROM fabric8/java-alpine-openjdk11-jre
RUN java -version
RUN curl -L -o validator_cli.jar https://github.com/hapifhir/org.hl7.fhir.core/releases/latest/download/validator_cli.jar
# Run the validator without anything to validate - to cache the fhir downloads in the image.
# "|| :" forces a '0' exit code as the validator will return an error code when doing no actual validation
RUN java -jar validator_cli.jar -ig hl7.fhir.r5.core#current || :

69
src/command.ts Normal file
Просмотреть файл

@ -0,0 +1,69 @@
import execa from 'execa';
import color from 'colors';
import Log from './logger';
function workingAnimation(message: string, interval = 200) {
const chars = ['|', '/', '―', '\\'];
let x = 0;
// For the animation, we write to stdout a message, then use process.stdout.clearLine() to erase the line
// and on the same line write an updated message. This keeps the message updating on the same console line.
// When we run the entire test suite using 'npm run test', process.stdout.clearline() is not available.
// We don't really need to print the animation in this case anyway, so we skip the animation when
// process.stdout.clearline() is not available.
if (!process.stdout.clearLine) return { stop: () => {/*noop*/ } };
const handle = setInterval(() => {
process.stdout.write(`\r ${color.green(chars[x++])} ${message}`);
x %= chars.length;
}, interval);
return {
stop: () => {
clearInterval(handle);
process.stdout.clearLine(0);
process.stdout.write('\n');
}
}
}
export function runCommandSync(command: string, log?: Log): CommandResult {
let result;
const start = Date.now();
try {
result = execa.commandSync(command) as CommandResult;
} catch (failed) {
result = failed as CommandResult;
}
log?.debug(resultToString(result, start));
return result;
}
export async function runCommand(command: string, message?: string, log?: Log): Promise<CommandResult> {
let result;
const start = Date.now();
const animation = workingAnimation(message || command);
try {
result = await execa.command(command) as CommandResult;
} catch (failed) {
result = failed as CommandResult;
}
animation.stop();
log?.debug(resultToString(result, start));
return result;
}
function resultToString(result: CommandResult, start: number): string {
return `Running command : ${result.command}\n \
duration: ${((Date.now() - start) / 1000).toFixed(2)} seconds\n \
exitcode : ${result.exitCode}\n \
stdout: ${result.stdout.split('\n').join("\n ")}\n \
stderr: ${result.stderr.split('\n').join("\n ")}`;
}

Просмотреть файл

@ -50,7 +50,14 @@ export enum ErrorCode {
INVALID_KEY_UNKNOWN,
// config errors
OPENSSL_NOT_AVAILABLE = 300
OPENSSL_NOT_AVAILABLE = 300,
// FHIR validator errors
FHIR_VALIDATOR_ERROR = 400,
JRE_OR_DOCKER_NOT_AVAILABLE,
DOCKER_ERROR,
DOCKER_PERMISSIONS,
DOCKER_DAEMON_NOT_RUNNING
}
class ExcludableError {

Просмотреть файл

@ -11,6 +11,7 @@ import patientDM from '../schema/patient-dm.json';
import Log from './logger';
import beautify from 'json-beautify'
import { propPath, walkProperties } from './utils';
import { validate as fhirValidator } from './fhirValidator';
// The CDC CVX covid vaccine codes (https://www.cdc.gov/vaccines/programs/iis/COVID-19-related-codes.html),
export const cdcCovidCvxCodes = ["207", "208", "210", "212", "217", "218", "219", "500", "501", "502", "503", "504", "505", "506", "507", "508", "509", "510", "511"];
@ -26,9 +27,14 @@ export enum ValidationProfiles {
'usa-covid19-immunization'
}
export enum Validators {
'default',
'fhirvalidator'
}
export class FhirOptions {
static LogOutputPath = '';
static ValidationProfile: ValidationProfiles = ValidationProfiles.any;
static ValidationProfile = ValidationProfiles.any;
static Validator = Validators.default;
}
// eslint-disable-next-line @typescript-eslint/require-await
@ -36,6 +42,7 @@ export async function validate(fhirBundleText: string): Promise<Log> {
const log = new Log('FhirBundle');
const profile: ValidationProfiles = FhirOptions.ValidationProfile;
const validator: Validators = FhirOptions.Validator;
if (fhirBundleText.trim() !== fhirBundleText) {
log.error(`FHIR bundle has leading or trailing spaces`, ErrorCode.TRAILING_CHARACTERS);
@ -54,6 +61,26 @@ export async function validate(fhirBundleText: string): Promise<Log> {
// failures will be recorded in the log
if (!validateSchema(fhirSchema, fhirBundle, log)) return log;
// use HL7 FHIR validator, if specified
if (validator === Validators['fhirvalidator']) {
log.info(`Applying validator : fhirvalidator`);
void await fhirValidator(fhirBundleText, log);
log.hasErrors || log.info("FHIR bundle validated");
log.debug("FHIR bundle contents:");
log.debug(beautify(fhirBundle, null as unknown as Array<string>, 3, 100));
return log;
}
log.note(`This tool's default FHIR validation is not as complete as the dedicated HL7 FHIR Validator at ${'http://hl7.org/fhir/validator/'} (hl7.org). \
If you have Docker or the Java JRE installed, this tool supports validating against the FHIR validator by using the '--validator fhirvalidator' on the command line. \
See README.md for more information.`);
// Begin 'default
// to continue validation, we must have a list of resources in .entry[]
if (!fhirBundle.entry ||
@ -207,8 +234,8 @@ export async function validate(fhirBundleText: string): Promise<Log> {
ValidationProfilesFunctions['usa-covid19-immunization'](fhirBundle.entry, log);
}
log.info("FHIR bundle validated");
log.debug("FHIR Bundle Contents:");
log.hasErrors || log.info("FHIR bundle validated");
log.debug("FHIR bundle contents:");
log.debug(beautify(fhirBundle, null as unknown as Array<string>, 3, 100));
return log;
@ -292,4 +319,3 @@ const ValidationProfilesFunctions = {
return true;
}
}

228
src/fhirValidator.ts Normal file
Просмотреть файл

@ -0,0 +1,228 @@
import fs from 'fs';
import path from 'path';
import Log from '../src/logger';
import { ErrorCode } from './error';
import color from 'colors';
import got from 'got';
import { runCommand, runCommandSync } from '../src/command';
const imageName = 'fhir.validator.image';
const dockerFile = 'fhir.validator.Dockerfile';
const dockerContainer = 'fhir.validator.container';
const validatorJarFile = 'validator_cli.jar';
const validatorUrl = 'https://github.com/hapifhir/org.hl7.fhir.core/releases/latest/download/validator_cli.jar';
const tempFileName = '~temp.fhirbundle.json';
// share a log between the functions. This can be passed in externally through the validate() function
let log: Log;
async function downloadFHIRValidator(): Promise<void> {
try {
fs.writeFileSync(validatorJarFile, (await got(validatorUrl, { followRedirect: true }).buffer()));
} catch (err) {
log.debug(`File download error ${(err as Error).toString()}`);
}
}
// Runs the FHIR validator using the installed JRE
async function runValidatorJRE(artifactPath: string): Promise<CommandResult | null> {
if (!fs.existsSync(validatorJarFile)) await downloadFHIRValidator();
if (!fs.existsSync(validatorJarFile)) {
log.error(`Failed to download FHIR Validator Jar file ${validatorJarFile} from ${validatorUrl}`);
return null;
}
log.info(`Validating ${artifactPath} with FHIR validator.`);
const result: CommandResult = await runCommand(`java -jar ${validatorJarFile} ${artifactPath}`, `Running HL7 FHIR validator with JRE`, log);
return result;
}
// Runs the FHIR validator in a Docker container
async function runValidatorDocker(artifactPath: string): Promise<CommandResult | null> {
if (!await Docker.checkPermissions()) return null;
if (!await Docker.imageExists(imageName)) {
log.debug(`Image ${imageName} not found. Attempting to build.`);
if (!await Docker.buildImage(dockerFile, imageName)) {
log.error('Could not build Docker image.');
return null;
}
}
const dockerCommand = `java -jar validator_cli.jar data/${artifactPath}`;
log.info(`Validating ${path.resolve(artifactPath)} with FHIR validator.`);
// create a new container from image, copies the
const command = `docker run --rm --name ${dockerContainer} -v ${path.resolve(path.dirname(artifactPath))}:/data ${imageName} ${dockerCommand}`;
const result = await runCommand(command, `Running HL7 FHIR validator with Docker (${imageName})`, log);
return result;
}
export async function validate(fileOrJSON: string, logger = new Log('FHIR Validator')): Promise<Log> {
log = logger;
const usingJre = JRE.isAvailable();
const usingDocker = !usingJre && Docker.isAvailable();
if (!usingJre && !usingDocker) {
return log.error(
`Validator: use of option ${color.italic('--validator fhirvalidator')} requires Docker or JRE to execute the FHIR Validator Java application. See: http://hl7.org/fhir/validator/`,
ErrorCode.JRE_OR_DOCKER_NOT_AVAILABLE
);
}
if (JSON.parse(fileOrJSON)) {
fs.writeFileSync(tempFileName, fileOrJSON); // overwrites by default
fileOrJSON = tempFileName;
}
const artifact = path.resolve(fileOrJSON);
if (!fs.existsSync(artifact)) {
return log.error(`Artifact ${artifact} not found.`);
}
const fileName = path.basename(artifact);
const result: CommandResult | null = await (usingJre ? runValidatorJRE(fileName) : runValidatorDocker(fileName));
if (fs.existsSync(tempFileName)) {
fs.rmSync(tempFileName);
}
// null returned if validator failed before validation actually checked
if (result === null) return log;
// if everything is ok, return
if (result && /Information: All OK/.test(result?.stdout)) return log;
const errors = result?.stdout.match(/(?<=\n\s*Error @ ).+/g) || [];
errors.forEach(err => {
const formattedError = err; // splitLines(err);
log.error(formattedError, ErrorCode.FHIR_VALIDATOR_ERROR);
});
const warnings = result?.stdout.match(/(?<=\n\s*Warning @ ).+/g) || [];
warnings.forEach(warn => {
const formattedError = warn; // splitLines(warn);
log.warn(formattedError, ErrorCode.FHIR_VALIDATOR_ERROR);
});
// if there are no errors or warnings but the validation is not 'All OK'
// something is wrong.
if (!errors && !warnings) {
log.error(`${fileName} : failed to find Errors or 'All OK'`);
}
return log;
}
const JRE = {
isAvailable: (): boolean => {
const result = runCommandSync(`java --version`, log);
if (result.exitCode === 0) {
const version = /^java \d+.+/.exec(result.stdout)?.[0] ?? 'unknown';
log?.debug(`Java detected : ${version}`);
}
return result.exitCode === 0;
}
}
const Docker = {
isAvailable: (): boolean => {
const result = runCommandSync(`docker --version`, log);
if (result.exitCode === 0) {
const version = /^Docker version \d+.+/.exec(result.stdout)?.[0] ?? 'unknown';
log?.debug(`Docker detected : ${version}`);
}
return result.exitCode === 0;
},
imageExists: async (imageName: string): Promise<boolean> => {
return (await runCommand(`docker image inspect ${imageName}`, `Check Docker image ${imageName} exists`, log)).exitCode === 0;
},
containerExists: async (name: string): Promise<boolean> => {
const stdout = (await runCommand(`docker ps -a --format '{{.Names}}'`, undefined, log)).stdout;
const names: string[] = stdout.replace(/'/g, '').split('\n');
return names.includes(name);
},
checkPermissions: async (): Promise<boolean> => {
const result = await runCommand(`docker image ls`, undefined, log);
if (result.exitCode !== 0) {
if (/permission denied/.test(result.stderr)) {
log.error(
`Selecting the '--validator fhirvalidator' option is attempting to run the HL7 FHIR Validator using a Docker image. However, Docker on this system requires elevated permissions to use. Run this tool as an elevated user or add yourself to the 'docker' group. See README.md for additional information.`,
ErrorCode.DOCKER_PERMISSIONS
);
} else if (/docker daemon is not running/.test(result.stderr)) {
log.error(
`Selecting the '--validator fhirvalidator' option is attempting to run the HL7 FHIR Validator using a Docker image. However, Docker may not be running. See README.md for additional information.`,
ErrorCode.DOCKER_DAEMON_NOT_RUNNING
);
} else {
log.error(
`Docker command failed ${result.stderr}`,
ErrorCode.DOCKER_ERROR
);
}
}
return result.exitCode === 0;
},
cleanupImage: async (imageName: string): Promise<void> => {
log && log.debug(`Remove Docker image ${imageName}`);
await runCommand(`docker image rm -f ${imageName}`, `Remove Docker image ${imageName}`, log);
},
buildImage: async (dockerFile: string, imageName: string): Promise<boolean> => {
if (!fs.existsSync(dockerFile)) {
log.error(`Cannot find Dockerfile ${dockerFile}`);
return false;
}
log.debug(`Building Docker image ${imageName} from ${dockerFile}`);
const result = await runCommand(`docker build -t ${imageName} -f ${dockerFile} .`, `Build Docker image ${imageName} from ${dockerFile}`, log);
if (result.exitCode === 0 && await Docker.imageExists(imageName)) {
log.debug(`Docker image ${imageName} created.`);
} else {
log.debug(`Failed to build image ${imageName}`);
return false;
}
// docker returns build steps on stderr
log.debug(result.stdout || result.stderr);
return result.exitCode === 0;
}
}
export function jreOrDockerAvailable(): boolean {
return JRE.isAvailable() || Docker.isAvailable();
}

Просмотреть файл

@ -15,6 +15,9 @@ export class LogItem {
export enum LogLevels {
// Always output Note
NOTE = -1,
// Print out everything
DEBUG = 0,
// Print out informational messages
@ -24,9 +27,7 @@ export enum LogLevels {
// Only print out errors
ERROR,
// Only print out fatal errors, where processing can't continue
FATAL,
// Always output Note
NOTE
FATAL
}
@ -103,6 +104,10 @@ export default class Log {
});
}
public get hasErrors(): boolean {
return !!(this.get(LogLevels.FATAL).length + this.get(LogLevels.ERROR).length);
}
// collects errors from all children into a single collection; specify level to filter >= level
flatten(level: LogLevels = LogLevels.DEBUG): { title: string, message: string, code: ErrorCode, level: LogLevels }[] {

Просмотреть файл

@ -14,7 +14,7 @@ import { ErrorCode, ExcludableErrors, getExcludeErrorCodes } from './error';
import * as utils from './utils'
import npmpackage from '../package.json';
import { KeySet } from './keys';
import { FhirOptions, ValidationProfiles } from './fhirBundle';
import { FhirOptions, ValidationProfiles, Validators } from './fhirBundle';
import * as versions from './check-for-update';
import semver from 'semver';
import { JwsValidationOptions } from './jws-compact';
@ -42,6 +42,7 @@ program.option('-k, --jwkset <key>', 'path to trusted issuer key set');
program.option('-e, --exclude <error>', 'error to exclude, can be repeated, can use a * wildcard. Valid options:' +
ExcludableErrors.map(e => ` "${e.error}"`).join(),
(e: string, errors: string[]) => errors.concat([e]), []);
program.addOption(new Option('-V, --validator <validator>', 'the choice of FHIR validator to use').choices(Object.keys(Validators).filter(x => Number.isNaN(Number(x)))).default('default'));
program.parse(process.argv);
export interface CliOptions {
@ -54,7 +55,8 @@ export interface CliOptions {
logout: string;
fhirout: string;
exclude: string[];
clearKeyStore? : boolean;
clearKeyStore?: boolean;
validator: string;
}
@ -116,6 +118,14 @@ async function processOptions(options: CliOptions) {
ValidationProfiles[options.profile as keyof typeof ValidationProfiles] :
FhirOptions.ValidationProfile = ValidationProfiles['any'];
// set the FHIR validator
FhirOptions.Validator =
options.validator ?
Validators[options.validator as keyof typeof Validators] :
FhirOptions.Validator = Validators['default'];
// requires both --path and --type properties
if (options.path.length === 0 || !options.type) {

Просмотреть файл

@ -7,13 +7,13 @@ import { ErrorCode } from './error';
import { validateSchema } from './schema';
import keySetSchema from '../schema/keyset-schema.json';
import keys, { KeySet } from './keys';
import execa from 'execa';
import fs from 'fs';
import path from 'path';
import { v4 as uuidv4 } from 'uuid';
import { isOpensslAvailable, parseJson } from './utils'
import { isOpensslAvailable } from './utils'
import { Certificate } from '@fidm/x509'
import { downloadAndValidateCRL } from './crl-validator';
import { runCommandSync } from './command';
// directory where to write cert files for openssl validation
const tmpDir = 'tmp';
@ -98,7 +98,7 @@ function validateX5c(x5c: string[], log: Log): CertFields | undefined {
//
const opensslVerifyCommand = "openssl verify " + rootCaArg + caArg + issuerCert;
log.debug('Calling openssl for x5c validation: ' + opensslVerifyCommand);
const result = execa.commandSync(opensslVerifyCommand);
const result = runCommandSync(opensslVerifyCommand, log);
if (result.exitCode != 0) {
log.debug(result.stderr);
throw 'OpenSSL returned an error: exit code ' + result.exitCode.toString();
@ -219,7 +219,7 @@ export async function verifyAndImportHealthCardIssuerKey(keySet: KeySet, log = n
}
// check for revocation file, we do this before parsing the key because the code below recasts the key to a
// JWK object overwritting the crlVersion field
// JWK object overwriting the crlVersion field
const keyWithCrl: JWK.Key & { crlVersion?: number } = key;
if (keyWithCrl?.crlVersion !== undefined) {
const crlVersion = keyWithCrl.crlVersion;

Просмотреть файл

@ -81,4 +81,11 @@ interface SchemaProperty {
additionalProperties?: boolean,
enum?: string[],
const?: string
}
}
interface CommandResult {
command: string;
exitCode: number;
stdout: string;
stderr: string;
}

Просмотреть файл

@ -5,7 +5,7 @@ import fs from 'fs';
import path from 'path';
import pako from 'pako';
import jose from 'node-jose';
import execa from 'execa';
import { runCommandSync } from './command';
export function parseJson<T>(json: string): T | undefined {
try {
@ -59,7 +59,7 @@ export function inflatePayload(verificationResult: jose.JWS.VerificationResult):
export function isOpensslAvailable(): boolean {
try {
const result = execa.commandSync("openssl version");
const result = runCommandSync("openssl version");
return (result.exitCode == 0);
} catch (err) {
return false;

Просмотреть файл

@ -14,7 +14,7 @@ import * as qr from './qr';
import * as image from './image';
import keys, { KeySet } from './keys';
import * as utils from './utils';
import { FhirOptions, ValidationProfiles } from './fhirBundle';
import { FhirOptions, ValidationProfiles, Validators } from './fhirBundle';
import { CliOptions } from './shc-validator';
import { clearTrustedIssuerDirectory, setTrustedIssuerDirectory } from './issuerDirectory';
@ -44,6 +44,13 @@ async function processOptions(options: CliOptions) {
clearTrustedIssuerDirectory();
}
if (options.validator) {
if(!Object.values(Validators).includes(options.validator)) {
throw new Error(`Invalid validator value ${options.validator}`);
}
FhirOptions.Validator = Validators[options.validator as keyof typeof Validators];
}
}

Просмотреть файл

@ -8,7 +8,7 @@ import { ErrorCode as ec } from '../src/error';
import Log, { LogLevels } from '../src/logger';
import { isOpensslAvailable } from '../src/utils';
import { CliOptions } from '../src/shc-validator';
import { jreOrDockerAvailable } from '../src/fhirValidator';
const testdataDir = './testdata/';
@ -269,7 +269,7 @@ test("Cards: nbf in milliseconds",
);
// one error for exp < nbf, one warning for card being expired
test("Cards: exp date before nbf", testCard('test-example-00-b-jws-payload-expanded-pre-expired.json', 'jwspayload', [[ec.EXPIRATION_ERROR],[ec.EXPIRATION_ERROR]]));
test("Cards: exp date before nbf", testCard('test-example-00-b-jws-payload-expanded-pre-expired.json', 'jwspayload', [[ec.EXPIRATION_ERROR], [ec.EXPIRATION_ERROR]]));
// the JWK's x5c value has the correct URL, so we get an extra x5c error due to URL mismatch
test("Cards: invalid issuer url (http)",
@ -387,4 +387,21 @@ test("Cards: unknown VC types", testCard('test-example-00-b-jws-payload-expanded
test("Cards: mismatch kid/issuer", testCard(['test-example-00-d-jws-issuer-kid-mismatch.txt'], "jws", [[ec.ISSUER_KID_MISMATCH]], { jwkset: 'testdata/issuer.jwks.public.not.smart.json' }));
test("Cards: immunization status not 'completed'", testCard('test-example-00-a-fhirBundle-status-not-completed.json', 'fhirbundle', [[ec.FHIR_SCHEMA_ERROR, ec.FHIR_SCHEMA_ERROR]]));
test("Cards: immunization status not 'completed'", testCard('test-example-00-a-fhirBundle-status-not-completed.json', 'fhirbundle', [[ec.FHIR_SCHEMA_ERROR, ec.FHIR_SCHEMA_ERROR]]));
// Tests using the HL7 FHIR Validator
// Since these tests require a Java runtime (JRE) or Docker to be installed, they are conditionally executed.
// These tests can also take a longer as they have to spin up a Docker image
describe('FHIR validator tests', () => {
const testif = (condition: boolean) => condition ? it : it.skip;
const canRunFhirValidator = jreOrDockerAvailable();
// shc-validator -p ./testdata/test-example-00-a-fhirBundle-profile-usa.json -t fhirbundle -l debug -V fhirvalidator
testif(canRunFhirValidator)("Cards: fhir validator test", testCard(['test-example-00-a-fhirBundle-profile-usa.json'], 'fhirbundle',
[8, 1], { validator: 'fhirvalidator' }), 1000 * 60 * 5 /*5 minutes*/);
});

Просмотреть файл

@ -1,8 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import execa from 'execa';
import fs from 'fs';
import { runCommandSync } from '../src/command';
import { ErrorCode as ec } from '../src/error';
import { LogItem } from '../src/logger';
import { CliOptions } from '../src/shc-validator';
@ -16,17 +16,6 @@ interface LogEntry {
log: LogItem[]
}
function runCommand(command: string) {
try {
return execa.commandSync(command);
} catch (error) {
// if exitCode !== 0 this error will be thrown
// this error object is similar to the result object that would be returned if successful.
// we'll return it an sort out the errors there.
return error as execa.ExecaSyncError;
}
}
// Puts the standard output into an array of line,
@ -81,7 +70,7 @@ function testLogFile(logPath: string, deleteLog = true): LogEntry[] {
}
function testCliCommand(command: string): number {
const commandResult = runCommand(command);
const commandResult = runCommandSync(command);
const out = parseStdout(commandResult.stdout);
console.log(out.join('\n'));
return commandResult.exitCode;
@ -133,7 +122,7 @@ test("Logs: valid 00-e health card single log file", () => {
const expectedEntries = 1;
const expectedLogItems = 8 + (OPENSSL_AVAILABLE ? 0 : 1);
runCommand('node . --path testdata/example-00-e-file.smart-health-card --type healthcard --loglevel info --logout ' + logFile);
runCommandSync(`node . --path testdata/example-00-e-file.smart-health-card --type healthcard --loglevel info --logout ${logFile}`);
const logs: LogEntry[] = testLogFile(logFile);
@ -148,8 +137,8 @@ test("Logs: valid 00-e health card append log file", () => {
const expectedEntries = 2;
const expectedLogItems = [8 + (OPENSSL_AVAILABLE ? 0 : 1), 8 + (OPENSSL_AVAILABLE ? 0 : 1)];
runCommand('node . --path testdata/example-00-e-file.smart-health-card --type healthcard --loglevel info --logout ' + logFile);
runCommand('node . --path testdata/example-00-e-file.smart-health-card --type healthcard --loglevel info --logout ' + logFile);
runCommandSync(`node . --path testdata/example-00-e-file.smart-health-card --type healthcard --loglevel info --logout ${logFile}`);
runCommandSync(`node . --path testdata/example-00-e-file.smart-health-card --type healthcard --loglevel info --logout ${logFile}`);
const logs: LogEntry[] = testLogFile(logFile);
@ -161,20 +150,20 @@ test("Logs: valid 00-e health card append log file", () => {
test("Logs: valid 00-e health card bad log path", () => {
const logFile = '../foo/log.txt';
const commandResult = runCommand('node . --path testdata/example-00-e-file.smart-health-card --type healthcard --loglevel info --logout ' + logFile);
const commandResult = runCommandSync(`node . --path testdata/example-00-e-file.smart-health-card --type healthcard --loglevel info --logout ${logFile}`);
expect(commandResult.exitCode).toBe(ec.LOG_PATH_NOT_FOUND);
});
test("Logs: valid 00-e health card fhir bundle log file", () => {
const logFile = 'fhirout.json.log'; // .log to be gitignored
const commandResult = runCommand('node . --path testdata/example-00-e-file.smart-health-card --type healthcard --loglevel info --fhirout ' + logFile);
runCommandSync(`node . --path testdata/example-00-e-file.smart-health-card --type healthcard --loglevel info --logout ${logFile}`);
// try parsing FHIR output log as a fhir bundle
expect(testCliCommand(`node . --path ${logFile} --type fhirbundle`)).toBe(0);
});
test("Logs: valid 00-e health card bad log path", () => {
const logFile = '../foo/log.txt';
const commandResult = runCommand('node . --path testdata/example-00-e-file.smart-health-card --type healthcard --loglevel info --fhirout ' + logFile);
const commandResult = runCommandSync(`node . --path testdata/example-00-e-file.smart-health-card --type healthcard --loglevel info --logout ${logFile}`);
expect(commandResult.exitCode).toBe(ec.LOG_PATH_NOT_FOUND);
});