//! Integration tests for Identity repository //! //! These tests verify CRUD operations, queries, and constraints //! for the Identity repository. mod helpers; use attune_common::{ repositories::{ identity::{CreateIdentityInput, IdentityRepository, UpdateIdentityInput}, Create, Delete, FindById, List, Update, }, Error, }; use helpers::*; use serde_json::json; #[tokio::test] async fn test_create_identity() { let pool = create_test_pool().await.unwrap(); let input = CreateIdentityInput { login: unique_pack_ref("testuser"), display_name: Some("Test User".to_string()), attributes: json!({"email": "test@example.com"}), password_hash: None, }; let identity = IdentityRepository::create(&pool, input.clone()) .await .unwrap(); assert!(identity.login.starts_with("testuser_")); assert_eq!(identity.display_name, Some("Test User".to_string())); assert_eq!(identity.attributes["email"], "test@example.com"); assert!(identity.created.timestamp() > 0); assert!(identity.updated.timestamp() > 0); } #[tokio::test] async fn test_create_identity_minimal() { let pool = create_test_pool().await.unwrap(); let input = CreateIdentityInput { login: unique_pack_ref("minimal"), display_name: None, attributes: json!({}), password_hash: None, }; let identity = IdentityRepository::create(&pool, input).await.unwrap(); assert!(identity.login.starts_with("minimal_")); assert_eq!(identity.display_name, None); assert_eq!(identity.attributes, json!({})); } #[tokio::test] async fn test_create_identity_duplicate_login() { let pool = create_test_pool().await.unwrap(); let login = unique_pack_ref("duplicate"); // Create first identity let input1 = CreateIdentityInput { login: login.clone(), display_name: Some("First".to_string()), attributes: json!({}), password_hash: None, }; IdentityRepository::create(&pool, input1).await.unwrap(); // Try to create second identity with same login let input2 = CreateIdentityInput { login: login.clone(), display_name: Some("Second".to_string()), attributes: json!({}), password_hash: None, }; let result = IdentityRepository::create(&pool, input2).await; assert!(result.is_err()); let err = result.unwrap_err(); println!("Actual error: {:?}", err); match err { Error::AlreadyExists { entity, field, .. } => { assert_eq!(entity, "Identity"); assert_eq!(field, "login"); } _ => panic!("Expected AlreadyExists error, got: {:?}", err), } } #[tokio::test] async fn test_find_identity_by_id() { let pool = create_test_pool().await.unwrap(); let input = CreateIdentityInput { login: unique_pack_ref("findbyid"), display_name: Some("Find By ID".to_string()), attributes: json!({"key": "value"}), password_hash: None, }; let created = IdentityRepository::create(&pool, input).await.unwrap(); let found = IdentityRepository::find_by_id(&pool, created.id) .await .unwrap() .expect("Identity not found"); assert_eq!(found.id, created.id); assert_eq!(found.login, created.login); assert_eq!(found.display_name, created.display_name); assert_eq!(found.attributes, created.attributes); } #[tokio::test] async fn test_find_identity_by_id_not_found() { let pool = create_test_pool().await.unwrap(); let found = IdentityRepository::find_by_id(&pool, 999999).await.unwrap(); assert!(found.is_none()); } #[tokio::test] async fn test_find_identity_by_login() { let pool = create_test_pool().await.unwrap(); let login = unique_pack_ref("findbylogin"); let input = CreateIdentityInput { login: login.clone(), display_name: Some("Find By Login".to_string()), attributes: json!({}), password_hash: None, }; let created = IdentityRepository::create(&pool, input).await.unwrap(); let found = IdentityRepository::find_by_login(&pool, &login) .await .unwrap() .expect("Identity not found"); assert_eq!(found.id, created.id); assert_eq!(found.login, login); } #[tokio::test] async fn test_find_identity_by_login_not_found() { let pool = create_test_pool().await.unwrap(); let found = IdentityRepository::find_by_login(&pool, "nonexistent_user_12345") .await .unwrap(); assert!(found.is_none()); } #[tokio::test] async fn test_list_identities() { let pool = create_test_pool().await.unwrap(); // Create multiple identities let input1 = CreateIdentityInput { login: unique_pack_ref("user1"), display_name: Some("User 1".to_string()), attributes: json!({}), password_hash: None, }; let identity1 = IdentityRepository::create(&pool, input1).await.unwrap(); let input2 = CreateIdentityInput { login: unique_pack_ref("user2"), display_name: Some("User 2".to_string()), attributes: json!({}), password_hash: None, }; let identity2 = IdentityRepository::create(&pool, input2).await.unwrap(); let identities = IdentityRepository::list(&pool).await.unwrap(); // Should contain at least our created identities assert!(identities.len() >= 2); let identity_ids: Vec = identities.iter().map(|i| i.id).collect(); assert!(identity_ids.contains(&identity1.id)); assert!(identity_ids.contains(&identity2.id)); } #[tokio::test] async fn test_update_identity() { let pool = create_test_pool().await.unwrap(); let input = CreateIdentityInput { login: unique_pack_ref("updatetest"), display_name: Some("Original Name".to_string()), attributes: json!({"key": "original"}), password_hash: None, }; let identity = IdentityRepository::create(&pool, input).await.unwrap(); let original_updated = identity.updated; // Wait a moment to ensure timestamp changes tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; let update_input = UpdateIdentityInput { display_name: Some("Updated Name".to_string()), password_hash: None, attributes: Some(json!({"key": "updated", "new_key": "new_value"})), }; let updated = IdentityRepository::update(&pool, identity.id, update_input) .await .unwrap(); assert_eq!(updated.id, identity.id); assert_eq!(updated.login, identity.login); // Login should not change assert_eq!(updated.display_name, Some("Updated Name".to_string())); assert_eq!(updated.attributes["key"], "updated"); assert_eq!(updated.attributes["new_key"], "new_value"); assert!(updated.updated > original_updated); } #[tokio::test] async fn test_update_identity_partial() { let pool = create_test_pool().await.unwrap(); let input = CreateIdentityInput { login: unique_pack_ref("partial"), display_name: Some("Original".to_string()), attributes: json!({"key": "value"}), password_hash: None, }; let identity = IdentityRepository::create(&pool, input).await.unwrap(); // Update only display_name let update_input = UpdateIdentityInput { display_name: Some("Only Display Name Changed".to_string()), password_hash: None, attributes: None, }; let updated = IdentityRepository::update(&pool, identity.id, update_input) .await .unwrap(); assert_eq!( updated.display_name, Some("Only Display Name Changed".to_string()) ); assert_eq!(updated.attributes, identity.attributes); // Should remain unchanged } #[tokio::test] async fn test_update_identity_not_found() { let pool = create_test_pool().await.unwrap(); let update_input = UpdateIdentityInput { display_name: Some("Updated Name".to_string()), password_hash: None, attributes: None, }; let result = IdentityRepository::update(&pool, 999999, update_input).await; assert!(result.is_err()); let err = result.unwrap_err(); println!("Actual error: {:?}", err); match err { Error::NotFound { entity, .. } => { assert_eq!(entity, "identity"); } _ => panic!("Expected NotFound error, got: {:?}", err), } } #[tokio::test] async fn test_delete_identity() { let pool = create_test_pool().await.unwrap(); let input = CreateIdentityInput { login: unique_pack_ref("deletetest"), display_name: Some("To Be Deleted".to_string()), attributes: json!({}), password_hash: None, }; let identity = IdentityRepository::create(&pool, input).await.unwrap(); // Verify identity exists let found = IdentityRepository::find_by_id(&pool, identity.id) .await .unwrap(); assert!(found.is_some()); // Delete the identity let deleted = IdentityRepository::delete(&pool, identity.id) .await .unwrap(); assert!(deleted); // Verify identity no longer exists let not_found = IdentityRepository::find_by_id(&pool, identity.id) .await .unwrap(); assert!(not_found.is_none()); } #[tokio::test] async fn test_delete_identity_not_found() { let pool = create_test_pool().await.unwrap(); let deleted = IdentityRepository::delete(&pool, 999999).await.unwrap(); assert!(!deleted); } #[tokio::test] async fn test_identity_timestamps_auto_populated() { let pool = create_test_pool().await.unwrap(); let input = CreateIdentityInput { login: unique_pack_ref("timestamps"), display_name: Some("Timestamp Test".to_string()), attributes: json!({}), password_hash: None, }; let identity = IdentityRepository::create(&pool, input).await.unwrap(); // Timestamps should be set assert!(identity.created.timestamp() > 0); assert!(identity.updated.timestamp() > 0); // Created and updated should be very close initially let diff = (identity.updated - identity.created) .num_milliseconds() .abs(); assert!(diff < 1000); // Within 1 second } #[tokio::test] async fn test_identity_updated_changes_on_update() { let pool = create_test_pool().await.unwrap(); let input = CreateIdentityInput { login: unique_pack_ref("updatetimestamp"), display_name: Some("Original".to_string()), attributes: json!({}), password_hash: None, }; let identity = IdentityRepository::create(&pool, input).await.unwrap(); let original_created = identity.created; let original_updated = identity.updated; // Wait a moment to ensure timestamp changes tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; let update_input = UpdateIdentityInput { display_name: Some("Updated".to_string()), password_hash: None, attributes: None, }; let updated = IdentityRepository::update(&pool, identity.id, update_input) .await .unwrap(); // Created should remain the same assert_eq!(updated.created, original_created); // Updated should be newer assert!(updated.updated > original_updated); } #[tokio::test] async fn test_identity_with_complex_attributes() { let pool = create_test_pool().await.unwrap(); let complex_attrs = json!({ "email": "complex@example.com", "roles": ["admin", "user"], "metadata": { "last_login": "2024-01-01T00:00:00Z", "login_count": 42 }, "preferences": { "theme": "dark", "notifications": true } }); let input = CreateIdentityInput { login: unique_pack_ref("complex"), display_name: Some("Complex User".to_string()), attributes: complex_attrs.clone(), password_hash: None, }; let identity = IdentityRepository::create(&pool, input).await.unwrap(); assert_eq!(identity.attributes, complex_attrs); assert_eq!(identity.attributes["roles"][0], "admin"); assert_eq!(identity.attributes["metadata"]["login_count"], 42); assert_eq!(identity.attributes["preferences"]["theme"], "dark"); // Verify it can be retrieved correctly let found = IdentityRepository::find_by_id(&pool, identity.id) .await .unwrap() .unwrap(); assert_eq!(found.attributes, complex_attrs); } #[tokio::test] async fn test_identity_login_case_sensitive() { let pool = create_test_pool().await.unwrap(); let base = unique_pack_ref("case"); let lower_login = format!("{}lower", base); let upper_login = format!("{}UPPER", base); // Create identity with lowercase login let input1 = CreateIdentityInput { login: lower_login.clone(), display_name: Some("Lower".to_string()), attributes: json!({}), password_hash: None, }; let identity1 = IdentityRepository::create(&pool, input1).await.unwrap(); // Create identity with uppercase login (should work - different login) let input2 = CreateIdentityInput { login: upper_login.clone(), display_name: Some("Upper".to_string()), attributes: json!({}), password_hash: None, }; let identity2 = IdentityRepository::create(&pool, input2).await.unwrap(); // Both should exist assert_ne!(identity1.id, identity2.id); assert_eq!(identity1.login, lower_login); assert_eq!(identity2.login, upper_login); // Find by login should be exact match let found_lower = IdentityRepository::find_by_login(&pool, &lower_login) .await .unwrap() .unwrap(); assert_eq!(found_lower.id, identity1.id); let found_upper = IdentityRepository::find_by_login(&pool, &upper_login) .await .unwrap() .unwrap(); assert_eq!(found_upper.id, identity2.id); }