Skip to main content

haste_testscript_runner/
lib.rs

1use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
2use haste_fhir_client::{
3    FHIRClient,
4    http::{HeaderMap, HeaderName, HeaderValue, WithRequestHeaders},
5    request::{
6        DeleteRequest, DeleteResponse, FHIRBatchRequest, FHIRConditionalUpdateRequest,
7        FHIRCreateRequest, FHIRDeleteInstanceRequest, FHIRDeleteSystemRequest,
8        FHIRDeleteTypeRequest, FHIRHistoryInstanceRequest, FHIRHistorySystemRequest,
9        FHIRHistoryTypeRequest, FHIRInvokeInstanceRequest, FHIRInvokeSystemRequest,
10        FHIRInvokeTypeRequest, FHIRPatchRequest, FHIRReadRequest, FHIRRequest, FHIRResponse,
11        FHIRSearchTypeRequest, FHIRTransactionRequest, FHIRUpdateInstanceRequest,
12        FHIRVersionReadRequest, HistoryRequest, HistoryResponse, InvocationRequest, InvokeResponse,
13        Operation, SearchRequest, SearchResponse, UpdateRequest,
14    },
15    url::ParsedParameters,
16};
17use haste_fhir_model::r4::generated::{
18    resources::{
19        Resource, ResourceType, TestReport, TestReportSetup, TestReportSetupAction,
20        TestReportSetupActionAssert, TestReportSetupActionOperation, TestReportTeardown,
21        TestReportTeardownAction, TestReportTest, TestReportTestAction, TestScript,
22        TestScriptFixture, TestScriptSetup, TestScriptSetupAction, TestScriptSetupActionAssert,
23        TestScriptSetupActionOperation, TestScriptTeardown, TestScriptTeardownAction,
24        TestScriptTest, TestScriptTestAction, TestScriptVariable,
25    },
26    terminology::{
27        AssertDirectionCodes, AssertOperatorCodes, BoundCode, BundleType, IssueType,
28        ReportActionResultCodes, ReportResultCodes, ReportStatusCodes, TestscriptOperationCodes,
29    },
30    types::{FHIRId, FHIRMarkdown, FHIRString, Reference},
31};
32use haste_fhir_operation_error::OperationOutcomeError;
33use haste_pointer::{Key, TypedPointer};
34use haste_reflect::MetaValue;
35use regex::Regex;
36use std::{
37    any::Any,
38    collections::HashMap,
39    sync::{Arc, LazyLock},
40    time::Duration,
41};
42use tokio::sync::Mutex;
43
44use crate::conversion::ConvertedValue;
45
46mod conversion;
47
48#[derive(Debug)]
49pub enum TestScriptError {
50    ExecutionError(String),
51    ValidationError(String),
52    FixtureNotFound,
53    InvalidFixture,
54    OperationError(OperationOutcomeError),
55}
56
57#[derive(Debug, Clone)]
58enum Response {
59    FHIRResponse(Box<FHIRResponse>),
60    OperationError(Arc<OperationOutcomeError>),
61}
62
63#[derive(Debug)]
64enum Fixtures {
65    Resource(Resource),
66    Request(FHIRRequest),
67    Response(Response),
68}
69
70// Internal structure to hold current test result and testing fixtures.
71struct TestState {
72    fp_engine: haste_fhirpath::FPEngine,
73    fixtures: HashMap<String, Fixtures>,
74    latest_request: Option<FHIRRequest>,
75    latest_response: Option<Response>,
76    result: BoundCode<ReportResultCodes>,
77    /// Writes not yet confirmed visible to search. All are waited for, since a
78    /// search (a chained one especially) may filter on any of them.
79    unindexed_writes: Vec<IndexedWrite>,
80}
81
82/// A write that searches must see before they run. Search indexing is
83/// asynchronous, so a search sent straight after a write can miss it. The
84/// worker indexes a tenant's writes in order, so once the latest write is
85/// visible every earlier one is too.
86#[derive(Debug, Clone)]
87struct IndexedWrite {
88    resource_type: ResourceType,
89    id: String,
90    /// The version search should return; `None` for a delete, which search
91    /// should no longer return at all.
92    version_id: Option<String>,
93}
94
95impl TestState {
96    fn new() -> Self {
97        TestState {
98            fp_engine: haste_fhirpath::FPEngine::new(),
99            fixtures: HashMap::new(),
100            latest_request: None,
101            latest_response: None,
102            result: ReportResultCodes::pending(),
103            unindexed_writes: Vec::new(),
104        }
105    }
106    fn resolve_fixture<'a>(
107        &'a self,
108        fixture_id: &str,
109    ) -> Result<&'a dyn MetaValue, TestScriptError> {
110        let fixture = self
111            .fixtures
112            .get(fixture_id)
113            .ok_or(TestScriptError::FixtureNotFound)?;
114
115        match fixture {
116            Fixtures::Resource(res) => Ok(res),
117            Fixtures::Request(req) => {
118                request_to_meta_value(req).ok_or_else(|| TestScriptError::InvalidFixture)
119            }
120            Fixtures::Response(response) => {
121                response_to_meta_value(response).ok_or_else(|| TestScriptError::InvalidFixture)
122            }
123        }
124    }
125}
126
127struct TestResult<T> {
128    pub state: Arc<Mutex<TestState>>,
129    pub value: T,
130}
131
132fn response_to_meta_value(response: &Response) -> Option<&dyn MetaValue> {
133    match response {
134        Response::FHIRResponse(fhir_response) => match &**fhir_response {
135            FHIRResponse::Create(res) => Some(&res.resource),
136            FHIRResponse::Read(res) => Some(&res.resource),
137            FHIRResponse::VersionRead(res) => Some(&res.resource),
138            FHIRResponse::Update(res) => Some(&res.resource),
139            FHIRResponse::Patch(res) => Some(&res.resource),
140            FHIRResponse::Batch(res) => Some(&res.resource),
141            FHIRResponse::Transaction(res) => Some(&res.resource),
142
143            FHIRResponse::Capabilities(res) => Some(&res.capabilities),
144            FHIRResponse::Search(res) => match res {
145                SearchResponse::Type(res) => Some(&res.bundle),
146                SearchResponse::System(res) => Some(&res.bundle),
147            },
148            FHIRResponse::History(res) => match res {
149                HistoryResponse::Instance(res) => Some(&res.bundle),
150                HistoryResponse::Type(res) => Some(&res.bundle),
151                HistoryResponse::System(res) => Some(&res.bundle),
152            },
153            FHIRResponse::Invoke(res) => match res {
154                InvokeResponse::Instance(res) => Some(&res.resource),
155                InvokeResponse::Type(res) => Some(&res.resource),
156                InvokeResponse::System(res) => Some(&res.resource),
157            },
158
159            FHIRResponse::Delete(_) => None,
160        },
161        Response::OperationError(op_error) => {
162            let outcome = op_error.outcome();
163            Some(outcome)
164        }
165    }
166}
167
168fn request_to_meta_value(request: &FHIRRequest) -> Option<&dyn MetaValue> {
169    match request {
170        FHIRRequest::Create(req) => Some(&req.resource),
171
172        FHIRRequest::Update(update_request) => match update_request {
173            UpdateRequest::Conditional(req) => Some(&req.resource),
174            UpdateRequest::Instance(req) => Some(&req.resource),
175        },
176
177        FHIRRequest::Batch(req) => Some(&req.resource),
178        FHIRRequest::Transaction(req) => Some(&req.resource),
179        FHIRRequest::Invocation(req) => match req {
180            haste_fhir_client::request::InvocationRequest::Instance(req) => Some(&req.parameters),
181            haste_fhir_client::request::InvocationRequest::Type(req) => Some(&req.parameters),
182            haste_fhir_client::request::InvocationRequest::System(req) => Some(&req.parameters),
183        },
184        FHIRRequest::Read(_)
185        | FHIRRequest::VersionRead(_)
186        | FHIRRequest::Compartment(_)
187        | FHIRRequest::Patch(_)
188        | FHIRRequest::Delete(_)
189        | FHIRRequest::Capabilities
190        | FHIRRequest::Search(_)
191        | FHIRRequest::History(_) => None,
192    }
193}
194
195fn associate_request_response_variables(
196    state: &mut TestState,
197    operation: &TestScriptSetupActionOperation,
198    request: FHIRRequest,
199    response: Response,
200) {
201    if let Some(request_var) = operation
202        .requestId
203        .as_ref()
204        .and_then(|id| id.value.as_ref())
205    {
206        // Associate request variable in state
207        state
208            .fixtures
209            .insert(request_var.clone(), Fixtures::Request(request.clone()));
210    }
211
212    if let Some(response_var) = operation
213        .responseId
214        .as_ref()
215        .and_then(|id| id.value.as_ref())
216    {
217        // Associate response variable in state
218        state
219            .fixtures
220            .insert(response_var.clone(), Fixtures::Response(response.clone()));
221    }
222
223    state.latest_request = Some(request);
224    state.latest_response = Some(response);
225}
226
227/// Derive the resource type from operation or from the metavalue if not present on operation.
228fn derive_resource_type(
229    operation: &TestScriptSetupActionOperation,
230    target: Option<&dyn MetaValue>,
231    path: &str,
232) -> Result<ResourceType, TestScriptError> {
233    if let Some(operation_resource_type) = operation.resource.as_ref() {
234        let string_type = operation_resource_type.as_str();
235        ResourceType::try_from(string_type.unwrap_or_default()).map_err(|_| {
236            TestScriptError::ExecutionError(format!(
237                "Unsupported resource type '{operation_resource_type:?}' for operation at '{path}'."
238            ))
239        })
240    } else if let Some(target) = target {
241        ResourceType::try_from(target.fhir_type()).map_err(|_| {
242            TestScriptError::ExecutionError(format!(
243                "Unsupported resource type '{}' for operation at '{path}'.",
244                target.fhir_type()
245            ))
246        })
247    } else {
248        Err(TestScriptError::ExecutionError(format!(
249            "Failed to derive resource type for operation at '{path}'.",
250        )))
251    }
252}
253
254static EXPRESSION_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\$\{([^}]*)\}").unwrap());
255
256async fn get_variable(
257    state: &TestState,
258    variables: &[TestScriptVariable],
259    variable_id: &str,
260) -> Result<ConvertedValue, TestScriptError> {
261    let Some(variable) = variables
262        .iter()
263        .find(|v| v.name.value.as_deref() == Some(variable_id))
264    else {
265        return Err(TestScriptError::ExecutionError(format!(
266            "Variable with id '{variable_id}' not found."
267        )));
268    };
269
270    if let Some(expression) = variable
271        .expression
272        .as_ref()
273        .and_then(|exp| exp.value.as_ref())
274    {
275        let values =
276            if let Some(source_id) = variable.sourceId.as_ref().and_then(|id| id.value.as_ref()) {
277                let source = state.resolve_fixture(source_id)?;
278                vec![source]
279            } else {
280                vec![]
281            };
282
283        let eval_result = state
284            .fp_engine
285            .evaluate(expression, values)
286            .await
287            .map_err(|e| {
288                TestScriptError::ExecutionError(format!(
289                    "Failed to evaluate FHIRPath expression for variable '{variable_id}': {e}"
290                ))
291            })?;
292
293        let converted_values = eval_result
294            .iter()
295            .map(conversion::convert_meta_value)
296            .collect::<Vec<_>>();
297
298        if converted_values.len() == 1 {
299            Ok(converted_values.into_iter().next().unwrap())
300        } else {
301            Err(TestScriptError::ExecutionError(format!(
302                "Variable '{variable_id}' evaluation returned multiple values; only single value supported.",
303            )))
304        }
305    } else {
306        Err(TestScriptError::ExecutionError(format!(
307            "Only support variable with expression for variable id '{variable_id}'.",
308        )))
309    }
310}
311
312async fn evaluate_variable(
313    state: &TestState,
314    pointer: TypedPointer<TestScript, TestScript>,
315    value: &str,
316) -> Result<String, TestScriptError> {
317    let mut result = value.to_string();
318    let variable_pointer =
319        pointer.descend::<Vec<TestScriptVariable>>(&Key::Field("variable".to_string()));
320    let default_variables = vec![];
321
322    let variables = if let Some(pointer) = variable_pointer.as_ref() {
323        pointer.value().unwrap_or(&default_variables)
324    } else {
325        &default_variables
326    };
327
328    for reg_match in EXPRESSION_REGEX.captures_iter(value) {
329        let full_match = reg_match.get(0).map_or("", |m| m.as_str());
330        let Some(variable_id) = reg_match.get(1).map(|m| m.as_str()) else {
331            return Err(TestScriptError::ExecutionError(format!(
332                "Invalid variable expression in '{value}'."
333            )));
334        };
335
336        let variable = get_variable(state, variables, variable_id).await?;
337        result = result.replace(full_match, variable.to_string().as_str());
338    }
339
340    Ok(result)
341}
342
343async fn testscript_operation_to_fhir_request(
344    state: &TestState,
345    pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
346) -> Result<FHIRRequest, TestScriptError> {
347    let operation = get_operation(pointer)?;
348    let op = get_operation_type(operation);
349
350    match op {
351        Some(op) if Some(op) == TestscriptOperationCodes::read().as_str() => {
352            read_request(state, pointer, operation)
353        }
354
355        Some(op) if Some(op) == TestscriptOperationCodes::vread().as_str() => {
356            version_read_request(state, pointer, operation)
357        }
358
359        Some(op) if Some(op) == TestscriptOperationCodes::search().as_str() => {
360            search_request(state, pointer, operation).await
361        }
362
363        Some(op) if Some(op) == TestscriptOperationCodes::history().as_str() => {
364            history_request(state, pointer, operation).await
365        }
366
367        Some(op) if Some(op) == TestscriptOperationCodes::transaction().as_str() => {
368            transaction_request(state, pointer, operation)
369        }
370
371        Some(op) if Some(op) == TestscriptOperationCodes::batch().as_str() => {
372            batch_request(state, pointer, operation)
373        }
374
375        Some(op) if Some(op) == TestscriptOperationCodes::create().as_str() => {
376            create_request(state, pointer, operation)
377        }
378
379        Some(op) if Some(op) == TestscriptOperationCodes::update().as_str() => {
380            update_request(state, pointer, operation)
381        }
382
383        Some(op) if Some(op) == TestscriptOperationCodes::update_create().as_str() => {
384            conditional_update_request(state, pointer, operation).await
385        }
386
387        Some(op) if Some(op) == TestscriptOperationCodes::patch().as_str() => {
388            patch_request(state, pointer, operation)
389        }
390
391        Some(op) if Some(op) == TestscriptOperationCodes::delete().as_str() => {
392            delete_request(state, pointer, operation)
393        }
394
395        Some(op)
396            if Some(op) == TestscriptOperationCodes::delete_cond_multiple().as_str()
397                || Some(op) == TestscriptOperationCodes::delete_cond_single().as_str() =>
398        {
399            conditional_delete_request(state, pointer, operation)
400        }
401
402        Some(op)
403            if Some(op) == TestscriptOperationCodes::capabilities().as_str()
404                || Some(op) == TestscriptOperationCodes::conforms().as_str() =>
405        {
406            Ok(FHIRRequest::Capabilities)
407        }
408
409        Some("invoke") => invoke_request(state, pointer, operation),
410
411        _ => Err(TestScriptError::ExecutionError(format!(
412            "Unsupported TestScript operation type: {op:?} at '{}'.",
413            pointer.path(),
414        ))),
415    }
416}
417
418fn read_request(
419    state: &TestState,
420    pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
421    operation: &TestScriptSetupActionOperation,
422) -> Result<FHIRRequest, TestScriptError> {
423    let target_id = require_target_id(operation, pointer.path(), "Read")?;
424    let target = state.resolve_fixture(target_id)?;
425
426    Ok(FHIRRequest::Read(FHIRReadRequest {
427        resource_type: derive_resource_type(operation, Some(target), pointer.path())?,
428        id: fixture_string_field(target, target_id, "id")?,
429    }))
430}
431
432fn version_read_request(
433    state: &TestState,
434    pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
435    operation: &TestScriptSetupActionOperation,
436) -> Result<FHIRRequest, TestScriptError> {
437    let target_id = require_target_id(operation, pointer.path(), "Version Read")?;
438    let target = state.resolve_fixture(target_id)?;
439
440    Ok(FHIRRequest::VersionRead(FHIRVersionReadRequest {
441        resource_type: derive_resource_type(operation, Some(target), pointer.path())?,
442        id: fixture_string_field(target, target_id, "id")?,
443        version_id: fixture_version_id(target, target_id)?.into(),
444    }))
445}
446
447async fn search_request(
448    state: &TestState,
449    pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
450    operation: &TestScriptSetupActionOperation,
451) -> Result<FHIRRequest, TestScriptError> {
452    let query = operation
453        .params
454        .as_ref()
455        .and_then(|p| p.value.as_deref())
456        .unwrap_or_default();
457
458    let parameters =
459        parsed_parameters(state, pointer.root(), query, pointer.path(), "Search").await?;
460
461    if let Ok(resource_type) = derive_resource_type(operation, None, pointer.path()) {
462        Ok(FHIRRequest::Search(
463            haste_fhir_client::request::SearchRequest::Type(
464                haste_fhir_client::request::FHIRSearchTypeRequest {
465                    resource_type,
466                    parameters,
467                },
468            ),
469        ))
470    } else {
471        Ok(FHIRRequest::Search(
472            haste_fhir_client::request::SearchRequest::System(
473                haste_fhir_client::request::FHIRSearchSystemRequest { parameters },
474            ),
475        ))
476    }
477}
478
479async fn history_request(
480    state: &TestState,
481    pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
482    operation: &TestScriptSetupActionOperation,
483) -> Result<FHIRRequest, TestScriptError> {
484    let parameters = parsed_parameters(
485        state,
486        pointer.root(),
487        operation
488            .params
489            .as_ref()
490            .and_then(|p| p.value.as_deref())
491            .unwrap_or_default(),
492        pointer.path(),
493        "History",
494    )
495    .await?;
496
497    if let Some(target_id) = operation
498        .targetId
499        .as_ref()
500        .and_then(|id| id.value.as_deref())
501    {
502        let target = state.resolve_fixture(target_id)?;
503
504        Ok(FHIRRequest::History(HistoryRequest::Instance(
505            FHIRHistoryInstanceRequest {
506                resource_type: derive_resource_type(operation, Some(target), pointer.path())?,
507                id: fixture_string_field(target, target_id, "id")?,
508                parameters,
509            },
510        )))
511    } else if operation.resource.is_some() {
512        Ok(FHIRRequest::History(HistoryRequest::Type(
513            FHIRHistoryTypeRequest {
514                resource_type: derive_resource_type(operation, None, pointer.path())?,
515                parameters,
516            },
517        )))
518    } else {
519        Ok(FHIRRequest::History(HistoryRequest::System(
520            FHIRHistorySystemRequest { parameters },
521        )))
522    }
523}
524
525fn transaction_request(
526    state: &TestState,
527    pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
528    operation: &TestScriptSetupActionOperation,
529) -> Result<FHIRRequest, TestScriptError> {
530    let source_id = require_source_id(operation, pointer.path(), "Transaction")?;
531    let source = state.resolve_fixture(source_id)?;
532    let resource = fixture_resource(source, source_id)?;
533
534    match resource {
535        Resource::Bundle(bundle) => {
536            if bundle.type_ != BundleType::transaction() {
537                return Err(TestScriptError::ExecutionError(format!(
538                    "Fixture must be a transaction bundle for transaction operations for sourceId '{source_id}'."
539                )));
540            }
541
542            Ok(FHIRRequest::Transaction(FHIRTransactionRequest {
543                resource: bundle,
544            }))
545        }
546        _ => Err(TestScriptError::ExecutionError(format!(
547            "Fixture '{source_id}' is not a transaction Bundle resource."
548        ))),
549    }
550}
551
552fn batch_request(
553    state: &TestState,
554    pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
555    operation: &TestScriptSetupActionOperation,
556) -> Result<FHIRRequest, TestScriptError> {
557    let source_id = require_source_id(operation, pointer.path(), "Batch")?;
558    let source = state.resolve_fixture(source_id)?;
559    let resource = fixture_resource(source, source_id)?;
560
561    match resource {
562        Resource::Bundle(bundle) => {
563            if bundle.type_ != BundleType::batch() {
564                return Err(TestScriptError::ExecutionError(format!(
565                    "Fixture must be a batch bundle for batch operations for sourceId '{source_id}'."
566                )));
567            }
568
569            Ok(FHIRRequest::Batch(FHIRBatchRequest { resource: bundle }))
570        }
571        _ => Err(TestScriptError::ExecutionError(format!(
572            "Fixture '{source_id}' is not a batch Bundle resource."
573        ))),
574    }
575}
576
577fn create_request(
578    state: &TestState,
579    pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
580    operation: &TestScriptSetupActionOperation,
581) -> Result<FHIRRequest, TestScriptError> {
582    let source_id = require_source_id(operation, pointer.path(), "Create")?;
583    let source = state.resolve_fixture(source_id)?;
584    let resource = fixture_resource(source, source_id)?;
585
586    Ok(FHIRRequest::Create(FHIRCreateRequest {
587        resource_type: derive_resource_type(operation, Some(source), pointer.path())?,
588        resource,
589    }))
590}
591
592fn update_request(
593    state: &TestState,
594    pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
595    operation: &TestScriptSetupActionOperation,
596) -> Result<FHIRRequest, TestScriptError> {
597    let source_id = require_source_id(operation, pointer.path(), "Update")?;
598    let source = state.resolve_fixture(source_id)?;
599    let resource = fixture_resource(source, source_id)?;
600
601    let target_id = require_target_id(operation, pointer.path(), "Update")?;
602    let target = state.resolve_fixture(target_id)?;
603    let target_resource = fixture_resource(target, target_id)?;
604
605    Ok(FHIRRequest::Update(UpdateRequest::Instance(
606        FHIRUpdateInstanceRequest {
607            resource_type: derive_resource_type(operation, Some(target), pointer.path())?,
608            id: fixture_string_field(&target_resource, target_id, "id")?,
609            resource,
610        },
611    )))
612}
613
614async fn conditional_update_request(
615    state: &TestState,
616    pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
617    operation: &TestScriptSetupActionOperation,
618) -> Result<FHIRRequest, TestScriptError> {
619    let source_id = require_source_id(operation, pointer.path(), "UpdateCreate")?;
620    let source = state.resolve_fixture(source_id)?;
621    let resource = fixture_resource(source, source_id)?;
622
623    let parameters = parsed_parameters(
624        state,
625        pointer.root(),
626        operation
627            .params
628            .as_ref()
629            .and_then(|p| p.value.as_deref())
630            .unwrap_or_default(),
631        pointer.path(),
632        "UpdateCreate",
633    )
634    .await?;
635
636    Ok(FHIRRequest::Update(UpdateRequest::Conditional(
637        FHIRConditionalUpdateRequest {
638            resource_type: derive_resource_type(operation, Some(source), pointer.path())?,
639            parameters,
640            resource,
641        },
642    )))
643}
644
645fn delete_request(
646    state: &TestState,
647    pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
648    operation: &TestScriptSetupActionOperation,
649) -> Result<FHIRRequest, TestScriptError> {
650    let target_id = require_target_id(operation, pointer.path(), "Delete")?;
651    let target = state.resolve_fixture(target_id)?;
652
653    Ok(FHIRRequest::Delete(DeleteRequest::Instance(
654        FHIRDeleteInstanceRequest {
655            resource_type: derive_resource_type(operation, Some(target), pointer.path())?,
656            id: fixture_string_field(target, target_id, "id")?,
657        },
658    )))
659}
660
661fn conditional_delete_request(
662    _state: &TestState,
663    pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
664    operation: &TestScriptSetupActionOperation,
665) -> Result<FHIRRequest, TestScriptError> {
666    let parameters = ParsedParameters::try_from(
667        operation
668            .params
669            .as_ref()
670            .and_then(|p| p.value.as_deref())
671            .unwrap_or_default(),
672    )
673    .map_err(|e| {
674        TestScriptError::ExecutionError(format!(
675            "Failed to parse parameters for conditional delete operation at '{}': {}",
676            pointer.path(),
677            e
678        ))
679    })?;
680
681    if operation.resource.is_some() {
682        Ok(FHIRRequest::Delete(DeleteRequest::Type(
683            FHIRDeleteTypeRequest {
684                resource_type: derive_resource_type(operation, None, pointer.path())?,
685                parameters,
686            },
687        )))
688    } else {
689        Ok(FHIRRequest::Delete(DeleteRequest::System(
690            FHIRDeleteSystemRequest { parameters },
691        )))
692    }
693}
694
695/// The patch source fixture must be a `Binary` resource carrying the JSON Patch
696/// document, base64-encoded in `data` with `contentType`
697/// `application/json-patch+json` — the standard way `TestScript` represents patch
698/// bodies, since there's no dedicated FHIR resource type for a JSON Patch document.
699fn patch_request(
700    state: &TestState,
701    pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
702    operation: &TestScriptSetupActionOperation,
703) -> Result<FHIRRequest, TestScriptError> {
704    let target_id = require_target_id(operation, pointer.path(), "Patch")?;
705    let target = state.resolve_fixture(target_id)?;
706
707    let source_id = require_source_id(operation, pointer.path(), "Patch")?;
708    let source = state.resolve_fixture(source_id)?;
709    let Resource::Binary(binary) = fixture_resource(source, source_id)? else {
710        return Err(TestScriptError::ExecutionError(format!(
711            "Patch source fixture '{source_id}' must be a Binary resource containing the JSON Patch document."
712        )));
713    };
714
715    let content_type = binary.contentType.value.as_deref().unwrap_or_default();
716    if content_type != "application/json-patch+json" {
717        return Err(TestScriptError::ExecutionError(format!(
718            "Unsupported patch content type '{content_type}' for fixture '{source_id}'; only 'application/json-patch+json' is supported."
719        )));
720    }
721
722    let encoded = binary
723        .data
724        .as_ref()
725        .and_then(|d| d.value.as_deref())
726        .ok_or_else(|| {
727            TestScriptError::ExecutionError(format!(
728                "Patch source fixture '{source_id}' Binary resource has no 'data'."
729            ))
730        })?;
731
732    let decoded = BASE64.decode(encoded).map_err(|e| {
733        TestScriptError::ExecutionError(format!(
734            "Failed to base64-decode patch content for fixture '{source_id}': {e}"
735        ))
736    })?;
737
738    let patch: json_patch::Patch = serde_json::from_slice(&decoded).map_err(|e| {
739        TestScriptError::ExecutionError(format!(
740            "Failed to parse JSON Patch document for fixture '{source_id}': {e}"
741        ))
742    })?;
743
744    Ok(FHIRRequest::Patch(FHIRPatchRequest {
745        resource_type: derive_resource_type(operation, Some(target), pointer.path())?,
746        id: fixture_string_field(target, target_id, "id")?,
747        patch,
748    }))
749}
750
751fn invoke_request(
752    state: &TestState,
753    pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
754    operation: &TestScriptSetupActionOperation,
755) -> Result<FHIRRequest, TestScriptError> {
756    let op_code = operation
757        .url
758        .as_ref()
759        .and_then(|u| u.value.as_deref())
760        .ok_or_else(|| {
761            TestScriptError::ExecutionError(format!(
762                "Invoke operation requires url at '{}' which is used for the operation code.",
763                pointer.path()
764            ))
765        })?;
766
767    let fhir_operation = Operation::new(op_code);
768
769    let source_id = require_source_id(operation, pointer.path(), "Invoke")?;
770    let source = state.resolve_fixture(source_id)?;
771
772    let Resource::Parameters(parameters) = fixture_resource(source, source_id)? else {
773        return Err(TestScriptError::ExecutionError(format!(
774            "Source fixture '{source_id}' is not a Parameters resource."
775        )));
776    };
777
778    if let Some(target_id) = operation
779        .targetId
780        .as_ref()
781        .and_then(|id| id.value.as_deref())
782    {
783        let target = state.resolve_fixture(target_id)?;
784
785        Ok(FHIRRequest::Invocation(InvocationRequest::Instance(
786            FHIRInvokeInstanceRequest {
787                operation: fhir_operation,
788                resource_type: derive_resource_type(operation, Some(target), pointer.path())?,
789                id: fixture_string_field(target, target_id, "id")?,
790                parameters,
791            },
792        )))
793    } else if let Ok(resource_type) = derive_resource_type(operation, None, pointer.path()) {
794        Ok(FHIRRequest::Invocation(InvocationRequest::Type(
795            FHIRInvokeTypeRequest {
796                operation: fhir_operation,
797                resource_type,
798                parameters,
799            },
800        )))
801    } else {
802        Ok(FHIRRequest::Invocation(InvocationRequest::System(
803            FHIRInvokeSystemRequest {
804                operation: fhir_operation,
805                parameters,
806            },
807        )))
808    }
809}
810
811fn get_operation(
812    pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
813) -> Result<&TestScriptSetupActionOperation, TestScriptError> {
814    pointer.value().ok_or_else(|| {
815        TestScriptError::ExecutionError(format!(
816            "Failed to retrieve TestScript operation at '{}'.",
817            pointer.path()
818        ))
819    })
820}
821
822fn get_operation_type(operation: &TestScriptSetupActionOperation) -> Option<&str> {
823    operation
824        .type_
825        .as_ref()
826        .and_then(|t| t.code.as_ref())
827        .and_then(|c| c.value.as_deref())
828}
829
830fn require_target_id<'a>(
831    operation: &'a TestScriptSetupActionOperation,
832    path: &str,
833    operation_name: &str,
834) -> Result<&'a str, TestScriptError> {
835    operation
836        .targetId
837        .as_ref()
838        .and_then(|id| id.value.as_deref())
839        .ok_or_else(|| {
840            TestScriptError::ExecutionError(format!(
841                "{operation_name} operation requires targetId at '{path}'."
842            ))
843        })
844}
845
846fn require_source_id<'a>(
847    operation: &'a TestScriptSetupActionOperation,
848    path: &str,
849    operation_name: &str,
850) -> Result<&'a str, TestScriptError> {
851    operation
852        .sourceId
853        .as_ref()
854        .and_then(|id| id.value.as_deref())
855        .ok_or_else(|| {
856            TestScriptError::ExecutionError(format!(
857                "{operation_name} operation requires sourceId at '{path}'."
858            ))
859        })
860}
861
862fn fixture_string_field(
863    fixture: &dyn MetaValue,
864    fixture_name: &str,
865    field: &str,
866) -> Result<String, TestScriptError> {
867    fixture
868        .get_field(field)
869        .ok_or_else(|| {
870            TestScriptError::ExecutionError(format!(
871                "Fixture '{fixture_name}' does not have '{field}' field."
872            ))
873        })?
874        .as_any()
875        .downcast_ref::<String>()
876        .cloned()
877        .ok_or_else(|| {
878            TestScriptError::ExecutionError(format!(
879                "Field '{field}' on fixture '{fixture_name}' is not a String."
880            ))
881        })
882}
883
884fn fixture_version_id(
885    fixture: &dyn MetaValue,
886    fixture_name: &str,
887) -> Result<String, TestScriptError> {
888    fixture
889        .get_field("meta")
890        .and_then(|meta| meta.get_field("versionId"))
891        .ok_or_else(|| {
892            TestScriptError::ExecutionError(format!(
893                "Fixture '{fixture_name}' does not have a 'versionId' field."
894            ))
895        })?
896        .as_any()
897        .downcast_ref::<Box<FHIRId>>()
898        .cloned()
899        .and_then(|v| v.value)
900        .ok_or_else(|| {
901            TestScriptError::ExecutionError(format!(
902                "Fixture '{fixture_name}' does not have a valid 'versionId' field."
903            ))
904        })
905}
906
907fn fixture_resource(
908    fixture: &dyn MetaValue,
909    fixture_name: &str,
910) -> Result<Resource, TestScriptError> {
911    (fixture as &dyn Any)
912        .downcast_ref::<Resource>()
913        .cloned()
914        .ok_or_else(|| {
915            TestScriptError::ExecutionError(format!("Fixture '{fixture_name}' is not a Resource."))
916        })
917}
918
919async fn parsed_parameters(
920    state: &TestState,
921    root: TypedPointer<TestScript, TestScript>,
922    raw: &str,
923    path: &str,
924    operation: &str,
925) -> Result<ParsedParameters, TestScriptError> {
926    let evaluated = evaluate_variable(state, root, raw).await?;
927
928    ParsedParameters::try_from(evaluated.as_str()).map_err(|e| {
929        TestScriptError::ExecutionError(format!(
930            "Failed to parse parameters for {operation} operation at '{path}': {e}"
931        ))
932    })
933}
934
935/// Builds the headers declared by an operation's `requestHeader` entries.
936///
937/// Values are variable-evaluated first, so a `TestScript` may parameterize a header
938/// the same way it parameterizes a URL or a search query.
939async fn testscript_request_headers(
940    state: &TestState,
941    pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
942    operation: &TestScriptSetupActionOperation,
943) -> Result<Option<HeaderMap>, TestScriptError> {
944    let Some(request_headers) = operation.requestHeader.as_ref() else {
945        return Ok(None);
946    };
947
948    let mut headers = HeaderMap::with_capacity(request_headers.len());
949
950    for request_header in request_headers {
951        let field = request_header.field.value.as_deref().ok_or_else(|| {
952            TestScriptError::ExecutionError(format!(
953                "Missing requestHeader field for operation at '{}'.",
954                pointer.path()
955            ))
956        })?;
957
958        let raw_value = request_header.value.value.as_deref().unwrap_or_default();
959        let value = evaluate_variable(state, pointer.root(), raw_value).await?;
960
961        let name = HeaderName::try_from(field).map_err(|e| {
962            TestScriptError::ExecutionError(format!(
963                "Invalid requestHeader field '{field}' for operation at '{}': {e}",
964                pointer.path()
965            ))
966        })?;
967
968        let value = HeaderValue::from_str(&value).map_err(|e| {
969            TestScriptError::ExecutionError(format!(
970                "Invalid requestHeader value for '{field}' for operation at '{}': {e}",
971                pointer.path()
972            ))
973        })?;
974
975        headers.insert(name, value);
976    }
977
978    Ok((!headers.is_empty()).then_some(headers))
979}
980
981/// How often search is polled while waiting for a write to be indexed.
982const INDEX_POLL_INTERVAL: Duration = Duration::from_millis(100);
983
984/// The write a response records, for requests that change a resource.
985fn indexed_write(response: &FHIRResponse) -> Option<IndexedWrite> {
986    let (resource, deleted) = match response {
987        FHIRResponse::Create(created) => (&created.resource, false),
988        FHIRResponse::Update(updated) => (&updated.resource, false),
989        FHIRResponse::Patch(patched) => (&patched.resource, false),
990        FHIRResponse::Delete(DeleteResponse::Instance(deleted)) => (&deleted.resource, true),
991        _ => return None,
992    };
993    let json = serde_json::to_value(resource).ok()?;
994    Some(IndexedWrite {
995        resource_type: ResourceType::try_from(json.get("resourceType")?.as_str()?).ok()?,
996        id: json.get("id")?.as_str()?.to_string(),
997        version_id: if deleted {
998            None
999        } else {
1000            Some(json.pointer("/meta/versionId")?.as_str()?.to_string())
1001        },
1002    })
1003}
1004
1005/// Whether a request's result depends on the search index.
1006fn reads_search_index(request: &FHIRRequest) -> bool {
1007    matches!(
1008        request,
1009        FHIRRequest::Search(_)
1010            | FHIRRequest::Delete(DeleteRequest::Type(_) | DeleteRequest::System(_))
1011            | FHIRRequest::Update(UpdateRequest::Conditional(_))
1012    )
1013}
1014
1015/// Polls search until `write` is visible (its version returned, or gone after
1016/// a delete) or `timeout` runs out. On timeout the caller carries on, and a
1017/// search that depended on the write fails its assertions as usual.
1018async fn wait_for_index<CTX: Clone, Client: FHIRClient<CTX, OperationOutcomeError>>(
1019    client: &Client,
1020    ctx: &CTX,
1021    write: &IndexedWrite,
1022    timeout: Duration,
1023) {
1024    let Ok(parameters) = ParsedParameters::try_from(format!("_id={}", write.id).as_str()) else {
1025        return;
1026    };
1027    let request = FHIRRequest::Search(SearchRequest::Type(FHIRSearchTypeRequest {
1028        resource_type: write.resource_type.clone(),
1029        parameters,
1030    }));
1031    let deadline = tokio::time::Instant::now() + timeout;
1032
1033    loop {
1034        let versions: Vec<String> = match client.request(ctx.clone(), request.clone()).await {
1035            Ok(FHIRResponse::Search(SearchResponse::Type(response))) => response
1036                .bundle
1037                .entry
1038                .unwrap_or_default()
1039                .into_iter()
1040                .filter_map(|entry| {
1041                    let json = serde_json::to_value(entry.resource?).ok()?;
1042                    Some(json.pointer("/meta/versionId")?.as_str()?.to_string())
1043                })
1044                .collect(),
1045            _ => vec![],
1046        };
1047        let visible = match &write.version_id {
1048            Some(version_id) => versions.contains(version_id),
1049            None => versions.is_empty(),
1050        };
1051        if visible {
1052            return;
1053        }
1054        if tokio::time::Instant::now() >= deadline {
1055            tracing::warn!(
1056                resource_type = write.resource_type.as_ref(),
1057                id = write.id,
1058                "Timed out waiting for a write to be indexed"
1059            );
1060            return;
1061        }
1062        tokio::time::sleep(INDEX_POLL_INTERVAL).await;
1063    }
1064}
1065
1066async fn run_operation<
1067    CTX: Clone + WithRequestHeaders,
1068    Client: FHIRClient<CTX, OperationOutcomeError>,
1069>(
1070    client: &Client,
1071    ctx: CTX,
1072    state: Arc<Mutex<TestState>>,
1073    pointer: TypedPointer<TestScript, TestScriptSetupActionOperation>,
1074    options: Arc<TestRunnerOptions>,
1075) -> Result<TestResult<TestReportSetupActionOperation>, TestScriptError> {
1076    let operation = pointer.value().ok_or_else(|| {
1077        TestScriptError::ExecutionError(format!(
1078            "Failed to retrieve TestScript operation at '{}'.",
1079            pointer.path()
1080        ))
1081    })?;
1082
1083    let mut state_guard = state.lock().await;
1084    let fhir_request = testscript_operation_to_fhir_request(&state_guard, &pointer).await?;
1085
1086    if let Some(timeout) = options.index_wait_timeout
1087        && reads_search_index(&fhir_request)
1088        && !state_guard.unindexed_writes.is_empty()
1089    {
1090        // One deadline for the whole set: the worker indexes them together.
1091        let deadline = tokio::time::Instant::now() + timeout;
1092        for write in std::mem::take(&mut state_guard.unindexed_writes) {
1093            let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
1094            if remaining.is_zero() {
1095                break;
1096            }
1097            wait_for_index(client, &ctx, &write, remaining).await;
1098        }
1099    }
1100
1101    let ctx = match testscript_request_headers(&state_guard, &pointer, operation).await? {
1102        Some(headers) => ctx.with_request_headers(headers),
1103        None => ctx,
1104    };
1105
1106    let fhir_response = client.request(ctx, fhir_request.clone()).await;
1107    if let Some(wait_duration) = options.wait_between_operations {
1108        tokio::time::sleep(wait_duration).await;
1109    }
1110
1111    match fhir_response {
1112        Ok(fhir_response) => {
1113            if let Some(write) = indexed_write(&fhir_response) {
1114                state_guard.unindexed_writes.push(write);
1115            }
1116            associate_request_response_variables(
1117                &mut state_guard,
1118                operation,
1119                fhir_request,
1120                Response::FHIRResponse(Box::new(fhir_response)),
1121            );
1122
1123            drop(state_guard);
1124
1125            Ok(TestResult {
1126                state: state.clone(),
1127                value: TestReportSetupActionOperation {
1128                    result: ReportActionResultCodes::pass(),
1129                    ..Default::default()
1130                },
1131            })
1132        }
1133        Err(op_error) => {
1134            let op_error = Arc::new(op_error);
1135            tracing::warn!(
1136                path = pointer.path(),
1137                operation.label = operation
1138                    .label
1139                    .as_ref()
1140                    .and_then(|l| l.value.as_deref())
1141                    .unwrap_or("<no-label>"),
1142                operation.operation_type = get_operation_type(operation).unwrap_or("<unknown>"),
1143                error = %op_error,
1144                "TestScript operation failed"
1145            );
1146            associate_request_response_variables(
1147                &mut state_guard,
1148                operation,
1149                fhir_request,
1150                Response::OperationError(op_error.clone()),
1151            );
1152
1153            Ok(TestResult {
1154                state: state.clone(),
1155                value: TestReportSetupActionOperation {
1156                    result: ReportActionResultCodes::warning(),
1157                    message: Some(Box::new(FHIRMarkdown {
1158                        value: Some(format!("Operation failed: {op_error}")),
1159                        ..Default::default()
1160                    })),
1161                    ..Default::default()
1162                },
1163            })
1164        }
1165    }
1166}
1167
1168fn get_source<'a>(
1169    state: &'a TestState,
1170    assertion: &TestScriptSetupActionAssert,
1171) -> Result<Option<&'a dyn MetaValue>, TestScriptError> {
1172    if let Some(source_id) = assertion.sourceId.as_ref().and_then(|id| id.value.as_ref()) {
1173        let source = state.resolve_fixture(source_id)?;
1174        Ok(Some(source))
1175    } else {
1176        match assertion
1177            .direction
1178            .as_ref()
1179            .unwrap_or(&AssertDirectionCodes::response())
1180        {
1181            assertion if assertion == &AssertDirectionCodes::request() => {
1182                if let Some(request) = state.latest_request.as_ref() {
1183                    request_to_meta_value(request)
1184                        .ok_or_else(|| TestScriptError::InvalidFixture)
1185                        .map(Some)
1186                } else {
1187                    Ok(None)
1188                }
1189            }
1190            assertion if assertion == &AssertDirectionCodes::response() => {
1191                if let Some(response) = state.latest_response.as_ref() {
1192                    response_to_meta_value(response)
1193                        .ok_or_else(|| TestScriptError::InvalidFixture)
1194                        .map(Some)
1195                } else {
1196                    Ok(None)
1197                }
1198            }
1199            _ => Err(TestScriptError::ExecutionError(
1200                "Assert direction cannot be 'null' when sourceId is not provided.".to_string(),
1201            )),
1202        }
1203    }
1204}
1205
1206fn evaluate_operator(
1207    operator: &BoundCode<AssertOperatorCodes>,
1208    a: &Vec<conversion::ConvertedValue>,
1209    b: &Vec<conversion::ConvertedValue>,
1210) -> bool {
1211    match operator {
1212        operator
1213            if operator == &AssertOperatorCodes::equals()
1214                || operator == &AssertOperatorCodes::null() =>
1215        {
1216            a == b
1217        }
1218        operator if operator == &AssertOperatorCodes::not_equals() => !(a == b),
1219
1220        operator if operator == &AssertOperatorCodes::contains() => {
1221            if a.len() != 1 || b.len() != 1 {
1222                return false;
1223            }
1224
1225            match (&a[0], &b[0]) {
1226                (ConvertedValue::String(a_str), ConvertedValue::String(b_str)) => {
1227                    a_str.contains(b_str)
1228                }
1229                _ => false,
1230            }
1231        }
1232        operator if operator == &AssertOperatorCodes::empty() => {
1233            todo!("Empty operator not implemented")
1234        }
1235        operator if operator == &AssertOperatorCodes::eval() => {
1236            todo!("Eval operator not implemented")
1237        }
1238        operator if operator == &AssertOperatorCodes::greater_than() => {
1239            todo!("GreaterThan operator not implemented")
1240        }
1241        operator if operator == &AssertOperatorCodes::in_() => todo!("In operator not implemented"),
1242        operator if operator == &AssertOperatorCodes::less_than() => {
1243            todo!("LessThan operator not implemented")
1244        }
1245        operator if operator == &AssertOperatorCodes::not_contains() => {
1246            todo!("NotContains operator not implemented")
1247        }
1248        operator if operator == &AssertOperatorCodes::not_empty() => {
1249            todo!("NotEmpty operator not implemented")
1250        }
1251        operator if operator == &AssertOperatorCodes::not_in() => {
1252            todo!("NotIn operator not implemented")
1253        }
1254        _ => {
1255            todo!("Operator '{:?}' not implemented", operator)
1256        }
1257    }
1258    // a == b
1259}
1260
1261async fn derive_comparison_to(
1262    state: &TestState,
1263    assertion: &TestScriptSetupActionAssert,
1264) -> Result<Vec<ConvertedValue>, TestScriptError> {
1265    if let Some(comparision_fixture_id) = assertion
1266        .compareToSourceId
1267        .as_ref()
1268        .and_then(|c| c.value.as_ref())
1269    {
1270        let comparison_fixture = state.resolve_fixture(comparision_fixture_id)?;
1271
1272        let Some(comparison_expression) = assertion
1273            .compareToSourceExpression
1274            .as_ref()
1275            .and_then(|exp| exp.value.as_ref())
1276        else {
1277            return Err(TestScriptError::ExecutionError(
1278                "compareToSourceExpression is required when compareToSourceId is provided."
1279                    .to_string(),
1280            ));
1281        };
1282
1283        let result = state
1284            .fp_engine
1285            .evaluate(comparison_expression, vec![comparison_fixture])
1286            .await
1287            .map_err(|e| {
1288                TestScriptError::ExecutionError(format!(
1289                    "FHIRPath evaluation error for comparison fixture '{comparision_fixture_id}': {e}"
1290                ))
1291            })?;
1292
1293        Ok(result
1294            .iter()
1295            .map(conversion::convert_meta_value)
1296            .collect::<Vec<_>>())
1297    } else if let Some(value) = assertion.value.as_ref().and_then(|v| v.value.as_ref())
1298        && let Some(converted_value) = conversion::convert_string_value(value.as_ref())
1299    {
1300        Ok(vec![converted_value])
1301    } else {
1302        Err(TestScriptError::ExecutionError(
1303            "Failed to derive comparison value for assertion.".to_string(),
1304        ))
1305    }
1306}
1307
1308/// Assertions carry a `label`/`description` intended by the FHIR spec for
1309/// exactly this purpose: identifying an assertion in test engine output.
1310fn assert_label(assertion: &TestScriptSetupActionAssert) -> &str {
1311    assertion
1312        .label
1313        .as_ref()
1314        .and_then(|l| l.value.as_deref())
1315        .or_else(|| {
1316            assertion
1317                .description
1318                .as_ref()
1319                .and_then(|d| d.value.as_deref())
1320        })
1321        .unwrap_or("<no-label>")
1322}
1323
1324/// Assertions are what determine the testreports ultimate pass/fail status.
1325/// So set that within state here depending on assertion success/failure.
1326async fn run_assertion(
1327    state: Arc<Mutex<TestState>>,
1328    pointer: TypedPointer<TestScript, TestScriptSetupActionAssert>,
1329) -> Result<TestResult<TestReportSetupActionAssert>, TestScriptError> {
1330    let assertion = pointer.value().ok_or_else(|| {
1331        TestScriptError::ExecutionError(format!(
1332            "Failed to retrieve TestScript assertion at '{}'.",
1333            pointer.path()
1334        ))
1335    })?;
1336
1337    let mut state_guard = state.lock().await;
1338
1339    let Some(source) = get_source(&state_guard, assertion)? else {
1340        return Err(TestScriptError::ExecutionError(format!(
1341            "Failed to resolve source for assertion at '{}'.",
1342            pointer.path()
1343        )));
1344    };
1345
1346    let default = AssertOperatorCodes::equals();
1347    let operator = assertion.operator.as_ref().unwrap_or(&default);
1348
1349    // Keep all uses of `source` inside this scope so the immutable borrow
1350    // of `state_guard` ends before we mutate `state_guard`.
1351    let failure_message = if let Some(message) =
1352        evaluate_resource_assertion(assertion, pointer.path(), source, operator)
1353    {
1354        Some(message)
1355    } else if let Some(expression) = assertion.expression.as_ref().and_then(|e| e.value.as_ref()) {
1356        evaluate_expression_assertion(
1357            &state_guard,
1358            assertion,
1359            pointer.path(),
1360            source,
1361            operator,
1362            expression,
1363        )
1364        .await?
1365    } else {
1366        None
1367    };
1368
1369    let (result, message) = match failure_message {
1370        Some(message) => {
1371            tracing::error!(
1372                assert.label = assert_label(assertion),
1373                path = pointer.path(),
1374                "{message}"
1375            );
1376
1377            state_guard.result = ReportResultCodes::fail();
1378
1379            (
1380                ReportActionResultCodes::fail(),
1381                Some(Box::new(FHIRMarkdown {
1382                    value: Some(message),
1383                    ..Default::default()
1384                })),
1385            )
1386        }
1387        None => (ReportActionResultCodes::pass(), None),
1388    };
1389
1390    Ok(TestResult {
1391        state: state.clone(),
1392        value: TestReportSetupActionAssert {
1393            result,
1394            message,
1395            ..Default::default()
1396        },
1397    })
1398}
1399
1400fn evaluate_resource_assertion(
1401    assertion: &TestScriptSetupActionAssert,
1402    path: &str,
1403    source: &dyn MetaValue,
1404    operator: &BoundCode<AssertOperatorCodes>,
1405) -> Option<String> {
1406    let resource_string = assertion.resource.as_ref()?.as_str()?;
1407
1408    let operation_evaluation_result = evaluate_operator(
1409        operator,
1410        &vec![conversion::ConvertedValue::String(
1411            resource_string.to_string(),
1412        )],
1413        &vec![conversion::ConvertedValue::String(
1414            source.fhir_type().to_string(),
1415        )],
1416    );
1417
1418    if operation_evaluation_result {
1419        return None;
1420    }
1421
1422    Some(format!(
1423        "Assertion '{}' at '{}' failed: resource type '{resource_string}' does not match '{}'.",
1424        assert_label(assertion),
1425        path,
1426        source.fhir_type()
1427    ))
1428}
1429
1430async fn evaluate_expression_assertion(
1431    state_guard: &TestState,
1432    assertion: &TestScriptSetupActionAssert,
1433    path: &str,
1434    source: &dyn MetaValue,
1435    operator: &BoundCode<AssertOperatorCodes>,
1436    expression: &str,
1437) -> Result<Option<String>, TestScriptError> {
1438    let comparison_to = derive_comparison_to(state_guard, assertion).await?;
1439
1440    let Ok(result) = state_guard
1441        .fp_engine
1442        .evaluate(expression, vec![source])
1443        .await
1444    else {
1445        tracing::error!(
1446            assert.label = assert_label(assertion),
1447            path = path,
1448            expression = expression,
1449            "Assertion '{}' at '{}' failed: FHIRPath expression '{expression}' failed to evaluate.",
1450            assert_label(assertion),
1451            path,
1452        );
1453
1454        return Err(TestScriptError::ExecutionError(format!(
1455            "Assertion '{}' at '{}': FHIRPath expression '{expression}' failed to evaluate.",
1456            assert_label(assertion),
1457            path,
1458        )));
1459    };
1460
1461    let converted_values = result
1462        .iter()
1463        .map(conversion::convert_meta_value)
1464        .collect::<Vec<_>>();
1465
1466    let operation_evaluation_result =
1467        evaluate_operator(operator, &converted_values, &comparison_to);
1468
1469    if operation_evaluation_result {
1470        return Ok(None);
1471    }
1472
1473    Ok(Some(format!(
1474        "Assertion '{}' at '{}' failed: '{converted_values:?}' {operator:?} '{comparison_to:?}'.",
1475        assert_label(assertion),
1476        path,
1477    )))
1478}
1479
1480async fn run_action<
1481    CTX: Clone + WithRequestHeaders,
1482    Client: FHIRClient<CTX, OperationOutcomeError>,
1483>(
1484    client: &Client,
1485    ctx: CTX,
1486    state: Arc<Mutex<TestState>>,
1487    pointer: TypedPointer<TestScript, TestScriptTestAction>,
1488    options: Arc<TestRunnerOptions>,
1489) -> Result<TestResult<TestReportSetupAction>, TestScriptError> {
1490    tracing::info!("Running TestScript action at path: {}", pointer.path());
1491    let action = pointer.value().ok_or_else(|| {
1492        TestScriptError::ExecutionError(format!(
1493            "Failed to retrieve TestScript action at '{}'.",
1494            pointer.path()
1495        ))
1496    })?;
1497
1498    // Should be either an operation or an assert.
1499    // Both should not exist at the same time.
1500    if action.operation.is_some() {
1501        let Some(operation_pointer) =
1502            pointer.descend::<TestScriptSetupActionOperation>(&Key::Field("operation".to_string()))
1503        else {
1504            return Err(TestScriptError::ExecutionError(format!(
1505                "Failed to retrieve TestScript operation at '{}'.",
1506                pointer.path()
1507            )));
1508        };
1509
1510        let result = run_operation(client, ctx, state, operation_pointer, options).await?;
1511
1512        Ok(TestResult {
1513            state: result.state,
1514            value: TestReportSetupAction {
1515                operation: Some(result.value),
1516                ..Default::default()
1517            },
1518        })
1519    } else if action.assert.is_some() {
1520        let Some(assertion_pointer) =
1521            pointer.descend::<TestScriptSetupActionAssert>(&Key::Field("assert".to_string()))
1522        else {
1523            return Err(TestScriptError::ExecutionError(format!(
1524                "Failed to retrieve TestScript assertion at '{}'.",
1525                pointer.path()
1526            )));
1527        };
1528
1529        let assertion = run_assertion(state, assertion_pointer).await?;
1530
1531        Ok(TestResult {
1532            state: assertion.state,
1533            value: TestReportSetupAction {
1534                assert: Some(assertion.value),
1535                ..Default::default()
1536            },
1537        })
1538    } else {
1539        Err(TestScriptError::ExecutionError(format!(
1540            "TestScript action must have either an operation or an assert at '{}'.",
1541            pointer.path()
1542        )))
1543    }
1544}
1545
1546async fn run_setup_action<
1547    CTX: Clone + WithRequestHeaders,
1548    Client: FHIRClient<CTX, OperationOutcomeError>,
1549>(
1550    client: &Client,
1551    ctx: CTX,
1552    state: Arc<Mutex<TestState>>,
1553    pointer: TypedPointer<TestScript, TestScriptSetupAction>,
1554    options: Arc<TestRunnerOptions>,
1555) -> Result<TestResult<TestReportSetupAction>, TestScriptError> {
1556    let action = pointer.value().ok_or_else(|| {
1557        TestScriptError::ExecutionError(format!(
1558            "Failed to retrieve TestScript action at '{}'.",
1559            pointer.path()
1560        ))
1561    })?;
1562
1563    tracing::info!("Running TestScript action at path: {}", pointer.path());
1564
1565    // Should be either an operation or an assert.
1566    // Both should not exist at the same time.
1567    if action.operation.is_some() {
1568        let Some(operation_pointer) =
1569            pointer.descend::<TestScriptSetupActionOperation>(&Key::Field("operation".to_string()))
1570        else {
1571            return Err(TestScriptError::ExecutionError(format!(
1572                "Failed to retrieve TestScript operation at '{}'.",
1573                pointer.path()
1574            )));
1575        };
1576
1577        let result = run_operation(client, ctx, state, operation_pointer, options).await?;
1578
1579        Ok(TestResult {
1580            state: result.state,
1581            value: TestReportSetupAction {
1582                operation: Some(result.value),
1583                ..Default::default()
1584            },
1585        })
1586    } else if action.assert.is_some() {
1587        let Some(assertion_pointer) =
1588            pointer.descend::<TestScriptSetupActionAssert>(&Key::Field("assert".to_string()))
1589        else {
1590            return Err(TestScriptError::ExecutionError(format!(
1591                "Failed to retrieve TestScript assertion at '{}'.",
1592                pointer.path()
1593            )));
1594        };
1595
1596        let assertion = run_assertion(state, assertion_pointer).await?;
1597
1598        Ok(TestResult {
1599            state: assertion.state,
1600            value: TestReportSetupAction {
1601                assert: Some(assertion.value),
1602                ..Default::default()
1603            },
1604        })
1605    } else {
1606        Err(TestScriptError::ExecutionError(format!(
1607            "TestScript action must have either an operation or an assert at '{}'.",
1608            pointer.path()
1609        )))
1610    }
1611}
1612
1613async fn setup_fixtures<
1614    CTX: Clone + WithRequestHeaders,
1615    Client: FHIRClient<CTX, OperationOutcomeError>,
1616>(
1617    client: &Client,
1618    ctx: CTX,
1619    state: Arc<Mutex<TestState>>,
1620    pointer: TypedPointer<TestScript, TestScript>,
1621    _options: Arc<TestRunnerOptions>,
1622) -> Result<Arc<Mutex<TestState>>, OperationOutcomeError> {
1623    let mut state_lock = state.lock().await;
1624
1625    let Some(fixtures_pointer) =
1626        pointer.descend::<Vec<TestScriptFixture>>(&Key::Field("fixture".to_string()))
1627    else {
1628        return Ok(state.clone());
1629    };
1630
1631    let Some(fixtures) = fixtures_pointer.value() else {
1632        return Ok(state.clone());
1633    };
1634
1635    for fixture in fixtures {
1636        if let Some(reference_string) = fixture
1637            .resource
1638            .as_ref()
1639            .and_then(|r| r.reference.as_ref())
1640            .and_then(|refe| refe.value.as_ref())
1641        {
1642            let resolved_resource = if reference_string.starts_with('#')
1643                && let Some(contained) =
1644                    pointer.descend::<Vec<Resource>>(&Key::Field("contained".to_string()))
1645                && let Some(contained) = contained.value()
1646            {
1647                let local_id = &reference_string[1..];
1648                let Some(resource) = contained.iter().find(|res| {
1649                    if let Some(id) = res.get_field("id")
1650                        && let Some(id) = id.as_any().downcast_ref::<String>()
1651                    {
1652                        id.as_str() == local_id
1653                    } else {
1654                        false
1655                    }
1656                }) else {
1657                    return Err(OperationOutcomeError::error(
1658                        IssueType::not_found(),
1659                        format!("Contained resource with id '{local_id}' not found."),
1660                    ));
1661                };
1662
1663                resource.clone()
1664            } else {
1665                let parts = reference_string.split('/').collect::<Vec<&str>>();
1666                if parts.len() != 2 {
1667                    return Err(OperationOutcomeError::error(
1668                        IssueType::invalid(),
1669                        format!("Invalid fixture reference: {reference_string}"),
1670                    ));
1671                }
1672
1673                let resource_type = parts[0];
1674                let id = parts[1];
1675
1676                let Some(remote_resource) = client
1677                    .read(
1678                        ctx.clone(),
1679                        ResourceType::try_from(resource_type).map_err(|_| {
1680                            OperationOutcomeError::error(
1681                                IssueType::invalid(),
1682                                format!(
1683                                    "Invalid resource type in fixture reference: '{resource_type}'"
1684                                ),
1685                            )
1686                        })?,
1687                        id.to_string(),
1688                    )
1689                    .await?
1690                else {
1691                    return Err(OperationOutcomeError::error(
1692                        IssueType::not_found(),
1693                        format!("Resource '{resource_type}' with id '{id}' not found."),
1694                    ));
1695                };
1696
1697                remote_resource
1698            };
1699
1700            state_lock.fixtures.insert(
1701                fixture.id.clone().unwrap_or_default(),
1702                Fixtures::Resource(resolved_resource),
1703            );
1704        }
1705    }
1706
1707    drop(state_lock);
1708
1709    Ok(state)
1710}
1711
1712async fn run_setup<
1713    CTX: Clone + WithRequestHeaders,
1714    Client: FHIRClient<CTX, OperationOutcomeError>,
1715>(
1716    client: &Client,
1717    ctx: CTX,
1718    state: Arc<Mutex<TestState>>,
1719    pointer: TypedPointer<TestScript, TestScriptSetup>,
1720    options: Arc<TestRunnerOptions>,
1721) -> Result<TestResult<TestReportSetup>, TestScriptError> {
1722    let mut cur_state = state;
1723
1724    let mut setup_results = TestReportSetup {
1725        action: vec![],
1726        ..Default::default()
1727    };
1728
1729    let Some(setup) = pointer.value() else {
1730        return Ok(TestResult {
1731            state: cur_state,
1732            value: setup_results,
1733        });
1734    };
1735
1736    for action in setup.action.iter().enumerate() {
1737        let action_pointer = pointer
1738            .descend::<Vec<TestScriptSetupAction>>(&Key::Field("action".to_string()))
1739            .and_then(|p| p.descend::<TestScriptSetupAction>(&Key::Index(action.0)));
1740
1741        let action_pointer = action_pointer.ok_or_else(|| {
1742            TestScriptError::ExecutionError(format!(
1743                "Failed to retrieve TestScript action at index {}.",
1744                action.0
1745            ))
1746        })?;
1747
1748        let result = run_setup_action(
1749            client,
1750            ctx.clone(),
1751            cur_state,
1752            action_pointer,
1753            options.clone(),
1754        )
1755        .await?;
1756        cur_state = result.state;
1757
1758        setup_results.action.push(result.value);
1759    }
1760
1761    Ok(TestResult {
1762        state: cur_state,
1763        value: setup_results,
1764    })
1765}
1766
1767async fn run_teardown<
1768    CTX: Clone + WithRequestHeaders,
1769    Client: FHIRClient<CTX, OperationOutcomeError>,
1770>(
1771    client: &Client,
1772    ctx: CTX,
1773    state: Arc<Mutex<TestState>>,
1774    pointer: TypedPointer<TestScript, TestScriptTeardown>,
1775    options: Arc<TestRunnerOptions>,
1776) -> Result<TestResult<TestReportTeardown>, TestScriptError> {
1777    let mut cur_state = state;
1778
1779    let mut teardown_results = TestReportTeardown {
1780        action: vec![],
1781        ..Default::default()
1782    };
1783
1784    let Some(actions) = pointer.value() else {
1785        return Ok(TestResult {
1786            state: cur_state,
1787            value: teardown_results,
1788        });
1789    };
1790
1791    for action in actions.action.iter().enumerate() {
1792        let action_pointer = pointer
1793            .descend::<Vec<TestScriptTeardownAction>>(&Key::Field("action".to_string()))
1794            .and_then(|p| p.descend::<TestScriptTeardownAction>(&Key::Index(action.0)));
1795
1796        let action_pointer = action_pointer.ok_or_else(|| {
1797            TestScriptError::ExecutionError(format!(
1798                "Failed to retrieve TestScript teardown action at index {}.",
1799                action.0
1800            ))
1801        })?;
1802
1803        let operation_pointer = action_pointer
1804            .descend::<TestScriptSetupActionOperation>(&Key::Field("operation".to_string()))
1805            .ok_or_else(|| {
1806                TestScriptError::ExecutionError(format!(
1807                    "Failed to retrieve TestScript teardown operation at index {}.",
1808                    action.0
1809                ))
1810            })?;
1811
1812        let result = run_operation(
1813            client,
1814            ctx.clone(),
1815            cur_state,
1816            operation_pointer,
1817            options.clone(),
1818        )
1819        .await?;
1820        cur_state = result.state;
1821
1822        teardown_results.action.push(TestReportTeardownAction {
1823            operation: result.value,
1824            ..Default::default()
1825        });
1826    }
1827
1828    Ok(TestResult {
1829        state: cur_state,
1830        value: teardown_results,
1831    })
1832}
1833
1834async fn run_test<
1835    CTX: Clone + WithRequestHeaders,
1836    Client: FHIRClient<CTX, OperationOutcomeError>,
1837>(
1838    client: &Client,
1839    ctx: CTX,
1840    state: Arc<Mutex<TestState>>,
1841    pointer: TypedPointer<TestScript, TestScriptTest>,
1842    options: Arc<TestRunnerOptions>,
1843) -> Result<TestResult<TestReportTest>, TestScriptError> {
1844    let mut cur_state = state;
1845    let mut test_report_test = TestReportTest {
1846        action: vec![],
1847        ..Default::default()
1848    };
1849
1850    let test = pointer.value().ok_or_else(|| {
1851        TestScriptError::ExecutionError(format!(
1852            "Failed to retrieve TestScript test at '{}'.",
1853            pointer.path()
1854        ))
1855    })?;
1856
1857    for action in test.action.iter().enumerate() {
1858        let Some(action_pointer) = pointer
1859            .descend::<Vec<TestScriptTestAction>>(&Key::Field("action".to_string()))
1860            .and_then(|p| p.descend(&Key::Index(action.0)))
1861        else {
1862            return Err(TestScriptError::ExecutionError(format!(
1863                "Failed to retrieve TestScript test action at index {}.",
1864                action.0
1865            )));
1866        };
1867        let result = run_action(
1868            client,
1869            ctx.clone(),
1870            cur_state,
1871            action_pointer,
1872            options.clone(),
1873        )
1874        .await?;
1875        cur_state = result.state;
1876        test_report_test.action.push(TestReportTestAction {
1877            operation: result.value.operation,
1878            assert: result.value.assert,
1879            ..Default::default()
1880        });
1881    }
1882
1883    Ok(TestResult {
1884        state: cur_state,
1885        value: test_report_test,
1886    })
1887}
1888
1889async fn run_tests<
1890    CTX: Clone + WithRequestHeaders,
1891    Client: FHIRClient<CTX, OperationOutcomeError>,
1892>(
1893    client: &Client,
1894    ctx: CTX,
1895    state: Arc<Mutex<TestState>>,
1896    pointer: TypedPointer<TestScript, Vec<TestScriptTest>>,
1897    options: Arc<TestRunnerOptions>,
1898) -> Result<TestResult<Vec<TestReportTest>>, TestScriptError> {
1899    let mut test_results = vec![];
1900    let mut cur_state = state;
1901
1902    let Some(tests) = pointer.value() else {
1903        return Ok(TestResult {
1904            state: cur_state,
1905            value: test_results,
1906        });
1907    };
1908
1909    for test in tests.iter().enumerate() {
1910        let Some(test_pointer) = pointer.descend(&Key::Index(test.0)) else {
1911            return Err(TestScriptError::ExecutionError(format!(
1912                "Failed to retrieve TestScript test at index {}.",
1913                test.0
1914            )));
1915        };
1916        let test_result = run_test(
1917            client,
1918            ctx.clone(),
1919            cur_state,
1920            test_pointer,
1921            options.clone(),
1922        )
1923        .await?;
1924        cur_state = test_result.state;
1925        test_results.push(test_result.value);
1926    }
1927
1928    Ok(TestResult {
1929        state: cur_state,
1930        value: test_results,
1931    })
1932}
1933
1934pub struct TestRunnerOptions {
1935    pub wait_between_operations: Option<Duration>,
1936    /// Before a request that reads the search index (a search, conditional
1937    /// delete or conditional update), wait up to this long for the latest
1938    /// write to be indexed. Waits only as long as indexing takes, where
1939    /// `wait_between_operations` waits a fixed time after every operation.
1940    pub index_wait_timeout: Option<Duration>,
1941}
1942
1943/// Runs a FHIR `TestScript` using the provided client and execution context.
1944///
1945/// This executes the `TestScript` lifecycle:
1946/// - fixture setup
1947/// - setup actions
1948/// - test actions
1949/// - teardown actions
1950///
1951/// # Errors
1952///
1953/// Returns [`TestScriptError`] if:
1954/// - fixture setup fails
1955/// - setup actions fail
1956/// - test execution fails
1957/// - teardown execution fails
1958/// - an operation performed by the FHIR client fails
1959#[tracing::instrument(
1960    name = "testscript_run",
1961    skip_all,
1962    fields(
1963        testscript.id = test_script.id.as_deref().unwrap_or("<no-id>"),
1964        testscript.name = test_script.name.value.as_deref().unwrap_or("<unnamed>"),
1965        testscript.url = test_script.url.value.as_deref().unwrap_or("<no-url>"),
1966    )
1967)]
1968pub async fn run<
1969    CTX: Clone + WithRequestHeaders,
1970    Client: FHIRClient<CTX, OperationOutcomeError>,
1971>(
1972    client: &Client,
1973    ctx: CTX,
1974    test_script: Arc<TestScript>,
1975    options: Arc<TestRunnerOptions>,
1976) -> Result<TestReport, TestScriptError> {
1977    tracing::info!("Starting TestScript run");
1978
1979    let mut test_report = TestReport {
1980        status: ReportStatusCodes::completed(),
1981        testScript: Box::new(Reference {
1982            reference: Some(Box::new(FHIRString {
1983                value: Some(format!(
1984                    "Testscript/{}",
1985                    test_script.id.clone().unwrap_or_default()
1986                )),
1987                ..Default::default()
1988            })),
1989            ..Default::default()
1990        }),
1991        ..Default::default()
1992    };
1993
1994    let mut state = Arc::new(Mutex::new(TestState::new()));
1995    let pointer = TypedPointer::<TestScript, TestScript>::new(test_script);
1996
1997    state = setup_fixtures(client, ctx.clone(), state, pointer.clone(), options.clone())
1998        .await
1999        .map_err(TestScriptError::OperationError)?;
2000
2001    let mut running_state = Ok(());
2002
2003    // Run setup actions
2004    if let Some(setup_pointer) =
2005        pointer.descend::<TestScriptSetup>(&Key::Field("setup".to_string()))
2006    {
2007        tracing::info!("Running TestScript setup...");
2008        let setup_result = run_setup(
2009            client,
2010            ctx.clone(),
2011            state.clone(),
2012            setup_pointer,
2013            options.clone(),
2014        )
2015        .await;
2016        match setup_result {
2017            Ok(res) => {
2018                state = res.state;
2019                test_report.setup = Some(res.value);
2020            }
2021            Err(e) => {
2022                tracing::error!(phase = "setup", error = ?e, "TestScript run failed during setup");
2023                running_state = Err(e);
2024            }
2025        }
2026    }
2027
2028    // Run Test actions
2029    if running_state.is_ok()
2030        && let Some(test_pointer) =
2031            pointer.descend::<Vec<TestScriptTest>>(&Key::Field("test".to_string()))
2032    {
2033        tracing::info!("Running TestScript tests...");
2034        let test_result = run_tests(
2035            client,
2036            ctx.clone(),
2037            state.clone(),
2038            test_pointer,
2039            options.clone(),
2040        )
2041        .await;
2042
2043        match test_result {
2044            Ok(res) => {
2045                state = res.state;
2046                test_report.test = Some(res.value);
2047            }
2048
2049            Err(e) => {
2050                tracing::error!(phase = "test", error = ?e, "TestScript run failed during test execution");
2051                running_state = Err(e);
2052            }
2053        }
2054    }
2055
2056    if let Some(teardown_pointer) =
2057        pointer.descend::<TestScriptTeardown>(&Key::Field("teardown".to_string()))
2058    {
2059        tracing::info!("Running TestScript teardown...");
2060
2061        let result = run_teardown(
2062            client,
2063            ctx.clone(),
2064            state.clone(),
2065            teardown_pointer,
2066            options.clone(),
2067        )
2068        .await?;
2069
2070        // state = result.state;
2071        test_report.teardown = Some(result.value);
2072    }
2073
2074    running_state?;
2075
2076    let state_guard = state.lock().await;
2077    // Only set result to fail so if still pending can assume pass.
2078    // Flip to fail in assertion tests if any fail.
2079    match &state_guard.result {
2080        state if state == &ReportResultCodes::pending() => {
2081            test_report.result = ReportResultCodes::pass();
2082        }
2083        status => test_report.result = status.clone(),
2084    }
2085
2086    if test_report.result == ReportResultCodes::fail() {
2087        tracing::error!(result = ?test_report.result, "TestScript run finished with failures");
2088    } else {
2089        tracing::info!(result = ?test_report.result, "TestScript run finished");
2090    }
2091
2092    Ok(test_report)
2093}
2094
2095#[cfg(test)]
2096mod tests {
2097    use super::*;
2098    use haste_fhir_client::request::{FHIRCreateResponse, FHIRDeleteInstanceResponse};
2099
2100    fn patient(id: &str, version_id: &str) -> Resource {
2101        serde_json::from_value(serde_json::json!({
2102            "resourceType": "Patient",
2103            "id": id,
2104            "meta": { "versionId": version_id }
2105        }))
2106        .expect("valid Patient")
2107    }
2108
2109    #[test]
2110    fn writes_record_the_version_search_must_return() {
2111        let created = FHIRResponse::Create(FHIRCreateResponse {
2112            resource: patient("p1", "v1"),
2113        });
2114        let write = indexed_write(&created).expect("a create is a write");
2115        assert_eq!(write.resource_type, ResourceType::Patient);
2116        assert_eq!(write.id, "p1");
2117        assert_eq!(write.version_id.as_deref(), Some("v1"));
2118
2119        // A delete must be gone from search, whatever version it was.
2120        let deleted = FHIRResponse::Delete(DeleteResponse::Instance(Box::new(
2121            FHIRDeleteInstanceResponse {
2122                resource: patient("p1", "v2"),
2123            },
2124        )));
2125        assert_eq!(
2126            indexed_write(&deleted)
2127                .expect("a delete is a write")
2128                .version_id,
2129            None
2130        );
2131    }
2132
2133    #[test]
2134    fn only_index_backed_requests_wait() {
2135        let search = FHIRRequest::Search(SearchRequest::Type(FHIRSearchTypeRequest {
2136            resource_type: ResourceType::Patient,
2137            parameters: ParsedParameters::try_from("_tag=x").unwrap(),
2138        }));
2139        assert!(reads_search_index(&search));
2140
2141        let read = FHIRRequest::Read(FHIRReadRequest {
2142            resource_type: ResourceType::Patient,
2143            id: "p1".to_string(),
2144        });
2145        assert!(!reads_search_index(&read), "a read goes to the repository");
2146    }
2147}