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
70struct 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 unindexed_writes: Vec<IndexedWrite>,
80}
81
82#[derive(Debug, Clone)]
87struct IndexedWrite {
88 resource_type: ResourceType,
89 id: String,
90 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 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 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
227fn 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
695fn 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
935async 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
981const INDEX_POLL_INTERVAL: Duration = Duration::from_millis(100);
983
984fn 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
1005fn 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
1015async 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 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 }
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
1308fn 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
1324async 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 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 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 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 pub index_wait_timeout: Option<Duration>,
1941}
1942
1943#[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 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 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 test_report.teardown = Some(result.value);
2072 }
2073
2074 running_state?;
2075
2076 let state_guard = state.lock().await;
2077 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 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}