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(sqlx::query(sqlx::AssertSqlSafe(format!(
46
7
            "SELECT pg_terminate_backend(pid) FROM pg_stat_activity \
47
7
             WHERE datname = '{db_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 \"{db_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 db_name = format!("nomi_user_{}", user_id.simple());
78

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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