Lines
100 %
Functions
Branches
//! Slice A: per-user database provisioning, exercised end to end against a real
//! Postgres via `server::provision::create_user_database`.
//!
//! Gated on the `db` feature (which turns on `server/test-utils`). These tests
//! create real databases through the admin `DATABASE_URL` and drop each one in
//! teardown — `sqlx::test` only cleans up the database it created itself, not
//! ones the application creates.
//! All scenarios run inside ONE `#[tokio::test]`. `server::db`'s admin pool is a
//! process-static `OnceCell<PgPool>` initialized lazily in the first runtime
//! that touches it; a separate `#[tokio::test]` per scenario would each spin up
//! its own runtime, and the pool bound to the first (now-dropped) runtime would
//! then time out for later tests. One runtime keeps the pool alive throughout —
//! and mirrors production, which provisions serially on one long-lived runtime.
//! Run via:
//! DATABASE_URL=postgres://… cargo test -p tests-integration --features db
#![cfg(feature = "db")]
use sqlx::postgres::PgPoolOptions;
use sqlx::{Executor, Row};
use uuid::Uuid;
async fn admin_connect() -> sqlx::PgPool {
let url =
std::env::var("DATABASE_URL").expect("DATABASE_URL must be set for provisioning tests");
PgPoolOptions::new()
.max_connections(1)
.connect(&url)
.await
.expect("connect to admin DB")
}
fn per_user_dsn(base: &str, db_name: &str) -> String {
let (prefix, rest) = base.split_once("://").expect("scheme");
let authority = rest.split('/').next().expect("authority");
format!("{prefix}://{authority}/{db_name}")
async fn drop_database(admin: &sqlx::PgPool, db_name: &str) {
// Terminate any lingering backends (a per-user pool may still hold a
// connection) before dropping.
let _ = admin
.execute(
format!(
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity \
WHERE datname = '{db_name}' AND pid <> pg_backend_pid()"
)
.as_str(),
.await;
admin
.execute(format!("DROP DATABASE IF EXISTS \"{db_name}\"").as_str())
.expect("drop provisioned database");
#[tokio::test]
async fn per_user_provisioning_end_to_end() -> anyhow::Result<()> {
let admin = admin_connect().await;
provisions_a_migrated_isolated_db(&admin).await?;
is_idempotent_on_retry(&admin).await?;
bootstrap_seed_is_idempotent(&admin).await?;
admin_has_backfilled_keypair(&admin).await?;
two_users_get_distinct_databases(&admin).await?;
private_key_is_stored_in_per_user_db(&admin).await?;
per_user_config_is_isolated(&admin).await?;
Ok(())
/// A freshly provisioned DB has the full schema, is usable, and contains no
/// seeded signing keys (those live only in the admin DB via `bootstrap::seed`).
async fn provisions_a_migrated_isolated_db(admin: &sqlx::PgPool) -> anyhow::Result<()> {
let user_id = Uuid::new_v4();
let db_name = format!("nomi_user_{}", user_id.simple());
let dsn = server::provision::create_user_database(user_id).await?;
assert!(
dsn.ends_with(&db_name),
"dsn should target the per-user db: {dsn}"
);
let user_pool = PgPoolOptions::new()
.connect(&dsn)
.await?;
let accounts: i64 = sqlx::query("SELECT count(*) FROM accounts")
.fetch_one(&user_pool)
.await?
.get(0);
assert_eq!(
accounts, 0,
"fresh per-user DB has the accounts table, empty"
let secret_rows: i64 =
sqlx::query("SELECT count(*) FROM config WHERE lower(field) LIKE '%token_private_key'")
assert_eq!(secret_rows, 0, "per-user DB must not contain signing keys");
user_pool.close().await;
drop_database(admin, &db_name).await;
/// A second provision for the same user id hits SQLSTATE 42P04 (duplicate
/// database) and succeeds, returning the same DSN.
async fn is_idempotent_on_retry(admin: &sqlx::PgPool) -> anyhow::Result<()> {
let first = server::provision::create_user_database(user_id).await?;
let second = server::provision::create_user_database(user_id).await?;
assert_eq!(first, second);
/// `seed()` is guarded by the `seed_complete` marker, so a second call is a
/// no-op and never duplicates rows (config has no UNIQUE(field) yet).
async fn bootstrap_seed_is_idempotent(admin: &sqlx::PgPool) -> anyhow::Result<()> {
server::bootstrap::seed().await?;
let after_first: i64 = sqlx::query("SELECT count(*) FROM config")
.fetch_one(admin)
let after_second: i64 = sqlx::query("SELECT count(*) FROM config")
after_first, after_second,
"re-running seed must not change config row count"
let marker: i64 =
sqlx::query("SELECT count(*) FROM config WHERE lower(field) = 'seed_complete'")
assert_eq!(marker, 1, "exactly one seed_complete marker");
// The admin's db_name must equal this deployment's DATABASE_URL (set by
// bootstrap), never the empty seed placeholder or a hardcoded URL.
let admin_db_name: String =
sqlx::query("SELECT db_name FROM users WHERE id = '22d36bde-d987-4927-b070-842efdcf9c59'")
let expected = std::env::var("DATABASE_URL")?;
admin_db_name, expected,
"admin db_name must track DATABASE_URL, not a placeholder or hardcoded URL"
/// After `bootstrap::seed`, the admin user has a public signing key in the
/// global directory and a matching private key in the admin DB — so admin login
/// keeps working once the global seed keys are retired (Slice B cutover).
async fn admin_has_backfilled_keypair(admin: &sqlx::PgPool) -> anyhow::Result<()> {
let public_key: Option<String> = sqlx::query(
"SELECT jwt_public_key FROM users WHERE id = '22d36bde-d987-4927-b070-842efdcf9c59'",
public_key.is_some_and(|k| !k.is_empty()),
"admin must have a backfilled public key"
let private_keys: i64 = sqlx::query("SELECT count(*) FROM user_auth_keys")
private_keys >= 1,
"admin DB must hold the admin private key"
// Repair path: a prior run that wrote only the public key (then crashed
// before the private insert) must be healed on the next seed, not skipped.
sqlx::query("DELETE FROM user_auth_keys")
.execute(admin)
let repaired: i64 = sqlx::query("SELECT count(*) FROM user_auth_keys")
repaired, 1,
"seed must repair a missing admin private key (public-set-but-private-missing)"
/// `store_user_private_key` writes the private key into the per-user DB only.
async fn private_key_is_stored_in_per_user_db(admin: &sqlx::PgPool) -> anyhow::Result<()> {
let keypair = server::auth_keys::generate().await?;
server::provision::store_user_private_key(&dsn, &keypair.private_pem_b64).await?;
let stored: String = sqlx::query("SELECT private_key FROM user_auth_keys")
stored, keypair.private_pem_b64,
"private key round-trips in per-user DB"
// Concurrent stores must never leave more than one key: the singleton
// constraint (0005) forces them to collide-and-upsert, not duplicate. Each
// store_user_private_key opens its own pool, so this is a real race.
let kp2 = server::auth_keys::generate().await?;
let kp3 = server::auth_keys::generate().await?;
let (r2, r3) = tokio::join!(
server::provision::store_user_private_key(&dsn, &kp2.private_pem_b64),
server::provision::store_user_private_key(&dsn, &kp3.private_pem_b64),
r2?;
r3?;
let key_count: i64 = sqlx::query("SELECT count(*) FROM user_auth_keys")
key_count, 1,
"concurrent stores must keep exactly one private key"
/// Two users in distinct provisioned databases set the SAME config key to
/// different values; each reads back only their own — per-user config is
/// isolated, and neither leaks into the other's DB.
async fn per_user_config_is_isolated(admin: &sqlx::PgPool) -> anyhow::Result<()> {
let a = Uuid::new_v4();
let b = Uuid::new_v4();
let da = format!("nomi_user_{}", a.simple());
let dbn = format!("nomi_user_{}", b.simple());
for (id, dsn) in [
(a, server::provision::create_user_database(a).await?),
(b, server::provision::create_user_database(b).await?),
] {
// Register in the global directory so User::get_connection routes to the
// per-user DB.
sqlx::query(
"INSERT INTO users (id, user_name, email, user_password, db_name) \
VALUES ($1, 'cfg-iso', $2, 'x', $3)",
.bind(id)
.bind(format!("cfg-iso-{id}@example.test"))
.bind(&dsn)
server::user::User { id: a }
.set_config("theme", "dark".into())
server::user::User { id: b }
.set_config("theme", "light".into())
// Re-set the same key (case-variant) on a freshly-provisioned DB: this
// exercises the ON CONFLICT (LOWER(field)) upsert against the 0006 index
// that provisioning installs, proving new per-user DBs carry the index.
.set_config("Theme", "midnight".into())
let a_val = server::user::User { id: a }.config("theme").await?;
let b_val = server::user::User { id: b }.config("theme").await?;
assert_eq!(a_val.map(|v| v.to_string()), Some("midnight".to_string()));
assert_eq!(b_val.map(|v| v.to_string()), Some("light".to_string()));
sqlx::query("DELETE FROM users WHERE id = $1 OR id = $2")
.bind(a)
.bind(b)
drop_database(admin, &da).await;
drop_database(admin, &dbn).await;
/// Distinct users get distinct, deterministically-named databases.
async fn two_users_get_distinct_databases(admin: &sqlx::PgPool) -> anyhow::Result<()> {
let db = format!("nomi_user_{}", b.simple());
let dsn_a = server::provision::create_user_database(a).await?;
let dsn_b = server::provision::create_user_database(b).await?;
assert_ne!(dsn_a, dsn_b, "distinct users must get distinct DSNs");
let base = std::env::var("DATABASE_URL")?;
assert_eq!(dsn_a, per_user_dsn(&base, &da));
assert_eq!(dsn_b, per_user_dsn(&base, &db));
drop_database(admin, &db).await;