1use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
2use haste_fhir_client::{
3 FHIRClient,
4 http::{
5 FHIRHttpExchange, HeaderMap, HeaderName, HeaderValue, HttpResponseMetadata, Method,
6 StatusCode, WithRequestHeaders, header,
7 },
8 request::{
9 DeleteRequest, DeleteResponse, FHIRBatchRequest, FHIRConditionalUpdateRequest,
10 FHIRCreateRequest, FHIRDeleteInstanceRequest, FHIRDeleteSystemRequest,
11 FHIRDeleteTypeRequest, FHIRHistoryInstanceRequest, FHIRHistorySystemRequest,
12 FHIRHistoryTypeRequest, FHIRInvokeInstanceRequest, FHIRInvokeSystemRequest,
13 FHIRInvokeTypeRequest, FHIRPatchRequest, FHIRReadRequest, FHIRRequest, FHIRResponse,
14 FHIRSearchTypeRequest, FHIRTransactionRequest, FHIRUpdateInstanceRequest,
15 FHIRVersionReadRequest, HistoryRequest, HistoryResponse, InvocationRequest, InvokeResponse,
16 Operation, SearchRequest, SearchResponse, UpdateRequest,
17 },
18 url::ParsedParameters,
19};
20use haste_fhir_model::r4::generated::{
21 resources::{
22 Resource, ResourceType, TestReport, TestReportSetup, TestReportSetupAction,
23 TestReportSetupActionAssert, TestReportSetupActionOperation, TestReportTeardown,
24 TestReportTeardownAction, TestReportTest, TestReportTestAction, TestScript,
25 TestScriptFixture, TestScriptSetup, TestScriptSetupAction, TestScriptSetupActionAssert,
26 TestScriptSetupActionOperation, TestScriptTeardown, TestScriptTeardownAction,
27 TestScriptTest, TestScriptTestAction, TestScriptVariable,
28 },
29 terminology::{
30 AssertDirectionCodes, AssertOperatorCodes, BoundCode, BundleType, IssueType,
31 ReportActionResultCodes, ReportResultCodes, ReportStatusCodes, TestscriptOperationCodes,
32 },
33 types::{ExtensionValueTypeChoice, FHIRId, FHIRMarkdown, FHIRString, Reference},
34};
35use haste_fhir_operation_error::OperationOutcomeError;
36use haste_pointer::{Key, TypedPointer};
37use haste_reflect::MetaValue;
38use regex::Regex;
39use std::{
40 any::Any,
41 collections::HashMap,
42 sync::{Arc, LazyLock},
43 time::Duration,
44};
45use tokio::sync::Mutex;
46
47use crate::conversion::ConvertedValue;
48
49mod conversion;
50
51#[derive(Debug)]
52pub enum TestScriptError {
53 ExecutionError(String),
54 ValidationError(String),
55 FixtureNotFound,
56 InvalidFixture,
57 OperationError(OperationOutcomeError),
58}
59
60#[derive(Debug, Clone)]
61enum Response {
62 Interaction(Box<FHIRResponse>),
63 OperationError(Arc<OperationOutcomeError>),
64 Http(Option<Box<Resource>>),
67}
68
69#[derive(Debug, Clone)]
73struct RecordedResponse {
74 response: Response,
75 http: Option<HttpResponseMetadata>,
76}
77
78#[derive(Debug)]
79enum Fixtures {
80 Resource(Resource),
81 Request(FHIRRequest),
82 Response(RecordedResponse),
83}
84
85struct TestState {
87 fp_engine: haste_fhirpath::FPEngine,
88 fixtures: HashMap<String, Fixtures>,
89 latest_request: Option<FHIRRequest>,
90 latest_response: Option<RecordedResponse>,
91 result: BoundCode<ReportResultCodes>,
92 unindexed_writes: Vec<IndexedWrite>,
96}
97
98#[derive(Debug, Clone)]
103struct IndexedWrite {
104 resource_type: ResourceType,
105 id: String,
106 version_id: Option<String>,
109}
110
111impl TestState {
112 fn new() -> Self {
113 TestState {
114 fp_engine: haste_fhirpath::FPEngine::new(),
115 fixtures: HashMap::new(),
116 latest_request: None,
117 latest_response: None,
118 result: ReportResultCodes::pending(),
119 unindexed_writes: Vec::new(),
120 }
121 }
122 fn resolve_fixture<'a>(
123 &'a self,
124 fixture_id: &str,
125 ) -> Result<&'a dyn MetaValue, TestScriptError> {
126 let fixture = self
127 .fixtures
128 .get(fixture_id)
129 .ok_or(TestScriptError::FixtureNotFound)?;
130
131 match fixture {
132 Fixtures::Resource(res) => Ok(res),
133 Fixtures::Request(req) => {
134 request_to_meta_value(req).ok_or_else(|| TestScriptError::InvalidFixture)
135 }
136 Fixtures::Response(recorded) => response_to_meta_value(&recorded.response)
137 .ok_or_else(|| TestScriptError::InvalidFixture),
138 }
139 }
140
141 fn resolve_response(
143 &self,
144 fixture_id: Option<&str>,
145 ) -> Result<&RecordedResponse, TestScriptError> {
146 match fixture_id {
147 Some(fixture_id) => match self.fixtures.get(fixture_id) {
148 Some(Fixtures::Response(recorded)) => Ok(recorded),
149 Some(_) => Err(TestScriptError::ExecutionError(format!(
150 "Fixture '{fixture_id}' is not a response."
151 ))),
152 None => Err(TestScriptError::FixtureNotFound),
153 },
154 None => self.latest_response.as_ref().ok_or_else(|| {
155 TestScriptError::ExecutionError("No response to evaluate yet.".to_string())
156 }),
157 }
158 }
159}
160
161struct TestResult<T> {
162 pub state: Arc<Mutex<TestState>>,
163 pub value: T,
164}
165
166fn response_to_meta_value(response: &Response) -> Option<&dyn MetaValue> {
167 match response {
168 Response::Interaction(fhir_response) => match &**fhir_response {
169 FHIRResponse::Create(res) => Some(&res.resource),
170 FHIRResponse::Read(res) => Some(&res.resource),
171 FHIRResponse::VersionRead(res) => Some(&res.resource),
172 FHIRResponse::Update(res) => Some(&res.resource),
173 FHIRResponse::Patch(res) => Some(&res.resource),
174 FHIRResponse::Batch(res) => Some(&res.resource),
175 FHIRResponse::Transaction(res) => Some(&res.resource),
176
177 FHIRResponse::Capabilities(res) => Some(&res.capabilities),
178 FHIRResponse::Search(res) => match res {
179 SearchResponse::Type(res) => Some(&res.bundle),
180 SearchResponse::System(res) => Some(&res.bundle),
181 },
182 FHIRResponse::History(res) => match res {
183 HistoryResponse::Instance(res) => Some(&res.bundle),
184 HistoryResponse::Type(res) => Some(&res.bundle),
185 HistoryResponse::System(res) => Some(&res.bundle),
186 },
187 FHIRResponse::Invoke(res) => match res {
188 InvokeResponse::Instance(res) => Some(&res.resource),
189 InvokeResponse::Type(res) => Some(&res.resource),
190 InvokeResponse::System(res) => Some(&res.resource),
191 },
192
193 FHIRResponse::Delete(_) | FHIRResponse::Accepted(_) => None,
194 },
195 Response::OperationError(op_error) => {
196 let outcome = op_error.outcome();
197 Some(outcome)
198 }
199 Response::Http(resource) => resource
200 .as_deref()
201 .map(|resource| resource as &dyn MetaValue),
202 }
203}
204
205fn request_to_meta_value(request: &FHIRRequest) -> Option<&dyn MetaValue> {
206 match request {
207 FHIRRequest::Create(req) => Some(&req.resource),
208
209 FHIRRequest::Update(update_request) => match update_request {
210 UpdateRequest::Conditional(req) => Some(&req.resource),
211 UpdateRequest::Instance(req) => Some(&req.resource),
212 },
213
214 FHIRRequest::Batch(req) => Some(&req.resource),
215 FHIRRequest::Transaction(req) => Some(&req.resource),
216 FHIRRequest::Invocation(req) => match req {
217 haste_fhir_client::request::InvocationRequest::Instance(req) => Some(&req.parameters),
218 haste_fhir_client::request::InvocationRequest::Type(req) => Some(&req.parameters),
219 haste_fhir_client::request::InvocationRequest::System(req) => Some(&req.parameters),
220 },
221 FHIRRequest::Read(_)
222 | FHIRRequest::VersionRead(_)
223 | FHIRRequest::Compartment(_)
224 | FHIRRequest::Patch(_)
225 | FHIRRequest::Delete(_)
226 | FHIRRequest::Capabilities
227 | FHIRRequest::Search(_)
228 | FHIRRequest::History(_) => None,
229 }
230}
231
232fn associate_request_response_variables(
233 state: &mut TestState,
234 operation: &TestScriptSetupActionOperation,
235 request: Option<FHIRRequest>,
236 response: RecordedResponse,
237) {
238 if let Some(request) = request.as_ref()
239 && let Some(request_var) = operation
240 .requestId
241 .as_ref()
242 .and_then(|id| id.value.as_ref())
243 {
244 state
246 .fixtures
247 .insert(request_var.clone(), Fixtures::Request(request.clone()));
248 }
249
250 if let Some(response_var) = operation
251 .responseId
252 .as_ref()
253 .and_then(|id| id.value.as_ref())
254 {
255 state
257 .fixtures
258 .insert(response_var.clone(), Fixtures::Response(response.clone()));
259 }
260
261 state.latest_request = request;
262 state.latest_response = Some(response);
263}
264
265fn derive_resource_type(
267 operation: &TestScriptSetupActionOperation,
268 target: Option<&dyn MetaValue>,
269 path: &str,
270) -> Result<ResourceType, TestScriptError> {
271 if let Some(operation_resource_type) = operation.resource.as_ref() {
272 let string_type = operation_resource_type.as_str();
273 ResourceType::try_from(string_type.unwrap_or_default()).map_err(|_| {
274 TestScriptError::ExecutionError(format!(
275 "Unsupported resource type '{operation_resource_type:?}' for operation at '{path}'."
276 ))
277 })
278 } else if let Some(target) = target {
279 ResourceType::try_from(target.fhir_type()).map_err(|_| {
280 TestScriptError::ExecutionError(format!(
281 "Unsupported resource type '{}' for operation at '{path}'.",
282 target.fhir_type()
283 ))
284 })
285 } else {
286 Err(TestScriptError::ExecutionError(format!(
287 "Failed to derive resource type for operation at '{path}'.",
288 )))
289 }
290}
291
292static EXPRESSION_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\$\{([^}]*)\}").unwrap());
293
294async fn get_variable(
295 state: &TestState,
296 variables: &[TestScriptVariable],
297 variable_id: &str,
298) -> Result<ConvertedValue, TestScriptError> {
299 let Some(variable) = variables
300 .iter()
301 .find(|v| v.name.value.as_deref() == Some(variable_id))
302 else {
303 return Err(TestScriptError::ExecutionError(format!(
304 "Variable with id '{variable_id}' not found."
305 )));
306 };
307
308 if let Some(field) = variable
309 .headerField
310 .as_ref()
311 .and_then(|field| field.value.as_deref())
312 {
313 let source_id = variable
314 .sourceId
315 .as_ref()
316 .and_then(|id| id.value.as_deref());
317 let recorded = state.resolve_response(source_id)?;
318
319 return header_value(recorded, field)
320 .map(ConvertedValue::String)
321 .ok_or_else(|| {
322 TestScriptError::ExecutionError(format!(
323 "Variable '{variable_id}': the response has no '{field}' header."
324 ))
325 });
326 }
327
328 if let Some(expression) = variable
329 .expression
330 .as_ref()
331 .and_then(|exp| exp.value.as_ref())
332 {
333 let values =
334 if let Some(source_id) = variable.sourceId.as_ref().and_then(|id| id.value.as_ref()) {
335 let source = state.resolve_fixture(source_id)?;
336 vec![source]
337 } else {
338 vec![]
339 };
340
341 let eval_result = state
342 .fp_engine
343 .evaluate(expression, values)
344 .await
345 .map_err(|e| {
346 TestScriptError::ExecutionError(format!(
347 "Failed to evaluate FHIRPath expression for variable '{variable_id}': {e}"
348 ))
349 })?;
350
351 let converted_values = eval_result
352 .iter()
353 .map(conversion::convert_meta_value)
354 .collect::<Vec<_>>();
355
356 if converted_values.len() == 1 {
357 Ok(converted_values.into_iter().next().unwrap())
358 } else {
359 Err(TestScriptError::ExecutionError(format!(
360 "Variable '{variable_id}' evaluation returned multiple values; only single value supported.",
361 )))
362 }
363 } else {
364 Err(TestScriptError::ExecutionError(format!(
365 "Only support variable with expression or headerField for variable id '{variable_id}'.",
366 )))
367 }
368}
369
370async fn evaluate_variable(
371 state: &TestState,
372 pointer: TypedPointer<TestScript, TestScript>,
373 value: &str,
374) -> Result<String, TestScriptError> {
375 let mut result = value.to_string();
376 let variable_pointer =
377 pointer.descend::<Vec<TestScriptVariable>>(&Key::Field("variable".to_string()));
378 let default_variables = vec![];
379
380 let variables = if let Some(pointer) = variable_pointer.as_ref() {
381 pointer.value().unwrap_or(&default_variables)
382 } else {
383 &default_variables
384 };
385
386 for reg_match in EXPRESSION_REGEX.captures_iter(value) {
387 let full_match = reg_match.get(0).map_or("", |m| m.as_str());
388 let Some(variable_id) = reg_match.get(1).map(|m| m.as_str()) else {
389 return Err(TestScriptError::ExecutionError(format!(
390 "Invalid variable expression in '{value}'."
391 )));
392 };
393
394 let variable = get_variable(state, variables, variable_id).await?;
395 result = result.replace(full_match, variable.to_string().as_str());
396 }
397
398 Ok(result)
399}
400
401enum OperationRequest {
403 Interaction(Box<FHIRRequest>),
404 Http { method: Method, url: String },
405}
406
407async fn testscript_operation_to_request(
408 state: &TestState,
409 pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
410) -> Result<OperationRequest, TestScriptError> {
411 let operation = get_operation(pointer)?;
412
413 if get_operation_type(operation).is_none()
417 && let Some(url) = operation.url.as_ref().and_then(|url| url.value.as_deref())
418 {
419 let method = operation
421 .method
422 .as_ref()
423 .and_then(BoundCode::as_str)
424 .unwrap_or("get");
425 let method = Method::from_bytes(method.to_ascii_uppercase().as_bytes()).map_err(|_| {
426 TestScriptError::ExecutionError(format!(
427 "Unsupported method '{method}' for operation at '{}'.",
428 pointer.path()
429 ))
430 })?;
431
432 return Ok(OperationRequest::Http {
433 method,
434 url: evaluate_variable(state, pointer.root(), url).await?,
435 });
436 }
437
438 testscript_operation_to_fhir_request(state, pointer)
439 .await
440 .map(|request| OperationRequest::Interaction(Box::new(request)))
441}
442
443async fn testscript_operation_to_fhir_request(
444 state: &TestState,
445 pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
446) -> Result<FHIRRequest, TestScriptError> {
447 let operation = get_operation(pointer)?;
448 let op = get_operation_type(operation);
449
450 match op {
451 Some(op) if Some(op) == TestscriptOperationCodes::read().as_str() => {
452 read_request(state, pointer, operation)
453 }
454
455 Some(op) if Some(op) == TestscriptOperationCodes::vread().as_str() => {
456 version_read_request(state, pointer, operation)
457 }
458
459 Some(op) if Some(op) == TestscriptOperationCodes::search().as_str() => {
460 search_request(state, pointer, operation).await
461 }
462
463 Some(op) if Some(op) == TestscriptOperationCodes::history().as_str() => {
464 history_request(state, pointer, operation).await
465 }
466
467 Some(op) if Some(op) == TestscriptOperationCodes::transaction().as_str() => {
468 transaction_request(state, pointer, operation)
469 }
470
471 Some(op) if Some(op) == TestscriptOperationCodes::batch().as_str() => {
472 batch_request(state, pointer, operation)
473 }
474
475 Some(op) if Some(op) == TestscriptOperationCodes::create().as_str() => {
476 create_request(state, pointer, operation)
477 }
478
479 Some(op) if Some(op) == TestscriptOperationCodes::update().as_str() => {
480 update_request(state, pointer, operation)
481 }
482
483 Some(op) if Some(op) == TestscriptOperationCodes::update_create().as_str() => {
484 conditional_update_request(state, pointer, operation).await
485 }
486
487 Some(op) if Some(op) == TestscriptOperationCodes::patch().as_str() => {
488 patch_request(state, pointer, operation)
489 }
490
491 Some(op) if Some(op) == TestscriptOperationCodes::delete().as_str() => {
492 delete_request(state, pointer, operation)
493 }
494
495 Some(op)
496 if Some(op) == TestscriptOperationCodes::delete_cond_multiple().as_str()
497 || Some(op) == TestscriptOperationCodes::delete_cond_single().as_str() =>
498 {
499 conditional_delete_request(state, pointer, operation)
500 }
501
502 Some(op)
503 if Some(op) == TestscriptOperationCodes::capabilities().as_str()
504 || Some(op) == TestscriptOperationCodes::conforms().as_str() =>
505 {
506 Ok(FHIRRequest::Capabilities)
507 }
508
509 Some("invoke") => invoke_request(state, pointer, operation),
510
511 _ => Err(TestScriptError::ExecutionError(format!(
512 "Unsupported TestScript operation type: {op:?} at '{}'.",
513 pointer.path(),
514 ))),
515 }
516}
517
518fn read_request(
519 state: &TestState,
520 pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
521 operation: &TestScriptSetupActionOperation,
522) -> Result<FHIRRequest, TestScriptError> {
523 let target_id = require_target_id(operation, pointer.path(), "Read")?;
524 let target = state.resolve_fixture(target_id)?;
525
526 Ok(FHIRRequest::Read(FHIRReadRequest {
527 resource_type: derive_resource_type(operation, Some(target), pointer.path())?,
528 id: fixture_string_field(target, target_id, "id")?,
529 }))
530}
531
532fn version_read_request(
533 state: &TestState,
534 pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
535 operation: &TestScriptSetupActionOperation,
536) -> Result<FHIRRequest, TestScriptError> {
537 let target_id = require_target_id(operation, pointer.path(), "Version Read")?;
538 let target = state.resolve_fixture(target_id)?;
539
540 Ok(FHIRRequest::VersionRead(FHIRVersionReadRequest {
541 resource_type: derive_resource_type(operation, Some(target), pointer.path())?,
542 id: fixture_string_field(target, target_id, "id")?,
543 version_id: fixture_version_id(target, target_id)?.into(),
544 }))
545}
546
547async fn search_request(
548 state: &TestState,
549 pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
550 operation: &TestScriptSetupActionOperation,
551) -> Result<FHIRRequest, TestScriptError> {
552 let query = operation
553 .params
554 .as_ref()
555 .and_then(|p| p.value.as_deref())
556 .unwrap_or_default();
557
558 let parameters =
559 parsed_parameters(state, pointer.root(), query, pointer.path(), "Search").await?;
560
561 if let Ok(resource_type) = derive_resource_type(operation, None, pointer.path()) {
562 Ok(FHIRRequest::Search(
563 haste_fhir_client::request::SearchRequest::Type(
564 haste_fhir_client::request::FHIRSearchTypeRequest {
565 resource_type,
566 parameters,
567 },
568 ),
569 ))
570 } else {
571 Ok(FHIRRequest::Search(
572 haste_fhir_client::request::SearchRequest::System(
573 haste_fhir_client::request::FHIRSearchSystemRequest { parameters },
574 ),
575 ))
576 }
577}
578
579async fn history_request(
580 state: &TestState,
581 pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
582 operation: &TestScriptSetupActionOperation,
583) -> Result<FHIRRequest, TestScriptError> {
584 let parameters = parsed_parameters(
585 state,
586 pointer.root(),
587 operation
588 .params
589 .as_ref()
590 .and_then(|p| p.value.as_deref())
591 .unwrap_or_default(),
592 pointer.path(),
593 "History",
594 )
595 .await?;
596
597 if let Some(target_id) = operation
598 .targetId
599 .as_ref()
600 .and_then(|id| id.value.as_deref())
601 {
602 let target = state.resolve_fixture(target_id)?;
603
604 Ok(FHIRRequest::History(HistoryRequest::Instance(
605 FHIRHistoryInstanceRequest {
606 resource_type: derive_resource_type(operation, Some(target), pointer.path())?,
607 id: fixture_string_field(target, target_id, "id")?,
608 parameters,
609 },
610 )))
611 } else if operation.resource.is_some() {
612 Ok(FHIRRequest::History(HistoryRequest::Type(
613 FHIRHistoryTypeRequest {
614 resource_type: derive_resource_type(operation, None, pointer.path())?,
615 parameters,
616 },
617 )))
618 } else {
619 Ok(FHIRRequest::History(HistoryRequest::System(
620 FHIRHistorySystemRequest { parameters },
621 )))
622 }
623}
624
625fn transaction_request(
626 state: &TestState,
627 pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
628 operation: &TestScriptSetupActionOperation,
629) -> Result<FHIRRequest, TestScriptError> {
630 let source_id = require_source_id(operation, pointer.path(), "Transaction")?;
631 let source = state.resolve_fixture(source_id)?;
632 let resource = fixture_resource(source, source_id)?;
633
634 match resource {
635 Resource::Bundle(bundle) => {
636 if bundle.type_ != BundleType::transaction() {
637 return Err(TestScriptError::ExecutionError(format!(
638 "Fixture must be a transaction bundle for transaction operations for sourceId '{source_id}'."
639 )));
640 }
641
642 Ok(FHIRRequest::Transaction(FHIRTransactionRequest {
643 resource: bundle,
644 }))
645 }
646 _ => Err(TestScriptError::ExecutionError(format!(
647 "Fixture '{source_id}' is not a transaction Bundle resource."
648 ))),
649 }
650}
651
652fn batch_request(
653 state: &TestState,
654 pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
655 operation: &TestScriptSetupActionOperation,
656) -> Result<FHIRRequest, TestScriptError> {
657 let source_id = require_source_id(operation, pointer.path(), "Batch")?;
658 let source = state.resolve_fixture(source_id)?;
659 let resource = fixture_resource(source, source_id)?;
660
661 match resource {
662 Resource::Bundle(bundle) => {
663 if bundle.type_ != BundleType::batch() {
664 return Err(TestScriptError::ExecutionError(format!(
665 "Fixture must be a batch bundle for batch operations for sourceId '{source_id}'."
666 )));
667 }
668
669 Ok(FHIRRequest::Batch(FHIRBatchRequest { resource: bundle }))
670 }
671 _ => Err(TestScriptError::ExecutionError(format!(
672 "Fixture '{source_id}' is not a batch Bundle resource."
673 ))),
674 }
675}
676
677fn create_request(
678 state: &TestState,
679 pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
680 operation: &TestScriptSetupActionOperation,
681) -> Result<FHIRRequest, TestScriptError> {
682 let source_id = require_source_id(operation, pointer.path(), "Create")?;
683 let source = state.resolve_fixture(source_id)?;
684 let resource = fixture_resource(source, source_id)?;
685
686 Ok(FHIRRequest::Create(FHIRCreateRequest {
687 resource_type: derive_resource_type(operation, Some(source), pointer.path())?,
688 resource,
689 }))
690}
691
692fn update_request(
693 state: &TestState,
694 pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
695 operation: &TestScriptSetupActionOperation,
696) -> Result<FHIRRequest, TestScriptError> {
697 let source_id = require_source_id(operation, pointer.path(), "Update")?;
698 let source = state.resolve_fixture(source_id)?;
699 let mut resource = fixture_resource(source, source_id)?;
700
701 let target_id = require_target_id(operation, pointer.path(), "Update")?;
702 let target = state.resolve_fixture(target_id)?;
703 let target_resource = fixture_resource(target, target_id)?;
704 let id = fixture_string_field(&target_resource, target_id, "id")?;
705
706 if let Some(body_id) = resource
709 .get_field_mut("id")
710 .and_then(|body_id| (body_id as &mut dyn Any).downcast_mut::<Option<String>>())
711 {
712 *body_id = Some(id.clone());
713 }
714
715 Ok(FHIRRequest::Update(UpdateRequest::Instance(
716 FHIRUpdateInstanceRequest {
717 resource_type: derive_resource_type(operation, Some(target), pointer.path())?,
718 id,
719 resource,
720 },
721 )))
722}
723
724async fn conditional_update_request(
725 state: &TestState,
726 pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
727 operation: &TestScriptSetupActionOperation,
728) -> Result<FHIRRequest, TestScriptError> {
729 let source_id = require_source_id(operation, pointer.path(), "UpdateCreate")?;
730 let source = state.resolve_fixture(source_id)?;
731 let resource = fixture_resource(source, source_id)?;
732
733 let parameters = parsed_parameters(
734 state,
735 pointer.root(),
736 operation
737 .params
738 .as_ref()
739 .and_then(|p| p.value.as_deref())
740 .unwrap_or_default(),
741 pointer.path(),
742 "UpdateCreate",
743 )
744 .await?;
745
746 Ok(FHIRRequest::Update(UpdateRequest::Conditional(
747 FHIRConditionalUpdateRequest {
748 resource_type: derive_resource_type(operation, Some(source), pointer.path())?,
749 parameters,
750 resource,
751 },
752 )))
753}
754
755fn delete_request(
756 state: &TestState,
757 pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
758 operation: &TestScriptSetupActionOperation,
759) -> Result<FHIRRequest, TestScriptError> {
760 let target_id = require_target_id(operation, pointer.path(), "Delete")?;
761 let target = state.resolve_fixture(target_id)?;
762
763 Ok(FHIRRequest::Delete(DeleteRequest::Instance(
764 FHIRDeleteInstanceRequest {
765 resource_type: derive_resource_type(operation, Some(target), pointer.path())?,
766 id: fixture_string_field(target, target_id, "id")?,
767 },
768 )))
769}
770
771fn conditional_delete_request(
772 _state: &TestState,
773 pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
774 operation: &TestScriptSetupActionOperation,
775) -> Result<FHIRRequest, TestScriptError> {
776 let parameters = ParsedParameters::try_from(
777 operation
778 .params
779 .as_ref()
780 .and_then(|p| p.value.as_deref())
781 .unwrap_or_default(),
782 )
783 .map_err(|e| {
784 TestScriptError::ExecutionError(format!(
785 "Failed to parse parameters for conditional delete operation at '{}': {}",
786 pointer.path(),
787 e
788 ))
789 })?;
790
791 if operation.resource.is_some() {
792 Ok(FHIRRequest::Delete(DeleteRequest::Type(
793 FHIRDeleteTypeRequest {
794 resource_type: derive_resource_type(operation, None, pointer.path())?,
795 parameters,
796 },
797 )))
798 } else {
799 Ok(FHIRRequest::Delete(DeleteRequest::System(
800 FHIRDeleteSystemRequest { parameters },
801 )))
802 }
803}
804
805fn patch_request(
810 state: &TestState,
811 pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
812 operation: &TestScriptSetupActionOperation,
813) -> Result<FHIRRequest, TestScriptError> {
814 let target_id = require_target_id(operation, pointer.path(), "Patch")?;
815 let target = state.resolve_fixture(target_id)?;
816
817 let source_id = require_source_id(operation, pointer.path(), "Patch")?;
818 let source = state.resolve_fixture(source_id)?;
819 let Resource::Binary(binary) = fixture_resource(source, source_id)? else {
820 return Err(TestScriptError::ExecutionError(format!(
821 "Patch source fixture '{source_id}' must be a Binary resource containing the JSON Patch document."
822 )));
823 };
824
825 let content_type = binary.contentType.value.as_deref().unwrap_or_default();
826 if content_type != "application/json-patch+json" {
827 return Err(TestScriptError::ExecutionError(format!(
828 "Unsupported patch content type '{content_type}' for fixture '{source_id}'; only 'application/json-patch+json' is supported."
829 )));
830 }
831
832 let encoded = binary
833 .data
834 .as_ref()
835 .and_then(|d| d.value.as_deref())
836 .ok_or_else(|| {
837 TestScriptError::ExecutionError(format!(
838 "Patch source fixture '{source_id}' Binary resource has no 'data'."
839 ))
840 })?;
841
842 let decoded = BASE64.decode(encoded).map_err(|e| {
843 TestScriptError::ExecutionError(format!(
844 "Failed to base64-decode patch content for fixture '{source_id}': {e}"
845 ))
846 })?;
847
848 let patch: json_patch::Patch = serde_json::from_slice(&decoded).map_err(|e| {
849 TestScriptError::ExecutionError(format!(
850 "Failed to parse JSON Patch document for fixture '{source_id}': {e}"
851 ))
852 })?;
853
854 Ok(FHIRRequest::Patch(FHIRPatchRequest {
855 resource_type: derive_resource_type(operation, Some(target), pointer.path())?,
856 id: fixture_string_field(target, target_id, "id")?,
857 patch,
858 }))
859}
860
861fn invoke_request(
862 state: &TestState,
863 pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
864 operation: &TestScriptSetupActionOperation,
865) -> Result<FHIRRequest, TestScriptError> {
866 let op_code = operation
867 .url
868 .as_ref()
869 .and_then(|u| u.value.as_deref())
870 .ok_or_else(|| {
871 TestScriptError::ExecutionError(format!(
872 "Invoke operation requires url at '{}' which is used for the operation code.",
873 pointer.path()
874 ))
875 })?;
876
877 let fhir_operation = Operation::new(op_code);
878
879 let source_id = require_source_id(operation, pointer.path(), "Invoke")?;
880 let source = state.resolve_fixture(source_id)?;
881
882 let Resource::Parameters(parameters) = fixture_resource(source, source_id)? else {
883 return Err(TestScriptError::ExecutionError(format!(
884 "Source fixture '{source_id}' is not a Parameters resource."
885 )));
886 };
887
888 if let Some(target_id) = operation
889 .targetId
890 .as_ref()
891 .and_then(|id| id.value.as_deref())
892 {
893 let target = state.resolve_fixture(target_id)?;
894
895 Ok(FHIRRequest::Invocation(InvocationRequest::Instance(
896 FHIRInvokeInstanceRequest {
897 operation: fhir_operation,
898 resource_type: derive_resource_type(operation, Some(target), pointer.path())?,
899 id: fixture_string_field(target, target_id, "id")?,
900 parameters,
901 },
902 )))
903 } else if let Ok(resource_type) = derive_resource_type(operation, None, pointer.path()) {
904 Ok(FHIRRequest::Invocation(InvocationRequest::Type(
905 FHIRInvokeTypeRequest {
906 operation: fhir_operation,
907 resource_type,
908 parameters,
909 },
910 )))
911 } else {
912 Ok(FHIRRequest::Invocation(InvocationRequest::System(
913 FHIRInvokeSystemRequest {
914 operation: fhir_operation,
915 parameters,
916 },
917 )))
918 }
919}
920
921fn get_operation(
922 pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
923) -> Result<&TestScriptSetupActionOperation, TestScriptError> {
924 pointer.value().ok_or_else(|| {
925 TestScriptError::ExecutionError(format!(
926 "Failed to retrieve TestScript operation at '{}'.",
927 pointer.path()
928 ))
929 })
930}
931
932fn get_operation_type(operation: &TestScriptSetupActionOperation) -> Option<&str> {
933 operation
934 .type_
935 .as_ref()
936 .and_then(|t| t.code.as_ref())
937 .and_then(|c| c.value.as_deref())
938}
939
940fn require_target_id<'a>(
941 operation: &'a TestScriptSetupActionOperation,
942 path: &str,
943 operation_name: &str,
944) -> Result<&'a str, TestScriptError> {
945 operation
946 .targetId
947 .as_ref()
948 .and_then(|id| id.value.as_deref())
949 .ok_or_else(|| {
950 TestScriptError::ExecutionError(format!(
951 "{operation_name} operation requires targetId at '{path}'."
952 ))
953 })
954}
955
956fn require_source_id<'a>(
957 operation: &'a TestScriptSetupActionOperation,
958 path: &str,
959 operation_name: &str,
960) -> Result<&'a str, TestScriptError> {
961 operation
962 .sourceId
963 .as_ref()
964 .and_then(|id| id.value.as_deref())
965 .ok_or_else(|| {
966 TestScriptError::ExecutionError(format!(
967 "{operation_name} operation requires sourceId at '{path}'."
968 ))
969 })
970}
971
972fn fixture_string_field(
973 fixture: &dyn MetaValue,
974 fixture_name: &str,
975 field: &str,
976) -> Result<String, TestScriptError> {
977 fixture
978 .get_field(field)
979 .ok_or_else(|| {
980 TestScriptError::ExecutionError(format!(
981 "Fixture '{fixture_name}' does not have '{field}' field."
982 ))
983 })?
984 .as_any()
985 .downcast_ref::<String>()
986 .cloned()
987 .ok_or_else(|| {
988 TestScriptError::ExecutionError(format!(
989 "Field '{field}' on fixture '{fixture_name}' is not a String."
990 ))
991 })
992}
993
994fn fixture_version_id(
995 fixture: &dyn MetaValue,
996 fixture_name: &str,
997) -> Result<String, TestScriptError> {
998 fixture
999 .get_field("meta")
1000 .and_then(|meta| meta.get_field("versionId"))
1001 .ok_or_else(|| {
1002 TestScriptError::ExecutionError(format!(
1003 "Fixture '{fixture_name}' does not have a 'versionId' field."
1004 ))
1005 })?
1006 .as_any()
1007 .downcast_ref::<FHIRId>()
1008 .and_then(|v| v.value.clone())
1009 .ok_or_else(|| {
1010 TestScriptError::ExecutionError(format!(
1011 "Fixture '{fixture_name}' does not have a valid 'versionId' field."
1012 ))
1013 })
1014}
1015
1016fn fixture_resource(
1017 fixture: &dyn MetaValue,
1018 fixture_name: &str,
1019) -> Result<Resource, TestScriptError> {
1020 (fixture as &dyn Any)
1021 .downcast_ref::<Resource>()
1022 .cloned()
1023 .ok_or_else(|| {
1024 TestScriptError::ExecutionError(format!("Fixture '{fixture_name}' is not a Resource."))
1025 })
1026}
1027
1028async fn parsed_parameters(
1029 state: &TestState,
1030 root: TypedPointer<TestScript, TestScript>,
1031 raw: &str,
1032 path: &str,
1033 operation: &str,
1034) -> Result<ParsedParameters, TestScriptError> {
1035 let evaluated = evaluate_variable(state, root, raw).await?;
1036
1037 ParsedParameters::try_from(evaluated.as_str()).map_err(|e| {
1038 TestScriptError::ExecutionError(format!(
1039 "Failed to parse parameters for {operation} operation at '{path}': {e}"
1040 ))
1041 })
1042}
1043
1044async fn testscript_request_headers(
1049 state: &TestState,
1050 pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
1051 operation: &TestScriptSetupActionOperation,
1052) -> Result<Option<HeaderMap>, TestScriptError> {
1053 let Some(request_headers) = operation.requestHeader.as_ref() else {
1054 return Ok(None);
1055 };
1056
1057 let mut headers = HeaderMap::with_capacity(request_headers.len());
1058
1059 for request_header in request_headers {
1060 let field = request_header.field.value.as_deref().ok_or_else(|| {
1061 TestScriptError::ExecutionError(format!(
1062 "Missing requestHeader field for operation at '{}'.",
1063 pointer.path()
1064 ))
1065 })?;
1066
1067 let raw_value = request_header.value.value.as_deref().unwrap_or_default();
1068 let value = evaluate_variable(state, pointer.root(), raw_value).await?;
1069
1070 let name = HeaderName::try_from(field).map_err(|e| {
1071 TestScriptError::ExecutionError(format!(
1072 "Invalid requestHeader field '{field}' for operation at '{}': {e}",
1073 pointer.path()
1074 ))
1075 })?;
1076
1077 let value = HeaderValue::from_str(&value).map_err(|e| {
1078 TestScriptError::ExecutionError(format!(
1079 "Invalid requestHeader value for '{field}' for operation at '{}': {e}",
1080 pointer.path()
1081 ))
1082 })?;
1083
1084 headers.insert(name, value);
1085 }
1086
1087 Ok((!headers.is_empty()).then_some(headers))
1088}
1089
1090const INDEX_POLL_INTERVAL: Duration = Duration::from_millis(100);
1092
1093fn indexed_write(response: &FHIRResponse) -> Option<IndexedWrite> {
1095 let (resource, deleted) = match response {
1096 FHIRResponse::Create(created) => (&created.resource, false),
1097 FHIRResponse::Update(updated) => (&updated.resource, false),
1098 FHIRResponse::Patch(patched) => (&patched.resource, false),
1099 FHIRResponse::Delete(DeleteResponse::Instance(deleted)) => (&deleted.resource, true),
1100 _ => return None,
1101 };
1102 let json = serde_json::to_value(resource).ok()?;
1103 Some(IndexedWrite {
1104 resource_type: ResourceType::try_from(json.get("resourceType")?.as_str()?).ok()?,
1105 id: json.get("id")?.as_str()?.to_string(),
1106 version_id: if deleted {
1107 None
1108 } else {
1109 Some(json.pointer("/meta/versionId")?.as_str()?.to_string())
1110 },
1111 })
1112}
1113
1114fn reads_search_index(request: &FHIRRequest) -> bool {
1119 matches!(
1120 request,
1121 FHIRRequest::Search(_)
1122 | FHIRRequest::Delete(DeleteRequest::Type(_) | DeleteRequest::System(_))
1123 | FHIRRequest::Update(UpdateRequest::Conditional(_))
1124 | FHIRRequest::Invocation(_)
1125 )
1126}
1127
1128async fn wait_for_index<CTX: Clone, Client: FHIRClient<CTX, OperationOutcomeError>>(
1132 client: &Client,
1133 ctx: &CTX,
1134 write: &IndexedWrite,
1135 timeout: Duration,
1136) {
1137 let Ok(parameters) = ParsedParameters::try_from(format!("_id={}", write.id).as_str()) else {
1138 return;
1139 };
1140 let request = FHIRRequest::Search(SearchRequest::Type(FHIRSearchTypeRequest {
1141 resource_type: write.resource_type.clone(),
1142 parameters,
1143 }));
1144 let deadline = tokio::time::Instant::now() + timeout;
1145
1146 loop {
1147 let versions: Vec<String> = match client.request(ctx.clone(), request.clone()).await {
1148 Ok(FHIRResponse::Search(SearchResponse::Type(response))) => response
1149 .bundle
1150 .entry
1151 .unwrap_or_default()
1152 .into_iter()
1153 .filter_map(|entry| {
1154 let json = serde_json::to_value(entry.resource?).ok()?;
1155 Some(json.pointer("/meta/versionId")?.as_str()?.to_string())
1156 })
1157 .collect(),
1158 _ => vec![],
1159 };
1160 let visible = match &write.version_id {
1161 Some(version_id) => versions.contains(version_id),
1162 None => versions.is_empty(),
1163 };
1164 if visible {
1165 return;
1166 }
1167 if tokio::time::Instant::now() >= deadline {
1168 tracing::warn!(
1169 resource_type = write.resource_type.as_ref(),
1170 id = write.id,
1171 "Timed out waiting for a write to be indexed"
1172 );
1173 return;
1174 }
1175 tokio::time::sleep(INDEX_POLL_INTERVAL).await;
1176 }
1177}
1178
1179const POLL_EXTENSION_URL: &str =
1183 "https://haste.health/fhir/StructureDefinition/testscript-poll-while-response-code";
1184
1185const POLL_TIMEOUT: Duration = Duration::from_secs(60);
1187
1188const POLL_INTERVAL: Duration = Duration::from_secs(1);
1190
1191fn poll_while(
1193 operation: &TestScriptSetupActionOperation,
1194 path: &str,
1195) -> Result<Option<StatusCode>, TestScriptError> {
1196 let Some(extension) = operation
1197 .extension
1198 .as_ref()
1199 .and_then(|extensions| extensions.iter().find(|e| e.url == POLL_EXTENSION_URL))
1200 else {
1201 return Ok(None);
1202 };
1203
1204 match &extension.value {
1205 Some(ExtensionValueTypeChoice::String(code)) => code
1206 .value
1207 .as_deref()
1208 .and_then(|code| StatusCode::from_bytes(code.trim().as_bytes()).ok()),
1209 _ => None,
1210 }
1211 .map(Some)
1212 .ok_or_else(|| {
1213 TestScriptError::ExecutionError(format!(
1214 "The poll extension on the operation at '{path}' needs a valueString holding an \
1215 HTTP status code."
1216 ))
1217 })
1218}
1219
1220fn header_value(recorded: &RecordedResponse, field: &str) -> Option<String> {
1222 let headers = &recorded.http.as_ref()?.headers;
1223 let values: Vec<&str> = headers
1224 .get_all(field)
1225 .iter()
1226 .filter_map(|value| value.to_str().ok())
1227 .collect();
1228
1229 (!values.is_empty()).then(|| values.join(", "))
1230}
1231
1232fn retry_after(recorded: &RecordedResponse) -> Option<Duration> {
1234 header_value(recorded, header::RETRY_AFTER.as_str())?
1235 .trim()
1236 .parse::<u64>()
1237 .ok()
1238 .map(Duration::from_secs)
1239}
1240
1241async fn send_operation<CTX: Clone + WithRequestHeaders, Client: FHIRHttpExchange<CTX>>(
1242 client: &Client,
1243 ctx: CTX,
1244 request: &OperationRequest,
1245) -> RecordedResponse {
1246 match request {
1247 OperationRequest::Interaction(request) => {
1248 let (http, result) = client
1249 .request_with_metadata(ctx, request.as_ref().clone())
1250 .await;
1251 let response = match result {
1252 Ok(response) => Response::Interaction(Box::new(response)),
1253 Err(error) => Response::OperationError(Arc::new(error)),
1254 };
1255
1256 RecordedResponse { response, http }
1257 }
1258 OperationRequest::Http { method, url } => match client.send(ctx, method.clone(), url).await
1259 {
1260 Ok((metadata, body)) => RecordedResponse {
1261 response: Response::Http(
1264 serde_json::from_slice::<Resource>(&body).ok().map(Box::new),
1265 ),
1266 http: Some(metadata),
1267 },
1268 Err(error) => RecordedResponse {
1269 response: Response::OperationError(Arc::new(error)),
1270 http: None,
1271 },
1272 },
1273 }
1274}
1275
1276async fn send_polling<CTX: Clone + WithRequestHeaders, Client: FHIRHttpExchange<CTX>>(
1280 client: &Client,
1281 ctx: CTX,
1282 request: &OperationRequest,
1283 poll_while: Option<StatusCode>,
1284) -> RecordedResponse {
1285 let Some(poll_while) = poll_while else {
1286 return send_operation(client, ctx, request).await;
1287 };
1288 let deadline = tokio::time::Instant::now() + POLL_TIMEOUT;
1289
1290 loop {
1291 let recorded = send_operation(client, ctx.clone(), request).await;
1292 let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
1293 if recorded.http.as_ref().map(|http| http.status) != Some(poll_while) || remaining.is_zero()
1294 {
1295 return recorded;
1296 }
1297
1298 let wait = retry_after(&recorded)
1299 .unwrap_or_default()
1300 .max(POLL_INTERVAL)
1301 .min(remaining);
1302 tokio::time::sleep(wait).await;
1303 }
1304}
1305
1306async fn run_operation<CTX: Clone + WithRequestHeaders, Client: FHIRHttpExchange<CTX>>(
1307 client: &Client,
1308 ctx: CTX,
1309 state: Arc<Mutex<TestState>>,
1310 pointer: TypedPointer<TestScript, TestScriptSetupActionOperation>,
1311 options: Arc<TestRunnerOptions>,
1312) -> Result<TestResult<TestReportSetupActionOperation>, TestScriptError> {
1313 let operation = pointer.value().ok_or_else(|| {
1314 TestScriptError::ExecutionError(format!(
1315 "Failed to retrieve TestScript operation at '{}'.",
1316 pointer.path()
1317 ))
1318 })?;
1319
1320 let mut state_guard = state.lock().await;
1321 let request = testscript_operation_to_request(&state_guard, &pointer).await?;
1322
1323 if let Some(timeout) = options.index_wait_timeout
1324 && let OperationRequest::Interaction(fhir_request) = &request
1325 && reads_search_index(fhir_request)
1326 && !state_guard.unindexed_writes.is_empty()
1327 {
1328 let deadline = tokio::time::Instant::now() + timeout;
1330 for write in std::mem::take(&mut state_guard.unindexed_writes) {
1331 let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
1332 if remaining.is_zero() {
1333 break;
1334 }
1335 wait_for_index(client, &ctx, &write, remaining).await;
1336 }
1337 }
1338
1339 let ctx = match testscript_request_headers(&state_guard, &pointer, operation).await? {
1340 Some(headers) => ctx.with_request_headers(headers),
1341 None => ctx,
1342 };
1343
1344 let poll_while = poll_while(operation, pointer.path())?;
1345 let recorded = send_polling(client, ctx, &request, poll_while).await;
1346
1347 if let Some(wait_duration) = options.wait_between_operations {
1348 tokio::time::sleep(wait_duration).await;
1349 }
1350
1351 let failure = match &recorded.response {
1352 Response::Interaction(response) => {
1353 if let Some(write) = indexed_write(response) {
1354 state_guard.unindexed_writes.retain(|earlier| {
1357 (&earlier.resource_type, &earlier.id) != (&write.resource_type, &write.id)
1358 });
1359 state_guard.unindexed_writes.push(write);
1360 }
1361 None
1362 }
1363 Response::OperationError(error) => Some(error.to_string()),
1364 Response::Http(_) => recorded
1365 .http
1366 .as_ref()
1367 .filter(|http| !http.status.is_success())
1368 .map(|http| format!("HTTP {}", http.status)),
1369 };
1370
1371 let fhir_request = match request {
1372 OperationRequest::Interaction(request) => Some(*request),
1373 OperationRequest::Http { .. } => None,
1374 };
1375 associate_request_response_variables(&mut state_guard, operation, fhir_request, recorded);
1376 drop(state_guard);
1377
1378 let value = match failure {
1379 None => TestReportSetupActionOperation {
1380 result: ReportActionResultCodes::pass(),
1381 ..Default::default()
1382 },
1383 Some(failure) => {
1384 tracing::warn!(
1385 path = pointer.path(),
1386 operation.label = operation
1387 .label
1388 .as_ref()
1389 .and_then(|l| l.value.as_deref())
1390 .unwrap_or("<no-label>"),
1391 operation.operation_type = get_operation_type(operation).unwrap_or("<unknown>"),
1392 error = %failure,
1393 "TestScript operation failed"
1394 );
1395
1396 TestReportSetupActionOperation {
1397 result: ReportActionResultCodes::warning(),
1398 message: Some(Box::new(FHIRMarkdown {
1399 value: Some(format!("Operation failed: {failure}")),
1400 ..Default::default()
1401 })),
1402 ..Default::default()
1403 }
1404 }
1405 };
1406
1407 Ok(TestResult {
1408 state: state.clone(),
1409 value,
1410 })
1411}
1412
1413fn get_source<'a>(
1414 state: &'a TestState,
1415 assertion: &TestScriptSetupActionAssert,
1416) -> Result<Option<&'a dyn MetaValue>, TestScriptError> {
1417 if let Some(source_id) = assertion.sourceId.as_ref().and_then(|id| id.value.as_ref()) {
1418 let source = state.resolve_fixture(source_id)?;
1419 Ok(Some(source))
1420 } else {
1421 match assertion
1422 .direction
1423 .as_ref()
1424 .unwrap_or(&AssertDirectionCodes::response())
1425 {
1426 assertion if assertion == &AssertDirectionCodes::request() => {
1427 if let Some(request) = state.latest_request.as_ref() {
1428 request_to_meta_value(request)
1429 .ok_or_else(|| TestScriptError::InvalidFixture)
1430 .map(Some)
1431 } else {
1432 Ok(None)
1433 }
1434 }
1435 assertion if assertion == &AssertDirectionCodes::response() => {
1436 if let Some(recorded) = state.latest_response.as_ref() {
1437 response_to_meta_value(&recorded.response)
1438 .ok_or_else(|| TestScriptError::InvalidFixture)
1439 .map(Some)
1440 } else {
1441 Ok(None)
1442 }
1443 }
1444 _ => Err(TestScriptError::ExecutionError(
1445 "Assert direction cannot be 'null' when sourceId is not provided.".to_string(),
1446 )),
1447 }
1448 }
1449}
1450
1451fn evaluate_operator(
1452 operator: &BoundCode<AssertOperatorCodes>,
1453 a: &Vec<conversion::ConvertedValue>,
1454 b: &Vec<conversion::ConvertedValue>,
1455) -> bool {
1456 match operator {
1457 operator
1458 if operator == &AssertOperatorCodes::equals()
1459 || operator == &AssertOperatorCodes::null() =>
1460 {
1461 a == b
1462 }
1463 operator if operator == &AssertOperatorCodes::not_equals() => !(a == b),
1464
1465 operator if operator == &AssertOperatorCodes::contains() => {
1466 if a.len() != 1 || b.len() != 1 {
1467 return false;
1468 }
1469
1470 match (&a[0], &b[0]) {
1471 (ConvertedValue::String(a_str), ConvertedValue::String(b_str)) => {
1472 a_str.contains(b_str)
1473 }
1474 _ => false,
1475 }
1476 }
1477 operator if operator == &AssertOperatorCodes::empty() => {
1478 todo!("Empty operator not implemented")
1479 }
1480 operator if operator == &AssertOperatorCodes::eval() => {
1481 todo!("Eval operator not implemented")
1482 }
1483 operator if operator == &AssertOperatorCodes::greater_than() => {
1484 todo!("GreaterThan operator not implemented")
1485 }
1486 operator if operator == &AssertOperatorCodes::in_() => todo!("In operator not implemented"),
1487 operator if operator == &AssertOperatorCodes::less_than() => {
1488 todo!("LessThan operator not implemented")
1489 }
1490 operator if operator == &AssertOperatorCodes::not_contains() => {
1491 todo!("NotContains operator not implemented")
1492 }
1493 operator if operator == &AssertOperatorCodes::not_empty() => {
1494 todo!("NotEmpty operator not implemented")
1495 }
1496 operator if operator == &AssertOperatorCodes::not_in() => {
1497 todo!("NotIn operator not implemented")
1498 }
1499 _ => {
1500 todo!("Operator '{:?}' not implemented", operator)
1501 }
1502 }
1503 }
1505
1506async fn derive_comparison_to(
1507 state: &TestState,
1508 assertion: &TestScriptSetupActionAssert,
1509) -> Result<Vec<ConvertedValue>, TestScriptError> {
1510 if let Some(comparision_fixture_id) = assertion
1511 .compareToSourceId
1512 .as_ref()
1513 .and_then(|c| c.value.as_ref())
1514 {
1515 let comparison_fixture = state.resolve_fixture(comparision_fixture_id)?;
1516
1517 let Some(comparison_expression) = assertion
1518 .compareToSourceExpression
1519 .as_ref()
1520 .and_then(|exp| exp.value.as_ref())
1521 else {
1522 return Err(TestScriptError::ExecutionError(
1523 "compareToSourceExpression is required when compareToSourceId is provided."
1524 .to_string(),
1525 ));
1526 };
1527
1528 let result = state
1529 .fp_engine
1530 .evaluate(comparison_expression, vec![comparison_fixture])
1531 .await
1532 .map_err(|e| {
1533 TestScriptError::ExecutionError(format!(
1534 "FHIRPath evaluation error for comparison fixture '{comparision_fixture_id}': {e}"
1535 ))
1536 })?;
1537
1538 Ok(result
1539 .iter()
1540 .map(conversion::convert_meta_value)
1541 .collect::<Vec<_>>())
1542 } else if let Some(value) = assertion.value.as_ref().and_then(|v| v.value.as_ref())
1543 && let Some(converted_value) = conversion::convert_string_value(value.as_ref())
1544 {
1545 Ok(vec![converted_value])
1546 } else {
1547 Err(TestScriptError::ExecutionError(
1548 "Failed to derive comparison value for assertion.".to_string(),
1549 ))
1550 }
1551}
1552
1553fn assert_label(assertion: &TestScriptSetupActionAssert) -> &str {
1556 assertion
1557 .label
1558 .as_ref()
1559 .and_then(|l| l.value.as_deref())
1560 .or_else(|| {
1561 assertion
1562 .description
1563 .as_ref()
1564 .and_then(|d| d.value.as_deref())
1565 })
1566 .unwrap_or("<no-label>")
1567}
1568
1569async fn run_assertion(
1572 state: Arc<Mutex<TestState>>,
1573 pointer: TypedPointer<TestScript, TestScriptSetupActionAssert>,
1574) -> Result<TestResult<TestReportSetupActionAssert>, TestScriptError> {
1575 let assertion = pointer.value().ok_or_else(|| {
1576 TestScriptError::ExecutionError(format!(
1577 "Failed to retrieve TestScript assertion at '{}'.",
1578 pointer.path()
1579 ))
1580 })?;
1581
1582 let mut state_guard = state.lock().await;
1583
1584 let default = AssertOperatorCodes::equals();
1585 let operator = assertion.operator.as_ref().unwrap_or(&default);
1586
1587 let failure_message = if let Some(message) =
1590 evaluate_http_assertion(&state_guard, assertion, pointer.path(), operator)?
1591 {
1592 Some(message)
1593 } else if assertion.resource.is_some() || assertion.expression.is_some() {
1594 let Some(source) = get_source(&state_guard, assertion)? else {
1595 return Err(TestScriptError::ExecutionError(format!(
1596 "Failed to resolve source for assertion at '{}'.",
1597 pointer.path()
1598 )));
1599 };
1600
1601 if let Some(message) =
1602 evaluate_resource_assertion(assertion, pointer.path(), source, operator)
1603 {
1604 Some(message)
1605 } else if let Some(expression) =
1606 assertion.expression.as_ref().and_then(|e| e.value.as_ref())
1607 {
1608 evaluate_expression_assertion(
1609 &state_guard,
1610 assertion,
1611 pointer.path(),
1612 source,
1613 operator,
1614 expression,
1615 )
1616 .await?
1617 } else {
1618 None
1619 }
1620 } else {
1621 None
1622 };
1623
1624 let (result, message) = match failure_message {
1625 Some(message) => {
1626 tracing::error!(
1627 assert.label = assert_label(assertion),
1628 path = pointer.path(),
1629 "{message}"
1630 );
1631
1632 state_guard.result = ReportResultCodes::fail();
1633
1634 (
1635 ReportActionResultCodes::fail(),
1636 Some(Box::new(FHIRMarkdown {
1637 value: Some(message),
1638 ..Default::default()
1639 })),
1640 )
1641 }
1642 None => (ReportActionResultCodes::pass(), None),
1643 };
1644
1645 Ok(TestResult {
1646 state: state.clone(),
1647 value: TestReportSetupActionAssert {
1648 result,
1649 message,
1650 ..Default::default()
1651 },
1652 })
1653}
1654
1655fn response_code_status(code: &str) -> Option<u16> {
1657 Some(match code {
1658 "okay" => 200,
1659 "created" => 201,
1660 "noContent" => 204,
1661 "notModified" => 304,
1662 "bad" => 400,
1663 "forbidden" => 403,
1664 "notFound" => 404,
1665 "methodNotAllowed" => 405,
1666 "conflict" => 409,
1667 "gone" => 410,
1668 "preconditionFailed" => 412,
1669 "unprocessable" => 422,
1670 _ => return None,
1671 })
1672}
1673
1674fn compare_http_value(operator: &str, actual: Option<&str>, expected: &str) -> Option<bool> {
1678 let listed = || {
1679 expected
1680 .split(',')
1681 .map(str::trim)
1682 .any(|item| Some(item) == actual)
1683 };
1684 let contains = actual.is_some_and(|actual| actual.contains(expected));
1685
1686 Some(match operator {
1687 "equals" => actual == Some(expected),
1688 "notEquals" => actual != Some(expected),
1689 "in" => listed(),
1690 "notIn" => !listed(),
1691 "contains" => contains,
1692 "notContains" => !contains,
1693 "empty" => actual.is_none_or(str::is_empty),
1694 "notEmpty" => actual.is_some_and(|actual| !actual.is_empty()),
1695 _ => return None,
1696 })
1697}
1698
1699fn evaluate_http_assertion(
1704 state: &TestState,
1705 assertion: &TestScriptSetupActionAssert,
1706 path: &str,
1707 operator: &BoundCode<AssertOperatorCodes>,
1708) -> Result<Option<String>, TestScriptError> {
1709 let response_code = assertion
1710 .responseCode
1711 .as_ref()
1712 .and_then(|code| code.value.as_deref());
1713 let response = assertion.response.as_ref().and_then(BoundCode::as_str);
1714 let header_field = assertion
1715 .headerField
1716 .as_ref()
1717 .and_then(|field| field.value.as_deref());
1718 let content_type = assertion
1719 .contentType
1720 .as_ref()
1721 .and_then(|content_type| content_type.value.as_deref());
1722
1723 if response_code.is_none()
1724 && response.is_none()
1725 && header_field.is_none()
1726 && content_type.is_none()
1727 {
1728 return Ok(None);
1729 }
1730
1731 let label = assert_label(assertion);
1732 let error = |message: String| {
1733 TestScriptError::ExecutionError(format!("Assertion '{label}' at '{path}': {message}"))
1734 };
1735 if assertion.direction.as_ref() == Some(&AssertDirectionCodes::request()) {
1736 return Err(error(
1737 "responseCode, response, headerField and contentType are checked against \
1738 responses only."
1739 .to_string(),
1740 ));
1741 }
1742
1743 let source_id = assertion
1744 .sourceId
1745 .as_ref()
1746 .and_then(|id| id.value.as_deref());
1747 let recorded = state.resolve_response(source_id)?;
1748 let Some(http) = recorded.http.as_ref() else {
1749 return Ok(Some(format!(
1750 "Assertion '{label}' at '{path}' failed: no HTTP response arrived."
1751 )));
1752 };
1753 let status = http.status.as_u16().to_string();
1754
1755 let (what, actual, expected) = if let Some(code) = response_code {
1757 ("response code".to_string(), Some(status), code.to_string())
1758 } else if let Some(code) = response {
1759 let expected = response_code_status(code)
1760 .ok_or_else(|| error(format!("unknown response code '{code}'.")))?;
1761 (
1762 format!("response code (for '{code}')"),
1763 Some(status),
1764 expected.to_string(),
1765 )
1766 } else if let Some(field) = header_field {
1767 let expected = assertion
1768 .value
1769 .as_ref()
1770 .and_then(|value| value.value.as_deref())
1771 .unwrap_or_default();
1772 (
1773 format!("header '{field}'"),
1774 header_value(recorded, field),
1775 expected.to_string(),
1776 )
1777 } else {
1778 let actual = header_value(recorded, header::CONTENT_TYPE.as_str()).map(|value| {
1780 value
1781 .split(';')
1782 .next()
1783 .unwrap_or_default()
1784 .trim()
1785 .to_ascii_lowercase()
1786 });
1787 (
1788 "content type".to_string(),
1789 actual,
1790 content_type.unwrap_or_default().to_ascii_lowercase(),
1791 )
1792 };
1793
1794 let operator = operator.as_str().unwrap_or("equals");
1795 let passed = compare_http_value(operator, actual.as_deref(), &expected).ok_or_else(|| {
1796 error(format!(
1797 "operator '{operator}' is not supported for responseCode, response, headerField \
1798 or contentType."
1799 ))
1800 })?;
1801 if passed {
1802 return Ok(None);
1803 }
1804
1805 let actual = actual.map_or_else(|| "absent".to_string(), |actual| format!("'{actual}'"));
1806 let expected = match operator {
1807 "empty" | "notEmpty" => String::new(),
1808 _ => format!(" '{expected}'"),
1809 };
1810 Ok(Some(format!(
1811 "Assertion '{label}' at '{path}' failed: {what} was {actual}, expected {operator}{expected}."
1812 )))
1813}
1814
1815fn evaluate_resource_assertion(
1816 assertion: &TestScriptSetupActionAssert,
1817 path: &str,
1818 source: &dyn MetaValue,
1819 operator: &BoundCode<AssertOperatorCodes>,
1820) -> Option<String> {
1821 let resource_string = assertion.resource.as_ref()?.as_str()?;
1822
1823 let operation_evaluation_result = evaluate_operator(
1824 operator,
1825 &vec![conversion::ConvertedValue::String(
1826 resource_string.to_string(),
1827 )],
1828 &vec![conversion::ConvertedValue::String(
1829 source.fhir_type().to_string(),
1830 )],
1831 );
1832
1833 if operation_evaluation_result {
1834 return None;
1835 }
1836
1837 Some(format!(
1838 "Assertion '{}' at '{}' failed: resource type '{resource_string}' does not match '{}'.",
1839 assert_label(assertion),
1840 path,
1841 source.fhir_type()
1842 ))
1843}
1844
1845async fn evaluate_expression_assertion(
1846 state_guard: &TestState,
1847 assertion: &TestScriptSetupActionAssert,
1848 path: &str,
1849 source: &dyn MetaValue,
1850 operator: &BoundCode<AssertOperatorCodes>,
1851 expression: &str,
1852) -> Result<Option<String>, TestScriptError> {
1853 let comparison_to = derive_comparison_to(state_guard, assertion).await?;
1854
1855 let Ok(result) = state_guard
1856 .fp_engine
1857 .evaluate(expression, vec![source])
1858 .await
1859 else {
1860 tracing::error!(
1861 assert.label = assert_label(assertion),
1862 path = path,
1863 expression = expression,
1864 "Assertion '{}' at '{}' failed: FHIRPath expression '{expression}' failed to evaluate.",
1865 assert_label(assertion),
1866 path,
1867 );
1868
1869 return Err(TestScriptError::ExecutionError(format!(
1870 "Assertion '{}' at '{}': FHIRPath expression '{expression}' failed to evaluate.",
1871 assert_label(assertion),
1872 path,
1873 )));
1874 };
1875
1876 let converted_values = result
1877 .iter()
1878 .map(conversion::convert_meta_value)
1879 .collect::<Vec<_>>();
1880
1881 let operation_evaluation_result =
1882 evaluate_operator(operator, &converted_values, &comparison_to);
1883
1884 if operation_evaluation_result {
1885 return Ok(None);
1886 }
1887
1888 Ok(Some(format!(
1889 "Assertion '{}' at '{}' failed: '{converted_values:?}' {operator:?} '{comparison_to:?}'.",
1890 assert_label(assertion),
1891 path,
1892 )))
1893}
1894
1895async fn run_action<CTX: Clone + WithRequestHeaders, Client: FHIRHttpExchange<CTX>>(
1896 client: &Client,
1897 ctx: CTX,
1898 state: Arc<Mutex<TestState>>,
1899 pointer: TypedPointer<TestScript, TestScriptTestAction>,
1900 options: Arc<TestRunnerOptions>,
1901) -> Result<TestResult<TestReportSetupAction>, TestScriptError> {
1902 tracing::info!("Running TestScript action at path: {}", pointer.path());
1903 let action = pointer.value().ok_or_else(|| {
1904 TestScriptError::ExecutionError(format!(
1905 "Failed to retrieve TestScript action at '{}'.",
1906 pointer.path()
1907 ))
1908 })?;
1909
1910 if action.operation.is_some() {
1913 let Some(operation_pointer) =
1914 pointer.descend::<TestScriptSetupActionOperation>(&Key::Field("operation".to_string()))
1915 else {
1916 return Err(TestScriptError::ExecutionError(format!(
1917 "Failed to retrieve TestScript operation at '{}'.",
1918 pointer.path()
1919 )));
1920 };
1921
1922 let result = run_operation(client, ctx, state, operation_pointer, options).await?;
1923
1924 Ok(TestResult {
1925 state: result.state,
1926 value: TestReportSetupAction {
1927 operation: Some(result.value),
1928 ..Default::default()
1929 },
1930 })
1931 } else if action.assert.is_some() {
1932 let Some(assertion_pointer) =
1933 pointer.descend::<TestScriptSetupActionAssert>(&Key::Field("assert".to_string()))
1934 else {
1935 return Err(TestScriptError::ExecutionError(format!(
1936 "Failed to retrieve TestScript assertion at '{}'.",
1937 pointer.path()
1938 )));
1939 };
1940
1941 let assertion = run_assertion(state, assertion_pointer).await?;
1942
1943 Ok(TestResult {
1944 state: assertion.state,
1945 value: TestReportSetupAction {
1946 assert: Some(assertion.value),
1947 ..Default::default()
1948 },
1949 })
1950 } else {
1951 Err(TestScriptError::ExecutionError(format!(
1952 "TestScript action must have either an operation or an assert at '{}'.",
1953 pointer.path()
1954 )))
1955 }
1956}
1957
1958async fn run_setup_action<CTX: Clone + WithRequestHeaders, Client: FHIRHttpExchange<CTX>>(
1959 client: &Client,
1960 ctx: CTX,
1961 state: Arc<Mutex<TestState>>,
1962 pointer: TypedPointer<TestScript, TestScriptSetupAction>,
1963 options: Arc<TestRunnerOptions>,
1964) -> Result<TestResult<TestReportSetupAction>, TestScriptError> {
1965 let action = pointer.value().ok_or_else(|| {
1966 TestScriptError::ExecutionError(format!(
1967 "Failed to retrieve TestScript action at '{}'.",
1968 pointer.path()
1969 ))
1970 })?;
1971
1972 tracing::info!("Running TestScript action at path: {}", pointer.path());
1973
1974 if action.operation.is_some() {
1977 let Some(operation_pointer) =
1978 pointer.descend::<TestScriptSetupActionOperation>(&Key::Field("operation".to_string()))
1979 else {
1980 return Err(TestScriptError::ExecutionError(format!(
1981 "Failed to retrieve TestScript operation at '{}'.",
1982 pointer.path()
1983 )));
1984 };
1985
1986 let result = run_operation(client, ctx, state, operation_pointer, options).await?;
1987
1988 Ok(TestResult {
1989 state: result.state,
1990 value: TestReportSetupAction {
1991 operation: Some(result.value),
1992 ..Default::default()
1993 },
1994 })
1995 } else if action.assert.is_some() {
1996 let Some(assertion_pointer) =
1997 pointer.descend::<TestScriptSetupActionAssert>(&Key::Field("assert".to_string()))
1998 else {
1999 return Err(TestScriptError::ExecutionError(format!(
2000 "Failed to retrieve TestScript assertion at '{}'.",
2001 pointer.path()
2002 )));
2003 };
2004
2005 let assertion = run_assertion(state, assertion_pointer).await?;
2006
2007 Ok(TestResult {
2008 state: assertion.state,
2009 value: TestReportSetupAction {
2010 assert: Some(assertion.value),
2011 ..Default::default()
2012 },
2013 })
2014 } else {
2015 Err(TestScriptError::ExecutionError(format!(
2016 "TestScript action must have either an operation or an assert at '{}'.",
2017 pointer.path()
2018 )))
2019 }
2020}
2021
2022async fn setup_fixtures<CTX: Clone + WithRequestHeaders, Client: FHIRHttpExchange<CTX>>(
2023 client: &Client,
2024 ctx: CTX,
2025 state: Arc<Mutex<TestState>>,
2026 pointer: TypedPointer<TestScript, TestScript>,
2027 _options: Arc<TestRunnerOptions>,
2028) -> Result<Arc<Mutex<TestState>>, OperationOutcomeError> {
2029 let mut state_lock = state.lock().await;
2030
2031 let Some(fixtures_pointer) =
2032 pointer.descend::<Vec<TestScriptFixture>>(&Key::Field("fixture".to_string()))
2033 else {
2034 return Ok(state.clone());
2035 };
2036
2037 let Some(fixtures) = fixtures_pointer.value() else {
2038 return Ok(state.clone());
2039 };
2040
2041 for fixture in fixtures {
2042 if let Some(reference_string) = fixture
2043 .resource
2044 .as_ref()
2045 .and_then(|r| r.reference.as_ref())
2046 .and_then(|refe| refe.value.as_ref())
2047 {
2048 let resolved_resource = if reference_string.starts_with('#')
2049 && let Some(contained) =
2050 pointer.descend::<Vec<Resource>>(&Key::Field("contained".to_string()))
2051 && let Some(contained) = contained.value()
2052 {
2053 let local_id = &reference_string[1..];
2054 let Some(resource) = contained.iter().find(|res| {
2055 if let Some(id) = res.get_field("id")
2056 && let Some(id) = id.as_any().downcast_ref::<String>()
2057 {
2058 id.as_str() == local_id
2059 } else {
2060 false
2061 }
2062 }) else {
2063 return Err(OperationOutcomeError::error(
2064 IssueType::not_found(),
2065 format!("Contained resource with id '{local_id}' not found."),
2066 ));
2067 };
2068
2069 resource.clone()
2070 } else {
2071 let parts = reference_string.split('/').collect::<Vec<&str>>();
2072 if parts.len() != 2 {
2073 return Err(OperationOutcomeError::error(
2074 IssueType::invalid(),
2075 format!("Invalid fixture reference: {reference_string}"),
2076 ));
2077 }
2078
2079 let resource_type = parts[0];
2080 let id = parts[1];
2081
2082 let Some(remote_resource) = client
2083 .read(
2084 ctx.clone(),
2085 ResourceType::try_from(resource_type).map_err(|_| {
2086 OperationOutcomeError::error(
2087 IssueType::invalid(),
2088 format!(
2089 "Invalid resource type in fixture reference: '{resource_type}'"
2090 ),
2091 )
2092 })?,
2093 id.to_string(),
2094 )
2095 .await?
2096 else {
2097 return Err(OperationOutcomeError::error(
2098 IssueType::not_found(),
2099 format!("Resource '{resource_type}' with id '{id}' not found."),
2100 ));
2101 };
2102
2103 remote_resource
2104 };
2105
2106 state_lock.fixtures.insert(
2107 fixture.id.clone().unwrap_or_default(),
2108 Fixtures::Resource(resolved_resource),
2109 );
2110 }
2111 }
2112
2113 drop(state_lock);
2114
2115 Ok(state)
2116}
2117
2118async fn run_setup<CTX: Clone + WithRequestHeaders, Client: FHIRHttpExchange<CTX>>(
2119 client: &Client,
2120 ctx: CTX,
2121 state: Arc<Mutex<TestState>>,
2122 pointer: TypedPointer<TestScript, TestScriptSetup>,
2123 options: Arc<TestRunnerOptions>,
2124) -> Result<TestResult<TestReportSetup>, TestScriptError> {
2125 let mut cur_state = state;
2126
2127 let mut setup_results = TestReportSetup {
2128 action: vec![],
2129 ..Default::default()
2130 };
2131
2132 let Some(setup) = pointer.value() else {
2133 return Ok(TestResult {
2134 state: cur_state,
2135 value: setup_results,
2136 });
2137 };
2138
2139 for action in setup.action.iter().enumerate() {
2140 let action_pointer = pointer
2141 .descend::<Vec<TestScriptSetupAction>>(&Key::Field("action".to_string()))
2142 .and_then(|p| p.descend::<TestScriptSetupAction>(&Key::Index(action.0)));
2143
2144 let action_pointer = action_pointer.ok_or_else(|| {
2145 TestScriptError::ExecutionError(format!(
2146 "Failed to retrieve TestScript action at index {}.",
2147 action.0
2148 ))
2149 })?;
2150
2151 let result = run_setup_action(
2152 client,
2153 ctx.clone(),
2154 cur_state,
2155 action_pointer,
2156 options.clone(),
2157 )
2158 .await?;
2159 cur_state = result.state;
2160
2161 setup_results.action.push(result.value);
2162 }
2163
2164 Ok(TestResult {
2165 state: cur_state,
2166 value: setup_results,
2167 })
2168}
2169
2170async fn run_teardown<CTX: Clone + WithRequestHeaders, Client: FHIRHttpExchange<CTX>>(
2171 client: &Client,
2172 ctx: CTX,
2173 state: Arc<Mutex<TestState>>,
2174 pointer: TypedPointer<TestScript, TestScriptTeardown>,
2175 options: Arc<TestRunnerOptions>,
2176) -> Result<TestResult<TestReportTeardown>, TestScriptError> {
2177 let mut cur_state = state;
2178
2179 let mut teardown_results = TestReportTeardown {
2180 action: vec![],
2181 ..Default::default()
2182 };
2183
2184 let Some(actions) = pointer.value() else {
2185 return Ok(TestResult {
2186 state: cur_state,
2187 value: teardown_results,
2188 });
2189 };
2190
2191 for action in actions.action.iter().enumerate() {
2192 let action_pointer = pointer
2193 .descend::<Vec<TestScriptTeardownAction>>(&Key::Field("action".to_string()))
2194 .and_then(|p| p.descend::<TestScriptTeardownAction>(&Key::Index(action.0)));
2195
2196 let action_pointer = action_pointer.ok_or_else(|| {
2197 TestScriptError::ExecutionError(format!(
2198 "Failed to retrieve TestScript teardown action at index {}.",
2199 action.0
2200 ))
2201 })?;
2202
2203 let operation_pointer = action_pointer
2204 .descend::<TestScriptSetupActionOperation>(&Key::Field("operation".to_string()))
2205 .ok_or_else(|| {
2206 TestScriptError::ExecutionError(format!(
2207 "Failed to retrieve TestScript teardown operation at index {}.",
2208 action.0
2209 ))
2210 })?;
2211
2212 let result = run_operation(
2213 client,
2214 ctx.clone(),
2215 cur_state,
2216 operation_pointer,
2217 options.clone(),
2218 )
2219 .await?;
2220 cur_state = result.state;
2221
2222 teardown_results.action.push(TestReportTeardownAction {
2223 operation: result.value,
2224 ..Default::default()
2225 });
2226 }
2227
2228 Ok(TestResult {
2229 state: cur_state,
2230 value: teardown_results,
2231 })
2232}
2233
2234async fn run_test<CTX: Clone + WithRequestHeaders, Client: FHIRHttpExchange<CTX>>(
2235 client: &Client,
2236 ctx: CTX,
2237 state: Arc<Mutex<TestState>>,
2238 pointer: TypedPointer<TestScript, TestScriptTest>,
2239 options: Arc<TestRunnerOptions>,
2240) -> Result<TestResult<TestReportTest>, TestScriptError> {
2241 let mut cur_state = state;
2242 let mut test_report_test = TestReportTest {
2243 action: vec![],
2244 ..Default::default()
2245 };
2246
2247 let test = pointer.value().ok_or_else(|| {
2248 TestScriptError::ExecutionError(format!(
2249 "Failed to retrieve TestScript test at '{}'.",
2250 pointer.path()
2251 ))
2252 })?;
2253
2254 for action in test.action.iter().enumerate() {
2255 let Some(action_pointer) = pointer
2256 .descend::<Vec<TestScriptTestAction>>(&Key::Field("action".to_string()))
2257 .and_then(|p| p.descend(&Key::Index(action.0)))
2258 else {
2259 return Err(TestScriptError::ExecutionError(format!(
2260 "Failed to retrieve TestScript test action at index {}.",
2261 action.0
2262 )));
2263 };
2264 let state = cur_state.clone();
2265 let result = run_action(
2266 client,
2267 ctx.clone(),
2268 cur_state,
2269 action_pointer.clone(),
2270 options.clone(),
2271 )
2272 .await;
2273 let result = match result {
2274 Ok(result) => result,
2275 Err(TestScriptError::ExecutionError(message)) => {
2280 tracing::error!(
2281 path = action_pointer.path(),
2282 "TestScript action could not run: {message}"
2283 );
2284 state.lock().await.result = ReportResultCodes::fail();
2285 test_report_test.action.push(TestReportTestAction {
2286 operation: Some(TestReportSetupActionOperation {
2287 result: ReportActionResultCodes::error(),
2288 message: Some(Box::new(FHIRMarkdown {
2289 value: Some(message),
2290 ..Default::default()
2291 })),
2292 ..Default::default()
2293 }),
2294 ..Default::default()
2295 });
2296 return Ok(TestResult {
2297 state,
2298 value: test_report_test,
2299 });
2300 }
2301 Err(e) => return Err(e),
2302 };
2303 cur_state = result.state;
2304 test_report_test.action.push(TestReportTestAction {
2305 operation: result.value.operation,
2306 assert: result.value.assert,
2307 ..Default::default()
2308 });
2309 }
2310
2311 Ok(TestResult {
2312 state: cur_state,
2313 value: test_report_test,
2314 })
2315}
2316
2317async fn run_tests<CTX: Clone + WithRequestHeaders, Client: FHIRHttpExchange<CTX>>(
2318 client: &Client,
2319 ctx: CTX,
2320 state: Arc<Mutex<TestState>>,
2321 pointer: TypedPointer<TestScript, Vec<TestScriptTest>>,
2322 options: Arc<TestRunnerOptions>,
2323) -> Result<TestResult<Vec<TestReportTest>>, TestScriptError> {
2324 let mut test_results = vec![];
2325 let mut cur_state = state;
2326
2327 let Some(tests) = pointer.value() else {
2328 return Ok(TestResult {
2329 state: cur_state,
2330 value: test_results,
2331 });
2332 };
2333
2334 for test in tests.iter().enumerate() {
2335 let Some(test_pointer) = pointer.descend(&Key::Index(test.0)) else {
2336 return Err(TestScriptError::ExecutionError(format!(
2337 "Failed to retrieve TestScript test at index {}.",
2338 test.0
2339 )));
2340 };
2341 let test_result = run_test(
2342 client,
2343 ctx.clone(),
2344 cur_state,
2345 test_pointer,
2346 options.clone(),
2347 )
2348 .await?;
2349 cur_state = test_result.state;
2350 test_results.push(test_result.value);
2351 }
2352
2353 Ok(TestResult {
2354 state: cur_state,
2355 value: test_results,
2356 })
2357}
2358
2359pub struct TestRunnerOptions {
2360 pub wait_between_operations: Option<Duration>,
2361 pub index_wait_timeout: Option<Duration>,
2367}
2368
2369#[tracing::instrument(
2386 name = "testscript_run",
2387 skip_all,
2388 fields(
2389 testscript.id = test_script.id.as_deref().unwrap_or("<no-id>"),
2390 testscript.name = test_script.name.value.as_deref().unwrap_or("<unnamed>"),
2391 testscript.url = test_script.url.value.as_deref().unwrap_or("<no-url>"),
2392 )
2393)]
2394pub async fn run<CTX: Clone + WithRequestHeaders, Client: FHIRHttpExchange<CTX>>(
2395 client: &Client,
2396 ctx: CTX,
2397 test_script: Arc<TestScript>,
2398 options: Arc<TestRunnerOptions>,
2399) -> Result<TestReport, TestScriptError> {
2400 tracing::info!("Starting TestScript run");
2401
2402 let mut test_report = TestReport {
2403 status: ReportStatusCodes::completed(),
2404 testScript: Box::new(Reference {
2405 reference: Some(Box::new(FHIRString {
2406 value: Some(format!(
2407 "Testscript/{}",
2408 test_script.id.clone().unwrap_or_default()
2409 )),
2410 ..Default::default()
2411 })),
2412 ..Default::default()
2413 }),
2414 ..Default::default()
2415 };
2416
2417 let mut state = Arc::new(Mutex::new(TestState::new()));
2418 let pointer = TypedPointer::<TestScript, TestScript>::new(test_script);
2419
2420 state = setup_fixtures(client, ctx.clone(), state, pointer.clone(), options.clone())
2421 .await
2422 .map_err(TestScriptError::OperationError)?;
2423
2424 let mut running_state = Ok(());
2425
2426 if let Some(setup_pointer) =
2428 pointer.descend::<TestScriptSetup>(&Key::Field("setup".to_string()))
2429 {
2430 tracing::info!("Running TestScript setup...");
2431 let setup_result = run_setup(
2432 client,
2433 ctx.clone(),
2434 state.clone(),
2435 setup_pointer,
2436 options.clone(),
2437 )
2438 .await;
2439 match setup_result {
2440 Ok(res) => {
2441 state = res.state;
2442 test_report.setup = Some(res.value);
2443 }
2444 Err(e) => {
2445 tracing::error!(phase = "setup", error = ?e, "TestScript run failed during setup");
2446 running_state = Err(e);
2447 }
2448 }
2449 }
2450
2451 if running_state.is_ok()
2453 && let Some(test_pointer) =
2454 pointer.descend::<Vec<TestScriptTest>>(&Key::Field("test".to_string()))
2455 {
2456 tracing::info!("Running TestScript tests...");
2457 let test_result = run_tests(
2458 client,
2459 ctx.clone(),
2460 state.clone(),
2461 test_pointer,
2462 options.clone(),
2463 )
2464 .await;
2465
2466 match test_result {
2467 Ok(res) => {
2468 state = res.state;
2469 test_report.test = Some(res.value);
2470 }
2471
2472 Err(e) => {
2473 tracing::error!(phase = "test", error = ?e, "TestScript run failed during test execution");
2474 running_state = Err(e);
2475 }
2476 }
2477 }
2478
2479 if let Some(teardown_pointer) =
2480 pointer.descend::<TestScriptTeardown>(&Key::Field("teardown".to_string()))
2481 {
2482 tracing::info!("Running TestScript teardown...");
2483
2484 let result = run_teardown(
2485 client,
2486 ctx.clone(),
2487 state.clone(),
2488 teardown_pointer,
2489 options.clone(),
2490 )
2491 .await?;
2492
2493 test_report.teardown = Some(result.value);
2495 }
2496
2497 running_state?;
2498
2499 let state_guard = state.lock().await;
2500 match &state_guard.result {
2503 state if state == &ReportResultCodes::pending() => {
2504 test_report.result = ReportResultCodes::pass();
2505 }
2506 status => test_report.result = status.clone(),
2507 }
2508
2509 if test_report.result == ReportResultCodes::fail() {
2510 tracing::error!(result = ?test_report.result, "TestScript run finished with failures");
2511 } else {
2512 tracing::info!(result = ?test_report.result, "TestScript run finished");
2513 }
2514
2515 Ok(test_report)
2516}
2517
2518#[cfg(test)]
2519mod tests {
2520 use super::*;
2521 use haste_fhir_client::request::{FHIRCreateResponse, FHIRDeleteInstanceResponse};
2522
2523 fn patient(id: &str, version_id: &str) -> Resource {
2524 serde_json::from_value(serde_json::json!({
2525 "resourceType": "Patient",
2526 "id": id,
2527 "meta": { "versionId": version_id }
2528 }))
2529 .expect("valid Patient")
2530 }
2531
2532 #[test]
2533 fn writes_record_the_version_search_must_return() {
2534 let created = FHIRResponse::Create(FHIRCreateResponse {
2535 resource: patient("p1", "v1"),
2536 });
2537 let write = indexed_write(&created).expect("a create is a write");
2538 assert_eq!(write.resource_type, ResourceType::Patient);
2539 assert_eq!(write.id, "p1");
2540 assert_eq!(write.version_id.as_deref(), Some("v1"));
2541
2542 let deleted = FHIRResponse::Delete(DeleteResponse::Instance(Box::new(
2544 FHIRDeleteInstanceResponse {
2545 resource: patient("p1", "v2"),
2546 },
2547 )));
2548 assert_eq!(
2549 indexed_write(&deleted)
2550 .expect("a delete is a write")
2551 .version_id,
2552 None
2553 );
2554 }
2555
2556 #[test]
2557 fn only_index_backed_requests_wait() {
2558 let search = FHIRRequest::Search(SearchRequest::Type(FHIRSearchTypeRequest {
2559 resource_type: ResourceType::Patient,
2560 parameters: ParsedParameters::try_from("_tag=x").unwrap(),
2561 }));
2562 assert!(reads_search_index(&search));
2563
2564 let read = FHIRRequest::Read(FHIRReadRequest {
2565 resource_type: ResourceType::Patient,
2566 id: "p1".to_string(),
2567 });
2568 assert!(!reads_search_index(&read), "a read goes to the repository");
2569 }
2570}