1
//! Slice A: per-user database provisioning, exercised end to end against a real
2
//! Postgres via `server::provision::create_user_database`.
3
//!
4
//! Gated on the `db` feature (which turns on `server/test-utils`). These tests
5
//! create real databases through the admin `DATABASE_URL` and drop each one in
6
//! teardown — `sqlx::test` only cleans up the database it created itself, not
7
//! ones the application creates.
8
//!
9
//! All scenarios run inside ONE `#[tokio::test]`. `server::db`'s admin pool is a
10
//! process-static `OnceCell<PgPool>` initialized lazily in the first runtime
11
//! that touches it; a separate `#[tokio::test]` per scenario would each spin up
12
//! its own runtime, and the pool bound to the first (now-dropped) runtime would
13
//! then time out for later tests. One runtime keeps the pool alive throughout —
14
//! and mirrors production, which provisions serially on one long-lived runtime.
15
//!
16
//! Run via:
17
//!   DATABASE_URL=postgres://… cargo test -p tests-integration --features db
18

            
19
#![cfg(feature = "db")]
20

            
21
use sqlx::postgres::PgPoolOptions;
22
use sqlx::{Executor, Row};
23
use uuid::Uuid;
24

            
25
1
async fn admin_connect() -> sqlx::PgPool {
26
1
    let url =
27
1
        std::env::var("DATABASE_URL").expect("DATABASE_URL must be set for provisioning tests");
28
1
    PgPoolOptions::new()
29
1
        .max_connections(1)
30
1
        .connect(&url)
31
1
        .await
32
1
        .expect("connect to admin DB")
33
1
}
34

            
35
2
fn per_user_dsn(base: &str, database_name: &str) -> String {
36
2
    let (prefix, rest) = base.split_once("://").expect("scheme");
37
2
    let authority = rest.split('/').next().expect("authority");
38
2
    format!("{prefix}://{authority}/{database_name}")
39
2
}
40

            
41
7
async fn drop_database(admin: &sqlx::PgPool, database_name: &str) {
42
    // Terminate any lingering backends (a per-user pool may still hold a
43
    // connection) before dropping.
44
7
    let _ = admin
45
7
        .execute(sqlx::query(sqlx::AssertSqlSafe(format!(
46
7
            "SELECT pg_terminate_backend(pid) FROM pg_stat_activity \
47
7
             WHERE datname = '{database_name}' AND pid <> pg_backend_pid()"
48
7
        ))))
49
7
        .await;
50
7
    admin
51
7
        .execute(sqlx::query(sqlx::AssertSqlSafe(format!(
52
7
            "DROP DATABASE IF EXISTS \"{database_name}\""
53
7
        ))))
54
7
        .await
55
7
        .expect("drop provisioned database");
56
7
}
57

            
58
#[tokio::test]
59
1
async fn per_user_provisioning_end_to_end() -> anyhow::Result<()> {
60
1
    let admin = admin_connect().await;
61

            
62
1
    provisions_a_migrated_isolated_db(&admin).await?;
63
1
    is_idempotent_on_retry(&admin).await?;
64
1
    bootstrap_seed_is_idempotent(&admin).await?;
65
1
    admin_has_backfilled_keypair(&admin).await?;
66
1
    two_users_get_distinct_databases(&admin).await?;
67
1
    private_key_is_stored_in_per_user_db(&admin).await?;
68
1
    per_user_config_is_isolated(&admin).await?;
69

            
70
2
    Ok(())
71
1
}
72

            
73
/// A freshly provisioned DB has the full schema, is usable, and contains no
74
/// seeded signing keys (those live only in the admin DB via `bootstrap::seed`).
75
1
async fn provisions_a_migrated_isolated_db(admin: &sqlx::PgPool) -> anyhow::Result<()> {
76
1
    let user_id = Uuid::new_v4();
77
1
    let database_name = format!("nomi_user_{}", user_id.simple());
78

            
79
    // Returns the database NAME, not a DSN: a connection string in
80
    // `users.database_name` would put a live credential in a row (ADR 0032).
81
1
    let returned = server::provision::create_user_database(user_id).await?;
82
1
    assert_eq!(
83
        returned, database_name,
84
        "provisioning must return the bare database name"
85
    );
86

            
87
1
    let base = std::env::var("DATABASE_URL")?;
88
1
    let user_pool = PgPoolOptions::new()
89
1
        .max_connections(1)
90
1
        .connect(&per_user_dsn(&base, &database_name))
91
1
        .await?;
92
1
    let accounts: i64 = sqlx::query("SELECT count(*) FROM accounts")
93
1
        .fetch_one(&user_pool)
94
1
        .await?
95
1
        .get(0);
96
1
    assert_eq!(
97
        accounts, 0,
98
        "fresh per-user DB has the accounts table, empty"
99
    );
100

            
101
1
    let secret_rows: i64 =
102
1
        sqlx::query("SELECT count(*) FROM config WHERE lower(field) LIKE '%token_private_key'")
103
1
            .fetch_one(&user_pool)
104
1
            .await?
105
1
            .get(0);
106
1
    assert_eq!(secret_rows, 0, "per-user DB must not contain signing keys");
107

            
108
1
    user_pool.close().await;
109
1
    drop_database(admin, &database_name).await;
110
1
    Ok(())
111
1
}
112

            
113
/// A second provision for the same user id hits SQLSTATE 42P04 (duplicate
114
/// database) and succeeds, returning the same DSN.
115
1
async fn is_idempotent_on_retry(admin: &sqlx::PgPool) -> anyhow::Result<()> {
116
1
    let user_id = Uuid::new_v4();
117
1
    let database_name = format!("nomi_user_{}", user_id.simple());
118

            
119
1
    let first = server::provision::create_user_database(user_id).await?;
120
1
    let second = server::provision::create_user_database(user_id).await?;
121
1
    assert_eq!(first, second);
122

            
123
1
    drop_database(admin, &database_name).await;
124
1
    Ok(())
125
1
}
126

            
127
/// `seed()` is guarded by the `seed_complete` marker, so a second call is a
128
/// no-op and never duplicates rows (config has no UNIQUE(field) yet).
129
1
async fn bootstrap_seed_is_idempotent(admin: &sqlx::PgPool) -> anyhow::Result<()> {
130
1
    server::bootstrap::seed().await?;
131
1
    let after_first: i64 = sqlx::query("SELECT count(*) FROM config")
132
1
        .fetch_one(admin)
133
1
        .await?
134
1
        .get(0);
135

            
136
1
    server::bootstrap::seed().await?;
137
1
    let after_second: i64 = sqlx::query("SELECT count(*) FROM config")
138
1
        .fetch_one(admin)
139
1
        .await?
140
1
        .get(0);
141

            
142
1
    assert_eq!(
143
        after_first, after_second,
144
        "re-running seed must not change config row count"
145
    );
146
1
    let marker: i64 =
147
1
        sqlx::query("SELECT count(*) FROM config WHERE lower(field) = 'seed_complete'")
148
1
            .fetch_one(admin)
149
1
            .await?
150
1
            .get(0);
151
1
    assert_eq!(marker, 1, "exactly one seed_complete marker");
152

            
153
    // The admin's data lives in the admin database itself, so bootstrap points
154
    // its database_name at *this* database — a bare name, never the empty seed
155
    // placeholder and never a DSN, which would store a live credential in a row.
156
1
    let admin_db_name: String = sqlx::query(
157
1
        "SELECT database_name FROM users WHERE id = '22d36bde-d987-4927-b070-842efdcf9c59'",
158
1
    )
159
1
    .fetch_one(admin)
160
1
    .await?
161
1
    .get(0);
162
1
    let expected: String = sqlx::query("SELECT current_database()")
163
1
        .fetch_one(admin)
164
1
        .await?
165
1
        .get(0);
166
1
    assert_eq!(
167
        admin_db_name, expected,
168
        "admin database_name must be this database's name, not a placeholder or a DSN"
169
    );
170
1
    assert!(
171
1
        !admin_db_name.contains("://"),
172
        "database_name must never hold a connection string: {admin_db_name}"
173
    );
174
1
    Ok(())
175
1
}
176

            
177
/// After `bootstrap::seed`, the admin user has a public signing key in the
178
/// global directory and a matching private key in the admin DB — so admin login
179
/// keeps working once the global seed keys are retired (Slice B cutover).
180
1
async fn admin_has_backfilled_keypair(admin: &sqlx::PgPool) -> anyhow::Result<()> {
181
1
    server::bootstrap::seed().await?;
182

            
183
1
    let public_key: Option<String> = sqlx::query(
184
1
        "SELECT jwt_public_key FROM users WHERE id = '22d36bde-d987-4927-b070-842efdcf9c59'",
185
1
    )
186
1
    .fetch_one(admin)
187
1
    .await?
188
1
    .get(0);
189
1
    assert!(
190
1
        public_key.is_some_and(|k| !k.is_empty()),
191
        "admin must have a backfilled public key"
192
    );
193

            
194
1
    let private_keys: i64 = sqlx::query("SELECT count(*) FROM user_auth_keys")
195
1
        .fetch_one(admin)
196
1
        .await?
197
1
        .get(0);
198
1
    assert!(
199
1
        private_keys >= 1,
200
        "admin DB must hold the admin private key"
201
    );
202

            
203
    // Repair path: a prior run that wrote only the public key (then crashed
204
    // before the private insert) must be healed on the next seed, not skipped.
205
1
    sqlx::query("DELETE FROM user_auth_keys")
206
1
        .execute(admin)
207
1
        .await?;
208
1
    server::bootstrap::seed().await?;
209
1
    let repaired: i64 = sqlx::query("SELECT count(*) FROM user_auth_keys")
210
1
        .fetch_one(admin)
211
1
        .await?
212
1
        .get(0);
213
1
    assert_eq!(
214
        repaired, 1,
215
        "seed must repair a missing admin private key (public-set-but-private-missing)"
216
    );
217
1
    Ok(())
218
1
}
219

            
220
/// `store_user_private_key` writes the private key into the per-user DB only.
221
1
async fn private_key_is_stored_in_per_user_db(admin: &sqlx::PgPool) -> anyhow::Result<()> {
222
1
    let user_id = Uuid::new_v4();
223
1
    let database_name = format!("nomi_user_{}", user_id.simple());
224

            
225
    // Both the provisioner and the key store speak database *names*; only
226
    // this test composes a DSN, to look inside the tenant database directly.
227
1
    let name = server::provision::create_user_database(user_id).await?;
228
1
    let keypair = server::auth_keys::generate().await?;
229
1
    server::provision::store_user_private_key(&name, &keypair.private_pem_b64).await?;
230

            
231
1
    let base = std::env::var("DATABASE_URL")?;
232
1
    let user_pool = PgPoolOptions::new()
233
1
        .max_connections(1)
234
1
        .connect(&per_user_dsn(&base, &database_name))
235
1
        .await?;
236
1
    let stored: String = sqlx::query("SELECT private_key FROM user_auth_keys")
237
1
        .fetch_one(&user_pool)
238
1
        .await?
239
1
        .get(0);
240
1
    assert_eq!(
241
        stored, keypair.private_pem_b64,
242
        "private key round-trips in per-user DB"
243
    );
244

            
245
    // Concurrent stores must never leave more than one key: the singleton
246
    // constraint (0005) forces them to collide-and-upsert, not duplicate. Each
247
    // store_user_private_key opens its own pool, so this is a real race.
248
1
    let kp2 = server::auth_keys::generate().await?;
249
1
    let kp3 = server::auth_keys::generate().await?;
250
1
    let (r2, r3) = tokio::join!(
251
1
        server::provision::store_user_private_key(&name, &kp2.private_pem_b64),
252
1
        server::provision::store_user_private_key(&name, &kp3.private_pem_b64),
253
    );
254
1
    r2?;
255
1
    r3?;
256
1
    let key_count: i64 = sqlx::query("SELECT count(*) FROM user_auth_keys")
257
1
        .fetch_one(&user_pool)
258
1
        .await?
259
1
        .get(0);
260
1
    assert_eq!(
261
        key_count, 1,
262
        "concurrent stores must keep exactly one private key"
263
    );
264
1
    user_pool.close().await;
265

            
266
1
    drop_database(admin, &database_name).await;
267
1
    Ok(())
268
1
}
269

            
270
/// Two users in distinct provisioned databases set the SAME config key to
271
/// different values; each reads back only their own — per-user config is
272
/// isolated, and neither leaks into the other's DB.
273
1
async fn per_user_config_is_isolated(admin: &sqlx::PgPool) -> anyhow::Result<()> {
274
1
    let a = Uuid::new_v4();
275
1
    let b = Uuid::new_v4();
276
1
    let da = format!("nomi_user_{}", a.simple());
277
1
    let dbn = format!("nomi_user_{}", b.simple());
278

            
279
2
    for (id, dsn) in [
280
1
        (a, server::provision::create_user_database(a).await?),
281
1
        (b, server::provision::create_user_database(b).await?),
282
    ] {
283
        // Register in the global directory so User::get_connection routes to the
284
        // per-user DB.
285
2
        sqlx::query(
286
2
            // `verified` defaults FALSE and the tenant pool refuses a disabled
287
2
            // account, so this fixture must opt in explicitly.
288
2
            "INSERT INTO users (id, user_name, email, user_password, database_name, verified) \
289
2
             VALUES ($1, 'cfg-iso', $2, 'x', $3, TRUE)",
290
2
        )
291
2
        .bind(id)
292
2
        .bind(format!("cfg-iso-{id}@example.test"))
293
2
        .bind(&dsn)
294
2
        .execute(admin)
295
2
        .await?;
296
    }
297

            
298
1
    server::user::User { id: a }
299
1
        .set_config("theme", "dark".into())
300
1
        .await?;
301
1
    server::user::User { id: b }
302
1
        .set_config("theme", "light".into())
303
1
        .await?;
304

            
305
    // Re-set the same key (case-variant) on a freshly-provisioned DB: this
306
    // exercises the ON CONFLICT (LOWER(field)) upsert against the 0006 index
307
    // that provisioning installs, proving new per-user DBs carry the index.
308
1
    server::user::User { id: a }
309
1
        .set_config("Theme", "midnight".into())
310
1
        .await?;
311

            
312
1
    let a_val = server::user::User { id: a }.config("theme").await?;
313
1
    let b_val = server::user::User { id: b }.config("theme").await?;
314
1
    assert_eq!(a_val.map(|v| v.to_string()), Some("midnight".to_string()));
315
1
    assert_eq!(b_val.map(|v| v.to_string()), Some("light".to_string()));
316

            
317
1
    sqlx::query("DELETE FROM users WHERE id = $1 OR id = $2")
318
1
        .bind(a)
319
1
        .bind(b)
320
1
        .execute(admin)
321
1
        .await?;
322
1
    drop_database(admin, &da).await;
323
1
    drop_database(admin, &dbn).await;
324
1
    Ok(())
325
1
}
326

            
327
/// Distinct users get distinct, deterministically-named databases.
328
1
async fn two_users_get_distinct_databases(admin: &sqlx::PgPool) -> anyhow::Result<()> {
329
1
    let a = Uuid::new_v4();
330
1
    let b = Uuid::new_v4();
331
1
    let da = format!("nomi_user_{}", a.simple());
332
1
    let db = format!("nomi_user_{}", b.simple());
333

            
334
1
    let name_a = server::provision::create_user_database(a).await?;
335
1
    let name_b = server::provision::create_user_database(b).await?;
336
1
    assert_ne!(name_a, name_b, "distinct users must get distinct databases");
337

            
338
1
    assert_eq!(name_a, da);
339
1
    assert_eq!(name_b, db);
340

            
341
1
    drop_database(admin, &da).await;
342
1
    drop_database(admin, &db).await;
343
1
    Ok(())
344
1
}