use serde_json::json;
use helios_persistence::core::{ResourceStorage, TransactionProvider};
use helios_persistence::tenant::{TenantContext, TenantId, TenantPermissions};
use helios_persistence::types::{BundleEntry, BundleRequest, TransactionBundle};
#[cfg(feature = "sqlite")]
use helios_persistence::backends::sqlite::SqliteBackend;
#[cfg(feature = "sqlite")]
fn create_sqlite_backend() -> SqliteBackend {
let backend = SqliteBackend::in_memory().expect("Failed to create SQLite backend");
backend.init_schema().expect("Failed to initialize schema");
backend
}
fn create_tenant() -> TenantContext {
TenantContext::new(TenantId::new("test-tenant"), TenantPermissions::full_access())
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_bundle_create_entries() {
let backend = create_sqlite_backend();
let tenant = create_tenant();
let bundle = TransactionBundle::new(vec![
BundleEntry {
full_url: Some("urn:uuid:patient-1".to_string()),
resource: Some(json!({
"resourceType": "Patient",
"name": [{"family": "BundlePatient1"}]
})),
request: BundleRequest {
method: "POST".to_string(),
url: "Patient".to_string(),
if_match: None,
if_none_match: None,
if_none_exist: None,
},
},
BundleEntry {
full_url: Some("urn:uuid:patient-2".to_string()),
resource: Some(json!({
"resourceType": "Patient",
"name": [{"family": "BundlePatient2"}]
})),
request: BundleRequest {
method: "POST".to_string(),
url: "Patient".to_string(),
if_match: None,
if_none_match: None,
if_none_exist: None,
},
},
]);
let result = backend.execute_transaction(&tenant, bundle).await.unwrap();
assert_eq!(result.entries.len(), 2);
for entry in &result.entries {
assert_eq!(entry.response.status, "201 Created");
assert!(entry.response.location.is_some());
}
let count = backend.count(&tenant, Some("Patient")).await.unwrap();
assert_eq!(count, 2);
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_bundle_put_entries() {
let backend = create_sqlite_backend();
let tenant = create_tenant();
let bundle = TransactionBundle::new(vec![BundleEntry {
full_url: Some("urn:uuid:patient-put".to_string()),
resource: Some(json!({
"resourceType": "Patient",
"id": "patient-123",
"name": [{"family": "PutPatient"}]
})),
request: BundleRequest {
method: "PUT".to_string(),
url: "Patient/patient-123".to_string(),
if_match: None,
if_none_match: None,
if_none_exist: None,
},
}]);
let result = backend.execute_transaction(&tenant, bundle).await.unwrap();
assert_eq!(result.entries.len(), 1);
assert!(
result.entries[0].response.status == "201 Created"
|| result.entries[0].response.status == "200 OK"
);
let read = backend
.read(&tenant, "Patient", "patient-123")
.await
.unwrap();
assert!(read.is_some());
assert_eq!(read.unwrap().content()["name"][0]["family"], "PutPatient");
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_bundle_delete_entries() {
let backend = create_sqlite_backend();
let tenant = create_tenant();
backend
.create_or_update(
&tenant,
"Patient",
"to-delete",
json!({"resourceType": "Patient"}),
)
.await
.unwrap();
let bundle = TransactionBundle::new(vec![BundleEntry {
full_url: None,
resource: None,
request: BundleRequest {
method: "DELETE".to_string(),
url: "Patient/to-delete".to_string(),
if_match: None,
if_none_match: None,
if_none_exist: None,
},
}]);
let result = backend.execute_transaction(&tenant, bundle).await.unwrap();
assert_eq!(result.entries.len(), 1);
assert!(
result.entries[0].response.status == "200 OK"
|| result.entries[0].response.status == "204 No Content"
);
assert!(!backend.exists(&tenant, "Patient", "to-delete").await.unwrap());
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_bundle_mixed_operations() {
let backend = create_sqlite_backend();
let tenant = create_tenant();
backend
.create_or_update(
&tenant,
"Patient",
"update-me",
json!({"resourceType": "Patient", "name": [{"family": "Original"}]}),
)
.await
.unwrap();
backend
.create_or_update(
&tenant,
"Patient",
"delete-me",
json!({"resourceType": "Patient"}),
)
.await
.unwrap();
let bundle = TransactionBundle::new(vec![
BundleEntry {
full_url: Some("urn:uuid:new-patient".to_string()),
resource: Some(json!({
"resourceType": "Patient",
"name": [{"family": "NewPatient"}]
})),
request: BundleRequest {
method: "POST".to_string(),
url: "Patient".to_string(),
if_match: None,
if_none_match: None,
if_none_exist: None,
},
},
BundleEntry {
full_url: None,
resource: Some(json!({
"resourceType": "Patient",
"id": "update-me",
"name": [{"family": "Updated"}]
})),
request: BundleRequest {
method: "PUT".to_string(),
url: "Patient/update-me".to_string(),
if_match: None,
if_none_match: None,
if_none_exist: None,
},
},
BundleEntry {
full_url: None,
resource: None,
request: BundleRequest {
method: "DELETE".to_string(),
url: "Patient/delete-me".to_string(),
if_match: None,
if_none_match: None,
if_none_exist: None,
},
},
]);
let result = backend.execute_transaction(&tenant, bundle).await.unwrap();
assert_eq!(result.entries.len(), 3);
let count = backend.count(&tenant, Some("Patient")).await.unwrap();
assert_eq!(count, 2);
let updated = backend
.read(&tenant, "Patient", "update-me")
.await
.unwrap()
.unwrap();
assert_eq!(updated.content()["name"][0]["family"], "Updated");
assert!(!backend.exists(&tenant, "Patient", "delete-me").await.unwrap());
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_bundle_internal_references() {
let backend = create_sqlite_backend();
let tenant = create_tenant();
let bundle = TransactionBundle::new(vec![
BundleEntry {
full_url: Some("urn:uuid:new-patient".to_string()),
resource: Some(json!({
"resourceType": "Patient",
"name": [{"family": "ReferencedPatient"}]
})),
request: BundleRequest {
method: "POST".to_string(),
url: "Patient".to_string(),
if_match: None,
if_none_match: None,
if_none_exist: None,
},
},
BundleEntry {
full_url: Some("urn:uuid:new-observation".to_string()),
resource: Some(json!({
"resourceType": "Observation",
"status": "final",
"code": {"coding": [{"code": "test"}]},
"subject": {"reference": "urn:uuid:new-patient"}
})),
request: BundleRequest {
method: "POST".to_string(),
url: "Observation".to_string(),
if_match: None,
if_none_match: None,
if_none_exist: None,
},
},
]);
let result = backend.execute_transaction(&tenant, bundle).await.unwrap();
assert_eq!(result.entries.len(), 2);
let patient_location = result.entries[0].response.location.as_ref().unwrap();
let patient_id = patient_location.split('/').last().unwrap();
let obs_location = result.entries[1].response.location.as_ref().unwrap();
let obs_id = obs_location.split('/').last().unwrap();
let observation = backend
.read(&tenant, "Observation", obs_id)
.await
.unwrap()
.unwrap();
let subject_ref = observation.content()["subject"]["reference"].as_str().unwrap();
assert!(
subject_ref.contains(patient_id),
"Reference should be resolved to actual patient ID"
);
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_bundle_conditional_create() {
let backend = create_sqlite_backend();
let tenant = create_tenant();
let bundle1 = TransactionBundle::new(vec![BundleEntry {
full_url: Some("urn:uuid:conditional".to_string()),
resource: Some(json!({
"resourceType": "Patient",
"identifier": [{"system": "http://example.org", "value": "12345"}],
"name": [{"family": "Conditional"}]
})),
request: BundleRequest {
method: "POST".to_string(),
url: "Patient".to_string(),
if_match: None,
if_none_match: None,
if_none_exist: Some("identifier=http://example.org|12345".to_string()),
},
}]);
let result1 = backend.execute_transaction(&tenant, bundle1).await.unwrap();
assert_eq!(result1.entries[0].response.status, "201 Created");
let bundle2 = TransactionBundle::new(vec![BundleEntry {
full_url: Some("urn:uuid:conditional".to_string()),
resource: Some(json!({
"resourceType": "Patient",
"identifier": [{"system": "http://example.org", "value": "12345"}],
"name": [{"family": "ShouldNotCreate"}]
})),
request: BundleRequest {
method: "POST".to_string(),
url: "Patient".to_string(),
if_match: None,
if_none_match: None,
if_none_exist: Some("identifier=http://example.org|12345".to_string()),
},
}]);
let result2 = backend.execute_transaction(&tenant, bundle2).await.unwrap();
assert_ne!(result2.entries[0].response.status, "201 Created");
let count = backend.count(&tenant, Some("Patient")).await.unwrap();
assert_eq!(count, 1);
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_bundle_conditional_update_if_match() {
let backend = create_sqlite_backend();
let tenant = create_tenant();
let created = backend
.create_or_update(
&tenant,
"Patient",
"conditional-update",
json!({"resourceType": "Patient", "name": [{"family": "Original"}]}),
)
.await
.unwrap();
let etag = format!("W/\"{}\"", created.version());
let bundle = TransactionBundle::new(vec![BundleEntry {
full_url: None,
resource: Some(json!({
"resourceType": "Patient",
"id": "conditional-update",
"name": [{"family": "UpdatedWithMatch"}]
})),
request: BundleRequest {
method: "PUT".to_string(),
url: "Patient/conditional-update".to_string(),
if_match: Some(etag),
if_none_match: None,
if_none_exist: None,
},
}]);
let result = backend.execute_transaction(&tenant, bundle).await.unwrap();
assert_eq!(result.entries[0].response.status, "200 OK");
let read = backend
.read(&tenant, "Patient", "conditional-update")
.await
.unwrap()
.unwrap();
assert_eq!(read.content()["name"][0]["family"], "UpdatedWithMatch");
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_bundle_if_match_failure() {
let backend = create_sqlite_backend();
let tenant = create_tenant();
backend
.create_or_update(
&tenant,
"Patient",
"version-conflict",
json!({"resourceType": "Patient"}),
)
.await
.unwrap();
let bundle = TransactionBundle::new(vec![BundleEntry {
full_url: None,
resource: Some(json!({
"resourceType": "Patient",
"id": "version-conflict",
"name": [{"family": "ShouldFail"}]
})),
request: BundleRequest {
method: "PUT".to_string(),
url: "Patient/version-conflict".to_string(),
if_match: Some("W/\"wrong-version\"".to_string()),
if_none_match: None,
if_none_exist: None,
},
}]);
let result = backend.execute_transaction(&tenant, bundle).await;
assert!(result.is_err() || result.unwrap().entries[0].response.status.contains("409"));
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_bundle_atomicity() {
let backend = create_sqlite_backend();
let tenant = create_tenant();
let bundle = TransactionBundle::new(vec![
BundleEntry {
full_url: Some("urn:uuid:valid".to_string()),
resource: Some(json!({
"resourceType": "Patient",
"name": [{"family": "Valid"}]
})),
request: BundleRequest {
method: "POST".to_string(),
url: "Patient".to_string(),
if_match: None,
if_none_match: None,
if_none_exist: None,
},
},
BundleEntry {
full_url: None,
resource: None,
request: BundleRequest {
method: "DELETE".to_string(),
url: "Patient/non-existent-id".to_string(),
if_match: None,
if_none_match: None,
if_none_exist: None,
},
},
]);
let result = backend.execute_transaction(&tenant, bundle).await;
if result.is_err() {
let count = backend.count(&tenant, Some("Patient")).await.unwrap();
assert_eq!(count, 0, "Transaction should be atomic - no partial commits");
}
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_bundle_empty() {
let backend = create_sqlite_backend();
let tenant = create_tenant();
let bundle = TransactionBundle::new(vec![]);
let result = backend.execute_transaction(&tenant, bundle).await;
assert!(result.is_ok());
assert!(result.unwrap().entries.is_empty());
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_bundle_single_entry() {
let backend = create_sqlite_backend();
let tenant = create_tenant();
let bundle = TransactionBundle::new(vec![BundleEntry {
full_url: Some("urn:uuid:single".to_string()),
resource: Some(json!({"resourceType": "Patient"})),
request: BundleRequest {
method: "POST".to_string(),
url: "Patient".to_string(),
if_match: None,
if_none_match: None,
if_none_exist: None,
},
}]);
let result = backend.execute_transaction(&tenant, bundle).await.unwrap();
assert_eq!(result.entries.len(), 1);
assert_eq!(result.entries[0].response.status, "201 Created");
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_bundle_tenant_isolation() {
let backend = create_sqlite_backend();
let tenant_a = TenantContext::new(TenantId::new("tenant-a"), TenantPermissions::full_access());
let tenant_b = TenantContext::new(TenantId::new("tenant-b"), TenantPermissions::full_access());
let bundle = TransactionBundle::new(vec![BundleEntry {
full_url: Some("urn:uuid:tenant-patient".to_string()),
resource: Some(json!({
"resourceType": "Patient",
"name": [{"family": "TenantA"}]
})),
request: BundleRequest {
method: "POST".to_string(),
url: "Patient".to_string(),
if_match: None,
if_none_match: None,
if_none_exist: None,
},
}]);
let result = backend.execute_transaction(&tenant_a, bundle).await.unwrap();
let location = result.entries[0].response.location.as_ref().unwrap();
let patient_id = location.split('/').last().unwrap();
assert!(backend.exists(&tenant_a, "Patient", patient_id).await.unwrap());
assert!(!backend.exists(&tenant_b, "Patient", patient_id).await.unwrap());
}