Files
core/test/rust/runtime/database/tenant/isolation.rs
T
2026-11-03 03:32:07 +00:00

333 lines
12 KiB
Rust

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();
}