dbnexus 0.3.1

An enterprise-grade database abstraction layer for Rust with built-in permission control and connection pooling
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
// Copyright (c) 2026 Kirky.X
//
// Licensed under the MIT License.
// See LICENSE file in project root for full license information.

//! database::pool::DbPool 单元测试
//!
//! 覆盖:
//! - `DbPool::new` / `with_config` / `try_from_config` 构造路径
//! - `DbPoolBuilder` 链式构造(url/config/max/min/admin_role/build)
//! - `status()` 初始状态与不变量(total = active + idle)
//! - `config()` / `get_actual_config()` 返回值
//! - `get_session(role)` 权限检查(admin/system 通过,无权限配置时其他角色拒绝)
//! - `Clone` 语义、并发 get_session、max_connections=1 边界
//!
//! 所有测试使用 SQLite 内存数据库,需要 `sqlite` feature。

#![cfg(feature = "sqlite")]

use dbnexus::{DbConfig, DbPool, DbPoolBuilder};

#[path = "../../common/mod.rs"]
mod common;

// ============================================================================
// 构造测试
// ============================================================================

/// TEST-U-DPOOL-001: DbPool::new(sqlite::memory:) 应成功
#[tokio::test]
async fn test_db_pool_new_sqlite_memory() {
    let pool = DbPool::new("sqlite::memory:").await;
    assert!(pool.is_ok(), "DbPool::new should succeed for sqlite::memory:");
    let pool = pool.unwrap();
    let status = pool.status();
    // 默认 min_connections=5,池会预热创建连接
    assert!(
        status.total >= 1,
        "pool should have at least 1 connection after warmup, got {}",
        status.total
    );
}

/// TEST-U-DPOOL-002: DbPool::with_config 应成功
#[tokio::test]
async fn test_db_pool_with_config() {
    let config = DbConfig {
        url: "sqlite::memory:".to_string(),
        max_connections: 10,
        min_connections: 1,
        ..Default::default()
    };
    let pool = DbPool::with_config(config).await;
    assert!(pool.is_ok(), "with_config should succeed");
}

/// TEST-U-DPOOL-003: DbPool::try_from_config 应与 with_config 行为一致
#[tokio::test]
async fn test_db_pool_try_from_config() {
    let config = DbConfig {
        url: "sqlite::memory:".to_string(),
        ..Default::default()
    };
    let pool = DbPool::try_from_config(config).await;
    assert!(pool.is_ok(), "try_from_config should succeed");
}

/// TEST-U-DPOOL-004: DbPool::new 无效 URL 应返回错误
#[tokio::test]
async fn test_db_pool_new_invalid_url_fails() {
    let result = DbPool::new("invalid://nonexistent").await;
    assert!(result.is_err(), "invalid URL should fail");
}

// ============================================================================
// DbPoolBuilder 测试
// ============================================================================

/// TEST-U-DPOOL-005: DbPoolBuilder 通过 url 构造
#[tokio::test]
async fn test_db_pool_builder_with_url() {
    let pool = DbPoolBuilder::new().url("sqlite::memory:").build().await;
    assert!(pool.is_ok(), "builder with url should succeed");
}

/// TEST-U-DPOOL-006: DbPoolBuilder 通过 config 构造
#[tokio::test]
async fn test_db_pool_builder_with_config() {
    let config = DbConfig {
        url: "sqlite::memory:".to_string(),
        max_connections: 5,
        ..Default::default()
    };
    let pool = DbPoolBuilder::new().config(config).build().await;
    assert!(pool.is_ok(), "builder with config should succeed");
    let pool = pool.unwrap();
    assert_eq!(pool.config().max_connections, 5);
}

/// TEST-U-DPOOL-007: DbPoolBuilder 链式设置 max/min connections
#[tokio::test]
async fn test_db_pool_builder_max_min_connections() {
    let pool = DbPoolBuilder::new()
        .url("sqlite::memory:")
        .max_connections(15)
        .min_connections(2)
        .build()
        .await;
    assert!(pool.is_ok());
    let pool = pool.unwrap();
    assert_eq!(pool.config().max_connections, 15);
    assert_eq!(pool.config().min_connections, 2);
}

/// TEST-U-DPOOL-008: DbPoolBuilder 设置 admin_role
#[tokio::test]
async fn test_db_pool_builder_admin_role() {
    let pool = DbPoolBuilder::new()
        .url("sqlite::memory:")
        .admin_role("root")
        .build()
        .await;
    assert!(pool.is_ok());
    let pool = pool.unwrap();
    assert_eq!(pool.config().admin_role, "root");
}

// ============================================================================
// status / config 测试
// ============================================================================

/// TEST-U-DPOOL-009: 初始 status 应满足 total = active + idle(min_connections=0 时为 0)
#[tokio::test]
async fn test_db_pool_status_initial() {
    // 使用 min_connections=0 确保初始状态无连接
    let config = DbConfig {
        url: "sqlite::memory:".to_string(),
        min_connections: 0,
        max_connections: 20,
        ..Default::default()
    };
    let pool = DbPool::with_config(config).await.unwrap();
    let status = pool.status();
    assert_eq!(status.total, 0);
    assert_eq!(status.active, 0);
    assert_eq!(status.idle, 0);
}

/// TEST-U-DPOOL-010: status 不变量 total = active + idle
#[tokio::test]
async fn test_db_pool_status_invariant_total_equals_active_plus_idle() {
    let pool = common::make_sqlite_memory_pool().await;
    let status = pool.status();
    assert_eq!(
        status.total,
        status.active + status.idle,
        "invariant: total == active + idle"
    );
}

/// TEST-U-DPOOL-011: config() 应返回构建时配置
#[tokio::test]
async fn test_db_pool_config_returns_built_config() {
    let config = DbConfig {
        url: "sqlite::memory:".to_string(),
        max_connections: 7,
        admin_role: "sa".to_string(),
        ..Default::default()
    };
    let pool = DbPool::with_config(config).await.unwrap();
    assert_eq!(pool.config().max_connections, 7);
    assert_eq!(pool.config().admin_role, "sa");
    assert_eq!(pool.config().url, "sqlite::memory:");
}

/// TEST-U-DPOOL-012: get_actual_config() 应与 config() 一致
#[tokio::test]
async fn test_db_pool_get_actual_config() {
    let pool = common::make_sqlite_memory_pool().await;
    let cfg1 = pool.config();
    let cfg2 = pool.get_actual_config();
    assert_eq!(cfg1.url, cfg2.url);
    assert_eq!(cfg1.max_connections, cfg2.max_connections);
}

// ============================================================================
// get_session 权限测试
// ============================================================================

/// TEST-U-DPOOL-013: get_session("admin") 应成功
#[tokio::test]
async fn test_db_pool_get_session_admin() {
    let pool = common::make_sqlite_memory_pool().await;
    let session = pool.get_session("admin").await;
    assert!(session.is_ok(), "admin role should always be allowed");
    let session = session.unwrap();
    assert_eq!(session.role(), "admin");
}

/// TEST-U-DPOOL-014: get_session("system") 应成功(无权限配置时安全角色)
#[tokio::test]
async fn test_db_pool_get_session_system() {
    let pool = common::make_sqlite_memory_pool().await;
    let session = pool.get_session("system").await;
    assert!(
        session.is_ok(),
        "system role should be allowed without permission config"
    );
}

/// TEST-U-DPOOL-015: 无权限配置时 get_session("guest") 应失败
#[cfg(feature = "permission")]
#[tokio::test]
async fn test_db_pool_get_session_unauthorized_role_fails() {
    let pool = common::make_sqlite_memory_pool().await;
    let result = pool.get_session("guest").await;
    assert!(
        result.is_err(),
        "non-safe role should be rejected without permission config"
    );
    // Session 不实现 Debug,用 match 代替 unwrap_err
    match result {
        Err(dbnexus::DbError::Permission(_)) => { /* expected */ }
        Err(other) => panic!("expected Permission error, got {:?}", other),
        Ok(_) => panic!("expected error, got Ok"),
    }
}

/// TEST-U-DPOOL-016: get_session 后 session role 应匹配请求
#[tokio::test]
async fn test_db_pool_get_session_role_matches() {
    let pool = common::make_sqlite_memory_pool().await;
    let session = pool.get_session("admin").await.unwrap();
    assert_eq!(session.role(), "admin");
    // Session 被 drop 后连接应归还
    drop(session);
}

// ============================================================================
// Clone 测试
// ============================================================================

/// TEST-U-DPOOL-017: Clone 后的 pool 应独立可用
#[tokio::test]
async fn test_db_pool_clone_usable() {
    let pool = common::make_sqlite_memory_pool().await;
    let cloned = pool.clone();
    // 克隆的 pool 也应能获取 session
    let session = cloned.get_session("admin").await;
    assert!(session.is_ok(), "cloned pool should be usable");
}

/// TEST-U-DPOOL-018: Clone 后 config 应相等
#[tokio::test]
async fn test_db_pool_clone_config_equal() {
    let pool = common::make_sqlite_memory_pool().await;
    let cloned = pool.clone();
    assert_eq!(pool.config().url, cloned.config().url);
    assert_eq!(pool.config().max_connections, cloned.config().max_connections);
}

// ============================================================================
// 边界测试
// ============================================================================

/// TEST-U-DPOOL-019: max_connections=1 边界
#[tokio::test]
async fn test_db_pool_max_connections_one() {
    let config = DbConfig {
        url: "sqlite::memory:".to_string(),
        max_connections: 1,
        min_connections: 0,
        ..Default::default()
    };
    let pool = DbPool::with_config(config).await.unwrap();
    let _session = pool.get_session("admin").await.expect("first session should succeed");
    // session 持有期间连接被占用;drop 后释放
}

/// TEST-U-DPOOL-020: 串行 get_session 多次应成功(连接回收)
#[tokio::test]
async fn test_db_pool_serial_get_session_reuses_connection() {
    let pool = common::make_sqlite_memory_pool().await;
    for i in 0..5 {
        let session = pool.get_session("admin").await;
        assert!(session.is_ok(), "iteration {} should succeed", i);
        // 每次 drop session 归还连接
        drop(session);
    }
}

// ============================================================================
// 并发测试
// ============================================================================

/// TEST-U-DPOOL-021: 并发 get_session 不破坏 status 不变量
#[tokio::test]
async fn test_db_pool_concurrent_get_session_preserves_invariants() {
    use std::sync::Arc;
    let pool = Arc::new(common::make_sqlite_memory_pool().await);
    let max = pool.config().max_connections;

    let mut handles = Vec::new();
    for _ in 0..max {
        let pool_clone = Arc::clone(&pool);
        handles.push(tokio::spawn(async move {
            let _session = pool_clone.get_session("admin").await.unwrap();
            // 持有 session 一小段时间
            tokio::time::sleep(std::time::Duration::from_millis(10)).await;
            // session drop 在 task 结束时发生
        }));
    }

    // 等待所有任务完成
    for handle in handles {
        handle.await.unwrap();
    }

    // 完成后 status 应恢复(active 回到 0 或接近 0)
    let status = pool.status();
    assert!(status.total >= status.active, "invariant violated: total >= active");
}

// ============================================================================
// Drop 测试
// ============================================================================

/// TEST-U-DPOOL-022: DbPool drop 不应 panic
#[tokio::test]
async fn test_db_pool_drop_no_panic() {
    let pool = common::make_sqlite_memory_pool().await;
    // 显式 drop,不应 panic
    drop(pool);
}

/// TEST-U-DPOOL-023: 带 session 的 DbPool drop 不应 panic
#[tokio::test]
async fn test_db_pool_drop_with_active_session_no_panic() {
    let pool = common::make_sqlite_memory_pool().await;
    let _session = pool.get_session("admin").await.unwrap();
    // pool 和 session 同时超出作用域,不应 panic
    drop(pool);
    drop(_session);
}

// ============================================================================
// try_from 同步构造器测试(permission feature 启用时返回 Err)
// ============================================================================

/// TEST-U-DPOOL-024: try_from 在 permission feature 启用时应返回 Err
#[cfg(feature = "permission")]
#[test]
fn test_db_pool_try_from_with_permission_returns_err() {
    let config = DbConfig {
        url: "sqlite::memory:".to_string(),
        max_connections: 5,
        ..Default::default()
    };
    let result = DbPool::try_from(&config);
    assert!(
        result.is_err(),
        "try_from should return Err when permission feature is enabled (sync constructor cannot init cache)"
    );
}

// ============================================================================
// DbPoolBuilder 边界测试
// ============================================================================

/// TEST-U-DPOOL-025: DbPoolBuilder::build() 未提供 url 或 config 应返回 Err
#[tokio::test]
async fn test_db_pool_builder_no_url_no_config_fails() {
    let result = DbPoolBuilder::new().build().await;
    assert!(result.is_err(), "build() without url or config should fail");
}

/// TEST-U-DPOOL-026: DbPoolBuilder::admin_role 在 config 已设置时应修改 config
#[tokio::test]
async fn test_db_pool_builder_admin_role_with_config() {
    let config = DbConfig {
        url: "sqlite::memory:".to_string(),
        admin_role: "initial".to_string(),
        ..Default::default()
    };
    let pool = DbPoolBuilder::new()
        .config(config)
        .admin_role("root")
        .build()
        .await
        .unwrap();
    assert_eq!(pool.config().admin_role, "root");
}

/// TEST-U-DPOOL-027: DbPoolBuilder::max_connections 在只有 url 时应创建 config
#[tokio::test]
async fn test_db_pool_builder_max_connections_with_url_only() {
    let pool = DbPoolBuilder::new()
        .url("sqlite::memory:")
        .max_connections(13)
        .build()
        .await
        .unwrap();
    assert_eq!(pool.config().max_connections, 13);
}

/// TEST-U-DPOOL-028: DbPoolBuilder::min_connections 在只有 url 时应创建 config
#[tokio::test]
async fn test_db_pool_builder_min_connections_with_url_only() {
    let pool = DbPoolBuilder::new()
        .url("sqlite::memory:")
        .min_connections(2)
        .build()
        .await
        .unwrap();
    assert_eq!(pool.config().min_connections, 2);
}

/// TEST-U-DPOOL-029: DbPoolBuilder deprecated setters 应为 no-op 但不 panic
#[allow(deprecated)]
#[tokio::test]
async fn test_db_pool_builder_deprecated_setters_are_noop() {
    // metrics_collector setter (deprecated, no-op)
    #[cfg(feature = "metrics")]
    {
        use dbnexus::MetricsCollector;
        let collector = std::sync::Arc::new(MetricsCollector::new());
        let _ = DbPoolBuilder::new()
            .url("sqlite::memory:")
            .metrics_collector(collector)
            .build()
            .await;
    }

    // permission_config setter (deprecated, no-op)
    #[cfg(feature = "permission")]
    {
        use dbnexus::access::permission::PermissionConfig;
        let _ = DbPoolBuilder::new()
            .url("sqlite::memory:")
            .permission_config(PermissionConfig::default())
            .build()
            .await;
    }

    // with_oxcache setter (deprecated, no-op)
    #[cfg(feature = "cache")]
    {
        use oxcache::Cache;
        let cache: std::sync::Arc<Cache<String, serde_json::Value>> =
            std::sync::Arc::new(Cache::builder().capacity(10).build().await.unwrap());
        let _ = DbPoolBuilder::new()
            .url("sqlite::memory:")
            .with_oxcache(cache)
            .build()
            .await;
    }
}

// ============================================================================
// parse_health_check_interval 单元测试
// ============================================================================

/// TEST-U-DPOOL-030: parse_health_check_interval 边界值
#[cfg(feature = "pool-health-check")]
#[test]
fn test_parse_health_check_interval_boundaries() {
    use dbnexus::DbPool;

    // 空字符串返回默认 30
    assert_eq!(DbPool::parse_health_check_interval(""), 30);
    // 有效值
    assert_eq!(DbPool::parse_health_check_interval("60"), 60);
    assert_eq!(DbPool::parse_health_check_interval("5"), 5);
    assert_eq!(DbPool::parse_health_check_interval("300"), 300);
    // 小于下限 → 5
    assert_eq!(DbPool::parse_health_check_interval("1"), 5);
    assert_eq!(DbPool::parse_health_check_interval("4"), 5);
    // 大于上限 → 300
    assert_eq!(DbPool::parse_health_check_interval("301"), 300);
    assert_eq!(DbPool::parse_health_check_interval("1000"), 300);
    // 无效字符串 → 默认 30
    assert_eq!(DbPool::parse_health_check_interval("abc"), 30);
    assert_eq!(DbPool::parse_health_check_interval("12.5"), 30);
    assert_eq!(DbPool::parse_health_check_interval("-5"), 30);
}

// ============================================================================
// check_connection_health 测试
// ============================================================================

/// TEST-U-DPOOL-031: check_connection_health 对健康的 SeaORM 连接应返回 true
#[tokio::test]
async fn test_check_connection_health_healthy_seaorm() {
    use dbnexus::DbConnection;
    let pool = common::make_sqlite_memory_pool().await;
    let sea_conn = sea_orm::Database::connect("sqlite::memory:").await.unwrap();
    let conn = DbConnection::SeaOrm(sea_conn);
    let is_healthy = pool.check_connection_health(&conn).await;
    assert!(is_healthy, "healthy SeaORM connection should return true");
}

// ============================================================================
// 健康检查与清理测试(pool-health-check feature)
// ============================================================================

/// TEST-U-DPOOL-032: clean_invalid_connections 对健康池应返回 0
#[cfg(feature = "pool-health-check")]
#[tokio::test]
async fn test_clean_invalid_connections_healthy_pool() {
    let pool = common::make_sqlite_memory_pool().await;
    let removed = pool.clean_invalid_connections().await;
    assert_eq!(removed, 0, "no invalid connections should be removed from healthy pool");
}

/// TEST-U-DPOOL-033: validate_and_recreate_connections 对健康池应返回 0
#[cfg(feature = "pool-health-check")]
#[tokio::test]
async fn test_validate_and_recreate_connections_healthy_pool() {
    let pool = common::make_sqlite_memory_pool().await;
    let recreated = pool.validate_and_recreate_connections().await;
    assert!(recreated.is_ok(), "should succeed on healthy pool");
    assert_eq!(
        recreated.unwrap(),
        0,
        "no connections should be recreated on healthy pool"
    );
}

// ============================================================================
// pool_metrics 测试(metrics feature)
// ============================================================================

/// TEST-U-DPOOL-034: pool_metrics 在未设置 collector 时应返回全零
#[cfg(feature = "metrics")]
#[tokio::test]
async fn test_pool_metrics_no_collector_returns_zeros() {
    let pool = common::make_sqlite_memory_pool().await;
    let metrics = pool.pool_metrics();
    assert_eq!(metrics.slow_acquires, 0);
    assert_eq!(metrics.timeout_errors, 0);
    assert_eq!(metrics.critical_timeouts, 0);
}

// ============================================================================
// run_auto_migrate 测试(auto-migrate feature)
// ============================================================================

/// TEST-U-DPOOL-035: run_auto_migrate 在无 migrations_dir 时应返回 Ok(0)
#[cfg(feature = "auto-migrate")]
#[tokio::test]
async fn test_run_auto_migrate_no_dir_returns_zero() {
    let config = DbConfig {
        url: "sqlite::memory:".to_string(),
        auto_migrate: true,
        migrations_dir: None,
        ..Default::default()
    };
    let pool = DbPool::with_config(config).await.unwrap();
    let result = pool.run_auto_migrate().await;
    assert!(result.is_ok(), "should succeed when no migrations_dir set");
    assert_eq!(result.unwrap(), 0, "should apply 0 migrations when no dir");
}

// ============================================================================
// DbConnection 方法测试
// ============================================================================

/// TEST-U-DPOOL-036: DbConnection::as_sea_orm 应返回 Ok
#[tokio::test]
async fn test_db_connection_as_sea_orm_ok() {
    use dbnexus::DbConnection;
    let sea_conn = sea_orm::Database::connect("sqlite::memory:").await.unwrap();
    let conn = DbConnection::SeaOrm(sea_conn);
    assert!(
        conn.as_sea_orm().is_ok(),
        "as_sea_orm on SeaOrm variant should return Ok"
    );
}

/// TEST-U-DPOOL-037: DbConnection::is_duckdb 应返回 false(SeaORM 连接)
#[tokio::test]
async fn test_db_connection_is_duckdb_seaorm() {
    use dbnexus::DbConnection;
    let sea_conn = sea_orm::Database::connect("sqlite::memory:").await.unwrap();
    let conn = DbConnection::SeaOrm(sea_conn);
    assert!(!conn.is_duckdb(), "SeaOrm connection should not be duckdb");
}

/// TEST-U-DPOOL-038: DbConnection::Debug 应不 panic
#[tokio::test]
async fn test_db_connection_debug_format() {
    use dbnexus::DbConnection;
    let sea_conn = sea_orm::Database::connect("sqlite::memory:").await.unwrap();
    let conn = DbConnection::SeaOrm(sea_conn);
    let debug_str = format!("{:?}", conn);
    assert!(
        debug_str.contains("SeaOrm"),
        "debug output should contain SeaOrm: {}",
        debug_str
    );
}