use axum::http::StatusCode;
use serde_json::json;
mod common;
#[tokio::test]
async fn test_health_endpoint() {
let server = common::test_server().await;
let response = server.get("/health").await;
assert_eq!(response.status_code(), StatusCode::OK);
let json: serde_json::Value = response.json();
assert_eq!(json["status"], "ok");
assert_eq!(json["service"], "sof-server");
assert_eq!(json["version"], env!("CARGO_PKG_VERSION"));
}
#[tokio::test]
async fn test_capability_statement() {
let server = common::test_server().await;
let response = server.get("/metadata").await;
assert_eq!(response.status_code(), StatusCode::OK);
let content_type = response.header("content-type");
assert_eq!(content_type.to_str().unwrap(), "application/fhir+json");
let json: serde_json::Value = response.json();
assert_eq!(json["resourceType"], "CapabilityStatement");
assert_eq!(json["kind"], "instance");
assert_eq!(json["fhirVersion"], helios_sof::get_fhir_version_string());
let operations = json["rest"][0]["operation"].as_array().unwrap();
let sql_run = operations
.iter()
.find(|op| op["name"] == "sql-run")
.expect("$sql-run must be declared in rest.operation");
assert_eq!(
sql_run["definition"], "/OperationDefinition/sof-sql-run",
"a server supporting a subset cites its own definition, not the guide's"
);
assert!(
json["rest"][0].get("resource").is_none(),
"the data operations no longer hang off a resource type"
);
}
#[tokio::test]
async fn test_run_view_definition_basic() {
let server = common::test_server().await;
let request_body = json!({
"resourceType": "Parameters",
"parameter": [
{
"name": "subjectResource",
"resource": {
"resourceType": "ViewDefinition",
"status": "active",
"resource": "Patient",
"select": [{
"column": [{
"name": "id",
"path": "id"
}, {
"name": "gender",
"path": "gender"
}]
}]
}
},
{
"name": "resource",
"resource": {
"resourceType": "Patient",
"id": "example",
"gender": "male"
}
}
]
});
let response = server
.post("/$sql-run")
.add_header("Accept", "application/json")
.json(&request_body)
.await;
assert_eq!(response.status_code(), StatusCode::OK);
let content_type = response.header("content-type");
assert_eq!(content_type.to_str().unwrap(), "application/json");
let json: serde_json::Value = response.json();
assert!(json.is_array());
let rows = json.as_array().unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0]["id"], "example");
assert_eq!(rows[0]["gender"], "male");
}
#[tokio::test]
async fn test_run_view_definition_csv_output() {
let server = common::test_server().await;
let request_body = json!({
"resourceType": "Parameters",
"parameter": [
{
"name": "subjectResource",
"resource": {
"resourceType": "ViewDefinition",
"status": "active",
"resource": "Patient",
"select": [{
"column": [{
"name": "id",
"path": "id"
}, {
"name": "name",
"path": "name.family"
}]
}]
}
},
{
"name": "resource",
"resource": {
"resourceType": "Patient",
"id": "123",
"name": [{
"family": "Doe",
"given": ["John"]
}]
}
}
]
});
let response = server
.post("/$sql-run")
.add_query_param("_format", "text/csv")
.add_query_param("header", "true")
.json(&request_body)
.await;
assert_eq!(response.status_code(), StatusCode::OK);
let content_type = response.header("content-type");
assert_eq!(content_type.to_str().unwrap(), "text/csv");
let csv_text = response.text();
let lines: Vec<&str> = csv_text.lines().collect();
assert_eq!(lines.len(), 2); assert_eq!(lines[0], "id,name");
assert!(lines[1].contains("123"));
assert!(lines[1].contains("Doe"));
}
#[tokio::test]
async fn test_run_view_definition_ndjson_output() {
let server = common::test_server().await;
let request_body = json!({
"resourceType": "Parameters",
"parameter": [
{
"name": "subjectResource",
"resource": {
"resourceType": "ViewDefinition",
"status": "active",
"resource": "Observation",
"select": [{
"column": [{
"name": "id",
"path": "id"
}, {
"name": "status",
"path": "status"
}]
}]
}
},
{
"name": "resource",
"resource": {
"resourceType": "Observation",
"id": "obs1",
"status": "final"
}
},
{
"name": "resource",
"resource": {
"resourceType": "Observation",
"id": "obs2",
"status": "preliminary"
}
}
]
});
let response = server
.post("/$sql-run")
.add_header("Accept", "application/ndjson")
.json(&request_body)
.await;
assert_eq!(response.status_code(), StatusCode::OK);
let content_type = response.header("content-type");
assert_eq!(content_type.to_str().unwrap(), "application/x-ndjson");
let ndjson_text = response.text();
let lines: Vec<&str> = ndjson_text.trim().lines().collect();
assert_eq!(lines.len(), 2);
let row1: serde_json::Value = serde_json::from_str(lines[0]).unwrap();
let row2: serde_json::Value = serde_json::from_str(lines[1]).unwrap();
assert_eq!(row1["id"], "obs1");
assert_eq!(row1["status"], "final");
assert_eq!(row2["id"], "obs2");
assert_eq!(row2["status"], "preliminary");
}
#[tokio::test]
async fn test_run_view_definition_error_invalid_parameters() {
let server = common::test_server().await;
let request_body = json!({
"resourceType": "Bundle", "type": "collection"
});
let response = server.post("/$sql-run").json(&request_body).await;
assert_eq!(response.status_code(), StatusCode::BAD_REQUEST);
let json: serde_json::Value = response.json();
assert_eq!(json["resourceType"], "OperationOutcome");
assert_eq!(json["issue"][0]["severity"], "error");
}
#[tokio::test]
async fn test_run_view_definition_error_no_view() {
let server = common::test_server().await;
let request_body = json!({
"resourceType": "Parameters",
"parameter": [] });
let response = server.post("/$sql-run").json(&request_body).await;
assert_eq!(response.status_code(), StatusCode::BAD_REQUEST);
let json: serde_json::Value = response.json();
assert_eq!(json["resourceType"], "OperationOutcome");
}
#[tokio::test]
async fn test_run_view_definition_bare_body() {
let server = common::test_server().await;
let bare_view = json!({
"resourceType": "ViewDefinition",
"status": "active",
"resource": "Patient",
"select": [{
"column": [
{"name": "id", "path": "id"},
{"name": "gender", "path": "gender"}
]
}]
});
let response = server
.post("/$sql-run")
.add_query_param("_format", "application/json")
.json(&bare_view)
.await;
assert_eq!(
response.status_code(),
StatusCode::OK,
"bare ViewDefinition body must be accepted: {:?}",
response.text()
);
}
#[tokio::test]
async fn test_run_view_definition_unsupported_format() {
let server = common::test_server().await;
let request_body = json!({
"resourceType": "Parameters",
"parameter": [{
"name": "subjectResource",
"resource": {
"resourceType": "ViewDefinition",
"status": "active",
"resource": "Patient",
"select": [{"column": [{"name": "id", "path": "id"}]}]
}
}]
});
let response = server
.post("/$sql-run")
.add_query_param("_format", "text/plain") .json(&request_body)
.await;
assert_eq!(response.status_code(), StatusCode::BAD_REQUEST);
let json: serde_json::Value = response.json();
assert_eq!(json["resourceType"], "OperationOutcome");
}
#[tokio::test]
async fn test_run_view_definition_post_with_source_parameter() {
let server = common::test_server().await;
let request_body = json!({
"resourceType": "Parameters",
"parameter": [{
"name": "source",
"valueString": "https://example.com/fhir-data"
}, {
"name": "subjectResource",
"resource": {
"resourceType": "ViewDefinition",
"status": "active",
"resource": "Patient",
"select": [{"column": [{"name": "id", "path": "id"}]}]
}
}]
});
let response = server.post("/$sql-run").json(&request_body).await;
assert!(
response.status_code() == StatusCode::NOT_IMPLEMENTED
|| response.status_code() == StatusCode::OK
|| response.status_code() == StatusCode::BAD_REQUEST
|| response.status_code() == StatusCode::UNPROCESSABLE_ENTITY
);
}
#[tokio::test]
async fn test_post_subject_reference_not_implemented() {
let server = common::test_server().await;
let request_body = json!({
"resourceType": "Parameters",
"parameter": [{
"name": "subjectReference",
"valueReference": {
"reference": "ViewDefinition/123"
}
}, {
"name": "resource",
"resource": {
"resourceType": "Patient",
"id": "example",
"gender": "male"
}
}]
});
let response = server.post("/$sql-run").json(&request_body).await;
assert_eq!(response.status_code(), StatusCode::NOT_IMPLEMENTED);
let json: serde_json::Value = response.json();
assert_eq!(json["resourceType"], "OperationOutcome");
assert!(
json["issue"][0]["details"]["text"]
.as_str()
.unwrap()
.contains("resolves neither subjectCanonical nor subjectReference")
);
}
#[tokio::test]
async fn test_post_group_unresolvable_returns_bad_request() {
let server = common::test_server().await;
let body = json!({
"resourceType": "Parameters",
"parameter": [
{
"name": "subjectResource",
"resource": {
"resourceType": "ViewDefinition",
"status": "active",
"resource": "Patient",
"select": [{"column": [{"name": "id", "path": "id"}]}]
}
},
{
"name": "group",
"valueReference": {
"reference": "Group/test-group"
}
}
]
});
let response = server
.post("/$sql-run")
.add_header("Content-Type", "application/json")
.json(&body)
.await;
assert_eq!(response.status_code(), StatusCode::BAD_REQUEST);
let json: serde_json::Value = response.json();
assert_eq!(json["resourceType"], "OperationOutcome");
assert_eq!(json["issue"][0]["code"], "not-found");
assert!(
json["issue"][0]["details"]["text"]
.as_str()
.unwrap()
.contains("Group/test-group")
);
}
#[tokio::test]
async fn test_post_source_not_implemented() {
let server = common::test_server().await;
let body = json!({
"resourceType": "Parameters",
"parameter": [
{
"name": "subjectResource",
"resource": {
"resourceType": "ViewDefinition",
"status": "active",
"resource": "Patient",
"select": [{"column": [{"name": "id", "path": "id"}]}]
}
},
{
"name": "source",
"valueString": "http://example.com/fhir"
}
]
});
let response = server
.post("/$sql-run")
.add_header("Content-Type", "application/json")
.json(&body)
.await;
assert!(
response.status_code() == StatusCode::NOT_IMPLEMENTED
|| response.status_code() == StatusCode::OK
|| response.status_code() == StatusCode::BAD_REQUEST
|| response.status_code() == StatusCode::UNPROCESSABLE_ENTITY
);
}
#[tokio::test]
async fn test_patient_filtering_incorrect_format() {
let server = common::test_server().await;
let body = json!({
"resourceType": "Parameters",
"parameter": [
{
"name": "patient",
"valueReference": "Patient/pt-1" },
{
"name": "subjectResource",
"resource": {
"resourceType": "ViewDefinition",
"resource": "Patient",
"select": [{
"column": [
{"name": "id", "path": "id"},
{"name": "family", "path": "name.family"}
]
}]
}
},
{
"name": "resource",
"resource": {
"resourceType": "Patient",
"id": "pt-1",
"name": [{"family": "Cole"}]
}
},
{
"name": "resource",
"resource": {
"resourceType": "Patient",
"id": "pt-2",
"name": [{"family": "Doe"}]
}
}
]
});
let response = server
.post("/$sql-run")
.add_header("Content-Type", "application/json")
.add_header("Accept", "application/json")
.json(&body)
.await;
assert_eq!(response.status_code(), StatusCode::OK);
let json: serde_json::Value = response.json();
assert!(json.is_array());
let results = json.as_array().unwrap();
assert_eq!(
results.len(),
2,
"Both patients returned when filter not parsed"
);
}
#[tokio::test]
async fn test_patient_filtering_correct_format() {
let server = common::test_server().await;
let body = json!({
"resourceType": "Parameters",
"parameter": [
{
"name": "patient",
"valueReference": {
"reference": "Patient/pt-1" }
},
{
"name": "subjectResource",
"resource": {
"resourceType": "ViewDefinition",
"resource": "Patient",
"select": [{
"column": [
{"name": "id", "path": "id"},
{"name": "family", "path": "name.family"}
]
}]
}
},
{
"name": "resource",
"resource": {
"resourceType": "Patient",
"id": "pt-1",
"name": [{"family": "Cole"}]
}
},
{
"name": "resource",
"resource": {
"resourceType": "Patient",
"id": "pt-2",
"name": [{"family": "Doe"}]
}
}
]
});
let response = server
.post("/$sql-run")
.add_header("Content-Type", "application/json")
.add_header("Accept", "application/json")
.json(&body)
.await;
assert_eq!(response.status_code(), StatusCode::OK);
let json: serde_json::Value = response.json();
assert!(json.is_array());
let results = json.as_array().unwrap();
assert_eq!(results.len(), 1, "Only pt-1 should be returned");
assert_eq!(results[0]["id"], "pt-1");
assert_eq!(results[0]["family"], "Cole");
}
#[tokio::test]
async fn test_since_parameter_in_post_body_valid() {
let server = common::test_server().await;
let body = json!({
"resourceType": "Parameters",
"parameter": [
{
"name": "_since",
"valueInstant": "2023-01-01T00:00:00Z"
},
{
"name": "subjectResource",
"resource": {
"resourceType": "ViewDefinition",
"status": "active",
"resource": "Patient",
"select": [{
"column": [
{"name": "id", "path": "id"}
]
}]
}
},
{
"name": "resource",
"resource": {
"resourceType": "Patient",
"id": "example"
}
}
]
});
let response = server
.post("/$sql-run")
.add_header("Content-Type", "application/json")
.add_header("Accept", "application/json")
.json(&body)
.await;
assert_eq!(response.status_code(), StatusCode::OK);
let json: serde_json::Value = response.json();
assert!(json.is_array());
}
#[tokio::test]
async fn test_since_parameter_in_post_body_invalid() {
let server = common::test_server().await;
let body = json!({
"resourceType": "Parameters",
"parameter": [
{
"name": "_since",
"valueInstant": "not-a-valid-timestamp"
},
{
"name": "subjectResource",
"resource": {
"resourceType": "ViewDefinition",
"status": "active",
"resource": "Patient",
"select": [{
"column": [
{"name": "id", "path": "id"}
]
}]
}
}
]
});
let response = server
.post("/$sql-run")
.add_header("Content-Type", "application/json")
.add_header("Accept", "application/json")
.json(&body)
.await;
assert_eq!(response.status_code(), StatusCode::OK);
let json: serde_json::Value = response.json();
assert_eq!(json, serde_json::json!([]));
}
#[tokio::test]
async fn test_since_parameter_filtering() {
let server = common::test_server().await;
let body = json!({
"resourceType": "Parameters",
"parameter": [
{
"name": "_since",
"valueInstant": "2023-06-01T00:00:00Z"
},
{
"name": "subjectResource",
"resource": {
"resourceType": "ViewDefinition",
"status": "active",
"resource": "Patient",
"select": [{
"column": [
{"name": "id", "path": "id"},
{"name": "lastUpdated", "path": "meta.lastUpdated"}
]
}]
}
},
{
"name": "resource",
"resource": {
"resourceType": "Patient",
"id": "old-patient",
"meta": {
"lastUpdated": "2023-01-01T00:00:00Z"
}
}
},
{
"name": "resource",
"resource": {
"resourceType": "Patient",
"id": "new-patient",
"meta": {
"lastUpdated": "2023-12-01T00:00:00Z"
}
}
}
]
});
let response = server
.post("/$sql-run")
.add_header("Content-Type", "application/json")
.add_header("Accept", "application/json")
.json(&body)
.await;
assert_eq!(response.status_code(), StatusCode::OK);
let json: serde_json::Value = response.json();
assert!(json.is_array());
let results = json.as_array().unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0]["id"], "new-patient");
assert_eq!(results[0]["lastUpdated"], "2023-12-01T00:00:00Z");
}
#[tokio::test]
async fn test_since_parameter_no_meta() {
let server = common::test_server().await;
let body = json!({
"resourceType": "Parameters",
"parameter": [
{
"name": "_since",
"valueInstant": "2023-06-01T00:00:00Z"
},
{
"name": "subjectResource",
"resource": {
"resourceType": "ViewDefinition",
"status": "active",
"resource": "Patient",
"select": [{
"column": [
{"name": "id", "path": "id"}
]
}]
}
},
{
"name": "resource",
"resource": {
"resourceType": "Patient",
"id": "patient-without-meta"
}
},
{
"name": "resource",
"resource": {
"resourceType": "Patient",
"id": "patient-with-meta",
"meta": {
"lastUpdated": "2023-12-01T00:00:00Z"
}
}
}
]
});
let response = server
.post("/$sql-run")
.add_header("Content-Type", "application/json")
.add_header("Accept", "application/json")
.json(&body)
.await;
assert_eq!(response.status_code(), StatusCode::OK);
let json: serde_json::Value = response.json();
assert!(json.is_array());
let results = json.as_array().unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0]["id"], "patient-with-meta");
}
#[tokio::test]
async fn test_since_parameter_wrong_value_type() {
let server = common::test_server().await;
let body = json!({
"resourceType": "Parameters",
"parameter": [
{
"name": "_since",
"valueString": "2023-01-01T00:00:00Z" },
{
"name": "subjectResource",
"resource": {
"resourceType": "ViewDefinition",
"status": "active",
"resource": "Patient",
"select": [{
"column": [
{"name": "id", "path": "id"}
]
}]
}
}
]
});
let response = server
.post("/$sql-run")
.add_header("Content-Type", "application/json")
.json(&body)
.await;
assert_eq!(response.status_code(), StatusCode::BAD_REQUEST);
let json: serde_json::Value = response.json();
assert_eq!(json["resourceType"], "OperationOutcome");
assert!(
json["issue"][0]["details"]["text"]
.as_str()
.unwrap()
.contains("_since parameter must use valueInstant or valueDateTime")
);
}
#[tokio::test]
async fn test_system_level_route_runs_view_definition() {
let server = common::test_server().await;
let body = json!({
"resourceType": "Parameters",
"parameter": [
{"name": "_format", "valueCode": "ndjson"},
{
"name": "subjectResource",
"resource": {
"resourceType": "ViewDefinition",
"status": "active",
"resource": "Patient",
"select": [{"column": [{"name": "id", "path": "id"}]}]
}
},
{
"name": "resource",
"resource": {"resourceType": "Patient", "id": "p1"}
}
]
});
let response = server
.post("/$sql-run")
.add_header("Content-Type", "application/json")
.json(&body)
.await;
assert_eq!(
response.status_code(),
StatusCode::OK,
"system-level POST /$sql-run must succeed; body: {}",
response.text()
);
let text = response.text();
assert!(
text.contains("\"id\":\"p1\""),
"response must contain the seeded Patient id: {text}"
);
}
#[tokio::test]
async fn test_pre_ballot_operation_urls_are_gone() {
let server = common::test_server().await;
for url in [
"/ViewDefinition/$viewdefinition-run",
"/ViewDefinition/some-id/$viewdefinition-run",
"/$viewdefinition-run",
"/Library/$sqlquery-run",
] {
let response = server
.post(url)
.add_header("Content-Type", "application/json")
.json(&json!({"resourceType": "Parameters"}))
.expect_failure()
.await;
assert_eq!(
response.status_code(),
StatusCode::NOT_FOUND,
"{url} was consolidated into $sql-run and must not be routed"
);
}
}
#[tokio::test]
async fn test_parquet_response_uses_native_parquet_content_type() {
let server = common::test_server().await;
let body = json!({
"resourceType": "Parameters",
"parameter": [
{"name": "_format", "valueCode": "application/octet-stream"},
{
"name": "subjectResource",
"resource": {
"resourceType": "ViewDefinition",
"status": "active",
"resource": "Patient",
"select": [{"column": [{"name": "id", "path": "id"}]}]
}
},
{
"name": "resource",
"resource": {"resourceType": "Patient", "id": "p1"}
}
]
});
let response = server
.post("/$sql-run")
.add_header("Content-Type", "application/json")
.json(&body)
.await;
assert_eq!(
response.status_code(),
StatusCode::OK,
"parquet request must succeed; body: {}",
response.text()
);
let ct = response
.header("content-type")
.to_str()
.unwrap_or("")
.to_string();
assert_eq!(
ct, "application/vnd.apache.parquet",
"parquet response must use its native media type per spec, got {ct}"
);
let cd = response
.headers()
.get("content-disposition")
.and_then(|v| v.to_str().ok())
.unwrap_or("")
.to_string();
assert!(
cd.contains("filename=") && cd.contains(".parquet"),
"parquet response must include Content-Disposition naming a .parquet file, got '{cd}'"
);
let bytes = response.as_bytes();
assert!(
bytes.starts_with(b"PAR1"),
"response body must be a Parquet file (PAR1 magic), got first 8 bytes: {:?}",
&bytes[..bytes.len().min(8)]
);
}
#[tokio::test]
async fn test_invalid_view_definition_returns_422() {
let server = common::test_server().await;
let body = json!({
"resourceType": "Parameters",
"parameter": [
{"name": "_format", "valueCode": "ndjson"},
{
"name": "subjectResource",
"resource": {
"resourceType": "ViewDefinition",
"status": "active",
"resource": "Patient",
"select": "not-an-array"
}
}
]
});
let response = server
.post("/$sql-run")
.add_header("Content-Type", "application/json")
.json(&body)
.await;
assert_eq!(
response.status_code(),
StatusCode::UNPROCESSABLE_ENTITY,
"invalid ViewDefinition must be 422 (audit #9), got {} with body: {}",
response.status_code(),
response.text()
);
let json: serde_json::Value = response.json();
assert_eq!(json["resourceType"], "OperationOutcome");
}
#[tokio::test]
async fn test_invalid_view_definition_returns_one_issue_per_lint_error() {
let server = common::test_server().await;
let bad_view = json!({
"resourceType": "ViewDefinition",
"status": "active",
"resource": "Patient",
"select": [{
"column": [{ "name": "id", "path": "getResourceKey(" }]
}],
"notAField": true
});
let expected: Vec<_> = helios_sof::lint::lint_view_definition(&bad_view)
.into_iter()
.filter(|d| d.severity == helios_sof::lint::Severity::Error)
.collect();
assert!(
expected.len() >= 2,
"fixture must exercise more than one lint error, got {expected:?}"
);
let response = server
.post("/$sql-run")
.add_header("Content-Type", "application/json")
.json(&bad_view)
.await;
assert_eq!(response.status_code(), StatusCode::UNPROCESSABLE_ENTITY);
let outcome: serde_json::Value = response.json();
assert_eq!(outcome["resourceType"], "OperationOutcome");
let issues = outcome["issue"].as_array().expect("issue must be an array");
assert_eq!(
issues.len(),
expected.len(),
"one issue per lint error, got {issues:?}"
);
for (issue, diagnostic) in issues.iter().zip(&expected) {
assert_eq!(issue["severity"], "error");
assert_eq!(issue["diagnostics"], diagnostic.message);
assert_eq!(
issue["expression"][0],
helios_sof::lint::pointer_to_fhirpath(&diagnostic.pointer)
);
}
}
#[tokio::test]
async fn test_unknown_key_in_select_returns_structure_issue() {
let server = common::test_server().await;
let bad_view = json!({
"resourceType": "ViewDefinition",
"status": "active",
"resource": "Patient",
"select": [{
"columns": [{ "name": "id", "path": "id" }]
}]
});
let response = server
.post("/$sql-run")
.add_header("Content-Type", "application/json")
.json(&bad_view)
.await;
assert_eq!(response.status_code(), StatusCode::UNPROCESSABLE_ENTITY);
let outcome: serde_json::Value = response.json();
let issues = outcome["issue"].as_array().expect("issue must be an array");
let unknown_key_issue = issues
.iter()
.find(|issue| issue["details"]["coding"][0]["code"] == "unknown-key")
.expect("must report the unknown `columns` key");
assert_eq!(unknown_key_issue["severity"], "error");
assert_eq!(unknown_key_issue["code"], "structure");
assert_eq!(
unknown_key_issue["expression"][0],
"ViewDefinition.select[0].columns"
);
assert_eq!(
unknown_key_issue["details"]["coding"][0]["system"],
"http://heliossoftware.com/fhir/CodeSystem/view-definition-lint"
);
}
#[tokio::test]
async fn test_invalid_view_definition_in_parameters_wrapper_returns_422() {
let server = common::test_server().await;
let body = json!({
"resourceType": "Parameters",
"parameter": [{
"name": "subjectResource",
"resource": {
"resourceType": "ViewDefinition",
"status": "active",
"select": [{
"column": [{ "name": "id", "path": "id" }]
}]
}
}]
});
let response = server
.post("/$sql-run")
.add_header("Content-Type", "application/json")
.json(&body)
.await;
assert_eq!(
response.status_code(),
StatusCode::UNPROCESSABLE_ENTITY,
"got {} with body: {}",
response.status_code(),
response.text()
);
let outcome: serde_json::Value = response.json();
assert_eq!(outcome["resourceType"], "OperationOutcome");
let issues = outcome["issue"].as_array().expect("issue must be an array");
assert!(
issues
.iter()
.any(|issue| issue["diagnostics"] == "missing required key `resource`"),
"must report the missing `resource` key: {issues:?}"
);
}
#[tokio::test]
async fn test_valid_view_definition_still_returns_200() {
let server = common::test_server().await;
let body = json!({
"resourceType": "Parameters",
"parameter": [
{
"name": "subjectResource",
"resource": {
"resourceType": "ViewDefinition",
"status": "active",
"resource": "Patient",
"select": [{
"column": [{ "name": "id", "path": "id" }]
}]
}
},
{
"name": "resource",
"resource": { "resourceType": "Patient", "id": "example" }
}
]
});
let response = server
.post("/$sql-run")
.add_header("Accept", "application/json")
.json(&body)
.await;
assert_eq!(
response.status_code(),
StatusCode::OK,
"got: {}",
response.text()
);
let rows: serde_json::Value = response.json();
assert_eq!(rows.as_array().unwrap().len(), 1);
assert_eq!(rows[0]["id"], "example");
}
#[tokio::test]
async fn test_pre_ballot_capabilities_endpoint_is_gone() {
let server = common::test_server().await;
let response = server
.get("/$sql-on-fhir-capabilities")
.expect_failure()
.await;
assert_eq!(response.status_code(), StatusCode::NOT_FOUND);
}
fn arrow_test_request_body() -> serde_json::Value {
json!({
"resourceType": "Parameters",
"parameter": [
{
"name": "subjectResource",
"resource": {
"resourceType": "ViewDefinition",
"status": "active",
"resource": "Patient",
"select": [{
"column": [
{ "name": "id", "path": "id" },
{ "name": "gender", "path": "gender" }
]
}]
}
},
{
"name": "resource",
"resource": {
"resourceType": "Patient",
"id": "example",
"gender": "male"
}
}
]
})
}
fn assert_arrow_ipc_response(bytes: &[u8]) {
use arrow::array::StringArray;
use arrow::ipc::reader::StreamReader;
let reader = StreamReader::try_new(std::io::Cursor::new(bytes), None)
.expect("Response body is not a valid Arrow IPC stream");
let batches: Vec<_> = reader
.collect::<Result<Vec<_>, _>>()
.expect("Failed to read Arrow IPC batches");
let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
assert_eq!(total_rows, 1);
let ids = batches[0]
.column(0)
.as_any()
.downcast_ref::<StringArray>()
.expect("id column should be Utf8");
assert_eq!(ids.value(0), "example");
}
#[tokio::test]
async fn test_run_view_definition_arrow_ipc_via_accept_header() {
let server = common::test_server().await;
let response = server
.post("/$sql-run")
.add_header("Accept", "application/vnd.apache.arrow.stream")
.json(&arrow_test_request_body())
.await;
assert_eq!(response.status_code(), StatusCode::OK);
let content_type = response.header("content-type");
assert_eq!(
content_type.to_str().unwrap(),
"application/vnd.apache.arrow.stream"
);
assert_arrow_ipc_response(response.as_bytes());
}
#[tokio::test]
async fn test_run_view_definition_arrow_ipc_via_format_param() {
let server = common::test_server().await;
let response = server
.post("/$sql-run?_format=arrow")
.json(&arrow_test_request_body())
.await;
assert_eq!(response.status_code(), StatusCode::OK);
let content_type = response.header("content-type");
assert_eq!(
content_type.to_str().unwrap(),
"application/vnd.apache.arrow.stream"
);
assert_arrow_ipc_response(response.as_bytes());
}