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, db_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}/{db_name}")
39
2
}
40

            
41
7
async fn drop_database(admin: &sqlx::PgPool, db_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(
46
7
            format!(
47
                "SELECT pg_terminate_backend(pid) FROM pg_stat_activity \
48
                 WHERE datname = '{db_name}' AND pid <> pg_backend_pid()"
49
            )
50
7
            .as_str(),
51
        )
52
7
        .await;
53
7
    admin
54
7
        .execute(format!("DROP DATABASE IF EXISTS \"{db_name}\"").as_str())
55
7
        .await
56
7
        .expect("drop provisioned database");
57
7
}
58

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

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

            
71
2
    Ok(())
72
1
}
73

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

            
80
1
    let dsn = server::provision::create_user_database(user_id).await?;
81
1
    assert!(
82
1
        dsn.ends_with(&db_name),
83
        "dsn should target the per-user db: {dsn}"
84
    );
85

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

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

            
106
1
    user_pool.close().await;
107
1
    drop_database(admin, &db_name).await;
108
1
    Ok(())
109
1
}
110

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

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

            
121
1
    drop_database(admin, &db_name).await;
122
1
    Ok(())
123
1
}
124

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

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

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

            
151
    // The admin's db_name must equal this deployment's DATABASE_URL (set by
152
    // bootstrap), never the empty seed placeholder or a hardcoded URL.
153
1
    let admin_db_name: String =
154
1
        sqlx::query("SELECT db_name FROM users WHERE id = '22d36bde-d987-4927-b070-842efdcf9c59'")
155
1
            .fetch_one(admin)
156
1
            .await?
157
1
            .get(0);
158
1
    let expected = std::env::var("DATABASE_URL")?;
159
1
    assert_eq!(
160
        admin_db_name, expected,
161
        "admin db_name must track DATABASE_URL, not a placeholder or hardcoded URL"
162
    );
163
1
    Ok(())
164
1
}
165

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

            
172
1
    let public_key: Option<String> = sqlx::query(
173
1
        "SELECT jwt_public_key FROM users WHERE id = '22d36bde-d987-4927-b070-842efdcf9c59'",
174
1
    )
175
1
    .fetch_one(admin)
176
1
    .await?
177
1
    .get(0);
178
1
    assert!(
179
1
        public_key.is_some_and(|k| !k.is_empty()),
180
        "admin must have a backfilled public key"
181
    );
182

            
183
1
    let private_keys: i64 = sqlx::query("SELECT count(*) FROM user_auth_keys")
184
1
        .fetch_one(admin)
185
1
        .await?
186
1
        .get(0);
187
1
    assert!(
188
1
        private_keys >= 1,
189
        "admin DB must hold the admin private key"
190
    );
191

            
192
    // Repair path: a prior run that wrote only the public key (then crashed
193
    // before the private insert) must be healed on the next seed, not skipped.
194
1
    sqlx::query("DELETE FROM user_auth_keys")
195
1
        .execute(admin)
196
1
        .await?;
197
1
    server::bootstrap::seed().await?;
198
1
    let repaired: i64 = sqlx::query("SELECT count(*) FROM user_auth_keys")
199
1
        .fetch_one(admin)
200
1
        .await?
201
1
        .get(0);
202
1
    assert_eq!(
203
        repaired, 1,
204
        "seed must repair a missing admin private key (public-set-but-private-missing)"
205
    );
206
1
    Ok(())
207
1
}
208

            
209
/// `store_user_private_key` writes the private key into the per-user DB only.
210
1
async fn private_key_is_stored_in_per_user_db(admin: &sqlx::PgPool) -> anyhow::Result<()> {
211
1
    let user_id = Uuid::new_v4();
212
1
    let db_name = format!("nomi_user_{}", user_id.simple());
213

            
214
1
    let dsn = server::provision::create_user_database(user_id).await?;
215
1
    let keypair = server::auth_keys::generate().await?;
216
1
    server::provision::store_user_private_key(&dsn, &keypair.private_pem_b64).await?;
217

            
218
1
    let user_pool = PgPoolOptions::new()
219
1
        .max_connections(1)
220
1
        .connect(&dsn)
221
1
        .await?;
222
1
    let stored: String = sqlx::query("SELECT private_key FROM user_auth_keys")
223
1
        .fetch_one(&user_pool)
224
1
        .await?
225
1
        .get(0);
226
1
    assert_eq!(
227
        stored, keypair.private_pem_b64,
228
        "private key round-trips in per-user DB"
229
    );
230

            
231
    // Concurrent stores must never leave more than one key: the singleton
232
    // constraint (0005) forces them to collide-and-upsert, not duplicate. Each
233
    // store_user_private_key opens its own pool, so this is a real race.
234
1
    let kp2 = server::auth_keys::generate().await?;
235
1
    let kp3 = server::auth_keys::generate().await?;
236
1
    let (r2, r3) = tokio::join!(
237
1
        server::provision::store_user_private_key(&dsn, &kp2.private_pem_b64),
238
1
        server::provision::store_user_private_key(&dsn, &kp3.private_pem_b64),
239
    );
240
1
    r2?;
241
1
    r3?;
242
1
    let key_count: i64 = sqlx::query("SELECT count(*) FROM user_auth_keys")
243
1
        .fetch_one(&user_pool)
244
1
        .await?
245
1
        .get(0);
246
1
    assert_eq!(
247
        key_count, 1,
248
        "concurrent stores must keep exactly one private key"
249
    );
250
1
    user_pool.close().await;
251

            
252
1
    drop_database(admin, &db_name).await;
253
1
    Ok(())
254
1
}
255

            
256
/// Two users in distinct provisioned databases set the SAME config key to
257
/// different values; each reads back only their own — per-user config is
258
/// isolated, and neither leaks into the other's DB.
259
1
async fn per_user_config_is_isolated(admin: &sqlx::PgPool) -> anyhow::Result<()> {
260
1
    let a = Uuid::new_v4();
261
1
    let b = Uuid::new_v4();
262
1
    let da = format!("nomi_user_{}", a.simple());
263
1
    let dbn = format!("nomi_user_{}", b.simple());
264

            
265
2
    for (id, dsn) in [
266
1
        (a, server::provision::create_user_database(a).await?),
267
1
        (b, server::provision::create_user_database(b).await?),
268
    ] {
269
        // Register in the global directory so User::get_connection routes to the
270
        // per-user DB.
271
2
        sqlx::query(
272
2
            "INSERT INTO users (id, user_name, email, user_password, db_name) \
273
2
             VALUES ($1, 'cfg-iso', $2, 'x', $3)",
274
2
        )
275
2
        .bind(id)
276
2
        .bind(format!("cfg-iso-{id}@example.test"))
277
2
        .bind(&dsn)
278
2
        .execute(admin)
279
2
        .await?;
280
    }
281

            
282
1
    server::user::User { id: a }
283
1
        .set_config("theme", "dark".into())
284
1
        .await?;
285
1
    server::user::User { id: b }
286
1
        .set_config("theme", "light".into())
287
1
        .await?;
288

            
289
    // Re-set the same key (case-variant) on a freshly-provisioned DB: this
290
    // exercises the ON CONFLICT (LOWER(field)) upsert against the 0006 index
291
    // that provisioning installs, proving new per-user DBs carry the index.
292
1
    server::user::User { id: a }
293
1
        .set_config("Theme", "midnight".into())
294
1
        .await?;
295

            
296
1
    let a_val = server::user::User { id: a }.config("theme").await?;
297
1
    let b_val = server::user::User { id: b }.config("theme").await?;
298
1
    assert_eq!(a_val.map(|v| v.to_string()), Some("midnight".to_string()));
299
1
    assert_eq!(b_val.map(|v| v.to_string()), Some("light".to_string()));
300

            
301
1
    sqlx::query("DELETE FROM users WHERE id = $1 OR id = $2")
302
1
        .bind(a)
303
1
        .bind(b)
304
1
        .execute(admin)
305
1
        .await?;
306
1
    drop_database(admin, &da).await;
307
1
    drop_database(admin, &dbn).await;
308
1
    Ok(())
309
1
}
310

            
311
/// Distinct users get distinct, deterministically-named databases.
312
1
async fn two_users_get_distinct_databases(admin: &sqlx::PgPool) -> anyhow::Result<()> {
313
1
    let a = Uuid::new_v4();
314
1
    let b = Uuid::new_v4();
315
1
    let da = format!("nomi_user_{}", a.simple());
316
1
    let db = format!("nomi_user_{}", b.simple());
317

            
318
1
    let dsn_a = server::provision::create_user_database(a).await?;
319
1
    let dsn_b = server::provision::create_user_database(b).await?;
320
1
    assert_ne!(dsn_a, dsn_b, "distinct users must get distinct DSNs");
321

            
322
1
    let base = std::env::var("DATABASE_URL")?;
323
1
    assert_eq!(dsn_a, per_user_dsn(&base, &da));
324
1
    assert_eq!(dsn_b, per_user_dsn(&base, &db));
325

            
326
1
    drop_database(admin, &da).await;
327
1
    drop_database(admin, &db).await;
328
1
    Ok(())
329
1
}