use scim_server::ResourceProvider;
use scim_server::providers::helpers::conditional::ConditionalOperations;
use scim_server::providers::{ProviderError, StandardResourceProvider};
use scim_server::resource::version::ConditionalResult;
use scim_server::resource::{ListQuery, RequestContext, TenantContext};
use scim_server::storage::InMemoryStorage;
use serde_json::json;
use std::sync::Arc;
fn create_test_user_data(username: &str) -> serde_json::Value {
json!({
"userName": username,
"displayName": format!("User {}", username),
"active": true
})
}
#[tokio::test]
async fn test_single_tenant_operations() {
let storage = InMemoryStorage::new();
let provider = StandardResourceProvider::new(storage);
let context = RequestContext::with_generated_id();
let user_data = create_test_user_data("john.doe");
let user = provider
.create_resource("User", user_data, &context)
.await
.unwrap();
let user_id = user.resource().get_id().unwrap();
let retrieved = provider
.get_resource("User", user_id, &context)
.await
.unwrap();
assert!(retrieved.is_some());
assert_eq!(
retrieved.unwrap().resource().get_username(),
Some("john.doe")
);
let update_data = json!({
"userName": "john.doe",
"displayName": "John Updated",
"active": false
});
let _updated = provider
.update_resource("User", user_id, update_data, None, &context)
.await
.unwrap();
let verified = provider
.get_resource("User", user_id, &context)
.await
.unwrap()
.unwrap();
assert_eq!(
verified.resource().get_attribute("displayName"),
Some(&json!("John Updated"))
);
let query = ListQuery::default();
let users = provider
.list_resources("User", Some(&query), &context)
.await
.unwrap();
assert_eq!(users.len(), 1);
provider
.delete_resource("User", user_id, None, &context)
.await
.unwrap();
let deleted = provider
.get_resource("User", user_id, &context)
.await
.unwrap();
assert!(deleted.is_none());
}
#[tokio::test]
async fn test_multi_tenant_isolation() {
let storage = InMemoryStorage::new();
let provider = StandardResourceProvider::new(storage);
let tenant_a_context = TenantContext::new("tenant-a".to_string(), "client-a".to_string());
let context_a = RequestContext::with_tenant_generated_id(tenant_a_context);
let tenant_b_context = TenantContext::new("tenant-b".to_string(), "client-b".to_string());
let context_b = RequestContext::with_tenant_generated_id(tenant_b_context);
let user_a_data = create_test_user_data("alice.tenant.a");
let user_a = provider
.create_resource("User", user_a_data, &context_a)
.await
.unwrap();
let _user_a_id = user_a.resource().get_id().unwrap();
let user_b_data = create_test_user_data("alice.tenant.b");
let user_b = provider
.create_resource("User", user_b_data, &context_b)
.await
.unwrap();
let _user_b_id = user_b.resource().get_id().unwrap();
let alice_a_from_b = provider
.find_resources_by_attribute("User", "userName", "alice.tenant.a", &context_b)
.await
.unwrap();
assert!(
alice_a_from_b.is_empty(),
"Tenant B should not find tenant A's user by username"
);
let alice_b_from_a = provider
.find_resources_by_attribute("User", "userName", "alice.tenant.b", &context_a)
.await
.unwrap();
assert!(
alice_b_from_a.is_empty(),
"Tenant A should not find tenant B's user by username"
);
let alice_a_from_a = provider
.find_resources_by_attribute("User", "userName", "alice.tenant.a", &context_a)
.await
.unwrap();
assert!(
!alice_a_from_a.is_empty(),
"Tenant A should find its own user"
);
let alice_b_from_b = provider
.find_resources_by_attribute("User", "userName", "alice.tenant.b", &context_b)
.await
.unwrap();
assert!(
!alice_b_from_b.is_empty(),
"Tenant B should find its own user"
);
let query = ListQuery::default();
let users_a = provider
.list_resources("User", Some(&query), &context_a)
.await
.unwrap();
let users_b = provider
.list_resources("User", Some(&query), &context_b)
.await
.unwrap();
assert_eq!(users_a.len(), 1);
assert_eq!(users_b.len(), 1);
assert_eq!(users_a[0].resource().get_username(), Some("alice.tenant.a"));
assert_eq!(users_b[0].resource().get_username(), Some("alice.tenant.b"));
}
#[tokio::test]
async fn test_username_duplicate_detection() {
let storage = InMemoryStorage::new();
let provider = StandardResourceProvider::new(storage);
let context = RequestContext::with_generated_id();
let user1_data = create_test_user_data("duplicate");
let _user1 = provider
.create_resource("User", user1_data, &context)
.await
.unwrap();
let user2_data = create_test_user_data("duplicate");
let result = provider.create_resource("User", user2_data, &context).await;
assert!(result.is_err());
match result.unwrap_err() {
ProviderError::DuplicateAttribute {
attribute, value, ..
} => {
assert_eq!(attribute, "userName");
assert_eq!(value, "duplicate");
}
_ => panic!("Expected DuplicateAttribute error"),
}
}
#[tokio::test]
async fn test_cross_tenant_username_allowed() {
let storage = InMemoryStorage::new();
let provider = StandardResourceProvider::new(storage);
let tenant_a_context = TenantContext::new("tenant-a".to_string(), "client-a".to_string());
let context_a = RequestContext::with_tenant_generated_id(tenant_a_context);
let tenant_b_context = TenantContext::new("tenant-b".to_string(), "client-b".to_string());
let context_b = RequestContext::with_tenant_generated_id(tenant_b_context);
let user_data = create_test_user_data("shared.name");
let user_a = provider
.create_resource("User", user_data.clone(), &context_a)
.await
.unwrap();
let user_b = provider
.create_resource("User", user_data, &context_b)
.await
.unwrap();
assert_eq!(user_a.resource().get_username(), Some("shared.name"));
assert_eq!(user_b.resource().get_username(), Some("shared.name"));
}
#[tokio::test]
async fn test_find_resource_by_attribute() {
let storage = InMemoryStorage::new();
let provider = StandardResourceProvider::new(storage);
let context = RequestContext::with_generated_id();
let user_data = create_test_user_data("john.doe");
let _user = provider
.create_resource("User", user_data, &context)
.await
.unwrap();
let found = provider
.find_resources_by_attribute("User", "userName", "john.doe", &context)
.await
.unwrap();
assert!(!found.is_empty());
assert_eq!(found[0].resource().get_username(), Some("john.doe"));
let not_found = provider
.find_resources_by_attribute("User", "userName", "nonexistent", &context)
.await
.unwrap();
assert!(not_found.is_empty());
}
#[tokio::test]
async fn test_resource_exists() {
let storage = InMemoryStorage::new();
let provider = StandardResourceProvider::new(storage);
let context = RequestContext::with_generated_id();
let user_data = create_test_user_data("test.user");
let user = provider
.create_resource("User", user_data, &context)
.await
.unwrap();
let user_id = user.resource().get_id().unwrap();
let exists = provider
.resource_exists("User", user_id, &context)
.await
.unwrap();
assert!(exists);
let not_exists = provider
.resource_exists("User", "nonexistent-id", &context)
.await
.unwrap();
assert!(!not_exists);
provider
.delete_resource("User", user_id, None, &context)
.await
.unwrap();
let exists_after_delete = provider
.resource_exists("User", user_id, &context)
.await
.unwrap();
assert!(!exists_after_delete);
}
#[tokio::test]
async fn test_provider_stats() {
let storage = InMemoryStorage::new();
let provider = StandardResourceProvider::new(storage);
let tenant_a_context = TenantContext::new("tenant-a".to_string(), "client-a".to_string());
let context_a = RequestContext::with_tenant_generated_id(tenant_a_context);
let single_context = RequestContext::with_generated_id();
let _user1 = provider
.create_resource("User", create_test_user_data("user1"), &context_a)
.await
.unwrap();
let _user2 = provider
.create_resource("User", create_test_user_data("user2"), &single_context)
.await
.unwrap();
let _group = provider
.create_resource("Group", json!({"displayName": "Test Group"}), &context_a)
.await
.unwrap();
let final_stats = provider.get_stats().await;
assert_eq!(final_stats.tenant_count, 2); assert_eq!(final_stats.total_resources, 3);
assert_eq!(final_stats.resource_type_count, 2); assert!(final_stats.resource_types.contains(&"User".to_string()));
assert!(final_stats.resource_types.contains(&"Group".to_string()));
}
#[tokio::test]
async fn test_dynamic_tenant_discovery() {
let storage = InMemoryStorage::new();
let provider = StandardResourceProvider::new(storage);
let tenants = vec!["arbitrary-tenant-123", "perf-tenant-999", "custom-org-456"];
for (idx, tenant_name) in tenants.iter().enumerate() {
let tenant_context = TenantContext::new(tenant_name.to_string(), "client".to_string());
let context = RequestContext::with_tenant_generated_id(tenant_context);
for i in 0..2 {
let user_data = create_test_user_data(&format!("user{}", idx * 2 + i));
provider
.create_resource("User", user_data, &context)
.await
.unwrap();
}
let group_data = json!({"displayName": format!("Group for {}", tenant_name)});
provider
.create_resource("Group", group_data, &context)
.await
.unwrap();
}
let stats = provider.get_stats().await;
assert_eq!(
stats.tenant_count, 3,
"Should discover all 3 tenants dynamically"
);
assert_eq!(
stats.total_resources, 9,
"Should count 6 users + 3 groups = 9 total"
);
assert_eq!(
stats.resource_type_count, 2,
"Should discover User and Group types"
);
assert!(stats.resource_types.contains(&"User".to_string()));
assert!(stats.resource_types.contains(&"Group".to_string()));
}
#[tokio::test]
async fn test_clear_functionality() {
let storage = InMemoryStorage::new();
let provider = StandardResourceProvider::new(storage);
let context = RequestContext::with_generated_id();
let _user = provider
.create_resource("User", create_test_user_data("test"), &context)
.await
.unwrap();
let stats_before = provider.get_stats().await;
assert!(stats_before.total_resources > 0);
provider.clear().await;
let stats_after = provider.get_stats().await;
assert_eq!(stats_after.total_resources, 0);
assert_eq!(stats_after.tenant_count, 0);
}
#[tokio::test]
async fn test_conditional_operations_via_resource_provider() {
async fn test_provider<P>(provider: &P, context: &RequestContext)
where
P: ResourceProvider<Error = ProviderError> + Sync,
{
let user_data = create_test_user_data("jane.doe");
let user = provider
.create_resource("User", user_data, context)
.await
.unwrap();
let user_id = user.resource().get_id().unwrap();
let versioned = provider
.get_resource("User", user_id, context)
.await
.unwrap()
.unwrap();
let update_data = json!({
"userName": "jane.doe",
"displayName": "Jane Updated",
"active": false
});
let result = provider
.update_resource(
"User",
user_id,
update_data,
Some(versioned.version()),
context,
)
.await
.unwrap();
assert!(result.resource().get_id().is_some());
}
let storage = InMemoryStorage::new();
let provider = StandardResourceProvider::new(storage);
let context = RequestContext::with_generated_id();
test_provider(&provider, &context).await;
}
#[tokio::test]
async fn test_conditional_provider_concurrent_updates() {
use tokio::task::JoinSet;
let storage = InMemoryStorage::new();
let provider = Arc::new(StandardResourceProvider::new(storage));
let context = RequestContext::with_generated_id();
let user_data = create_test_user_data("concurrent.user");
let user = provider
.create_resource("User", user_data, &context)
.await
.unwrap();
let user_id = user.resource().get_id().unwrap().to_string();
let initial_versioned = provider
.get_versioned_resource("User", &user_id, &context)
.await
.unwrap()
.unwrap();
let mut tasks = JoinSet::new();
let num_concurrent = 10;
for i in 0..num_concurrent {
let provider_clone: Arc<StandardResourceProvider<InMemoryStorage>> = Arc::clone(&provider);
let context_clone = context.clone();
let user_id_clone = user_id.clone();
let version_clone = initial_versioned.version().clone();
tasks.spawn(async move {
let update_data = json!({
"userName": "concurrent.user",
"displayName": format!("Update {}", i),
"active": true
});
provider_clone
.update_resource(
"User",
&user_id_clone,
update_data,
Some(&version_clone),
&context_clone,
)
.await
});
}
let mut success_count = 0;
let mut conflict_count = 0;
while let Some(result) = tasks.join_next().await {
match result.unwrap() {
Ok(_) => success_count += 1,
Err(_) => conflict_count += 1,
}
}
assert_eq!(success_count, 1, "Exactly one update should succeed");
assert_eq!(
conflict_count,
num_concurrent - 1,
"Other updates should conflict"
);
}
#[tokio::test]
async fn test_conditional_provider_delete_version_conflict() {
let storage = InMemoryStorage::new();
let provider = StandardResourceProvider::new(storage);
let context = RequestContext::with_generated_id();
let user_data = create_test_user_data("delete.user");
let user = provider
.create_resource("User", user_data, &context)
.await
.unwrap();
let user_id = user.resource().get_id().unwrap();
let initial_versioned = provider
.get_versioned_resource("User", user_id, &context)
.await
.unwrap()
.unwrap();
let update_data = json!({
"userName": "delete.user",
"displayName": "Updated Before Delete",
"active": false
});
provider
.update_resource("User", user_id, update_data, None, &context)
.await
.unwrap();
let delete_result = provider
.conditional_delete_resource("User", user_id, initial_versioned.version(), &context)
.await
.unwrap();
assert!(matches!(
delete_result,
ConditionalResult::VersionMismatch(_)
));
let still_exists = provider
.resource_exists("User", user_id, &context)
.await
.unwrap();
assert!(still_exists);
}
#[tokio::test]
async fn test_conditional_provider_successful_delete() {
let storage = InMemoryStorage::new();
let provider = StandardResourceProvider::new(storage);
let context = RequestContext::with_generated_id();
let user_data = create_test_user_data("delete.success");
let user = provider
.create_resource("User", user_data, &context)
.await
.unwrap();
let user_id = user.resource().get_id().unwrap();
let current_versioned = provider
.get_versioned_resource("User", user_id, &context)
.await
.unwrap()
.unwrap();
let delete_result = provider
.conditional_delete_resource("User", user_id, current_versioned.version(), &context)
.await
.unwrap();
assert!(matches!(delete_result, ConditionalResult::Success(())));
let exists = provider
.resource_exists("User", user_id, &context)
.await
.unwrap();
assert!(!exists);
}