Skip to content

Commit 8b24dca

Browse files
committed
Add Integration Test for Validator Modes
Signed-off-by: Jonas Wagner <jwagner@knoppiks.de>
1 parent 127ea87 commit 8b24dca

8 files changed

Lines changed: 364 additions & 1 deletion

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
docker-compose.override.yml
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
4+
# Checks the `reject` failure mode of the external validator.
5+
#
6+
# In this mode a resource that fails external validation is not persisted.
7+
# Instead the interaction is rejected with `400 Bad Request` and an
8+
# OperationOutcome carrying an issue with code `invalid`.
9+
#
10+
# This is asserted for the create interaction and for a transaction bundle. A
11+
# valid resource is used to show that conforming resources still pass.
12+
13+
script_dir="$(dirname "$(readlink -f "$0")")"
14+
. "$script_dir/../scripts/util.sh"
15+
16+
base="http://localhost:8080/fhir"
17+
18+
# --- a valid resource is accepted -------------------------------------------
19+
20+
status=$(curl -s -o /dev/null -w '%{http_code}' \
21+
-H 'Accept: application/fhir+json' -H 'Content-Type: application/fhir+json' \
22+
-d @"$script_dir/valid-patient.json" "$base/Patient")
23+
24+
test "create status of the valid Patient" "$status" "201"
25+
26+
# --- an invalid resource is rejected on create ------------------------------
27+
28+
response=$(curl -s -w '\n%{http_code}' \
29+
-H 'Accept: application/fhir+json' -H 'Content-Type: application/fhir+json' \
30+
-d @"$script_dir/invalid-patient.json" "$base/Patient")
31+
32+
status=$(echo "$response" | tail -n1)
33+
body=$(echo "$response" | sed '$d')
34+
35+
test "create status of the invalid Patient" "$status" "400"
36+
test "resourceType of the create response" "$(echo "$body" | jq -r '.resourceType')" "OperationOutcome"
37+
test "issue code of the create response" "$(echo "$body" | jq -r '.issue[0].code')" "invalid"
38+
39+
# --- an invalid resource is rejected inside a transaction -------------------
40+
41+
bundle=$(jq -n --slurpfile p "$script_dir/invalid-patient.json" '
42+
{
43+
resourceType: "Bundle",
44+
type: "transaction",
45+
entry: [
46+
{
47+
resource: $p[0],
48+
request: { method: "POST", url: "Patient" }
49+
}
50+
]
51+
}')
52+
53+
response=$(echo "$bundle" | curl -s -w '\n%{http_code}' \
54+
-H 'Accept: application/fhir+json' -H 'Content-Type: application/fhir+json' \
55+
-d @- "$base")
56+
57+
status=$(echo "$response" | tail -n1)
58+
body=$(echo "$response" | sed '$d')
59+
60+
test "transaction status of the invalid Patient" "$status" "400"
61+
test "resourceType of the transaction response" "$(echo "$body" | jq -r '.resourceType')" "OperationOutcome"
62+
test "issue code of the transaction response" "$(echo "$body" | jq -r '.issue[0].code')" "invalid"
63+
64+
# --- no invalid resource was persisted --------------------------------------
65+
66+
tag_system="https://blaze-server.org/fhir/CodeSystem/ValidationStatus"
67+
tagged=$(curl -s -H 'Accept: application/fhir+json' \
68+
"$base/Patient?_tag=${tag_system}|invalid&_summary=count" | jq -r '.total')
69+
70+
test "number of persisted invalid Patients" "$tagged" "0"
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
4+
# Creates a small subset of the Synthea test data in the `synthea-subset`
5+
# directory. Validating every resource of the full Synthea data set against the
6+
# external validator would be too slow, so only a few bundles are loaded: the
7+
# hospital and practitioner information bundles the patient bundle references,
8+
# plus one small patient bundle.
9+
10+
script_dir="$(dirname "$(readlink -f "$0")")"
11+
synthea_dir="$script_dir/../test-data/synthea"
12+
target_dir="synthea-subset"
13+
14+
files=(
15+
"0-hospitalInformation1625911868739.json.bz2"
16+
"0-practitionerInformation1625911868739.json.bz2"
17+
"8a4c9c04-1524-9f1c-d65b-9b17e4520fef.json.bz2"
18+
)
19+
20+
mkdir -p "$target_dir"
21+
for file in "${files[@]}"; do
22+
cp "$synthea_dir/$file" "$target_dir/"
23+
done
24+
25+
echo "ℹ️ created Synthea subset with ${#files[@]} bundles in $target_dir"
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
services:
2+
data-server:
3+
image: "blaze:latest"
4+
environment:
5+
JAVA_TOOL_OPTIONS: "-Xmx2g"
6+
EXTERN_VALIDATOR_URL: "http://fhir-validator:8080"
7+
VALIDATOR_FAILURE_MODE: "${VALIDATOR_FAILURE_MODE:-tag-outcome}"
8+
LOG_LEVEL: debug
9+
ports:
10+
- "8080:8080"
11+
volumes:
12+
- "blaze-data:/app/data"
13+
healthcheck:
14+
test: [ "CMD", "wget", "--spider", "http://localhost:8080/health" ]
15+
interval: 10s
16+
timeout: 5s
17+
retries: 5
18+
start_period: 30s
19+
depends_on:
20+
fhir-validator:
21+
condition: service_started
22+
23+
terminology-server:
24+
image: "blaze:latest"
25+
environment:
26+
JAVA_TOOL_OPTIONS: "-Xmx4g"
27+
ENABLE_TERMINOLOGY_SERVICE: "true"
28+
ENABLE_TERMINOLOGY_LOINC: "true"
29+
LOG_LEVEL: debug
30+
ports:
31+
- "8082:8080"
32+
volumes:
33+
- "blaze-terminology-data:/app/data"
34+
healthcheck:
35+
test: [ "CMD", "wget", "--spider", "http://localhost:8080/health" ]
36+
interval: 10s
37+
timeout: 5s
38+
retries: 5
39+
start_period: 30s
40+
41+
fhir-validator:
42+
image: "ghcr.io/medizininformatik-initiative/mii-fhir-validator:0.0.1-alpha.7@sha256:c237b8b36a641cc57a59be2d2a29035ca4f39e0b6c71b26ab4e88e7f7c4fd6a1"
43+
environment:
44+
JAVA_OPTS: "-Xmx2g"
45+
TX_SERVER: "http://terminology-server:8080/fhir"
46+
# Override the MII IG defaults of the image with US Core, the profiles the
47+
# Synthea test data is generated against.
48+
IG_PARAMS: "-ig hl7.fhir.us.core#9.0.0"
49+
ports:
50+
- "8084:8080"
51+
depends_on:
52+
terminology-server:
53+
condition: service_healthy
54+
55+
volumes:
56+
blaze-data:
57+
blaze-terminology-data:
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"resourceType": "Patient",
3+
"meta": {
4+
"profile": [
5+
"http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"
6+
]
7+
},
8+
"gender": "unknown"
9+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"resourceType": "Patient",
3+
"meta": {
4+
"profile": [
5+
"http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"
6+
]
7+
},
8+
"identifier": [
9+
{
10+
"system": "http://hl7.org/fhir/sid/us-ssn",
11+
"value": "999-99-9999"
12+
}
13+
],
14+
"name": [
15+
{
16+
"family": "Tester",
17+
"given": [
18+
"Valid"
19+
]
20+
}
21+
],
22+
"gender": "female"
23+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
4+
# Checks the resources that the external validator flagged as invalid while the
5+
# data was loaded into the data server.
6+
#
7+
# Every invalid resource carries a meta tag with system
8+
# `https://blaze-server.org/fhir/CodeSystem/ValidationStatus` and code
9+
# `invalid`.
10+
#
11+
# With failure mode `tag-outcome` (the default) each invalid resource
12+
# additionally carries:
13+
# * a meta extension referencing the contained OperationOutcome, and
14+
# * a contained OperationOutcome (id `validation-outcome`) with at least one
15+
# issue of severity `error` or `fatal`.
16+
#
17+
# With failure mode `tag-only` the resource carries neither the meta extension
18+
# nor a contained OperationOutcome.
19+
#
20+
# Usage: check-invalid-resources.sh <count> [tag-outcome|tag-only] [eq|ge]
21+
#
22+
# The comparison mode controls how <count> is checked against the actual number
23+
# of invalid resources:
24+
# * `eq` (default) — the number must equal <count>.
25+
# * `ge` — the number must be greater than or equal to <count>. Use
26+
# this when the exact number is not deterministic (e.g. it
27+
# depends on the validator's terminology configuration).
28+
29+
script_dir="$(dirname "$(readlink -f "$0")")"
30+
. "$script_dir/util.sh"
31+
32+
base="http://localhost:8080/fhir"
33+
tag_system="https://blaze-server.org/fhir/CodeSystem/ValidationStatus"
34+
outcome_ext="https://blaze-server.org/fhir/StructureDefinition/validation-outcome"
35+
36+
expected_count="$1"
37+
mode="${2:-tag-outcome}"
38+
compare="${3:-eq}"
39+
40+
# Gather all invalid resources across all resource types via the system-wide
41+
# `_tag` search into a JSON stream (one resource per line). blazectl handles
42+
# paging.
43+
invalid=$(blazectl --server "$base" download -q "_tag=${tag_system}|invalid" 2>/dev/null)
44+
45+
# --- assert the number of invalid resources ---------------------------------
46+
47+
count=$(echo "$invalid" | jq -s 'length')
48+
49+
if [ "$compare" = "ge" ]; then
50+
if [ "$count" -ge "$expected_count" ]; then
51+
echo "✅ the number of invalid resources of $count is >= $expected_count"
52+
else
53+
echo "🆘 the number of invalid resources is $count, expected >= $expected_count"
54+
exit 1
55+
fi
56+
else
57+
test "number of invalid resources" "$count" "$expected_count"
58+
fi
59+
60+
echo "ℹ️ invalid resources by type:"
61+
echo "$invalid" | jq -rs 'group_by(.resourceType)[] | " \(length) \(.[0].resourceType)"'
62+
63+
# --- assert every invalid resource has the expected shape -------------------
64+
65+
if [ "$mode" = "tag-only" ]; then
66+
# Every invalid resource must carry the invalid tag but neither the meta
67+
# extension nor a contained validation OperationOutcome.
68+
malformed=$(echo "$invalid" | jq -rs --arg sys "$tag_system" --arg ext "$outcome_ext" '
69+
[.[] | select(
70+
([.meta.tag[]? | select(.system == $sys and .code == "invalid")] | length == 0)
71+
or ([.meta.extension[]? | select(.url == $ext)] | length > 0)
72+
or ([.contained[]? | select(.resourceType == "OperationOutcome" and .id == "validation-outcome")] | length > 0))
73+
| "\(.resourceType)/\(.id)"] | join(", ")')
74+
75+
test_empty "set of invalid resources not matching the tag-only shape" "$malformed"
76+
else
77+
# Every invalid resource must carry the invalid tag, the meta extension and a
78+
# contained validation OperationOutcome with an error or fatal issue.
79+
malformed=$(echo "$invalid" | jq -rs --arg sys "$tag_system" --arg ext "$outcome_ext" '
80+
[.[] | select(
81+
([.meta.tag[]? | select(.system == $sys and .code == "invalid")] | length == 0)
82+
or ([.meta.extension[]? | select(.url == $ext)] | length == 0)
83+
or ([.contained[]? | select(.resourceType == "OperationOutcome" and .id == "validation-outcome")] | length == 0)
84+
or ([.contained[]? | select(.resourceType == "OperationOutcome")
85+
| .issue[] | select(.severity == "error" or .severity == "fatal")] | length == 0))
86+
| "\(.resourceType)/\(.id)"] | join(", ")')
87+
88+
test_empty "set of invalid resources without a proper validation OperationOutcome" "$malformed"
89+
90+
# --- show the OperationOutcome of some invalid resources ------------------
91+
92+
# The output is truncated inside jq because a downstream `head` would close
93+
# the pipe early and kill jq with SIGPIPE, failing the script under
94+
# `pipefail`.
95+
echo "ℹ️ validation issues of the invalid resources:"
96+
echo "$invalid" | jq -rs '
97+
[.[] | " \(.resourceType)/\(.id):",
98+
(.contained[]? | select(.resourceType == "OperationOutcome")
99+
| .issue[] | select(.severity == "error" or .severity == "fatal")
100+
| " [\(.severity)] \(.details.text // .diagnostics // (.code | tostring))")]
101+
| .[:60][]'
102+
fi

.github/workflows/build.yml

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2061,7 +2061,7 @@ jobs:
20612061
run: .github/scripts/check-total-number-of-resources.sh 345
20622062

20632063
- name: Check Number of Invalid Resources
2064-
run: .github/integration-test-kds/check-invalid-resources.sh 67
2064+
run: .github/scripts/check-invalid-resources.sh 67
20652065

20662066
- name: Download MedicationStatement Resources with ATC Code B01AC06
20672067
run: .github/scripts/download-resources-query.sh MedicationStatement "medication.code=http://fhir.de/CodeSystem/bfarm/atc|B01AC06" 1
@@ -2075,6 +2075,81 @@ jobs:
20752075
- name: Run Observation Quality Report
20762076
run: .github/integration-test-kds/evaluate-measure.sh quality-report-observation 53
20772077

2078+
integration-test-external-validator:
2079+
needs: build
2080+
runs-on: ubuntu-24.04
2081+
2082+
strategy:
2083+
matrix:
2084+
# Each failure mode needs its own Blaze process because
2085+
# VALIDATOR_FAILURE_MODE is read once at startup.
2086+
failure-mode:
2087+
- tag-only
2088+
- tag-outcome
2089+
- reject
2090+
fail-fast: false
2091+
2092+
steps:
2093+
- name: Check out Git repository
2094+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
2095+
2096+
- name: Install Blazectl
2097+
env:
2098+
GH_TOKEN: ${{ github.token }}
2099+
run: .github/scripts/install-blazectl.sh
2100+
2101+
- name: Install xq
2102+
run: .github/scripts/install-xq.sh
2103+
2104+
- name: Login to GHCR
2105+
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
2106+
with:
2107+
registry: ghcr.io
2108+
username: ${{ github.repository_owner }}
2109+
password: ${{ secrets.GITHUB_TOKEN }}
2110+
2111+
- name: Pull Blaze Image
2112+
run: docker pull ghcr.io/${{ github.repository_owner }}/blaze:${{ github.sha }} && docker tag ghcr.io/${{ github.repository_owner }}/blaze:${{ github.sha }} blaze:latest
2113+
2114+
- name: Run Blaze
2115+
env:
2116+
VALIDATOR_FAILURE_MODE: ${{ matrix.failure-mode }}
2117+
run: docker compose -f .github/integration-test-validator/docker-compose.yml up --wait
2118+
2119+
- name: Docker Logs
2120+
run: docker compose -f .github/integration-test-validator/docker-compose.yml logs -t
2121+
2122+
- name: Check Capability Statement
2123+
run: .github/scripts/check-capability-statement-no-auth.sh
2124+
2125+
- name: Check $versions Operation
2126+
run: .github/scripts/check-versions.sh
2127+
2128+
# --- reject mode -------------------------------------------------------
2129+
2130+
- name: Check Reject Mode
2131+
if: ${{ matrix.failure-mode == 'reject' }}
2132+
run: .github/integration-test-validator/check-reject-mode.sh
2133+
2134+
# --- tag modes ---------------------------------------------------------
2135+
2136+
# Only a small subset of the Synthea data is loaded. Validating every
2137+
# resource against the external validator is slow, and the exact number of
2138+
# invalid resources depends on the validator's terminology configuration, so
2139+
# the assertions below only check the tagging shape and that at least one
2140+
# resource was flagged invalid.
2141+
- name: Create Synthea Subset
2142+
if: ${{ matrix.failure-mode != 'reject' }}
2143+
run: .github/integration-test-validator/create-synthea-subset.sh
2144+
2145+
- name: Load Data
2146+
if: ${{ matrix.failure-mode != 'reject' }}
2147+
run: blazectl --no-progress --server http://localhost:8080/fhir upload synthea-subset
2148+
2149+
- name: Check Invalid Resources are Tagged Correctly
2150+
if: ${{ matrix.failure-mode != 'reject' }}
2151+
run: .github/scripts/check-invalid-resources.sh 1 ${{ matrix.failure-mode }} ge
2152+
20782153
integration-test-extern-terminology:
20792154
needs: build
20802155
runs-on: ubuntu-24.04
@@ -3379,6 +3454,7 @@ jobs:
33793454
- integration-test
33803455
- integration-test-synthea-1000
33813456
- integration-test-kds
3457+
- integration-test-external-validator
33823458
- integration-test-extern-terminology
33833459
- integration-test-patient-purge
33843460
- integration-test-value-set-expand

0 commit comments

Comments
 (0)