use serde_json::json;
use helios_persistence::core::{ResourceStorage, VersionedStorage};
use helios_persistence::error::{ConcurrencyError, StorageError};
use helios_persistence::tenant::{TenantContext, TenantId, TenantPermissions};
#[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())
}
fn create_patient_json(name: &str) -> serde_json::Value {
json!({
"resourceType": "Patient",
"name": [{"family": name}],
"active": true
})
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_update_with_match_success() {
let backend = create_sqlite_backend();
let tenant = create_tenant();
let patient = create_patient_json("Smith");
let created = backend.create(&tenant, "Patient", patient).await.unwrap();
let etag = created.etag().to_string();
let mut content = created.content().clone();
content["name"][0]["family"] = json!("Jones");
let result = backend
.update_with_match(&tenant, "Patient", created.id(), &etag, content)
.await;
assert!(result.is_ok());
let updated = result.unwrap();
assert_eq!(updated.version_id(), "2");
assert_eq!(updated.content()["name"][0]["family"], "Jones");
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_update_with_match_etag_mismatch() {
let backend = create_sqlite_backend();
let tenant = create_tenant();
let patient = create_patient_json("Smith");
let v1 = backend.create(&tenant, "Patient", patient).await.unwrap();
let v2 = backend
.update(&tenant, &v1, v1.content().clone())
.await
.unwrap();
let mut content = v2.content().clone();
content["name"][0]["family"] = json!("Jones");
let result = backend
.update_with_match(&tenant, "Patient", v1.id(), v1.etag(), content)
.await;
assert!(result.is_err());
match result {
Err(StorageError::Concurrency(ConcurrencyError::VersionConflict { .. })) => {}
Err(e) => panic!("Expected VersionConflict error, got {:?}", e),
Ok(_) => panic!("Expected error"),
}
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_update_with_match_etag_formats() {
let backend = create_sqlite_backend();
let tenant = create_tenant();
let patient = create_patient_json("Smith");
let created = backend.create(&tenant, "Patient", patient).await.unwrap();
let etag_formats = vec![
r#"W/"1""#.to_string(), r#""1""#.to_string(), "1".to_string(), ];
for etag in etag_formats {
let read = backend
.read(&tenant, "Patient", created.id())
.await
.unwrap()
.unwrap();
let result = backend
.update_with_match(&tenant, "Patient", created.id(), &etag, read.content().clone())
.await;
if result.is_ok() {
break; }
}
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_concurrent_update_serialization() {
let backend = create_sqlite_backend();
let tenant = create_tenant();
let patient = create_patient_json("Original");
let created = backend.create(&tenant, "Patient", patient).await.unwrap();
let id = created.id().to_string();
let user1_read = backend
.read(&tenant, "Patient", &id)
.await
.unwrap()
.unwrap();
let user2_read = backend
.read(&tenant, "Patient", &id)
.await
.unwrap()
.unwrap();
let mut user1_content = user1_read.content().clone();
user1_content["name"][0]["family"] = json!("User1Edit");
let user1_result = backend
.update_with_match(&tenant, "Patient", &id, user1_read.etag(), user1_content)
.await;
assert!(user1_result.is_ok());
let mut user2_content = user2_read.content().clone();
user2_content["name"][0]["family"] = json!("User2Edit");
let user2_result = backend
.update_with_match(&tenant, "Patient", &id, user2_read.etag(), user2_content)
.await;
assert!(user2_result.is_err());
match user2_result {
Err(StorageError::Concurrency(_)) => {}
Err(e) => panic!("Expected ConcurrencyError, got {:?}", e),
Ok(_) => panic!("Expected error"),
}
let final_state = backend
.read(&tenant, "Patient", &id)
.await
.unwrap()
.unwrap();
assert_eq!(final_state.content()["name"][0]["family"], "User1Edit");
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_concurrent_update_retry() {
let backend = create_sqlite_backend();
let tenant = create_tenant();
let patient = create_patient_json("Original");
let created = backend.create(&tenant, "Patient", patient).await.unwrap();
let id = created.id().to_string();
let user1_read = backend
.read(&tenant, "Patient", &id)
.await
.unwrap()
.unwrap();
let user2_read = backend
.read(&tenant, "Patient", &id)
.await
.unwrap()
.unwrap();
let mut user1_content = user1_read.content().clone();
user1_content["name"][0]["family"] = json!("User1Edit");
backend
.update_with_match(&tenant, "Patient", &id, user1_read.etag(), user1_content)
.await
.unwrap();
let mut user2_content = user2_read.content().clone();
user2_content["name"][0]["family"] = json!("User2Edit");
let first_attempt = backend
.update_with_match(&tenant, "Patient", &id, user2_read.etag(), user2_content.clone())
.await;
assert!(first_attempt.is_err());
let user2_refresh = backend
.read(&tenant, "Patient", &id)
.await
.unwrap()
.unwrap();
let mut retry_content = user2_refresh.content().clone();
retry_content["name"][0]["family"] = json!("User2Edit");
let retry_result = backend
.update_with_match(&tenant, "Patient", &id, user2_refresh.etag(), retry_content)
.await;
assert!(retry_result.is_ok());
let final_state = backend
.read(&tenant, "Patient", &id)
.await
.unwrap()
.unwrap();
assert_eq!(final_state.content()["name"][0]["family"], "User2Edit");
assert_eq!(final_state.version_id(), "3");
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_update_with_match_nonexistent() {
let backend = create_sqlite_backend();
let tenant = create_tenant();
let result = backend
.update_with_match(
&tenant,
"Patient",
"nonexistent",
"W/\"1\"",
create_patient_json("Smith"),
)
.await;
assert!(result.is_err());
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_update_with_match_deleted() {
let backend = create_sqlite_backend();
let tenant = create_tenant();
let patient = create_patient_json("Smith");
let created = backend.create(&tenant, "Patient", patient).await.unwrap();
let id = created.id().to_string();
let etag = created.etag().to_string();
backend.delete(&tenant, "Patient", &id).await.unwrap();
let result = backend
.update_with_match(&tenant, "Patient", &id, &etag, create_patient_json("Jones"))
.await;
assert!(result.is_err());
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_etag_generation() {
let backend = create_sqlite_backend();
let tenant = create_tenant();
let patient = create_patient_json("Smith");
let v1 = backend.create(&tenant, "Patient", patient).await.unwrap();
let v2 = backend
.update(&tenant, &v1, v1.content().clone())
.await
.unwrap();
let v3 = backend
.update(&tenant, &v2, v2.content().clone())
.await
.unwrap();
assert!(v1.etag().contains("1"));
assert!(v2.etag().contains("2"));
assert!(v3.etag().contains("3"));
assert_ne!(v1.etag(), v2.etag());
assert_ne!(v2.etag(), v3.etag());
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_stored_resource_matches_etag() {
let backend = create_sqlite_backend();
let tenant = create_tenant();
let patient = create_patient_json("Smith");
let created = backend.create(&tenant, "Patient", patient).await.unwrap();
assert!(created.matches_etag("W/\"1\""));
assert!(created.matches_etag("\"1\""));
assert!(created.matches_etag("1"));
assert!(!created.matches_etag("W/\"2\""));
assert!(!created.matches_etag("\"2\""));
assert!(!created.matches_etag("2"));
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_sequential_updates_with_etag() {
let backend = create_sqlite_backend();
let tenant = create_tenant();
let patient = create_patient_json("Version0");
let mut current = backend.create(&tenant, "Patient", patient).await.unwrap();
for i in 1..=10 {
let mut content = current.content().clone();
content["name"][0]["family"] = json!(format!("Version{}", i));
current = backend
.update_with_match(
&tenant,
"Patient",
current.id(),
current.etag(),
content,
)
.await
.expect(&format!("Update {} should succeed", i));
assert_eq!(current.version_id(), (i + 1).to_string());
}
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_if_none_match_create_only() {
let backend = create_sqlite_backend();
let tenant = create_tenant();
let patient1 = create_patient_json("First");
let (created, is_new) = backend
.create_or_update(&tenant, "Patient", "test-id", patient1)
.await
.unwrap();
assert!(is_new);
assert_eq!(created.version_id(), "1");
let patient2 = create_patient_json("Second");
let (updated, is_new2) = backend
.create_or_update(&tenant, "Patient", "test-id", patient2)
.await
.unwrap();
assert!(!is_new2);
assert_eq!(updated.version_id(), "2");
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_rapid_sequential_updates() {
let backend = create_sqlite_backend();
let tenant = create_tenant();
let patient = create_patient_json("Initial");
let mut current = backend.create(&tenant, "Patient", patient).await.unwrap();
let id = current.id().to_string();
for i in 1..=100 {
let mut content = current.content().clone();
content["active"] = json!(i % 2 == 0);
current = backend
.update_with_match(&tenant, "Patient", &id, current.etag(), content)
.await
.expect(&format!("Update {} should succeed", i));
}
let final_state = backend
.read(&tenant, "Patient", &id)
.await
.unwrap()
.unwrap();
assert_eq!(final_state.version_id(), "101");
}