use super::*; #[test] fn explicit_migration_routes_models_roles_and_control_to_distinct_physical_storage() { let directory = Directory::new(); runtime() .run(async |scope| { let registry = Registry::prepare(&directory.0, settings(true, 4), &bindings(), scope) .await .unwrap(); assert!(registry.tenant_enabled()); assert!(registry.is_tenant(0)); let mut missing = Context::new(registry.clone()); assert_eq!( missing.on(0, scope).await.err().unwrap().kind, Kind::InvalidData ); assert_eq!( missing.select_tenant(scope, 1).await.unwrap_err().kind, Kind::Migration ); assert!(!directory.0.join("tenants").exists()); let rows = missing .control(scope) .await .unwrap() .query( scope, "SELECT COUNT(*) FROM sqlite_schema WHERE name='items'", vec![], 1, |row| row.get::<_, i64>(0), ) .await .unwrap(); assert_eq!(rows, [0]); registry.migrate_tenant(scope, 1).await.unwrap(); registry.migrate_tenant(scope, 2).await.unwrap(); registry.migrate_tenant(scope, 1).await.unwrap(); assert_eq!( registry.queue_tenants(0, scope).await.unwrap(), [Some(1), Some(2)] ); assert_eq!(registry.queue_tenants(1, scope).await.unwrap(), [None]); for id in [1, 2] { let mut context = Context::new(registry.clone()); context.select_tenant(scope, id).await.unwrap(); assert_eq!(context.tenant_id(), Some(id)); assert_eq!(context.detached().tenant_id(), Some(id)); let owner = context.enter(); context .on(0, scope) .await .unwrap() .execute( scope, "INSERT INTO items(id,value) VALUES(1,$1)", vec![crate::database::Parameter::Text(format!("tenant {id}"))], ) .await .unwrap(); context .on(2, scope) .await .unwrap() .execute(scope, "INSERT INTO roles(id) VALUES(1)", vec![]) .await .unwrap(); context.finish(scope, owner, true).await.unwrap(); let rows = context .on(0, scope) .await .unwrap() .query(scope, "SELECT value FROM items", vec![], 1, |row| { row.get::<_, String>(0) }) .await .unwrap(); assert_eq!(rows, [format!("tenant {id}")]); let count = context .control(scope) .await .unwrap() .query(scope, "SELECT COUNT(*) FROM roles", vec![], 1, |row| { row.get::<_, i64>(0) }) .await .unwrap(); assert_eq!(count, [0]); } registry.close().await; assert!(directory.0.join("tenants/1.db").is_file()); assert!(directory.0.join("tenants/2.db").is_file()); }) .unwrap(); } #[test] fn component_switches_are_immediate_validated_and_do_not_hide_ready_queues() { let directory = Directory::new(); runtime() .run(async |scope| { let registry = Registry::prepare(&directory.0, settings(true, 2), &bindings(), scope) .await .unwrap(); registry.migrate_tenant(scope, 1).await.unwrap(); let mut context = Context::new(registry.clone()); context.select_tenant(scope, 1).await.unwrap(); assert!(context.components_enabled(scope, &["shop"]).await.unwrap()); registry .set_component(scope, 1, "shop", false) .await .unwrap(); assert!(!context.components_enabled(scope, &["shop"]).await.unwrap()); assert!(context.require_components(scope, &["shop"]).await.is_err()); assert!( context .components_enabled(scope, &["platform"]) .await .unwrap() ); assert_eq!(registry.queue_tenants(0, scope).await.unwrap(), [Some(1)]); for invalid in ["platform", "unknown", "dever"] { assert_eq!( registry .set_component(scope, 1, invalid, false) .await .unwrap_err() .kind, Kind::InvalidData ); } registry .set_component(scope, 1, "shop", true) .await .unwrap(); assert!(context.components_enabled(scope, &["shop"]).await.unwrap()); registry.close().await; }) .unwrap(); } #[test] fn disabled_component_listing_paginates_and_observes_current_tenant_state() { let directory = Directory::new(); runtime() .run(async |scope| { let mut models = bindings().to_vec(); for component in ["billing", "catalog"] { models.push(crate::database::Binding { component, table: component, schema: if component == "billing" { Sql::same("CREATE TABLE billing(id BIGINT PRIMARY KEY)") } else { Sql::same("CREATE TABLE catalog(id BIGINT PRIMARY KEY)") }, ..bindings()[0].clone() }); } let registry = Registry::prepare(&directory.0, settings(true, 2), &models, scope) .await .unwrap(); registry.migrate_tenant(scope, 1).await.unwrap(); registry.migrate_tenant(scope, 2).await.unwrap(); let components = ["billing", "catalog", "shop", "platform", "unknown"] .into_iter() .collect(); let mut context = Context::new(registry.clone()); assert!( context .disabled_components(scope, &components) .await .is_err() ); context.select_tenant(scope, 1).await.unwrap(); for component in ["billing", "catalog", "shop"] { registry .set_component(scope, 1, component, false) .await .unwrap(); } assert_eq!( context .disabled_components(scope, &components) .await .unwrap(), ["billing", "catalog", "shop"] .map(String::from) .into_iter() .collect() ); registry .set_component(scope, 1, "catalog", true) .await .unwrap(); assert_eq!( context .disabled_components(scope, &components) .await .unwrap(), ["billing", "shop"].map(String::from).into_iter().collect() ); context.select_tenant(scope, 2).await.unwrap(); assert!( context .disabled_components(scope, &components) .await .unwrap() .is_empty() ); registry.close().await; }) .unwrap(); } #[test] fn transactions_and_foreign_keys_cannot_cross_tenant_boundaries() { let directory = Directory::new(); runtime() .run(async |scope| { let registry = Registry::prepare(&directory.0, settings(true, 2), &bindings(), scope) .await .unwrap(); assert!(registry.check_transactions(&[&[0, 1]]).is_err()); registry.check_transactions(&[&[0, 2]]).unwrap(); registry.check_transactions(&[&[1, 2]]).unwrap(); assert!(registry.check_transactions(&[&[2, 0, 1]]).is_err()); registry.migrate_tenant(scope, 1).await.unwrap(); let mut context = Context::new(registry.clone()); context.select_tenant(scope, 1).await.unwrap(); context .on(1, scope) .await .unwrap() .query(scope, "SELECT COUNT(*) FROM accounts", vec![], 1, |row| { row.get::<_, i64>(0) }) .await .unwrap(); let owner = context.enter(); context .on(0, scope) .await .unwrap() .execute( scope, "INSERT INTO items(id,value) VALUES(1,'rollback')", vec![], ) .await .unwrap(); assert!(context.on(1, scope).await.is_err()); assert!(context.select_tenant(scope, 2).await.is_err()); context.finish(scope, owner, false).await.unwrap(); assert_eq!( context .on(0, scope) .await .unwrap() .query(scope, "SELECT COUNT(*) FROM items", vec![], 1, |row| row .get::<_, i64>(0)) .await .unwrap(), [0] ); let owner = context.enter(); context .on(1, scope) .await .unwrap() .execute(scope, "INSERT INTO accounts(id) VALUES(1)", vec![]) .await .unwrap(); assert!(context.on(0, scope).await.is_err()); context.finish(scope, owner, true).await.unwrap(); assert_eq!( context .on(1, scope) .await .unwrap() .query(scope, "SELECT COUNT(*) FROM accounts", vec![], 1, |row| row .get::<_, i64>(0)) .await .unwrap(), [1] ); drop(context); registry.close().await; let mut invalid = bindings(); invalid[0].references = &[1]; assert_eq!( Registry::prepare(&directory.0, settings(true, 2), &invalid, scope) .await .err() .unwrap() .kind, Kind::Connection ); }) .unwrap(); } #[test] fn without_tenant_configuration_preserves_ordinary_database_routing() { let directory = Directory::new(); runtime() .run(async |scope| { let registry = Registry::prepare(&directory.0, settings(false, 2), &bindings(), scope) .await .unwrap(); assert!(!registry.tenant_enabled()); registry.check_transactions(&[&[0, 1, 2]]).unwrap(); let mut context = Context::new(registry.clone()); assert!(context.select_tenant(scope, 1).await.is_err()); context .on(0, scope) .await .unwrap() .execute( scope, "INSERT INTO items(id,value) VALUES(1,'platform')", vec![], ) .await .unwrap(); assert!(context.require_components(scope, &["shop"]).await.is_ok()); drop(context); registry.close().await; }) .unwrap(); }