re-uploading work
This commit is contained in:
464
crates/common/tests/identity_repository_tests.rs
Normal file
464
crates/common/tests/identity_repository_tests.rs
Normal file
@@ -0,0 +1,464 @@
|
||||
//! 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<i64> = 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);
|
||||
}
|
||||
Reference in New Issue
Block a user