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