|
| 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 |
0 commit comments