#![cfg(feature = "sqlite")] use nx9_auth::db::{ self, models::Tenant, provider::{DatabaseProvider, SqliteProvider}, }; use nx9_auth::identity::{applications, users}; use std::sync::Arc; async fn setup_test_provider() -> (Arc, String) { let db_id = uuid::Uuid::new_v4().to_string(); let db_path = format!("target/test_tenant_mgmt_{}.db", db_id); let pool = db::create_pool(&db_path) .await .expect("Failed to create test pool"); db::run_migrations(&pool) .await .expect("Failed to run migrations"); (Arc::new(SqliteProvider::new(pool)), db_path) } async fn teardown_test_db(path: String) { let _ = std::fs::remove_file(path); } #[tokio::test] async fn test_tenant_user_listing_and_assignment() { let (provider, db_path) = setup_test_provider().await; let provider_dyn: Arc = provider.clone(); // Create a new tenant let tenant_id = uuid::Uuid::new_v4().to_string(); provider .tenants() .create(&tenant_id, "Acme Org", Some("acme-org")) .await .expect("Tenant creation should succeed"); let dummy_cfg = nx9_auth::config::SecurityConfig::default(); let user = users::create_user( &provider_dyn, &dummy_cfg, Tenant::DEFAULT_ID, "employee_1", "X9#mK$9qL!2zP0", None, None, None, ) .await .expect("User creation should succeed"); assert_eq!(user.tenant_id, Tenant::DEFAULT_ID); // Reassign user to Acme Org provider .users() .reassign_user_tenant_with_audit(&user.id, &tenant_id, None, None, None) .await .expect("Tenant assignment should succeed"); let tenant_users = provider .users() .list(&tenant_id) .await .expect("Listing tenant users should succeed"); assert_eq!(tenant_users.len(), 1); assert_eq!(tenant_users[0].username, "employee_1"); teardown_test_db(db_path).await; } #[tokio::test] async fn test_assign_user_username_collision_rejection() { let (provider, db_path) = setup_test_provider().await; let provider_dyn: Arc = provider.clone(); let dummy_cfg = nx9_auth::config::SecurityConfig::default(); let tenant_id = uuid::Uuid::new_v4().to_string(); provider .tenants() .create(&tenant_id, "Beta Corp", Some("beta-corp")) .await .unwrap(); // Create user in Default tenant named "common_user" let u1 = users::create_user( &provider_dyn, &dummy_cfg, Tenant::DEFAULT_ID, "common_user", "X9#mK$9qL!2zP0", None, None, None, ) .await .unwrap(); // Create user in Beta Corp also named "common_user" let _u2 = users::create_user( &provider_dyn, &dummy_cfg, &tenant_id, "common_user", "X9#mK$9qL!2zP0", None, None, None, ) .await .unwrap(); // Attempting to move u1 into Beta Corp must collide because "common_user" already exists in Beta Corp let exists = provider .users() .username_exists(&tenant_id, &u1.username) .await .unwrap(); assert!( exists, "Username existence check must return true for colliding username in destination tenant" ); teardown_test_db(db_path).await; } #[tokio::test] async fn test_tenant_application_listing_isolation() { let (provider, db_path) = setup_test_provider().await; let provider_dyn: Arc = provider.clone(); let tenant_a = uuid::Uuid::new_v4().to_string(); let tenant_b = uuid::Uuid::new_v4().to_string(); provider .tenants() .create(&tenant_a, "Tenant A", Some("tenant-a")) .await .unwrap(); provider .tenants() .create(&tenant_b, "Tenant B", Some("tenant-b")) .await .unwrap(); // Create application in Tenant A let (app_a, _) = applications::create( &provider_dyn, &tenant_a, "App A", "app-a-slug", None, None, None, None, None, None, ) .await .unwrap(); // Create application in Tenant B let (app_b, _) = applications::create( &provider_dyn, &tenant_b, "App B", "app-b-slug", None, None, None, None, None, None, ) .await .unwrap(); let apps_a = provider.applications().list(&tenant_a).await.unwrap(); let apps_b = provider.applications().list(&tenant_b).await.unwrap(); assert_eq!(apps_a.len(), 1); assert_eq!(apps_a[0].id, app_a.id); assert_eq!(apps_b.len(), 1); assert_eq!(apps_b[0].id, app_b.id); teardown_test_db(db_path).await; } #[tokio::test] async fn test_session_identity_immediately_reflects_tenant_reassignment() { let (provider, db_path) = setup_test_provider().await; let provider_dyn: Arc = provider.clone(); let dummy_cfg = nx9_auth::config::SecurityConfig::default(); let tenant_b = uuid::Uuid::new_v4().to_string(); provider .tenants() .create(&tenant_b, "Tenant B", Some("tenant-b-session")) .await .unwrap(); // 1. Create user in Default Tenant let user = users::create_user( &provider_dyn, &dummy_cfg, Tenant::DEFAULT_ID, "session_user", "X9#mK$9qL!2zP0", None, None, None, ) .await .unwrap(); // 2. Create active session for user let session_id = uuid::Uuid::new_v4().to_string(); let token_hash = "hash_123456"; let expires_at = "2030-01-01T00:00:00Z"; provider .sessions() .create( &session_id, &user.id, token_hash, Some("127.0.0.1"), Some("TestAgent"), expires_at, ) .await .unwrap(); // 3. Resolve user identity via session user_id -> tenant_id must be Default Tenant let session_user_before = provider .users() .find_by_id(&user.id) .await .unwrap() .unwrap(); assert_eq!(session_user_before.tenant_id, Tenant::DEFAULT_ID); // 4. Reassign user to Tenant B provider .users() .reassign_user_tenant_with_audit(&user.id, &tenant_b, None, None, None) .await .unwrap(); // 5. Subsequent request resolving user identity for the same active session MUST immediately yield Tenant B let session_user_after = provider .users() .find_by_id(&user.id) .await .unwrap() .unwrap(); assert_eq!(session_user_after.tenant_id, tenant_b); teardown_test_db(db_path).await; } #[tokio::test] async fn test_prevent_last_admin_reassignment_validation() { let (provider, db_path) = setup_test_provider().await; let provider_dyn: Arc = provider.clone(); let dummy_cfg = nx9_auth::config::SecurityConfig::default(); // Create an admin user let admin_user = users::create_user( &provider_dyn, &dummy_cfg, Tenant::DEFAULT_ID, "admin_user", "X9#mK$9qL!2zP0", None, None, None, ) .await .unwrap(); let admin_role = provider .roles() .find_by_name("admin") .await .unwrap() .expect("admin role must exist"); provider .roles() .assign_to_user(&admin_user.id, &admin_role.id) .await .unwrap(); // Check system admin count let admin_count = provider.users().count_admins().await.unwrap(); assert_eq!(admin_count, 1, "Should count 1 system admin user"); teardown_test_db(db_path).await; } #[tokio::test] async fn test_reassignment_audit_event_metadata_invariants() { let (provider, db_path) = setup_test_provider().await; let provider_dyn: Arc = provider.clone(); let dummy_cfg = nx9_auth::config::SecurityConfig::default(); let user = users::create_user( &provider_dyn, &dummy_cfg, Tenant::DEFAULT_ID, "audit_user", "X9#mK$9qL!2zP0", None, None, None, ) .await .unwrap(); let audit_id = uuid::Uuid::new_v4().to_string(); let from_tenant_id = Tenant::DEFAULT_ID; let to_tenant_id = uuid::Uuid::new_v4().to_string(); let metadata = serde_json::json!({ "user_id": user.id, "from_tenant_id": from_tenant_id, "to_tenant_id": to_tenant_id, }) .to_string(); provider .audit() .insert( &audit_id, Some(&user.id), Some(&user.id), "user.tenant_reassigned", "user", Some(&user.id), "info", Some("127.0.0.1"), Some("TestRunner"), Some(&metadata), ) .await .unwrap(); let entries = provider .audit() .list_filtered(&nx9_auth::db::models::AuditFilter { resource_type: Some("user".to_string()), limit: 10, ..Default::default() }) .await .unwrap(); let event = entries .into_iter() .find(|e| e.id == audit_id) .expect("Audit event must exist"); assert_eq!(event.action, "user.tenant_reassigned"); let parsed_meta: serde_json::Value = serde_json::from_str(event.metadata_json.as_deref().unwrap()).unwrap(); assert_eq!(parsed_meta["from_tenant_id"], from_tenant_id); assert_eq!(parsed_meta["to_tenant_id"], to_tenant_id); teardown_test_db(db_path).await; } #[tokio::test] async fn test_reassign_user_tenant_with_audit_atomic_success() { let (provider, db_path) = setup_test_provider().await; let provider_dyn: Arc = provider.clone(); let dummy_cfg = nx9_auth::config::SecurityConfig::default(); let tenant_dest = uuid::Uuid::new_v4().to_string(); provider .tenants() .create(&tenant_dest, "Dest Tenant", Some("dest-tenant")) .await .unwrap(); let user = users::create_user( &provider_dyn, &dummy_cfg, Tenant::DEFAULT_ID, "atomic_user", "X9#mK$9qL!2zP0", None, None, None, ) .await .unwrap(); // Call atomic reassign provider .users() .reassign_user_tenant_with_audit( &user.id, &tenant_dest, Some(&user.id), Some("127.0.0.1"), Some("TestRunner"), ) .await .unwrap(); // Verify tenant update let updated_user = provider .users() .find_by_id(&user.id) .await .unwrap() .unwrap(); assert_eq!(updated_user.tenant_id, tenant_dest); // Verify audit record was inserted inside transaction let audit_entries = provider .audit() .list_filtered(&nx9_auth::db::models::AuditFilter { resource_type: Some("user".to_string()), limit: 10, ..Default::default() }) .await .unwrap(); let audit_event = audit_entries .into_iter() .find(|e| { e.action == "user.tenant_reassigned" && e.target_user_id.as_deref() == Some(&user.id) }) .expect("Atomic reassignment audit event must exist"); let meta: serde_json::Value = serde_json::from_str(audit_event.metadata_json.as_deref().unwrap()).unwrap(); assert_eq!(meta["from_tenant_id"], Tenant::DEFAULT_ID); assert_eq!(meta["to_tenant_id"], tenant_dest); teardown_test_db(db_path).await; } #[tokio::test] async fn test_reassign_user_tenant_no_op_behavior() { let (provider, db_path) = setup_test_provider().await; let provider_dyn: Arc = provider.clone(); let dummy_cfg = nx9_auth::config::SecurityConfig::default(); let user = users::create_user( &provider_dyn, &dummy_cfg, Tenant::DEFAULT_ID, "noop_user", "X9#mK$9qL!2zP0", None, None, None, ) .await .unwrap(); // Reassigning to SAME tenant must be a defined no-op provider .users() .reassign_user_tenant_with_audit( &user.id, Tenant::DEFAULT_ID, Some("actor_1"), Some("127.0.0.1"), Some("TestRunner"), ) .await .unwrap(); // Audit logs MUST NOT contain a false tenant_reassigned event let audit_entries = provider .audit() .list_filtered(&nx9_auth::db::models::AuditFilter { resource_type: Some("user".to_string()), limit: 10, ..Default::default() }) .await .unwrap(); let noop_audit = audit_entries.into_iter().find(|e| { e.action == "user.tenant_reassigned" && e.target_user_id.as_deref() == Some(&user.id) }); assert!( noop_audit.is_none(), "No-op reassignment must NOT write a false audit log entry" ); teardown_test_db(db_path).await; } #[tokio::test] async fn test_concurrent_admin_reassignment_cannot_remove_all_system_admins() { let (provider, db_path) = setup_test_provider().await; let provider_dyn: Arc = provider.clone(); let dummy_cfg = nx9_auth::config::SecurityConfig::default(); let dest_tenant = uuid::Uuid::new_v4().to_string(); provider .tenants() .create(&dest_tenant, "Dest", Some("dest")) .await .unwrap(); // Create single system admin in Default Tenant let admin_user = users::create_user( &provider_dyn, &dummy_cfg, Tenant::DEFAULT_ID, "single_admin", "X9#mK$9qL!2zP0", None, None, None, ) .await .unwrap(); let admin_role = provider .roles() .find_by_name("admin") .await .unwrap() .unwrap(); provider .roles() .assign_to_user(&admin_user.id, &admin_role.id) .await .unwrap(); // Single system admin reassignment away from Default Tenant MUST be rejected let res = provider .users() .reassign_user_tenant_with_audit( &admin_user.id, &dest_tenant, Some(&admin_user.id), Some("127.0.0.1"), Some("TestRunner"), ) .await; assert!( res.is_err(), "Moving the last system admin away from Default Tenant MUST fail" ); // Invariant check: system admin count MUST remain >= 1 in Default Tenant let admin_count = provider.users().count_admins().await.unwrap(); assert_eq!(admin_count, 1, "System admin count must never drop to 0"); teardown_test_db(db_path).await; }