refactor: complete v0.8 core architecture

This commit is contained in:
thakares committed 2026-08-21 13:56:36 +05:30
1 parent f138a645f8
commit d7e0ac7679
98 files changed
+8413 -3044

No files matched your search

+11 -2
View File
@@ -20,6 +20,15 @@ async fn test_admin_user_listing() {
let db = Db::init(&config).expect("Failed to init Db");
let _ = bzod::cli::create_admin::run(
Some("admin".to_string()),
Some("adminpass123".to_string()),
None,
config.clone(),
)
.await
.unwrap();
let _ = bzod::cli::create_user::run(
Some("usera".to_string()),
Some("password123".to_string()),
@@ -32,9 +41,9 @@ async fn test_admin_user_listing() {
let conn = db.users.lock().unwrap();
let users = bzod::db::users::list_users(&conn).unwrap();
// Verify list contains legacy_admin and usera
// Verify list contains admin and usera
assert!(users.len() >= 2);
assert!(users.iter().any(|u| u.username == "legacy_admin"));
assert!(users.iter().any(|u| u.username == "admin"));
assert!(users.iter().any(|u| u.username == "usera"));
let _ = fs::remove_dir_all(&temp_dir);
-2
View File
@@ -33,8 +33,6 @@ fn build_state(config: Config) -> (Db, AppState) {
let (queue, _) = AnalyticsQueue::new(db.clone(), 1000, rx);
let state = AppState {
admin_db: db.admin.clone(),
content_db: db.content.clone(),
analytics_db: db.analytics.clone(),
system_db: db.system.clone(),
users_db: db.users.clone(),
user_dbs: std::sync::Arc::new(std::sync::Mutex::new(HashMap::new())),
+4
View File
@@ -51,6 +51,7 @@ fn test_advanced_analytics_pagination_and_sorting() {
accept_language: "en".to_string(),
country: "US".to_string(),
status_code: 200,
owner_tenant_id: None,
owner_user_id: None,
});
}
@@ -92,6 +93,7 @@ fn test_advanced_analytics_date_filtering() {
accept_language: "en".to_string(),
country: "US".to_string(),
status_code: 200,
owner_tenant_id: None,
owner_user_id: None,
},
VisitRecord {
@@ -105,6 +107,7 @@ fn test_advanced_analytics_date_filtering() {
accept_language: "en".to_string(),
country: "US".to_string(),
status_code: 200,
owner_tenant_id: None,
owner_user_id: None,
},
VisitRecord {
@@ -118,6 +121,7 @@ fn test_advanced_analytics_date_filtering() {
accept_language: "en".to_string(),
country: "US".to_string(),
status_code: 200,
owner_tenant_id: None,
owner_user_id: None,
},
];
+43 -43
View File
@@ -50,13 +50,11 @@ async fn start_test_server(
let state = AppState {
admin_db: db.admin.clone(),
content_db: db.content.clone(),
analytics_db: db.analytics.clone(),
system_db: db.system.clone(),
users_db: db.users.clone(),
user_dbs: std::sync::Arc::new(std::sync::Mutex::new(HashMap::new())),
db: db.clone(),
config,
config: config.clone(),
analytics_queue: queue,
start_time: Instant::now(),
};
@@ -111,11 +109,29 @@ async fn test_analytics_and_table_parity() {
.unwrap();
assert_eq!(res.status(), reqwest::StatusCode::SEE_OTHER);
// Admin adds a URL to the global content_db
// Create User B to own global links
let _ = bzod::cli::create_user::run(
Some("userb".to_string()),
Some("password123".to_string()),
None,
create_temp_config(temp_dir.clone()),
)
.await
.unwrap();
let (id_b, tid_b) = {
let conn = db.users.lock().unwrap();
let u = bzod::db::users::get_user_by_username(&conn, "userb")
.unwrap()
.unwrap();
(u.id, u.tenant_id.unwrap())
};
// User B adds a URL
let _admin_url_id = {
let conn_admin = db.content.lock().unwrap();
let conn_b = bzod::jobs::open_user_content_conn(&db, id_b).unwrap();
let url = bzod::db::content::create_url_extended(
&conn_admin,
&conn_b,
"!admin-slug",
"https://admin.com",
None,
@@ -127,24 +143,17 @@ async fn test_analytics_and_table_parity() {
)
.unwrap();
let system_conn = db.system.lock().unwrap();
bzod::db::users::register_global_slug(
&system_conn,
"!admin-slug",
1,
"url",
&url.id,
"active",
)
.unwrap();
let urls_conn = db.global_urls.lock().unwrap();
bzod::db::slugs::register_url_slug(&urls_conn, "!admin-slug", &tid_b, &url.id, "active")
.unwrap();
url.id
};
// Admin adds a Landing Page to the global content_db
// User B adds a Landing Page
let _admin_page_id = {
let conn_admin = db.content.lock().unwrap();
let conn_b = bzod::jobs::open_user_content_conn(&db, id_b).unwrap();
let page = bzod::db::content::create_landing_page(
&conn_admin,
&conn_b,
"!admin-page",
"admin-page",
"Title Admin",
@@ -153,12 +162,11 @@ async fn test_analytics_and_table_parity() {
)
.unwrap();
let system_conn = db.system.lock().unwrap();
bzod::db::users::register_global_slug(
&system_conn,
let pages_conn = db.global_landing_pages.lock().unwrap();
bzod::db::slugs::register_landing_page_slug(
&pages_conn,
"!admin-page",
1,
"page",
&tid_b,
&page.id,
"active",
)
@@ -176,12 +184,12 @@ async fn test_analytics_and_table_parity() {
.await
.unwrap();
let id_a = {
let (id_a, tid_a) = {
let conn = db.users.lock().unwrap();
bzod::db::users::get_user_by_username(&conn, "usera")
let u = bzod::db::users::get_user_by_username(&conn, "usera")
.unwrap()
.unwrap()
.id
.unwrap();
(u.id, u.tenant_id.unwrap())
};
// User A adds a URL
@@ -200,16 +208,9 @@ async fn test_analytics_and_table_parity() {
)
.unwrap();
let system_conn = db.system.lock().unwrap();
bzod::db::users::register_global_slug(
&system_conn,
"!usera-slug",
id_a,
"url",
&url.id,
"active",
)
.unwrap();
let urls_conn = db.global_urls.lock().unwrap();
bzod::db::slugs::register_url_slug(&urls_conn, "!usera-slug", &tid_a, &url.id, "active")
.unwrap();
url.id
};
@@ -226,12 +227,11 @@ async fn test_analytics_and_table_parity() {
)
.unwrap();
let system_conn = db.system.lock().unwrap();
bzod::db::users::register_global_slug(
&system_conn,
let pages_conn = db.global_landing_pages.lock().unwrap();
bzod::db::slugs::register_landing_page_slug(
&pages_conn,
"!usera-page",
id_a,
"page",
&tid_a,
&page.id,
"active",
)
+3
View File
@@ -121,6 +121,7 @@ fn test_target_analytics_queries() {
accept_language: "en".to_string(),
country: "US".to_string(),
status_code: 200,
owner_tenant_id: None,
owner_user_id: None,
},
VisitRecord {
@@ -134,6 +135,7 @@ fn test_target_analytics_queries() {
accept_language: "en".to_string(),
country: "US".to_string(),
status_code: 200,
owner_tenant_id: None,
owner_user_id: None,
},
VisitRecord {
@@ -147,6 +149,7 @@ fn test_target_analytics_queries() {
accept_language: "en".to_string(),
country: "US".to_string(),
status_code: 200,
owner_tenant_id: None,
owner_user_id: None,
},
];
+8 -4
View File
@@ -42,8 +42,6 @@ fn build_state(config: Config) -> (Db, bzod::state::AppState) {
let (queue, _) = AnalyticsQueue::new(db.clone(), 1000, rx);
let state = bzod::state::AppState {
admin_db: db.admin.clone(),
content_db: db.content.clone(),
analytics_db: db.analytics.clone(),
system_db: db.system.clone(),
users_db: db.users.clone(),
user_dbs: Arc::new(Mutex::new(HashMap::new())),
@@ -60,14 +58,20 @@ fn test_migration_idempotent() {
let mut conn = Connection::open_in_memory().unwrap();
run_migrations(&mut conn, "users", USERS_MIGRATIONS, None).unwrap();
run_migrations(&mut conn, "users", USERS_MIGRATIONS, None).unwrap();
assert_eq!(get_user_version(&conn).unwrap(), 2);
assert_eq!(
get_user_version(&conn).unwrap(),
USERS_MIGRATIONS.last().unwrap().version
);
}
#[test]
fn test_users_db_schema_version_2() {
let mut conn = Connection::open_in_memory().unwrap();
run_migrations(&mut conn, "users", USERS_MIGRATIONS, None).unwrap();
assert_eq!(get_user_version(&conn).unwrap(), 2);
assert_eq!(
get_user_version(&conn).unwrap(),
USERS_MIGRATIONS.last().unwrap().version
);
}
#[test]
+69 -94
View File
@@ -96,22 +96,12 @@ async fn test_backup_restore_roundtrip() {
let db = Db::init(&config).expect("Failed to init Db");
let _ = bzod::cli::create_user::run(
Some("testuser".to_string()),
Some("password123".to_string()),
None,
config.clone(),
)
.await
.unwrap();
let user_id = {
let user = {
let conn = db.users.lock().unwrap();
bzod::db::users::get_user_by_username(&conn, "testuser")
.unwrap()
.unwrap()
.id
bzod::db::users::create_user(&conn, "testuser", "password123", "standard", None).unwrap()
};
db.init_user_databases(user.id).unwrap();
let user_id = user.id;
// Add a link for user
{
@@ -129,21 +119,25 @@ async fn test_backup_restore_roundtrip() {
)
.unwrap();
// Increment quota counter
let users_conn = db.users.lock().unwrap();
bzod::db::users::increment_quota_counter(&users_conn, user_id, "urls").unwrap();
let tenant_id = {
let users_conn = db.users.lock().unwrap();
bzod::db::users::increment_quota_counter(&users_conn, user_id, "urls").unwrap();
bzod::db::users::get_user_by_id(&users_conn, user_id)
.unwrap()
.unwrap()
.tenant_id
.unwrap()
};
// Register global slug
let system_conn = db.system.lock().unwrap();
bzod::db::users::register_global_slug(
&system_conn,
"!rt-slug",
user_id,
"url",
"rt-id",
"active",
)
.unwrap();
let urls_conn = db.global_urls.lock().unwrap();
let now = chrono::Utc::now().to_rfc3339();
urls_conn
.execute(
"INSERT INTO global_urls (slug, owner_tenant_id, target_id, created_at, updated_at, status, retired_at)
VALUES ('!rt-slug', ?1, 'rt-id', ?2, ?3, 'active', NULL);",
rusqlite::params![tenant_id.as_str(), now, now],
)
.unwrap();
}
// Backup user
@@ -172,7 +166,7 @@ async fn test_backup_restore_roundtrip() {
.unwrap();
// Verify restored user
{
let restored_id = {
let conn = db.users.lock().unwrap();
let user = bzod::db::users::get_user_by_username(&conn, "testuser")
.unwrap()
@@ -183,14 +177,14 @@ async fn test_backup_restore_roundtrip() {
.unwrap()
.unwrap();
assert_eq!(quota.current_urls, 1);
user.id
};
// Verify content restored
let user_content_conn = bzod::jobs::open_user_content_conn(&db, user.id).unwrap();
let url = bzod::db::content::get_url_by_code(&user_content_conn, "!rt-slug")
.unwrap()
.unwrap();
assert_eq!(url.destination, "https://example.com/rt");
}
let user_content_conn = bzod::jobs::open_user_content_conn(&db, restored_id).unwrap();
let url = bzod::db::content::get_url_by_code(&user_content_conn, "!rt-slug")
.unwrap()
.unwrap();
assert_eq!(url.destination, "https://example.com/rt");
let _ = fs::remove_dir_all(&temp_dir);
}
@@ -198,33 +192,30 @@ async fn test_backup_restore_roundtrip() {
#[tokio::test]
async fn test_restore_slug_collision_rejection() {
let temp_dir = std::env::temp_dir().join(format!(
"bzod_test_restore_collision_{}",
uuid::Uuid::new_v4()
"bzod_test_collision_{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
fs::create_dir_all(&temp_dir).unwrap();
let config = create_temp_config(temp_dir.clone());
let db = Db::init(&config).expect("Failed to init Db");
let mut config = Config::load();
config.data_dir = temp_dir.clone();
config.backup_dir = temp_dir.join("backups");
fs::create_dir_all(&config.backup_dir).unwrap();
let db = Db::init(&config).unwrap();
// Create User A
let _ = bzod::cli::create_user::run(
Some("usera".to_string()),
Some("password123".to_string()),
None,
config.clone(),
)
.await
.unwrap();
let id_a = {
let user_a = {
let conn = db.users.lock().unwrap();
bzod::db::users::get_user_by_username(&conn, "usera")
.unwrap()
.unwrap()
.id
bzod::db::users::create_user(&conn, "usera", "password123", "standard", None).unwrap()
};
db.init_user_databases(user_a.id).unwrap();
let (id_a, tid_a) = (user_a.id, user_a.tenant_id.unwrap());
// Add a link for User A
// Add slug for User A
{
let conn_a = bzod::jobs::open_user_content_conn(&db, id_a).unwrap();
bzod::db::content::create_url_extended(
@@ -240,16 +231,13 @@ async fn test_restore_slug_collision_rejection() {
)
.unwrap();
let system_conn = db.system.lock().unwrap();
bzod::db::users::register_global_slug(
&system_conn,
"!collision-slug",
id_a,
"url",
"a-id",
"active",
)
.unwrap();
let urls_conn = db.global_urls.lock().unwrap();
let now = chrono::Utc::now().to_rfc3339();
urls_conn.execute(
"INSERT INTO global_urls (slug, owner_tenant_id, target_id, created_at, updated_at, status, retired_at)
VALUES ('!collision-slug', ?1, 'a-id', ?2, ?3, 'active', NULL);",
rusqlite::params![tid_a.as_str(), now, now],
).unwrap();
}
// Backup User A
@@ -269,22 +257,12 @@ async fn test_restore_slug_collision_rejection() {
.unwrap();
// Create User B
let _ = bzod::cli::create_user::run(
Some("userb".to_string()),
Some("password123".to_string()),
None,
config.clone(),
)
.await
.unwrap();
let id_b = {
let user_b = {
let conn = db.users.lock().unwrap();
bzod::db::users::get_user_by_username(&conn, "userb")
.unwrap()
.unwrap()
.id
bzod::db::users::create_user(&conn, "userb", "password123", "standard", None).unwrap()
};
db.init_user_databases(user_b.id).unwrap();
let (id_b, tid_b) = (user_b.id, user_b.tenant_id.unwrap());
// Add colliding slug for User B
{
@@ -302,16 +280,13 @@ async fn test_restore_slug_collision_rejection() {
)
.unwrap();
let system_conn = db.system.lock().unwrap();
bzod::db::users::register_global_slug(
&system_conn,
"!collision-slug",
id_b,
"url",
"b-id",
"active",
)
.unwrap();
let urls_conn = db.global_urls.lock().unwrap();
let now = chrono::Utc::now().to_rfc3339();
urls_conn.execute(
"INSERT INTO global_urls (slug, owner_tenant_id, target_id, created_at, updated_at, status, retired_at)
VALUES ('!collision-slug', ?1, 'b-id', ?2, ?3, 'active', NULL);",
rusqlite::params![tid_b.as_str(), now, now],
).unwrap();
}
// Attempt to restore User A from backup - must fail on collision
@@ -323,17 +298,17 @@ async fn test_restore_slug_collision_rejection() {
.await;
assert!(res.is_err());
// Verify that User B still owns the slug in global_slugs and User A's slug registration was skipped/rejected
// Verify that User B still owns the slug in global_urls and User A's slug registration was skipped/rejected
{
let system_conn = db.system.lock().unwrap();
let owner_id: i64 = system_conn
let urls_conn = db.global_urls.lock().unwrap();
let owner_tid: String = urls_conn
.query_row(
"SELECT owner_user_id FROM global_slugs WHERE slug = '!collision-slug';",
"SELECT owner_tenant_id FROM global_urls WHERE slug = '!collision-slug';",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(owner_id, id_b);
assert_eq!(owner_tid, tid_b.as_str());
}
let _ = fs::remove_dir_all(&temp_dir);
+6 -8
View File
@@ -55,8 +55,6 @@ async fn start_test_server(
let state = AppState {
admin_db: db.admin.clone(),
content_db: db.content.clone(),
analytics_db: db.analytics.clone(),
system_db: db.system.clone(),
users_db: db.users.clone(),
user_dbs: std::sync::Arc::new(std::sync::Mutex::new(HashMap::new())),
@@ -190,9 +188,9 @@ async fn test_scenario_a_user_create_login_shorten_visit_analytics() {
// 6. Verify visit is logged in analytics
let url_uuid = {
let conn = state.system_db.lock().unwrap();
let conn = state.db.global_urls.lock().unwrap();
conn.query_row(
"SELECT target_id FROM global_slugs WHERE slug = '!mygoogle';",
"SELECT target_id FROM global_urls WHERE slug = '!mygoogle';",
[],
|row| row.get::<_, String>(0),
)
@@ -505,9 +503,9 @@ async fn test_scenario_c_slug_transfer_workflow() {
// Verify visit recorded for standard_c1
let url_uuid = {
let conn = state.system_db.lock().unwrap();
let conn = state.db.global_urls.lock().unwrap();
conn.query_row(
"SELECT target_id FROM global_slugs WHERE slug = '!myyahoo';",
"SELECT target_id FROM global_urls WHERE slug = '!myyahoo';",
[],
|row| row.get::<_, String>(0),
)
@@ -607,9 +605,9 @@ async fn test_scenario_c_slug_transfer_workflow() {
// standard_c2 analytics should display Yahoo!
let url_uuid = {
let conn = state.system_db.lock().unwrap();
let conn = state.db.global_urls.lock().unwrap();
conn.query_row(
"SELECT target_id FROM global_slugs WHERE slug = '!myyahoo';",
"SELECT target_id FROM global_urls WHERE slug = '!myyahoo';",
[],
|row| row.get::<_, String>(0),
)
+17 -7
View File
@@ -23,14 +23,19 @@ async fn test_concurrent_slug_creation() {
let db = Db::init(&config).expect("Failed to init Db");
let barrier = Arc::new(Barrier::new(2));
let db_path = temp_dir.join("admin/system.db");
let db_path = temp_dir.join("slugs/global_urls.db");
let b1 = barrier.clone();
let path1 = db_path.clone();
let task1 = tokio::spawn(async move {
let conn = rusqlite::Connection::open(&path1).unwrap();
b1.wait().await;
bzod::db::users::register_global_slug(&conn, "!conc-slug", 10, "url", "url10", "active")
let now = chrono::Utc::now().to_rfc3339();
conn.execute(
"INSERT INTO global_urls (slug, owner_tenant_id, target_id, created_at, updated_at, status, retired_at)
VALUES ('!conc-slug', 't-tenant10', 'url10', ?1, ?2, 'active', NULL);",
rusqlite::params![now, now],
)
});
let b2 = barrier.clone();
@@ -38,7 +43,12 @@ async fn test_concurrent_slug_creation() {
let task2 = tokio::spawn(async move {
let conn = rusqlite::Connection::open(&path2).unwrap();
b2.wait().await;
bzod::db::users::register_global_slug(&conn, "!conc-slug", 20, "url", "url20", "active")
let now = chrono::Utc::now().to_rfc3339();
conn.execute(
"INSERT INTO global_urls (slug, owner_tenant_id, target_id, created_at, updated_at, status, retired_at)
VALUES ('!conc-slug', 't-tenant20', 'url20', ?1, ?2, 'active', NULL);",
rusqlite::params![now, now],
)
});
let res1 = task1.await.unwrap();
@@ -54,12 +64,12 @@ async fn test_concurrent_slug_creation() {
),
}
// Verify exactly 1 record exists in global_slugs
// Verify exactly 1 record exists in global_urls
{
let system_conn = db.system.lock().unwrap();
let count: i64 = system_conn
let urls_conn = db.global_urls.lock().unwrap();
let count: i64 = urls_conn
.query_row(
"SELECT COUNT(*) FROM global_slugs WHERE slug = '!conc-slug';",
"SELECT COUNT(*) FROM global_urls WHERE slug = '!conc-slug';",
[],
|row| row.get(0),
)
+1 -2
View File
@@ -84,8 +84,7 @@ async fn test_partial_restore_failure_rollback() {
let count: i64 = conn
.query_row("SELECT COUNT(*) FROM users;", [], |row| row.get(0))
.unwrap();
// Only legacy_admin (ID 1) should exist
assert_eq!(count, 1);
assert_eq!(count, 0, "No users should exist after failed restore");
}
let _ = fs::remove_dir_all(&temp_dir);
-2
View File
@@ -54,8 +54,6 @@ async fn start_test_server(
let state = AppState {
admin_db: db.admin.clone(),
content_db: db.content.clone(),
analytics_db: db.analytics.clone(),
system_db: db.system.clone(),
users_db: db.users.clone(),
user_dbs: std::sync::Arc::new(std::sync::Mutex::new(HashMap::new())),
+37 -36
View File
@@ -29,12 +29,12 @@ async fn test_global_slug_index_consistency() {
.await
.unwrap();
let user_id = {
let (user_id, tid) = {
let conn = db.users.lock().unwrap();
bzod::db::users::get_user_by_username(&conn, "testuser")
let u = bzod::db::users::get_user_by_username(&conn, "testuser")
.unwrap()
.unwrap()
.id
.unwrap();
(u.id, u.tenant_id.unwrap())
};
// Add a URL for the user
@@ -57,46 +57,47 @@ async fn test_global_slug_index_consistency() {
// Register globally
{
let system_conn = db.system.lock().unwrap();
bzod::db::users::register_global_slug(
&system_conn,
"!integ-slug",
user_id,
"url",
&url_id,
"active",
)
.unwrap();
let urls_conn = db.global_urls.lock().unwrap();
let now = chrono::Utc::now().to_rfc3339();
urls_conn
.execute(
"INSERT INTO global_urls (slug, owner_tenant_id, target_id, created_at, updated_at, status, retired_at)
VALUES ('!integ-slug', ?1, ?2, ?3, ?4, 'active', NULL);",
rusqlite::params![tid.as_str(), &url_id, now, now],
)
.unwrap();
}
// Consistency Check:
// 1. Every active slug in user databases exists in global_slugs
// 1. Every active slug in user databases exists in global_urls
{
let user_ids: Vec<i64> = {
let users = {
let conn = db.users.lock().unwrap();
let mut stmt = conn.prepare("SELECT id FROM users;").unwrap();
let rows = stmt.query_map([], |row| row.get(0)).unwrap();
rows.filter_map(|r| r.ok()).collect()
bzod::db::users::list_users(&conn).unwrap()
};
let system_conn = db.system.lock().unwrap();
for id in user_ids {
let conn = bzod::jobs::open_user_content_conn(&db, id).unwrap();
let mut stmt = conn.prepare("SELECT code FROM urls;").unwrap();
let rows = stmt.query_map([], |row| row.get::<_, String>(0)).unwrap();
let urls_conn = db.global_urls.lock().unwrap();
for u in users {
if let Some(tenant_id) = u.tenant_id {
let conn = bzod::jobs::open_user_content_conn(&db, u.id).unwrap();
let mut stmt = conn.prepare("SELECT code FROM urls;").unwrap();
let rows = stmt.query_map([], |row| row.get::<_, String>(0)).unwrap();
for code_res in rows {
let code = code_res.unwrap();
let exists: bool = system_conn.query_row(
"SELECT EXISTS(SELECT 1 FROM global_slugs WHERE slug = ?1 AND owner_user_id = ?2);",
rusqlite::params![code, id],
|row| row.get(0),
).unwrap();
assert!(
exists,
"Active user slug '{}' missing from global_slugs",
code
);
for code_res in rows {
let code = code_res.unwrap();
let exists: bool = urls_conn
.query_row(
"SELECT EXISTS(SELECT 1 FROM global_urls WHERE slug = ?1 AND owner_tenant_id = ?2);",
rusqlite::params![code, tenant_id.as_str()],
|row| row.get(0),
)
.unwrap();
assert!(
exists,
"Active user slug '{}' missing from global_urls",
code
);
}
}
}
}
+17 -10
View File
@@ -546,16 +546,23 @@ async fn test_current_backup_restore_roundtrip() {
)
.unwrap();
let system_conn = db.system.lock().unwrap();
bzod::db::users::register_global_slug(
&system_conn,
"!current-test",
user_id,
"url",
"current-id",
"active",
)
.unwrap();
let tenant_id = {
let conn = db.users.lock().unwrap();
bzod::db::users::get_user_by_id(&conn, user_id)
.unwrap()
.unwrap()
.tenant_id
.unwrap()
};
let urls_conn = db.global_urls.lock().unwrap();
let now = chrono::Utc::now().to_rfc3339();
urls_conn
.execute(
"INSERT INTO global_urls (slug, owner_tenant_id, target_id, created_at, updated_at, status, retired_at)
VALUES ('!current-test', ?1, 'current-id', ?2, ?3, 'active', NULL);",
rusqlite::params![tenant_id.as_str(), now, now],
)
.unwrap();
}
// Create a native backup
+10 -21
View File
@@ -79,8 +79,11 @@ async fn test_legacy_migration() {
.unwrap();
}
// Call Db::init which triggers the legacy migration
let db = Db::init(&config).expect("Failed to init Db and run legacy migration");
// Call normalize_restored_layout to move legacy flat files into multi-tenant paths
bzod::services::backup_layout::normalize_restored_layout(&temp_dir).unwrap();
// Call Db::init
let _db = Db::init(&config).expect("Failed to init Db and run legacy migration");
// Verify files moved to correct directories
assert!(!legacy_admin_path.exists());
@@ -88,26 +91,12 @@ async fn test_legacy_migration() {
assert!(temp_dir.join("admin/admin.db").exists());
assert!(temp_dir.join("users/1/content.db").exists());
// Verify legacy_admin created as system and disabled
// Verify legacy content is preserved in users/1/content.db
{
let conn = db.users.lock().unwrap();
let user = bzod::db::users::get_user_by_id(&conn, 1).unwrap().unwrap();
assert_eq!(user.username, "legacy_admin");
assert_eq!(user.status, "disabled");
assert_eq!(user.account_type, "system");
}
// Verify slug registered in global_slugs
{
let conn = db.system.lock().unwrap();
let exists: bool = conn
.query_row(
"SELECT EXISTS(SELECT 1 FROM global_slugs WHERE slug = '!legacy-slug');",
[],
|row| row.get(0),
)
.unwrap();
assert!(exists);
let conn = rusqlite::Connection::open(temp_dir.join("users/1/content.db")).unwrap();
let url = bzod::db::content::get_url_by_code(&conn, "!legacy-slug").unwrap();
assert!(url.is_some());
assert_eq!(url.unwrap().destination, "https://legacy.com");
}
let _ = fs::remove_dir_all(&temp_dir);
+18 -12
View File
@@ -22,16 +22,22 @@ async fn test_content_flagging_and_disabling() {
let db = Db::init(&config).expect("Failed to init Db");
let system_conn = db.system.lock().unwrap();
let urls_conn = db.global_urls.lock().unwrap();
// Register slug
bzod::db::users::register_global_slug(&system_conn, "!badslug", 10, "url", "url_abc", "active")
let now = Utc::now().to_rfc3339();
urls_conn
.execute(
"INSERT INTO global_urls (slug, owner_tenant_id, target_id, created_at, updated_at, status, retired_at)
VALUES ('!badslug', 't-tenant10', 'url_abc', ?1, ?2, 'active', NULL);",
rusqlite::params![now, now],
)
.unwrap();
// Verify it is active initially
let status: String = system_conn
let status: String = urls_conn
.query_row(
"SELECT status FROM global_slugs WHERE slug = ?1;",
"SELECT status FROM global_urls WHERE slug = ?1;",
["!badslug"],
|row| row.get(0),
)
@@ -39,17 +45,17 @@ async fn test_content_flagging_and_disabling() {
assert_eq!(status, "active");
// Moderate/disable slug
system_conn
urls_conn
.execute(
"UPDATE global_slugs SET status = 'disabled' WHERE slug = ?1;",
"UPDATE global_urls SET status = 'disabled' WHERE slug = ?1;",
["!badslug"],
)
.unwrap();
// Verify it is disabled
let status2: String = system_conn
let status2: String = urls_conn
.query_row(
"SELECT status FROM global_slugs WHERE slug = ?1;",
"SELECT status FROM global_urls WHERE slug = ?1;",
["!badslug"],
|row| row.get(0),
)
@@ -57,16 +63,16 @@ async fn test_content_flagging_and_disabling() {
assert_eq!(status2, "disabled");
// Moderate/re-enable slug
system_conn
urls_conn
.execute(
"UPDATE global_slugs SET status = 'active' WHERE slug = ?1;",
"UPDATE global_urls SET status = 'active' WHERE slug = ?1;",
["!badslug"],
)
.unwrap();
let status3: String = system_conn
let status3: String = urls_conn
.query_row(
"SELECT status FROM global_slugs WHERE slug = ?1;",
"SELECT status FROM global_urls WHERE slug = ?1;",
["!badslug"],
|row| row.get(0),
)
+41 -22
View File
@@ -47,6 +47,16 @@ async fn test_cli_shorten_and_expand() {
fs::create_dir_all(&temp_dir).unwrap();
let config = create_temp_config(temp_dir.clone());
// 0. Create a standard user
let _ = bzod::cli::create_user::run(
Some("cliuser".to_string()),
Some("password123".to_string()),
None,
config.clone(),
)
.await
.unwrap();
// 1. Shorten with generated code
let res = bzod::cli::shorten::run(
"https://example.com/one".to_string(),
@@ -78,20 +88,11 @@ async fn test_cli_shorten_and_expand() {
assert!(res_dup.is_err());
assert!(res_dup.unwrap_err().to_string().contains("already exists"));
// 4. Expand custom slug
{
let db = Db::init(&config).unwrap();
let conn = db.content.lock().unwrap();
let url_opt = bzod::db::content::get_url_by_code(&conn, "!office").unwrap();
assert!(url_opt.is_some());
assert_eq!(url_opt.unwrap().destination, "https://example.com/two");
}
// 5. CLI expand round-trip validation
// 4. CLI expand round-trip validation
let expand_res = bzod::cli::expand::run("!office".to_string(), None, config.clone()).await;
assert!(expand_res.is_ok());
// 6. Case-insensitive CLI expand validation
// 5. Case-insensitive CLI expand validation
let expand_res_upper =
bzod::cli::expand::run("!OFFICE".to_string(), None, config.clone()).await;
assert!(expand_res_upper.is_ok());
@@ -108,12 +109,30 @@ async fn test_perform_restore_and_validation() {
fs::create_dir_all(&restore_dir).unwrap();
let config = create_temp_config(temp_dir.clone());
// Create user and url
let _ = bzod::cli::create_user::run(
Some("restoreuser".to_string()),
Some("password123".to_string()),
None,
config.clone(),
)
.await
.unwrap();
let db = Db::init(&config).unwrap();
let (uid, tid) = {
let conn = db.users.lock().unwrap();
let u = bzod::db::users::get_user_by_username(&conn, "restoreuser")
.unwrap()
.unwrap();
(u.id, u.tenant_id.unwrap())
};
// 1. Create a mock database record
{
let conn = db.content.lock().unwrap();
bzod::db::content::create_url_extended(
let conn = bzod::jobs::open_user_content_conn(&db, uid).unwrap();
let url = bzod::db::content::create_url_extended(
&conn,
"!home",
"https://my-home.com",
@@ -125,6 +144,9 @@ async fn test_perform_restore_and_validation() {
None,
)
.unwrap();
let urls_conn = db.global_urls.lock().unwrap();
bzod::db::slugs::register_url_slug(&urls_conn, "!home", &tid, &url.id, "active").unwrap();
}
// 2. Perform a backup
@@ -142,18 +164,15 @@ async fn test_perform_restore_and_validation() {
assert!(restore_dir.join("admin/admin.db").exists());
assert!(restore_dir.join("admin/users.db").exists());
assert!(restore_dir.join("admin/system.db").exists());
assert!(restore_dir.join("users/1/content.db").exists());
assert!(restore_dir.join("users/1/analytics.db").exists());
assert!(restore_dir.join("slugs/global_urls.db").exists());
assert!(restore_dir
.join(format!("users/{}/content.db", tid.as_str()))
.exists());
// Verify custom slug was preserved in the restored DB
let restore_config = create_temp_config(restore_dir.clone());
let restore_db = Db::init(&restore_config).unwrap();
{
let conn = restore_db.content.lock().unwrap();
let url = bzod::db::content::get_url_by_code(&conn, "!home").unwrap();
assert!(url.is_some());
assert_eq!(url.unwrap().destination, "https://my-home.com");
}
let expand_res = bzod::cli::expand::run("!home".to_string(), None, restore_config).await;
assert!(expand_res.is_ok());
let _ = fs::remove_dir_all(&temp_dir);
let _ = fs::remove_dir_all(&restore_dir);
+11 -23
View File
@@ -50,8 +50,6 @@ async fn start_test_server(
let state = AppState {
admin_db: db.admin.clone(),
content_db: db.content.clone(),
analytics_db: db.analytics.clone(),
system_db: db.system.clone(),
users_db: db.users.clone(),
user_dbs: std::sync::Arc::new(std::sync::Mutex::new(HashMap::new())),
@@ -107,17 +105,15 @@ async fn test_ownership_isolation_endpoints() {
.await
.unwrap();
let (id_a, _id_b) = {
let (id_a, tid_a, _id_b) = {
let conn = db.users.lock().unwrap();
let a = bzod::db::users::get_user_by_username(&conn, "usera")
.unwrap()
.unwrap()
.id;
.unwrap();
let b = bzod::db::users::get_user_by_username(&conn, "userb")
.unwrap()
.unwrap()
.id;
(a, b)
.unwrap();
(a.id, a.tenant_id.unwrap(), b.id)
};
// User A adds a URL
@@ -136,16 +132,9 @@ async fn test_ownership_isolation_endpoints() {
)
.unwrap();
let system_conn = db.system.lock().unwrap();
bzod::db::users::register_global_slug(
&system_conn,
"!usera-slug",
id_a,
"url",
&url.id,
"active",
)
.unwrap();
let urls_conn = db.global_urls.lock().unwrap();
bzod::db::slugs::register_url_slug(&urls_conn, "!usera-slug", &tid_a, &url.id, "active")
.unwrap();
url.id
};
@@ -162,12 +151,11 @@ async fn test_ownership_isolation_endpoints() {
)
.unwrap();
let system_conn = db.system.lock().unwrap();
bzod::db::users::register_global_slug(
&system_conn,
let pages_conn = db.global_landing_pages.lock().unwrap();
bzod::db::slugs::register_landing_page_slug(
&pages_conn,
"!usera-page",
id_a,
"page",
&tid_a,
&page.id,
"active",
)
+40 -46
View File
@@ -39,8 +39,6 @@ async fn start_test_server(
let state = AppState {
admin_db: db.admin.clone(),
content_db: db.content.clone(),
analytics_db: db.analytics.clone(),
system_db: db.system.clone(),
users_db: db.users.clone(),
user_dbs: std::sync::Arc::new(std::sync::Mutex::new(HashMap::new())),
@@ -85,12 +83,12 @@ async fn test_qr_endpoints_and_redirection_hardening() {
.await
.unwrap();
let id_a = {
let (id_a, tid_a) = {
let conn = db.users.lock().unwrap();
bzod::db::users::get_user_by_username(&conn, "usera")
let u = bzod::db::users::get_user_by_username(&conn, "usera")
.unwrap()
.unwrap()
.id
.unwrap();
(u.id, u.tenant_id.unwrap())
};
// User A adds an active URL
@@ -109,16 +107,15 @@ async fn test_qr_endpoints_and_redirection_hardening() {
)
.unwrap();
let system_conn = db.system.lock().unwrap();
bzod::db::users::register_global_slug(
&system_conn,
"a1b2c3",
id_a,
"url",
&url.id,
"active",
)
.unwrap();
let urls_conn = db.global_urls.lock().unwrap();
let now = chrono::Utc::now().to_rfc3339();
urls_conn
.execute(
"INSERT INTO global_urls (slug, owner_tenant_id, target_id, created_at, updated_at, status, retired_at)
VALUES ('a1b2c3', ?1, ?2, ?3, ?4, 'active', NULL);",
rusqlite::params![tid_a.as_str(), &url.id, now, now],
)
.unwrap();
}
// User A adds a disabled URL
@@ -137,16 +134,15 @@ async fn test_qr_endpoints_and_redirection_hardening() {
)
.unwrap();
let system_conn = db.system.lock().unwrap();
bzod::db::users::register_global_slug(
&system_conn,
"d4e5f6",
id_a,
"url",
&url.id,
"disabled",
)
.unwrap();
let urls_conn = db.global_urls.lock().unwrap();
let now = chrono::Utc::now().to_rfc3339();
urls_conn
.execute(
"INSERT INTO global_urls (slug, owner_tenant_id, target_id, created_at, updated_at, status, retired_at)
VALUES ('d4e5f6', ?1, ?2, ?3, ?4, 'disabled', NULL);",
rusqlite::params![tid_a.as_str(), &url.id, now, now],
)
.unwrap();
}
// User A adds an active Page
@@ -162,16 +158,15 @@ async fn test_qr_endpoints_and_redirection_hardening() {
)
.unwrap();
let system_conn = db.system.lock().unwrap();
bzod::db::users::register_global_slug(
&system_conn,
"a1b2",
id_a,
"page",
&page.id,
"active",
)
.unwrap();
let pages_conn = db.global_landing_pages.lock().unwrap();
let now = chrono::Utc::now().to_rfc3339();
pages_conn
.execute(
"INSERT INTO global_landing_pages (slug, owner_tenant_id, target_id, created_at, updated_at, status, retired_at)
VALUES ('a1b2', ?1, ?2, ?3, ?4, 'active', NULL);",
rusqlite::params![tid_a.as_str(), &page.id, now, now],
)
.unwrap();
}
// User A adds a disabled Page
@@ -187,16 +182,15 @@ async fn test_qr_endpoints_and_redirection_hardening() {
)
.unwrap();
let system_conn = db.system.lock().unwrap();
bzod::db::users::register_global_slug(
&system_conn,
"c3d4",
id_a,
"page",
&page.id,
"disabled",
)
.unwrap();
let pages_conn = db.global_landing_pages.lock().unwrap();
let now = chrono::Utc::now().to_rfc3339();
pages_conn
.execute(
"INSERT INTO global_landing_pages (slug, owner_tenant_id, target_id, created_at, updated_at, status, retired_at)
VALUES ('c3d4', ?1, ?2, ?3, ?4, 'disabled', NULL);",
rusqlite::params![tid_a.as_str(), &page.id, now, now],
)
.unwrap();
}
// 1. Verify Active URL QR endpoints return 200 with correct content types
+1 -1
View File
@@ -126,8 +126,8 @@ async fn test_quota_reconcile_job() {
// Run reconciliation
{
let users_conn = db.users.lock().unwrap();
let user_content_conn = bzod::jobs::open_user_content_conn(&db, user_id).unwrap();
let users_conn = db.users.lock().unwrap();
bzod::db::users::reconcile_user_quotas(&users_conn, user_id, &user_content_conn).unwrap();
// Verify drift is repaired
+87 -26
View File
@@ -48,10 +48,15 @@ async fn start_test_server(
Box::leak(Box::new(tx));
let (queue, _) = AnalyticsQueue::new(db.clone(), 1000, rx);
// Create a standard tenant user
{
let users = db.users.lock().unwrap();
let _ = bzod::db::users::create_user(&users, "testuser", "dummy_hash", "standard", None);
}
let _ = db.init_user_databases(1);
let state = AppState {
admin_db: db.admin.clone(),
content_db: db.content.clone(),
analytics_db: db.analytics.clone(),
system_db: db.system.clone(),
users_db: db.users.clone(),
user_dbs: std::sync::Arc::new(std::sync::Mutex::new(HashMap::new())),
@@ -95,25 +100,27 @@ fn seed_url(db: &Db, seed: SeedUrl<'_>) {
db.init_user_databases(seed.owner_user_id)
.expect("init user dbs");
let (content_path, tid) = {
let users = db.users.lock().unwrap();
let user = bzod::db::users::get_user_by_id(&users, seed.owner_user_id)
.unwrap()
.expect("seed owner must be a registered user");
let path = bzod::db::tenant::location_for_user(&user)
.unwrap()
.dir(&db.topology)
.unwrap()
.join("content.db");
(path, user.tenant_id.unwrap())
};
let id = uuid::Uuid::new_v4().to_string();
{
let system = db.system.lock().unwrap();
let _ = bzod::db::users::register_global_slug(
&system,
seed.code,
seed.owner_user_id,
"url",
"seed",
"active",
);
let urls_conn = db.global_urls.lock().unwrap();
let _ = bzod::db::slugs::register_url_slug(&urls_conn, seed.code, &tid, &id, "active");
}
let content_path = db
.data_dir
.join("users")
.join(seed.owner_user_id.to_string())
.join("content.db");
let conn = rusqlite::Connection::open(content_path).unwrap();
let id = uuid::Uuid::new_v4().to_string();
let now = chrono::Utc::now().to_rfc3339();
conn.execute(
"INSERT INTO urls (id, code, destination, title, description, status, created_at, updated_at, expires_at, expired, password_hash, max_access_count, access_count)
@@ -271,7 +278,15 @@ async fn wall_clock_expiry_returns_410_without_hot_path_write_dependency() {
assert_eq!(res.status(), reqwest::StatusCode::GONE);
// Column may still be 0 until sweeper runs — correctness does not require write.
let content_path = db.data_dir.join("users/1/content.db");
let content_path = {
let users = db.users.lock().unwrap();
let user = bzod::db::users::get_user_by_id(&users, 1).unwrap().unwrap();
bzod::db::tenant::location_for_user(&user)
.unwrap()
.dir(&db.topology)
.unwrap()
.join("content.db")
};
let conn = rusqlite::Connection::open(content_path).unwrap();
let expired_flag: i64 = conn
.query_row("SELECT expired FROM urls WHERE code = 'ab3333';", [], |r| {
@@ -347,7 +362,15 @@ async fn password_gate_before_access_increment() {
assert!(loc.contains("/gate/ab5555"));
// Access count must not have incremented
let content_path = db.data_dir.join("users/1/content.db");
let content_path = {
let users = db.users.lock().unwrap();
let user = bzod::db::users::get_user_by_id(&users, 1).unwrap().unwrap();
bzod::db::tenant::location_for_user(&user)
.unwrap()
.dir(&db.topology)
.unwrap()
.join("content.db")
};
let conn = rusqlite::Connection::open(content_path).unwrap();
let count: i64 = conn
.query_row(
@@ -403,7 +426,15 @@ async fn concurrent_redirects_increment_access_count() {
}
assert_eq!(ok_301, N);
let content_path = db.data_dir.join("users/1/content.db");
let content_path = {
let users = db.users.lock().unwrap();
let user = bzod::db::users::get_user_by_id(&users, 1).unwrap().unwrap();
bzod::db::tenant::location_for_user(&user)
.unwrap()
.dir(&db.topology)
.unwrap()
.join("content.db")
};
let conn = rusqlite::Connection::open(content_path).unwrap();
let count: i64 = conn
.query_row(
@@ -514,6 +545,10 @@ async fn destination_audit_finds_legacy_invalid_without_rewriting() {
fs::create_dir_all(&temp_dir).unwrap();
let config = create_temp_config(temp_dir.clone());
let db = Db::init(&config).unwrap();
{
let users = db.users.lock().unwrap();
let _ = bzod::db::users::create_user(&users, "testuser", "dummy_hash", "standard", None);
}
seed_url(
&db,
@@ -563,7 +598,15 @@ async fn destination_audit_finds_legacy_invalid_without_rewriting() {
assert_eq!(report.unsupported_scheme, 1);
// Data not rewritten
let content_path = db.data_dir.join("users/1/content.db");
let content_path = {
let users = db.users.lock().unwrap();
let user = bzod::db::users::get_user_by_id(&users, 1).unwrap().unwrap();
bzod::db::tenant::location_for_user(&user)
.unwrap()
.dir(&db.topology)
.unwrap()
.join("content.db")
};
let conn = rusqlite::Connection::open(content_path).unwrap();
let dest: String = conn
.query_row(
@@ -597,10 +640,10 @@ async fn disabled_slug_returns_410() {
},
);
{
let system = db.system.lock().unwrap();
system
let urls_conn = db.global_urls.lock().unwrap();
urls_conn
.execute(
"UPDATE global_slugs SET status = 'disabled' WHERE slug = 'ab7777';",
"UPDATE global_urls SET status = 'disabled' WHERE slug = 'ab7777';",
[],
)
.unwrap();
@@ -640,7 +683,15 @@ async fn redirect_performance_smoke() {
}
// Reset access count after warm-up for clean correctness check
{
let content_path = db.data_dir.join("users/1/content.db");
let content_path = {
let users = db.users.lock().unwrap();
let user = bzod::db::users::get_user_by_id(&users, 1).unwrap().unwrap();
bzod::db::tenant::location_for_user(&user)
.unwrap()
.dir(&db.topology)
.unwrap()
.join("content.db")
};
let conn = rusqlite::Connection::open(content_path).unwrap();
conn.execute(
"UPDATE urls SET access_count = 0 WHERE code = 'ab8888';",
@@ -702,7 +753,17 @@ async fn redirect_performance_smoke() {
assert_eq!(errors, 0, "error rate must be zero");
assert_eq!(latencies_ms.len(), REQUESTS);
let content_path = db.data_dir.join("users/1/content.db");
let content_path = {
let users = db.users.lock().unwrap();
let user = bzod::db::users::get_user_by_id(&users, 1)
.unwrap()
.expect("user 1 must exist");
bzod::db::tenant::location_for_user(&user)
.unwrap()
.dir(&db.topology)
.unwrap()
.join("content.db")
};
let conn = rusqlite::Connection::open(content_path).unwrap();
let count: i64 = conn
.query_row(
+59 -54
View File
@@ -20,18 +20,21 @@ async fn test_registry_repair_dry_run() {
let config = create_temp_config(temp_dir.clone());
let db = Db::init(&config).expect("Failed to init Db");
let tid = {
let conn = db.users.lock().unwrap();
let u = bzod::db::users::create_user(&conn, "repairdry", "pass", "user", None).unwrap();
u.tenant_id.unwrap()
};
db.init_user_databases(1).unwrap();
{
let system_conn = db.system.lock().unwrap();
// Insert orphaned slug (owner exists, but no target db/record)
bzod::db::users::register_global_slug(
&system_conn,
"orphan1",
1,
"url",
"target1",
"active",
)
.unwrap();
let urls_conn = db.global_urls.lock().unwrap();
let now = chrono::Utc::now().to_rfc3339();
urls_conn.execute(
"INSERT INTO global_urls (slug, owner_tenant_id, target_id, created_at, updated_at, status, retired_at)
VALUES ('!orphan1', ?1, 'target1', ?2, ?3, 'active', NULL);",
rusqlite::params![tid.as_str(), now, now],
).unwrap();
}
let command = RepairCommands::Registry {
@@ -47,10 +50,10 @@ async fn test_registry_repair_dry_run() {
// Verify it was NOT deleted
{
let system_conn = db.system.lock().unwrap();
let exists: bool = system_conn
let urls_conn = db.global_urls.lock().unwrap();
let exists: bool = urls_conn
.query_row(
"SELECT EXISTS(SELECT 1 FROM global_slugs WHERE slug = 'orphan1')",
"SELECT EXISTS(SELECT 1 FROM global_urls WHERE slug = '!orphan1')",
[],
|r| r.get(0),
)
@@ -69,18 +72,21 @@ async fn test_registry_repair_force() {
let config = create_temp_config(temp_dir.clone());
let db = Db::init(&config).expect("Failed to init Db");
let tid = {
let conn = db.users.lock().unwrap();
let u = bzod::db::users::create_user(&conn, "repairforce", "pass", "user", None).unwrap();
u.tenant_id.unwrap()
};
db.init_user_databases(1).unwrap();
{
let system_conn = db.system.lock().unwrap();
// Insert orphaned slug
bzod::db::users::register_global_slug(
&system_conn,
"orphan2",
1,
"url",
"target2",
"active",
)
.unwrap();
let urls_conn = db.global_urls.lock().unwrap();
let now = chrono::Utc::now().to_rfc3339();
urls_conn.execute(
"INSERT INTO global_urls (slug, owner_tenant_id, target_id, created_at, updated_at, status, retired_at)
VALUES ('!orphan2', ?1, 'target2', ?2, ?3, 'active', NULL);",
rusqlite::params![tid.as_str(), now, now],
).unwrap();
}
let command = RepairCommands::Registry {
@@ -96,10 +102,10 @@ async fn test_registry_repair_force() {
// Verify it WAS deleted
{
let system_conn = db.system.lock().unwrap();
let exists: bool = system_conn
let urls_conn = db.global_urls.lock().unwrap();
let exists: bool = urls_conn
.query_row(
"SELECT EXISTS(SELECT 1 FROM global_slugs WHERE slug = 'orphan2')",
"SELECT EXISTS(SELECT 1 FROM global_urls WHERE slug = '!orphan2')",
[],
|r| r.get(0),
)
@@ -118,33 +124,32 @@ async fn test_registry_repair_single_slug() {
let config = create_temp_config(temp_dir.clone());
let db = Db::init(&config).expect("Failed to init Db");
let tid = {
let conn = db.users.lock().unwrap();
let u = bzod::db::users::create_user(&conn, "repairsingle", "pass", "user", None).unwrap();
u.tenant_id.unwrap()
};
db.init_user_databases(1).unwrap();
{
let system_conn = db.system.lock().unwrap();
// Insert two orphaned slugs
bzod::db::users::register_global_slug(
&system_conn,
"orphan3",
1,
"url",
"target3",
"active",
)
.unwrap();
bzod::db::users::register_global_slug(
&system_conn,
"orphan4",
1,
"url",
"target4",
"active",
)
.unwrap();
let urls_conn = db.global_urls.lock().unwrap();
let now = chrono::Utc::now().to_rfc3339();
urls_conn.execute(
"INSERT INTO global_urls (slug, owner_tenant_id, target_id, created_at, updated_at, status, retired_at)
VALUES ('!orphan3', ?1, 'target3', ?2, ?3, 'active', NULL);",
rusqlite::params![tid.as_str(), now, now],
).unwrap();
urls_conn.execute(
"INSERT INTO global_urls (slug, owner_tenant_id, target_id, created_at, updated_at, status, retired_at)
VALUES ('!orphan4', ?1, 'target4', ?2, ?3, 'active', NULL);",
rusqlite::params![tid.as_str(), now, now],
).unwrap();
}
let command = RepairCommands::Registry {
dry_run: false,
force: true,
slug: Some("orphan3".to_string()),
slug: Some("!orphan3".to_string()),
data_dir: Some(temp_dir.to_string_lossy().to_string()),
};
@@ -154,19 +159,19 @@ async fn test_registry_repair_single_slug() {
// Verify targeted slug was deleted
{
let system_conn = db.system.lock().unwrap();
let exists3: bool = system_conn
let urls_conn = db.global_urls.lock().unwrap();
let exists3: bool = urls_conn
.query_row(
"SELECT EXISTS(SELECT 1 FROM global_slugs WHERE slug = 'orphan3')",
"SELECT EXISTS(SELECT 1 FROM global_urls WHERE slug = '!orphan3')",
[],
|r| r.get(0),
)
.unwrap();
assert!(!exists3, "Targeted slug should be deleted");
let exists4: bool = system_conn
let exists4: bool = urls_conn
.query_row(
"SELECT EXISTS(SELECT 1 FROM global_slugs WHERE slug = 'orphan4')",
"SELECT EXISTS(SELECT 1 FROM global_urls WHERE slug = '!orphan4')",
[],
|r| r.get(0),
)
+25 -39
View File
@@ -31,8 +31,6 @@ async fn test_global_slug_lookup_and_redirection() {
let state = AppState {
admin_db: db.admin.clone(),
content_db: db.content.clone(),
analytics_db: db.analytics.clone(),
system_db: db.system.clone(),
users_db: db.users.clone(),
user_dbs: std::sync::Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())),
@@ -52,17 +50,17 @@ async fn test_global_slug_lookup_and_redirection() {
.await
.unwrap();
let user_id = {
let (user_id, tid) = {
let conn = db.users.lock().unwrap();
bzod::db::users::get_user_by_username(&conn, "testuser")
let u = bzod::db::users::get_user_by_username(&conn, "testuser")
.unwrap()
.unwrap()
.id
.unwrap();
(u.id, u.tenant_id.unwrap())
};
{
let conn = bzod::jobs::open_user_content_conn(&db, user_id).unwrap();
bzod::db::content::create_url_extended(
let url = bzod::db::content::create_url_extended(
&conn,
"!my-routing-slug",
"https://example.com/target",
@@ -75,16 +73,15 @@ async fn test_global_slug_lookup_and_redirection() {
)
.unwrap();
let system_conn = db.system.lock().unwrap();
bzod::db::users::register_global_slug(
&system_conn,
"!my-routing-slug",
user_id,
"url",
"xyz",
"active",
)
.unwrap();
let urls_conn = db.global_urls.lock().unwrap();
let now = chrono::Utc::now().to_rfc3339();
urls_conn
.execute(
"INSERT INTO global_urls (slug, owner_tenant_id, target_id, created_at, updated_at, status, retired_at)
VALUES ('!my-routing-slug', ?1, ?2, ?3, ?4, 'active', NULL);",
rusqlite::params![tid.as_str(), &url.id, now, now],
)
.unwrap();
}
// Call resolve_redirect directly
@@ -128,8 +125,6 @@ async fn test_disabled_slug_returns_410() {
let state = AppState {
admin_db: db.admin.clone(),
content_db: db.content.clone(),
analytics_db: db.analytics.clone(),
system_db: db.system.clone(),
users_db: db.users.clone(),
user_dbs: std::sync::Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())),
@@ -148,17 +143,17 @@ async fn test_disabled_slug_returns_410() {
.await
.unwrap();
let user_id = {
let (user_id, tid) = {
let conn = db.users.lock().unwrap();
bzod::db::users::get_user_by_username(&conn, "testuser")
let u = bzod::db::users::get_user_by_username(&conn, "testuser")
.unwrap()
.unwrap()
.id
.unwrap();
(u.id, u.tenant_id.unwrap())
};
{
let conn = bzod::jobs::open_user_content_conn(&db, user_id).unwrap();
bzod::db::content::create_url_extended(
let url = bzod::db::content::create_url_extended(
&conn,
"!disabled-slug",
"https://example.com/target",
@@ -171,22 +166,13 @@ async fn test_disabled_slug_returns_410() {
)
.unwrap();
let system_conn = db.system.lock().unwrap();
bzod::db::users::register_global_slug(
&system_conn,
"!disabled-slug",
user_id,
"url",
"xyz",
"active",
)
.unwrap();
// Disable slug
system_conn
let urls_conn = db.global_urls.lock().unwrap();
let now = chrono::Utc::now().to_rfc3339();
urls_conn
.execute(
"UPDATE global_slugs SET status = 'disabled' WHERE slug = '!disabled-slug';",
[],
"INSERT INTO global_urls (slug, owner_tenant_id, target_id, created_at, updated_at, status, retired_at)
VALUES ('!disabled-slug', ?1, ?2, ?3, ?4, 'disabled', NULL);",
rusqlite::params![tid.as_str(), &url.id, now, now],
)
.unwrap();
}
+50 -19
View File
@@ -20,14 +20,24 @@ async fn test_global_slug_uniqueness() {
let db = Db::init(&config).expect("Failed to init Db");
let system_conn = db.system.lock().unwrap();
let urls_conn = db.global_urls.lock().unwrap();
let reserved_conn = db.reserved.lock().unwrap();
let pages_conn = db.global_landing_pages.lock().unwrap();
// Register a slug
bzod::db::users::register_global_slug(&system_conn, "!myslug", 1, "url", "url1", "active")
let now = chrono::Utc::now().to_rfc3339();
urls_conn
.execute(
"INSERT INTO global_urls (slug, owner_tenant_id, target_id, created_at, updated_at, status, retired_at)
VALUES ('!myslug', 't-tenant1234', 'url1', ?1, ?2, 'active', NULL);",
rusqlite::params![now, now],
)
.unwrap();
// Verify it is not available
let avail = bzod::db::users::is_slug_available(&system_conn, "!myslug").unwrap();
let avail =
bzod::db::slugs::is_slug_available(&reserved_conn, &urls_conn, &pages_conn, "!myslug")
.unwrap();
assert!(!avail);
let _ = fs::remove_dir_all(&temp_dir);
@@ -41,7 +51,9 @@ async fn test_reserved_slug_rejection() {
let config = create_temp_config(temp_dir.clone());
let db = Db::init(&config).expect("Failed to init Db");
let system_conn = db.system.lock().unwrap();
let urls_conn = db.global_urls.lock().unwrap();
let reserved_conn = db.reserved.lock().unwrap();
let pages_conn = db.global_landing_pages.lock().unwrap();
let reserved = vec![
"admin",
@@ -72,7 +84,9 @@ async fn test_reserved_slug_rejection() {
];
for slug in reserved {
let avail = bzod::db::users::is_slug_available(&system_conn, slug).unwrap();
let avail =
bzod::db::slugs::is_slug_available(&reserved_conn, &urls_conn, &pages_conn, slug)
.unwrap();
assert!(!avail, "Reserved slug '{}' should not be available", slug);
}
@@ -97,27 +111,35 @@ async fn test_slug_release_on_user_deletion() {
.await
.unwrap();
let user_id = {
let (user_id, tid) = {
let conn = db.users.lock().unwrap();
bzod::db::users::get_user_by_username(&conn, "testuser")
let u = bzod::db::users::get_user_by_username(&conn, "testuser")
.unwrap()
.unwrap()
.id
.unwrap();
(u.id, u.tenant_id.unwrap())
};
// Register slug for this user
{
let system_conn = db.system.lock().unwrap();
bzod::db::users::register_global_slug(
&system_conn,
let urls_conn = db.global_urls.lock().unwrap();
let reserved_conn = db.reserved.lock().unwrap();
let pages_conn = db.global_landing_pages.lock().unwrap();
let now = chrono::Utc::now().to_rfc3339();
urls_conn
.execute(
"INSERT INTO global_urls (slug, owner_tenant_id, target_id, created_at, updated_at, status, retired_at)
VALUES ('!user-slug', ?1, 'url_xyz', ?2, ?3, 'active', NULL);",
rusqlite::params![tid.as_str(), now, now],
)
.unwrap();
let avail = bzod::db::slugs::is_slug_available(
&reserved_conn,
&urls_conn,
&pages_conn,
"!user-slug",
user_id,
"url",
"url_xyz",
"active",
)
.unwrap();
let avail = bzod::db::users::is_slug_available(&system_conn, "!user-slug").unwrap();
assert!(!avail);
}
@@ -128,11 +150,20 @@ async fn test_slug_release_on_user_deletion() {
// Slug should now be released and available
{
let system_conn = db.system.lock().unwrap();
let avail = bzod::db::users::is_slug_available(&system_conn, "!user-slug").unwrap();
let urls_conn = db.global_urls.lock().unwrap();
let reserved_conn = db.reserved.lock().unwrap();
let pages_conn = db.global_landing_pages.lock().unwrap();
let avail = bzod::db::slugs::is_slug_available(
&reserved_conn,
&urls_conn,
&pages_conn,
"!user-slug",
)
.unwrap();
assert!(avail);
// Verify history populated
let system_conn = db.system.lock().unwrap();
let count: i64 = system_conn
.query_row(
"SELECT COUNT(*) FROM slug_history WHERE slug = ?1 AND action = 'deleted';",
+2
View File
@@ -1,3 +1,5 @@
#![allow(deprecated)]
use bzod::config::Config;
use bzod::db::Db;
use std::fs;
+17 -20
View File
@@ -41,17 +41,15 @@ async fn test_slug_transfer() {
.await
.unwrap();
let (id_a, id_b) = {
let (id_a, tid_a, id_b, tid_b) = {
let conn = db.users.lock().unwrap();
let a = bzod::db::users::get_user_by_username(&conn, "usera")
.unwrap()
.unwrap()
.id;
.unwrap();
let b = bzod::db::users::get_user_by_username(&conn, "userb")
.unwrap()
.unwrap()
.id;
(a, b)
.unwrap();
(a.id, a.tenant_id.unwrap(), b.id, b.tenant_id.unwrap())
};
// User A creates a URL
@@ -71,16 +69,15 @@ async fn test_slug_transfer() {
// Register globally
{
let system_conn = db.system.lock().unwrap();
bzod::db::users::register_global_slug(
&system_conn,
"!trans-slug",
id_a,
"url",
&url.id,
"active",
)
.unwrap();
let urls_conn = db.global_urls.lock().unwrap();
let now = chrono::Utc::now().to_rfc3339();
urls_conn
.execute(
"INSERT INTO global_urls (slug, owner_tenant_id, target_id, created_at, updated_at, status, retired_at)
VALUES ('!trans-slug', ?1, ?2, ?3, ?4, 'active', NULL);",
rusqlite::params![tid_a.as_str(), &url.id, now, now],
)
.unwrap();
let users_conn = db.users.lock().unwrap();
bzod::db::users::increment_quota_counter(&users_conn, id_a, "urls").unwrap();
@@ -112,12 +109,12 @@ async fn test_slug_transfer() {
bzod::db::content::delete_url(&old_conn, &url_to_copy.id).unwrap();
// 3. Update global slugs and quotas
let system_conn = db.system.lock().unwrap();
let urls_conn = db.global_urls.lock().unwrap();
let now = Utc::now().to_rfc3339();
system_conn
urls_conn
.execute(
"UPDATE global_slugs SET owner_user_id = ?1, updated_at = ?2 WHERE slug = ?3;",
rusqlite::params![id_b, now, "!trans-slug"],
"UPDATE global_urls SET owner_tenant_id = ?1, updated_at = ?2 WHERE slug = ?3;",
rusqlite::params![tid_b.as_str(), now, "!trans-slug"],
)
.unwrap();
+51 -27
View File
@@ -19,29 +19,41 @@ async fn test_soft_delete_reserves_slug() {
let config = create_temp_config(temp_dir.clone());
let db = Db::init(&config).expect("Failed to init Db");
let system_conn = db.system.lock().unwrap();
let urls_conn = db.global_urls.lock().unwrap();
let reserved_conn = db.reserved.lock().unwrap();
let pages_conn = db.global_landing_pages.lock().unwrap();
// Register slug
bzod::db::users::register_global_slug(
&system_conn,
"!slug-to-delete",
10,
"url",
"url_123",
"active",
)
.unwrap();
let now = chrono::Utc::now().to_rfc3339();
urls_conn
.execute(
"INSERT INTO global_urls (slug, owner_tenant_id, target_id, created_at, updated_at, status, retired_at)
VALUES ('!slug-to-delete', 't-tenant10', 'url_123', ?1, ?2, 'active', NULL);",
rusqlite::params![now, now],
)
.unwrap();
// Soft delete slug
bzod::db::users::soft_delete_global_slug(&system_conn, "!slug-to-delete", 10).unwrap();
// Soft delete slug (disable it)
urls_conn
.execute(
"UPDATE global_urls SET status = 'disabled', retired_at = ?1 WHERE slug = ?2;",
rusqlite::params![now, "!slug-to-delete"],
)
.unwrap();
// Verify it is not available (keeps the slug reserved)
let avail = bzod::db::users::is_slug_available(&system_conn, "!slug-to-delete").unwrap();
let avail = bzod::db::slugs::is_slug_available(
&reserved_conn,
&urls_conn,
&pages_conn,
"!slug-to-delete",
)
.unwrap();
assert!(!avail);
let status: String = system_conn
let status: String = urls_conn
.query_row(
"SELECT status FROM global_slugs WHERE slug = ?1;",
"SELECT status FROM global_urls WHERE slug = ?1;",
["!slug-to-delete"],
|row| row.get(0),
)
@@ -59,24 +71,36 @@ async fn test_permanent_delete_releases_slug() {
let config = create_temp_config(temp_dir.clone());
let db = Db::init(&config).expect("Failed to init Db");
let system_conn = db.system.lock().unwrap();
let urls_conn = db.global_urls.lock().unwrap();
let reserved_conn = db.reserved.lock().unwrap();
let pages_conn = db.global_landing_pages.lock().unwrap();
// Register slug
bzod::db::users::register_global_slug(
&system_conn,
"!slug-to-purge",
10,
"url",
"url_456",
"active",
)
.unwrap();
let now = chrono::Utc::now().to_rfc3339();
urls_conn
.execute(
"INSERT INTO global_urls (slug, owner_tenant_id, target_id, created_at, updated_at, status, retired_at)
VALUES ('!slug-to-purge', 't-tenant10', 'url_456', ?1, ?2, 'active', NULL);",
rusqlite::params![now, now],
)
.unwrap();
// Release global slug (permanent deletion)
bzod::db::users::release_global_slug(&system_conn, "!slug-to-purge", 10).unwrap();
urls_conn
.execute(
"DELETE FROM global_urls WHERE slug = ?1;",
["!slug-to-purge"],
)
.unwrap();
// Verify slug is released (available for reuse)
let avail = bzod::db::users::is_slug_available(&system_conn, "!slug-to-purge").unwrap();
let avail = bzod::db::slugs::is_slug_available(
&reserved_conn,
&urls_conn,
&pages_conn,
"!slug-to-purge",
)
.unwrap();
assert!(avail);
let _ = fs::remove_dir_all(&temp_dir);
+30 -22
View File
@@ -18,34 +18,38 @@ async fn test_cleanup_stale_reservations() {
let config = create_temp_config(temp_dir.clone());
let db = Db::init(&config).expect("Failed to init Db");
let system_conn = db.system.lock().unwrap();
let urls_conn = db.global_urls.lock().unwrap();
let pages_conn = db.global_landing_pages.lock().unwrap();
// Insert a reserving slug older than 15 minutes (e.g. 20 minutes ago)
let old_time = (chrono::Utc::now() - chrono::Duration::minutes(20)).to_rfc3339();
system_conn.execute(
"INSERT INTO global_slugs (slug, owner_user_id, target_type, target_id, created_at, updated_at, status)
VALUES ('stale-slug', 2, 'url', '', ?1, ?1, 'reserving')",
[&old_time]
).unwrap();
urls_conn
.execute(
"INSERT INTO global_urls (slug, owner_tenant_id, target_id, created_at, updated_at, status, retired_at)
VALUES ('!stale-slug', 't-tenant1234', '', ?1, ?1, 'reserving', NULL)",
[&old_time],
)
.unwrap();
// Verify it exists before cleanup
let exists: bool = system_conn
let exists: bool = urls_conn
.query_row(
"SELECT EXISTS(SELECT 1 FROM global_slugs WHERE slug = 'stale-slug')",
"SELECT EXISTS(SELECT 1 FROM global_urls WHERE slug = '!stale-slug')",
[],
|r| r.get(0),
)
.unwrap();
assert!(exists);
// Run cleanup
let cleaned = bzod::db::users::cleanup_stale_reservations(&system_conn, &temp_dir).unwrap();
// Run cleanup for records older than 15 minutes (900 seconds)
let cleaned =
bzod::db::slugs::cleanup_stale_reservations(&urls_conn, &pages_conn, 900).unwrap();
assert_eq!(cleaned, 1);
// Verify it is gone
let exists: bool = system_conn
let exists: bool = urls_conn
.query_row(
"SELECT EXISTS(SELECT 1 FROM global_slugs WHERE slug = 'stale-slug')",
"SELECT EXISTS(SELECT 1 FROM global_urls WHERE slug = '!stale-slug')",
[],
|r| r.get(0),
)
@@ -62,24 +66,28 @@ async fn test_cleanup_preserves_valid_reservations() {
let config = create_temp_config(temp_dir.clone());
let db = Db::init(&config).expect("Failed to init Db");
let system_conn = db.system.lock().unwrap();
let urls_conn = db.global_urls.lock().unwrap();
let pages_conn = db.global_landing_pages.lock().unwrap();
// Insert a reserving slug that is brand new (e.g. 1 minute ago)
let new_time = (chrono::Utc::now() - chrono::Duration::minutes(1)).to_rfc3339();
system_conn.execute(
"INSERT INTO global_slugs (slug, owner_user_id, target_type, target_id, created_at, updated_at, status)
VALUES ('fresh-slug', 2, 'url', '', ?1, ?1, 'reserving')",
[&new_time]
).unwrap();
urls_conn
.execute(
"INSERT INTO global_urls (slug, owner_tenant_id, target_id, created_at, updated_at, status, retired_at)
VALUES ('!fresh-slug', 't-tenant1234', '', ?1, ?1, 'reserving', NULL)",
[&new_time],
)
.unwrap();
// Run cleanup
let cleaned = bzod::db::users::cleanup_stale_reservations(&system_conn, &temp_dir).unwrap();
// Run cleanup for records older than 15 minutes (900 seconds)
let cleaned =
bzod::db::slugs::cleanup_stale_reservations(&urls_conn, &pages_conn, 900).unwrap();
assert_eq!(cleaned, 0);
// Verify it is still there
let exists: bool = system_conn
let exists: bool = urls_conn
.query_row(
"SELECT EXISTS(SELECT 1 FROM global_slugs WHERE slug = 'fresh-slug')",
"SELECT EXISTS(SELECT 1 FROM global_urls WHERE slug = '!fresh-slug')",
[],
|r| r.get(0),
)
+26 -3
View File
@@ -99,6 +99,15 @@ async fn test_upgrade_from_v0_4_0() {
)
.unwrap();
let now = chrono::Utc::now().to_rfc3339();
system_conn
.execute(
"INSERT INTO global_slugs (slug, owner_user_id, target_type, target_id, created_at, updated_at, status)
VALUES ('!legacy-link', 1, 'url', ?1, ?2, ?3, 'active');",
rusqlite::params![&url.id, now, now],
)
.unwrap();
// 4. analytics.db
let mut analytics_conn = rusqlite::Connection::open(&legacy_analytics_path).unwrap();
bzod::db::migrations::run_migrations(
@@ -131,10 +140,17 @@ async fn test_upgrade_from_v0_4_0() {
.unwrap();
}
// Normalize legacy flat layout to multi-tenant paths
bzod::services::backup_layout::normalize_restored_layout(&temp_dir).unwrap();
// Now start the application setup which should trigger migrations/upgrade
let config = create_temp_config(temp_dir.clone());
let db = Db::init(&config).expect("Db::init failed to upgrade v0.4.0 database directory");
// Run identity and slug migrations for legacy data
let _ = bzod::db::identity_migrate::run_identity_migration(&config, false, true).await;
let _ = bzod::db::slug_migrate::run_global_slug_migration(&config, false, true).await;
// Verify file layout was reorganized
assert!(!legacy_admin_path.exists());
assert!(!legacy_content_path.exists());
@@ -153,8 +169,6 @@ async fn test_upgrade_from_v0_4_0() {
let (queue, _) = AnalyticsQueue::new(db.clone(), 10, rx);
let state = AppState {
admin_db: db.admin.clone(),
content_db: db.content.clone(),
analytics_db: db.analytics.clone(),
system_db: db.system.clone(),
users_db: db.users.clone(),
user_dbs: std::sync::Arc::new(std::sync::Mutex::new(HashMap::new())),
@@ -206,7 +220,16 @@ async fn test_upgrade_from_v0_4_0() {
// 2. Existing links redirect correctly
let redirect_url = format!("{}/!legacy-link", base_url);
let redirect_res = client.get(&redirect_url).send().await.unwrap();
let res_headers = redirect_res.headers();
let status = redirect_res.status();
let res_headers = redirect_res.headers().clone();
let body = redirect_res.text().await.unwrap();
assert_eq!(
status,
reqwest::StatusCode::MOVED_PERMANENTLY,
"Got status {} with body {}",
status,
body
);
assert!(res_headers.get("location").is_some());
assert_eq!(
res_headers.get("location").unwrap().to_str().unwrap(),
+8 -5
View File
@@ -31,12 +31,16 @@ async fn test_user_deletion_cleanup() {
.await
.unwrap();
let user_id = {
let (user_id, user_dir) = {
let conn = db.users.lock().unwrap();
bzod::db::users::get_user_by_username(&conn, "testuser")
let user = bzod::db::users::get_user_by_username(&conn, "testuser")
.unwrap()
.unwrap();
let dir = bzod::db::tenant::location_for_user(&user)
.unwrap()
.id
.dir(&bzod::db::topology::Topology::new(&temp_dir))
.unwrap();
(user.id, dir)
};
// Add session and API token
@@ -52,8 +56,7 @@ async fn test_user_deletion_cleanup() {
.await
.unwrap();
// Verify directory is deleted
let user_dir = temp_dir.join("users").join(user_id.to_string());
// Verify tenant directory is deleted
assert!(!user_dir.exists());
// Verify sessions/tokens are cascadingly deleted
+12 -7
View File
@@ -38,21 +38,26 @@ async fn test_user_database_isolation() {
.await
.unwrap();
let (id_a, id_b) = {
let (user_a, user_b) = {
let conn = db.users.lock().unwrap();
let a = bzod::db::users::get_user_by_username(&conn, "usera")
.unwrap()
.unwrap()
.id;
.unwrap();
let b = bzod::db::users::get_user_by_username(&conn, "userb")
.unwrap()
.unwrap()
.id;
.unwrap();
(a, b)
};
let dir_a = temp_dir.join("users").join(id_a.to_string());
let dir_b = temp_dir.join("users").join(id_b.to_string());
let topology = bzod::db::topology::Topology::new(&temp_dir);
let dir_a = bzod::db::tenant::location_for_user(&user_a)
.unwrap()
.dir(&topology)
.unwrap();
let dir_b = bzod::db::tenant::location_for_user(&user_b)
.unwrap()
.dir(&topology)
.unwrap();
assert_ne!(dir_a, dir_b);
assert!(dir_a.join("content.db").exists());
+4 -1
View File
@@ -36,7 +36,10 @@ async fn test_user_creation() {
.unwrap();
assert_eq!(user.status, "active");
let user_dir = temp_dir.join("users").join(user.id.to_string());
let user_dir = bzod::db::tenant::location_for_user(&user)
.unwrap()
.dir(&bzod::db::topology::Topology::new(&temp_dir))
.unwrap();
assert!(user_dir.join("content.db").exists());
assert!(user_dir.join("analytics.db").exists());
assert!(user_dir.join("profile.db").exists());
+382
View File
@@ -0,0 +1,382 @@
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::time::Instant;
use bzod::analytics::AnalyticsQueue;
use bzod::config::Config;
use bzod::db::topology::Topology;
use bzod::db::Db;
use bzod::identity::TenantId;
use bzod::state::AppState;
use bzod::web::create_router;
fn create_temp_config(temp_dir: PathBuf) -> Config {
let mut config = Config::load();
config.data_dir = temp_dir.clone();
config.backup_dir = temp_dir.join("backups");
config.base_url = Some("http://localhost:8080".to_string());
config
}
fn build_test_state(db: &Db, config: &Config) -> AppState {
let (queue, _) = AnalyticsQueue::new(db.clone(), 10, tokio::sync::watch::channel(false).1);
AppState {
admin_db: db.admin.clone(),
system_db: db.system.clone(),
users_db: db.users.clone(),
user_dbs: Arc::new(Mutex::new(HashMap::new())),
db: db.clone(),
config: config.clone(),
analytics_queue: queue,
start_time: Instant::now(),
}
}
#[tokio::test]
async fn test_admin_provisioning_has_no_tenant_directory() {
let temp_dir = std::env::temp_dir().join(format!("bzod_p5_admin_dir_{}", uuid::Uuid::new_v4()));
fs::create_dir_all(&temp_dir).unwrap();
let config = create_temp_config(temp_dir.clone());
// 1. Initialize Db
let db = Db::init(&config).expect("Db::init failed");
let topology = Topology::new(&temp_dir);
// 2. Create Admin user via CLI
let admin_res = bzod::cli::create_admin::run(
Some("platform_admin".to_string()),
Some("supersecretpass123".to_string()),
None,
config.clone(),
)
.await;
assert!(admin_res.is_ok());
// 3. Verify Admin in admin.db and users.db
let admin_user = {
let users_conn = db.users.lock().unwrap();
let u = bzod::db::users::get_user_by_username(&users_conn, "platform_admin")
.unwrap()
.expect("Admin user must exist in users.db");
assert_eq!(u.account_type, "admin");
assert!(u.tenant_id.is_none(), "Admin must NOT have a TenantId");
u
};
// 4. Verify NO tenant directory was created for admin
let users_dir = topology.users_dir();
if users_dir.exists() {
let entries: Vec<_> = fs::read_dir(&users_dir)
.unwrap()
.filter_map(|e| e.ok())
.collect();
for entry in entries {
let name = entry.file_name().to_string_lossy().to_string();
assert_ne!(name, format!("{}", admin_user.id));
assert_ne!(name, "platform_admin");
}
}
let _ = fs::remove_dir_all(&temp_dir);
}
#[tokio::test]
async fn test_admin_cannot_create_unowned_application_resources() {
let temp_dir = std::env::temp_dir().join(format!("bzod_p5_unowned_{}", uuid::Uuid::new_v4()));
fs::create_dir_all(&temp_dir).unwrap();
let config = create_temp_config(temp_dir.clone());
let db = Db::init(&config).expect("Db::init failed");
let state = build_test_state(&db, &config);
let router = create_router(state);
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let base_url = format!("http://{}", addr);
tokio::spawn(async move {
axum::serve(listener, router).await.unwrap();
});
let client = reqwest::Client::builder()
.cookie_store(true)
.redirect(reqwest::redirect::Policy::none())
.build()
.unwrap();
// 1. Create Admin
let _ = bzod::cli::create_admin::run(
Some("operator".to_string()),
Some("pass123456".to_string()),
None,
config.clone(),
)
.await
.unwrap();
// 2. Log in as admin
let login_page = client
.get(format!("{}/admin/login", base_url))
.send()
.await
.unwrap()
.text()
.await
.unwrap();
// Extract CSRF token
let csrf = login_page
.split("name=\"csrf_token\" value=\"")
.nth(1)
.and_then(|s| s.split('"').next())
.unwrap_or_default();
let mut params = HashMap::new();
params.insert("username", "operator");
params.insert("password", "pass123456");
params.insert("csrf_token", csrf);
let login_res = client
.post(format!("{}/admin/login", base_url))
.form(&params)
.send()
.await
.unwrap();
assert_eq!(login_res.status(), reqwest::StatusCode::SEE_OTHER);
// 3. Admin attempts to create URL via POST /admin/urls/create -> Should redirect with error
let mut url_form = HashMap::new();
url_form.insert("destination", "https://example.com");
url_form.insert("code", "!admin_link");
url_form.insert("csrf_token", csrf);
let create_url_res = client
.post(format!("{}/admin/urls/create", base_url))
.form(&url_form)
.send()
.await
.unwrap();
assert_eq!(create_url_res.status(), reqwest::StatusCode::SEE_OTHER);
let location = create_url_res
.headers()
.get("location")
.unwrap()
.to_str()
.unwrap();
assert!(
location.contains("error="),
"Admin URL creation must be rejected with error redirect"
);
// 4. Admin attempts to create Landing Page via POST /admin/pages/create -> Should redirect with error
let mut page_form = HashMap::new();
page_form.insert("title", "Admin Page");
page_form.insert("slug", "!admin_page");
page_form.insert("code", "!admin_page");
page_form.insert("custom_slug", "");
page_form.insert("state", "published");
page_form.insert("html_content", "<h1>Admin</h1>");
page_form.insert("csrf_token", csrf);
let create_page_res = client
.post(format!("{}/admin/pages/create", base_url))
.form(&page_form)
.send()
.await
.unwrap();
assert_eq!(create_page_res.status(), reqwest::StatusCode::SEE_OTHER);
let location = create_page_res
.headers()
.get("location")
.unwrap()
.to_str()
.unwrap();
assert!(
location.contains("error="),
"Admin Landing Page creation must be rejected with error redirect"
);
let _ = fs::remove_dir_all(&temp_dir);
}
#[tokio::test]
async fn test_platform_click_aggregation_without_admin_analytics() {
let temp_dir = std::env::temp_dir().join(format!("bzod_p5_agg_{}", uuid::Uuid::new_v4()));
fs::create_dir_all(&temp_dir).unwrap();
let config = create_temp_config(temp_dir.clone());
let db = Db::init(&config).expect("Db::init failed");
// 1. Create two standard tenant users
let _ = bzod::cli::create_user::run(
Some("tenant_one".to_string()),
Some("pass123".to_string()),
None,
config.clone(),
)
.await
.unwrap();
let _ = bzod::cli::create_user::run(
Some("tenant_two".to_string()),
Some("pass123".to_string()),
None,
config.clone(),
)
.await
.unwrap();
let (u1, u2) = {
let conn = db.users.lock().unwrap();
let user1 = bzod::db::users::get_user_by_username(&conn, "tenant_one")
.unwrap()
.unwrap();
let user2 = bzod::db::users::get_user_by_username(&conn, "tenant_two")
.unwrap()
.unwrap();
(user1, user2)
};
// 2. Insert clicks directly into tenant 1 and tenant 2 analytics DBs
let t1_tid = u1.tenant_id.unwrap();
let t2_tid = u2.tenant_id.unwrap();
let t1_analytics_path = db.topology.tenant_analytics_db(t1_tid);
let t2_analytics_path = db.topology.tenant_analytics_db(t2_tid);
{
let conn1 = rusqlite::Connection::open(&t1_analytics_path).unwrap();
let _ = conn1.execute(
"INSERT INTO visits (id, target_type, target_id, timestamp, ip_address, country, referer, user_agent, accept_language, status_code)
VALUES ('v1', 'url', 'url1', '2026-08-20T10:00:00Z', '1.1.1.1', 'US', 'direct', 'ua', 'en', 301);",
[],
).unwrap();
let _ = conn1.execute(
"INSERT INTO visits (id, target_type, target_id, timestamp, ip_address, country, referer, user_agent, accept_language, status_code)
VALUES ('v2', 'url', 'url1', '2026-08-20T10:01:00Z', '1.1.1.2', 'US', 'direct', 'ua', 'en', 301);",
[],
).unwrap();
}
{
let conn2 = rusqlite::Connection::open(&t2_analytics_path).unwrap();
let _ = conn2.execute(
"INSERT INTO visits (id, target_type, target_id, timestamp, ip_address, country, referer, user_agent, accept_language, status_code)
VALUES ('v3', 'url', 'url2', '2026-08-20T10:02:00Z', '2.2.2.2', 'CA', 'direct', 'ua', 'en', 301);",
[],
).unwrap();
}
// 3. Aggregate platform total clicks
let total_clicks = {
let users_conn = db.users.lock().unwrap();
bzod::db::users::get_platform_total_clicks(&db.topology, &users_conn).unwrap()
};
assert_eq!(
total_clicks, 3,
"Platform clicks must aggregate across all active tenant databases"
);
let _ = fs::remove_dir_all(&temp_dir);
}
#[tokio::test]
async fn test_admin_resource_inspection_and_moderation() {
let temp_dir = std::env::temp_dir().join(format!("bzod_p5_mod_{}", uuid::Uuid::new_v4()));
fs::create_dir_all(&temp_dir).unwrap();
let config = create_temp_config(temp_dir.clone());
let db = Db::init(&config).expect("Db::init failed");
let state = build_test_state(&db, &config);
// 1. Create standard tenant user
let _ = bzod::cli::create_user::run(
Some("content_author".to_string()),
Some("pass123".to_string()),
None,
config.clone(),
)
.await
.unwrap();
let (author_id, author_tid) = {
let conn = db.users.lock().unwrap();
let u = bzod::db::users::get_user_by_username(&conn, "content_author")
.unwrap()
.unwrap();
(u.id, u.tenant_id.unwrap())
};
// 2. Author creates URL
let url_id = {
let conn = bzod::jobs::open_user_content_conn(&db, author_id).unwrap();
let u = bzod::db::content::create_url_extended(
&conn,
"!moderated-link",
"https://bad-site.com",
Some("Spam Link"),
None,
&[],
None,
None,
None,
)
.unwrap();
let urls_conn = db.global_urls.lock().unwrap();
bzod::db::slugs::register_url_slug(
&urls_conn,
"!moderated-link",
&author_tid,
&u.id,
"active",
)
.unwrap();
u.id
};
// 3. Verify Admin can look up slug and inspect owner tenant
let slug_info = state
.lookup_slug("!moderated-link")
.unwrap()
.expect("Slug must exist in global registry");
assert_eq!(slug_info.owner_tenant_id, author_tid.as_str());
// 4. Admin opens tenant content DB in CoreJob mode to inspect
let tenant_dbs = state
.open_tenant(author_tid, bzod::state::TenantOpenMode::CoreJob)
.unwrap();
{
let conn = tenant_dbs.content.lock().unwrap();
let fetched_url = bzod::db::content::get_url_by_id(&conn, &url_id)
.unwrap()
.expect("URL must exist in tenant content.db");
assert_eq!(fetched_url.destination, "https://bad-site.com");
}
// 5. Admin retires/moderates the slug
{
let urls_conn = db.global_urls.lock().unwrap();
let pages_conn = db.global_landing_pages.lock().unwrap();
let retired =
bzod::db::slugs::retire_slug(&urls_conn, &pages_conn, "!moderated-link").unwrap();
assert!(retired);
}
// 6. Verify slug cannot be re-registered by another tenant
let _other_tid = TenantId::generate();
let r_conn = db.reserved.lock().unwrap();
let u_conn = db.global_urls.lock().unwrap();
let p_conn = db.global_landing_pages.lock().unwrap();
let available =
bzod::db::slugs::is_slug_available(&r_conn, &u_conn, &p_conn, "!moderated-link").unwrap();
assert!(
!available,
"Retired slug must remain unavailable across the platform"
);
let _ = fs::remove_dir_all(&temp_dir);
}
+170
View File
@@ -0,0 +1,170 @@
//! v0.8.0 Phase 1: frozen database topology under the existing physical root.
use bzod::config::Config;
use bzod::db::slugs::CORE_RESERVED_SLUGS;
use bzod::db::sqlite::get_user_version;
use bzod::db::topology::{is_valid_user_dir_name, Topology};
use bzod::db::Db;
use rusqlite::Connection;
use std::fs;
use std::path::PathBuf;
fn temp_config() -> (PathBuf, Config) {
let temp_dir = std::env::temp_dir().join(format!("bzod_v08_topology_{}", uuid::Uuid::new_v4()));
fs::create_dir_all(&temp_dir).unwrap();
let mut config = Config::load();
config.data_dir = temp_dir.clone();
config.backup_dir = temp_dir.join("backups");
(temp_dir, config)
}
#[test]
fn physical_root_stays_data_dir_not_database() {
let t = Topology::new("./data");
assert_eq!(t.root(), std::path::Path::new("./data"));
assert!(!t.root().ends_with("database"));
assert!(t.slugs_dir().ends_with("data/slugs"));
}
#[tokio::test]
async fn db_init_creates_frozen_slug_topology() {
let (temp_dir, config) = temp_config();
let db = Db::init(&config).expect("init");
assert!(db.topology.admin_db().exists());
assert!(db.topology.system_db().exists());
assert!(db.topology.users_registry_db().exists());
assert!(db.topology.global_urls_db().exists());
assert!(db.topology.global_landing_pages_db().exists());
assert!(db.topology.reserved_db().exists());
assert!(!temp_dir.join("database").exists());
{
let conn = db.global_urls.lock().unwrap();
assert_eq!(
get_user_version(&conn).unwrap(),
bzod::db::schema_v08::GLOBAL_URLS_MIGRATIONS
.last()
.unwrap()
.version
);
let n: i64 = conn
.query_row("SELECT COUNT(*) FROM global_urls;", [], |r| r.get(0))
.unwrap();
assert_eq!(n, 0, "Phase 1 does not move live slugs off system.db");
}
{
let conn = db.global_landing_pages.lock().unwrap();
assert_eq!(
get_user_version(&conn).unwrap(),
bzod::db::schema_v08::GLOBAL_LANDING_PAGES_MIGRATIONS
.last()
.unwrap()
.version
);
let n: i64 = conn
.query_row("SELECT COUNT(*) FROM global_landing_pages;", [], |r| {
r.get(0)
})
.unwrap();
assert_eq!(n, 0);
}
{
let conn = db.reserved.lock().unwrap();
assert_eq!(get_user_version(&conn).unwrap(), 1);
let n: i64 = conn
.query_row("SELECT COUNT(*) FROM reserved_slugs;", [], |r| r.get(0))
.unwrap();
assert_eq!(n, CORE_RESERVED_SLUGS.len() as i64);
let has_admin: bool = conn
.query_row(
"SELECT EXISTS(SELECT 1 FROM reserved_slugs WHERE slug = 'admin');",
[],
|r| r.get(0),
)
.unwrap();
assert!(has_admin);
}
// Live slug registry is still system.db (Phase 4 moves ownership).
{
let conn = db.system.lock().unwrap();
let has_table: bool = conn
.query_row(
"SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type='table' AND name='global_slugs');",
[],
|r| r.get(0),
)
.unwrap();
assert!(has_table);
}
let _ = fs::remove_dir_all(&temp_dir);
}
#[tokio::test]
async fn backup_includes_slugs_directory() {
let (temp_dir, config) = temp_config();
let db = Db::init(&config).expect("init");
let backup_path = bzod::jobs::backup::perform_backup(&db, &config)
.await
.expect("backup");
let file = fs::File::open(&backup_path).unwrap();
let dec = flate2::read::GzDecoder::new(file);
let mut archive = tar::Archive::new(dec);
let mut found_reserved = false;
let mut found_global_urls = false;
for entry in archive.entries().unwrap() {
let entry = entry.unwrap();
let path = entry.path().unwrap().to_string_lossy().to_string();
if path.contains("slugs/reserved.db") {
found_reserved = true;
}
if path.contains("slugs/global_urls.db") {
found_global_urls = true;
}
}
assert!(found_reserved, "backup missing slugs/reserved.db");
assert!(found_global_urls, "backup missing slugs/global_urls.db");
let _ = fs::remove_dir_all(&temp_dir);
}
#[test]
fn tenant_path_resolution_rejects_traversal() {
let t = Topology::new("/app/data");
assert!(t.user_dir("..").is_err());
assert!(t.user_dir("../etc").is_err());
assert!(t.content_db_i64(-1).is_err());
assert!(is_valid_user_dir_name("2"));
assert!(is_valid_user_dir_name("a1b2c3d4e5f6"));
assert!(!is_valid_user_dir_name("admin"));
}
#[tokio::test]
async fn open_user_content_rejects_non_positive_id() {
let (temp_dir, config) = temp_config();
let db = Db::init(&config).expect("init");
assert!(bzod::jobs::open_user_content_conn(&db, 0).is_err());
assert!(bzod::jobs::open_user_content_conn(&db, -3).is_err());
let _ = fs::remove_dir_all(&temp_dir);
}
#[test]
fn restore_normalization_creates_slugs_dir() {
let dir = std::env::temp_dir().join(format!("bzod_v08_restore_{}", uuid::Uuid::new_v4()));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
fs::write(dir.join("admin.db"), b"a").unwrap();
fs::write(dir.join("system.db"), b"s").unwrap();
fs::write(dir.join("users.db"), b"u").unwrap();
fs::write(dir.join("content.db"), b"c").unwrap();
fs::write(dir.join("analytics.db"), b"an").unwrap();
bzod::services::backup_layout::normalize_restored_layout(&dir).unwrap();
assert!(dir.join("slugs").is_dir());
assert!(Connection::open(dir.join("admin/admin.db")).is_ok());
let _ = fs::remove_dir_all(&dir);
}
+348
View File
@@ -0,0 +1,348 @@
//! v0.8.0 Phase 3: Identity Migration Integration Tests.
//!
//! Tests:
//! 1. New users automatically get TenantId + UUID.
//! 2. Explicit identity migration lifecycle (preflight, backup, backfill, directory rename, validation).
//! 3. Legacy Admin (users/1) is preserved and not converted into a normal tenant.
//! 4. Restart-safety & idempotency (repeated migration runs).
//! 5. Recovery when target directory already exists.
//! 6. Admin bootstrap concurrency without re-entrant mutex deadlock.
//! 7. Cross-tenant isolation after identity migration.
use bzod::config::Config;
use bzod::db::identity_migrate::run_identity_migration;
use bzod::db::tenant::TenantOpenMode;
use bzod::db::Db;
use bzod::state::AppState;
use rusqlite::Connection;
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::time::Instant;
fn temp_config() -> (PathBuf, Config) {
let temp_dir =
std::env::temp_dir().join(format!("bzod_v08_id_migrate_{}", uuid::Uuid::new_v4()));
fs::create_dir_all(&temp_dir).unwrap();
let mut config = Config::load();
config.data_dir = temp_dir.clone();
config.backup_dir = temp_dir.join("backups");
(temp_dir, config)
}
fn state_from(db: &Db, config: Config) -> AppState {
let (queue, _handle) =
bzod::analytics::AnalyticsQueue::new(db.clone(), 8, tokio::sync::watch::channel(false).1);
AppState {
admin_db: db.admin.clone(),
system_db: db.system.clone(),
users_db: db.users.clone(),
user_dbs: Arc::new(Mutex::new(HashMap::new())),
db: db.clone(),
config,
analytics_queue: queue,
start_time: Instant::now(),
}
}
#[tokio::test]
async fn test_new_user_gets_tenant_id_and_uuid() {
let (temp_dir, config) = temp_config();
let db = Db::init(&config).unwrap();
bzod::cli::create_user::run(
Some("carol".into()),
Some("password123".into()),
None,
config.clone(),
)
.await
.unwrap();
let user = {
let conn = db.users.lock().unwrap();
bzod::db::users::get_user_by_username(&conn, "carol")
.unwrap()
.unwrap()
};
let tid = user.tenant_id.expect("new user must receive TenantId");
assert_eq!(tid.as_str().len(), 12);
let uuid_str = user.uuid.expect("new user must receive UUID");
assert!(uuid::Uuid::parse_str(&uuid_str).is_ok());
// Directory is created under users/<TenantId>/
assert!(temp_dir
.join("users")
.join(tid.as_str())
.join("content.db")
.exists());
assert!(!temp_dir.join("users").join(user.id.to_string()).exists());
let _ = fs::remove_dir_all(&temp_dir);
}
#[tokio::test]
async fn test_full_identity_migration_lifecycle() {
let (temp_dir, config) = temp_config();
let db = Db::init(&config).unwrap();
// 1. Manually insert legacy admin and legacy users without tenant_id/uuid in users.db
let hash = bzod::auth::hash_password("password123").unwrap();
let now = chrono::Utc::now().to_rfc3339();
let (id_bob, id_dave) = {
let conn = db.users.lock().unwrap();
conn.execute(
"INSERT INTO users (username, password_hash, status, created_at, account_type) VALUES ('legacy_admin', ?1, 'disabled', ?2, 'system');",
rusqlite::params![hash, now],
).unwrap();
conn.execute(
"INSERT INTO users (username, password_hash, status, created_at, account_type) VALUES ('bob', ?1, 'active', ?2, 'standard');",
rusqlite::params![hash, now],
).unwrap();
let b = conn.last_insert_rowid();
conn.execute(
"INSERT INTO users (username, password_hash, status, created_at, account_type) VALUES ('dave', ?1, 'active', ?2, 'standard');",
rusqlite::params![hash, now],
).unwrap();
let d = conn.last_insert_rowid();
(b, d)
};
// 2. Provision legacy directories at users/1 (legacy admin) and users/<id>/ (bob & dave)
let legacy_admin_dir = temp_dir.join("users").join("1");
let legacy_bob_dir = temp_dir.join("users").join(id_bob.to_string());
let legacy_dave_dir = temp_dir.join("users").join(id_dave.to_string());
fs::create_dir_all(&legacy_admin_dir).unwrap();
fs::create_dir_all(&legacy_bob_dir).unwrap();
fs::create_dir_all(&legacy_dave_dir).unwrap();
// Seed content in Bob's legacy content.db
let bob_content_path = legacy_bob_dir.join("content.db");
{
let mut conn = Connection::open(&bob_content_path).unwrap();
bzod::db::migrations::run_migrations(
&mut conn,
"content",
bzod::db::migrations::CONTENT_MIGRATIONS,
None,
)
.unwrap();
bzod::db::content::create_url_extended(
&conn,
"!bob-link",
"https://bob.example.com",
None,
None,
&vec![],
None,
None,
None,
)
.unwrap();
}
// 3. Run Dry Run Preflight
let dry_report = run_identity_migration(&config, true, true)
.await
.expect("dry run");
assert_eq!(dry_report.users_assigned_tenant_id, 2);
assert_eq!(dry_report.users_assigned_uuid, 2);
assert!(legacy_bob_dir.exists(), "dry run must not move directory");
// 4. Run Execution
let report = run_identity_migration(&config, false, true)
.await
.expect("migration");
assert_eq!(report.users_assigned_tenant_id, 2);
assert_eq!(report.users_assigned_uuid, 2);
assert_eq!(report.directories_moved, 2); // Both Bob and Dave had directories moved
assert!(report.validation_passed);
assert!(report.legacy_admin_preserved);
// 5. Verify Bob's identity and migrated directory
let bob = {
let conn = db.users.lock().unwrap();
bzod::db::users::get_user_by_username(&conn, "bob")
.unwrap()
.unwrap()
};
let bob_tid = bob.tenant_id.expect("bob must have TenantId");
assert_eq!(bob_tid.as_str().len(), 12);
assert!(bob.uuid.is_some());
let migrated_bob_dir = temp_dir.join("users").join(bob_tid.as_str());
assert!(
migrated_bob_dir.exists(),
"migrated directory must exist at users/<TenantId>/"
);
assert!(!legacy_bob_dir.exists(), "legacy directory must be moved");
// Verify Bob's content is preserved exactly
{
let conn = Connection::open(migrated_bob_dir.join("content.db")).unwrap();
let url = bzod::db::content::get_url_by_code(&conn, "!bob-link")
.unwrap()
.expect("bob's link must exist in migrated content.db");
assert_eq!(url.destination, "https://bob.example.com");
}
// 6. Verify legacy admin (users/1) was preserved
assert!(
temp_dir.join("users").join("1").exists(),
"legacy admin directory users/1 must be preserved"
);
// 7. Verify Completion Marker in system.db
{
let system_conn = db.system.lock().unwrap();
let marker: Option<String> = system_conn
.query_row(
"SELECT value FROM settings WHERE key = 'v08_identity_migration_completed';",
[],
|r| r.get(0),
)
.ok();
assert!(marker.is_some());
}
let _ = fs::remove_dir_all(&temp_dir);
}
#[tokio::test]
async fn test_migration_is_restart_safe_and_idempotent() {
let (temp_dir, config) = temp_config();
let db = Db::init(&config).unwrap();
// Create a normal user
bzod::cli::create_user::run(
Some("eva".into()),
Some("password123".into()),
None,
config.clone(),
)
.await
.unwrap();
let original_user = {
let conn = db.users.lock().unwrap();
bzod::db::users::get_user_by_username(&conn, "eva")
.unwrap()
.unwrap()
};
let original_tid = original_user.tenant_id.unwrap();
let original_uuid = original_user.uuid.unwrap();
// Run migration twice
let report1 = run_identity_migration(&config, false, true).await.unwrap();
let report2 = run_identity_migration(&config, false, true).await.unwrap();
assert_eq!(report1.users_assigned_tenant_id, 0); // already had TenantId
assert_eq!(report2.users_assigned_tenant_id, 0);
let current_user = {
let conn = db.users.lock().unwrap();
bzod::db::users::get_user_by_username(&conn, "eva")
.unwrap()
.unwrap()
};
// Identity mappings must be identical (never regenerated)
assert_eq!(current_user.tenant_id.unwrap(), original_tid);
assert_eq!(current_user.uuid.unwrap(), original_uuid);
assert!(temp_dir.join("users").join(original_tid.as_str()).exists());
let _ = fs::remove_dir_all(&temp_dir);
}
#[tokio::test]
async fn test_admin_bootstrap_concurrency_no_deadlock() {
let (temp_dir, config) = temp_config();
let db = Db::init(&config).unwrap();
let state = state_from(&db, config.clone());
// Call create_admin_user multiple times or simulate bootstrap login
let hash = bzod::auth::hash_password("securepassword").unwrap();
let res = {
let conn = db.users.lock().unwrap();
bzod::db::users::create_admin_user(&conn, "admin_user", &hash)
};
assert!(res.is_ok());
let admin = res.unwrap();
assert_eq!(admin.account_type, "admin");
assert!(admin.uuid.is_some());
assert_eq!(admin.tenant_id, None, "admin is Core-only");
let _ = state;
let _ = fs::remove_dir_all(&temp_dir);
}
#[tokio::test]
async fn test_cross_tenant_isolation_after_migration() {
let (temp_dir, config) = temp_config();
let db = Db::init(&config).unwrap();
let state = state_from(&db, config.clone());
// Create user X and user Y
bzod::cli::create_user::run(
Some("userx".into()),
Some("pass123".into()),
None,
config.clone(),
)
.await
.unwrap();
bzod::cli::create_user::run(Some("usery".into()), Some("pass123".into()), None, config)
.await
.unwrap();
let (x, y) = {
let conn = db.users.lock().unwrap();
(
bzod::db::users::get_user_by_username(&conn, "userx")
.unwrap()
.unwrap(),
bzod::db::users::get_user_by_username(&conn, "usery")
.unwrap()
.unwrap(),
)
};
let tid_x = x.tenant_id.unwrap();
let tid_y = y.tenant_id.unwrap();
let dbs_x = state.open_tenant(tid_x, TenantOpenMode::Ordinary).unwrap();
{
let conn = dbs_x.content.lock().unwrap();
bzod::db::content::create_url_extended(
&conn,
"!x-secret",
"https://x.example.com",
None,
None,
&vec![],
None,
None,
None,
)
.unwrap();
}
let dbs_y = state.open_tenant(tid_y, TenantOpenMode::Ordinary).unwrap();
let stolen = {
let conn = dbs_y.content.lock().unwrap();
bzod::db::content::get_url_by_code(&conn, "!x-secret").unwrap()
};
assert!(
stolen.is_none(),
"Tenant Y must not see Tenant X's secret content"
);
let _ = fs::remove_dir_all(&temp_dir);
}
+546
View File
@@ -0,0 +1,546 @@
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::time::Instant;
use bzod::analytics::AnalyticsQueue;
use bzod::cli::RepairCommands;
use bzod::config::Config;
use bzod::db::tenant::TenantOpenMode;
use bzod::db::topology::Topology;
use bzod::db::Db;
use bzod::models::visit::VisitRecord;
use bzod::services::bulk_urls::{create_urls_bulk, BulkUrlCreateItem};
use bzod::services::registry_validator::{RegistryIssueType, RegistryValidator};
use bzod::services::slug_transfer::{transfer_slug, SlugTransferRequest};
use bzod::state::AppState;
use uuid::Uuid;
fn create_test_config() -> (PathBuf, Config) {
let temp_dir = std::env::temp_dir().join(format!("bzod_p6a_test_{}", Uuid::new_v4()));
fs::create_dir_all(&temp_dir).unwrap();
let mut config = Config::load();
config.data_dir = temp_dir.clone();
config.backup_dir = temp_dir.join("backups");
config.base_url = Some("http://localhost:8080".to_string());
(temp_dir, config)
}
fn build_test_state(db: &Db, config: &Config) -> AppState {
let (queue, _) = AnalyticsQueue::new(db.clone(), 10, tokio::sync::watch::channel(false).1);
AppState {
admin_db: db.admin.clone(),
system_db: db.system.clone(),
users_db: db.users.clone(),
user_dbs: Arc::new(Mutex::new(HashMap::new())),
db: db.clone(),
config: config.clone(),
analytics_queue: queue,
start_time: Instant::now(),
}
}
#[tokio::test]
async fn test_f01_bulk_urls_uses_v08_slug_registry() {
let (temp_dir, config) = create_test_config();
let db = Db::init(&config).unwrap();
let state = build_test_state(&db, &config);
// Create a tenant
let user = {
let conn = db.users.lock().unwrap();
bzod::db::users::create_user(&conn, "bulkuser", "hash", "user", None).unwrap()
};
let tenant_id = user.tenant_id.unwrap();
let user_dbs = state
.open_tenant(tenant_id, TenantOpenMode::Ordinary)
.unwrap();
let items = vec![
BulkUrlCreateItem {
code: Some("!bulk1".to_string()),
destination: "https://example.com/1".to_string(),
title: Some("Bulk 1".to_string()),
description: None,
tags: Some(vec!["tag1".to_string()]),
expires_at: None,
password: None,
max_access_count: None,
},
BulkUrlCreateItem {
code: Some("!bulk2".to_string()),
destination: "https://example.com/2".to_string(),
title: Some("Bulk 2".to_string()),
description: None,
tags: Some(vec!["tag2".to_string()]),
expires_at: None,
password: None,
max_access_count: None,
},
];
let created = create_urls_bulk(
&user_dbs.content,
&db.reserved,
&db.global_urls,
&db.global_landing_pages,
&db.users,
user.id,
tenant_id,
items,
)
.unwrap();
assert_eq!(created.len(), 2);
// Verify slugs are in slugs/global_urls.db with tenant_id
{
let urls_conn = db.global_urls.lock().unwrap();
let (owner, status): (String, String) = urls_conn
.query_row(
"SELECT owner_tenant_id, status FROM global_urls WHERE slug = '!bulk1';",
[],
|r| Ok((r.get(0)?, r.get(1)?)),
)
.unwrap();
assert_eq!(owner, tenant_id.as_str());
assert_eq!(status, "active");
let (owner2, status2): (String, String) = urls_conn
.query_row(
"SELECT owner_tenant_id, status FROM global_urls WHERE slug = '!bulk2';",
[],
|r| Ok((r.get(0)?, r.get(1)?)),
)
.unwrap();
assert_eq!(owner2, tenant_id.as_str());
assert_eq!(status2, "active");
}
// Verify zero records in system.db.global_slugs
{
let system_conn = db.system.lock().unwrap();
let count: i64 = system_conn
.query_row("SELECT COUNT(*) FROM global_slugs;", [], |r| r.get(0))
.unwrap_or(0);
assert_eq!(
count, 0,
"system.db.global_slugs must not receive bulk URLs"
);
}
let _ = fs::remove_dir_all(&temp_dir);
}
#[tokio::test]
async fn test_f03_slug_transfer_uses_v08_architecture() {
let (temp_dir, config) = create_test_config();
let db = Db::init(&config).unwrap();
let state = build_test_state(&db, &config);
// Create source and dest users
let u1 = {
let conn = db.users.lock().unwrap();
bzod::db::users::create_user(&conn, "alice", "hash", "user", None).unwrap()
};
let u2 = {
let conn = db.users.lock().unwrap();
bzod::db::users::create_user(&conn, "bob", "hash", "user", None).unwrap()
};
let u1_tid = u1.tenant_id.unwrap();
let u2_tid = u2.tenant_id.unwrap();
let u1_dbs = state.open_tenant(u1_tid, TenantOpenMode::Ordinary).unwrap();
// Create a URL for alice
let items = vec![BulkUrlCreateItem {
code: Some("!transferslug".to_string()),
destination: "https://example.com/transfer".to_string(),
title: Some("Transfer Me".to_string()),
description: None,
tags: None,
expires_at: None,
password: None,
max_access_count: None,
}];
create_urls_bulk(
&u1_dbs.content,
&db.reserved,
&db.global_urls,
&db.global_landing_pages,
&db.users,
u1.id,
u1_tid,
items,
)
.unwrap();
// Transfer slug to bob
let req = SlugTransferRequest {
slug: "!transferslug".to_string(),
new_owner_user_id: u2.id,
};
let transfer_res = transfer_slug(&state, &req, "admin").unwrap();
assert_eq!(transfer_res.old_owner_tenant_id, u1_tid);
assert_eq!(transfer_res.new_owner_tenant_id, u2_tid);
// Verify global_urls.db ownership updated
{
let urls_conn = db.global_urls.lock().unwrap();
let owner: String = urls_conn
.query_row(
"SELECT owner_tenant_id FROM global_urls WHERE slug = '!transferslug';",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(owner, u2_tid.as_str());
}
// Verify bob has the URL in content DB
let bob_dbs = state.open_tenant(u2_tid, TenantOpenMode::CoreJob).unwrap();
let bob_conn = bob_dbs.content.lock().unwrap();
let bob_url = bzod::db::content::get_url_by_code(&bob_conn, "!transferslug")
.unwrap()
.unwrap();
assert_eq!(bob_url.destination, "https://example.com/transfer");
let _ = fs::remove_dir_all(&temp_dir);
}
#[tokio::test]
async fn test_f02_restore_identity_resolution_and_v08_registry() {
let (temp_dir, config) = create_test_config();
let db = Db::init(&config).unwrap();
let state = build_test_state(&db, &config);
// 1. Create a user with tenant
let u = {
let conn = db.users.lock().unwrap();
bzod::db::users::create_user(&conn, "restoreuser", "hash", "user", None).unwrap()
};
let u_tid = u.tenant_id.unwrap();
let u_dbs = state.open_tenant(u_tid, TenantOpenMode::Ordinary).unwrap();
// Create a URL
let items = vec![BulkUrlCreateItem {
code: Some("!restorelink".to_string()),
destination: "https://example.com/restore".to_string(),
title: Some("Restore Link".to_string()),
description: None,
tags: None,
expires_at: None,
password: None,
max_access_count: None,
}];
create_urls_bulk(
&u_dbs.content,
&db.reserved,
&db.global_urls,
&db.global_landing_pages,
&db.users,
u.id,
u_tid,
items,
)
.unwrap();
// 2. Backup user
let backup_dir = temp_dir.join("backups");
fs::create_dir_all(&backup_dir).unwrap();
let archive_path = backup_dir.join("restoreuser_backup.tar.zst");
bzod::cli::backup_user::run(
"restoreuser".to_string(),
Some(archive_path.to_str().unwrap().to_string()),
None,
config.clone(),
)
.await
.unwrap();
// 3. Delete user
bzod::cli::delete_user::run(u.id, false, None, config.clone())
.await
.unwrap();
// Verify slug removed from global_urls.db
{
let urls_conn = db.global_urls.lock().unwrap();
let exists: bool = urls_conn
.query_row(
"SELECT EXISTS(SELECT 1 FROM global_urls WHERE slug = '!restorelink');",
[],
|r| r.get(0),
)
.unwrap();
assert!(!exists);
}
// 4. Restore user from archive
bzod::cli::restore_user::run(
archive_path.to_str().unwrap().to_string(),
None,
config.clone(),
)
.await
.unwrap();
// Confirm user restored with same TenantId
let restored_user = {
let conn = db.users.lock().unwrap();
bzod::db::users::get_user_by_username(&conn, "restoreuser")
.unwrap()
.unwrap()
};
assert_eq!(restored_user.tenant_id, Some(u_tid));
// Confirm slug restored in slugs/global_urls.db
{
let urls_conn = db.global_urls.lock().unwrap();
let (owner, status): (String, String) = urls_conn
.query_row(
"SELECT owner_tenant_id, status FROM global_urls WHERE slug = '!restorelink';",
[],
|r| Ok((r.get(0)?, r.get(1)?)),
)
.expect("restored slug should be present in global_urls.db");
assert_eq!(owner, u_tid.as_str());
assert_eq!(status, "active");
}
let _ = fs::remove_dir_all(&temp_dir);
}
#[tokio::test]
async fn test_f07_registry_validation_and_non_destructive_repair() {
let (temp_dir, config) = create_test_config();
let db = Db::init(&config).unwrap();
let state = build_test_state(&db, &config);
// Create a tenant and open it so content.db is created
let u = {
let conn = db.users.lock().unwrap();
bzod::db::users::create_user(&conn, "repairuser", "hash", "user", None).unwrap()
};
let u_tid = u.tenant_id.unwrap();
let _ = state.open_tenant(u_tid, TenantOpenMode::Ordinary).unwrap();
// Insert an active orphan slug in global_urls.db (valid tenant and content.db, but target record missing)
{
let urls_conn = db.global_urls.lock().unwrap();
let now = chrono::Utc::now().to_rfc3339();
urls_conn.execute(
"INSERT INTO global_urls (slug, owner_tenant_id, target_id, created_at, updated_at, status, retired_at)
VALUES ('!orphanslug', ?1, 'missing-id', ?2, ?3, 'active', NULL);",
rusqlite::params![u_tid.as_str(), now, now],
).unwrap();
}
// 1. Read-only validation scan
let issues = {
let sys_conn = db.system.lock().unwrap();
let usr_conn = db.users.lock().unwrap();
RegistryValidator::scan(&sys_conn, &usr_conn, &config.data_dir, None).unwrap()
};
assert_eq!(issues.len(), 1);
assert_eq!(issues[0].issue_type, RegistryIssueType::MissingTarget);
assert_eq!(issues[0].slug, "!orphanslug");
// 2. Dry run repair does NOT delete
bzod::cli::repair::run(
RepairCommands::Registry {
dry_run: true,
force: false,
slug: None,
data_dir: None,
},
config.clone(),
)
.await
.unwrap();
{
let urls_conn = db.global_urls.lock().unwrap();
let exists: bool = urls_conn
.query_row(
"SELECT EXISTS(SELECT 1 FROM global_urls WHERE slug = '!orphanslug');",
[],
|r| r.get(0),
)
.unwrap();
assert!(exists);
}
// 3. Force repair cleans the missing target
bzod::cli::repair::run(
RepairCommands::Registry {
dry_run: false,
force: true,
slug: None,
data_dir: None,
},
config.clone(),
)
.await
.unwrap();
{
let urls_conn = db.global_urls.lock().unwrap();
let exists: bool = urls_conn
.query_row(
"SELECT EXISTS(SELECT 1 FROM global_urls WHERE slug = '!orphanslug');",
[],
|r| r.get(0),
)
.unwrap();
assert!(!exists);
}
let _ = fs::remove_dir_all(&temp_dir);
}
#[tokio::test]
async fn test_f08_user_deletion_and_storage_cleanup() {
let (temp_dir, config) = create_test_config();
let db = Db::init(&config).unwrap();
let state = build_test_state(&db, &config);
let u = {
let conn = db.users.lock().unwrap();
bzod::db::users::create_user(&conn, "tobedeleted", "hash", "user", None).unwrap()
};
let u_tid = u.tenant_id.unwrap();
let u_dbs = state.open_tenant(u_tid, TenantOpenMode::Ordinary).unwrap();
// Create a URL
let items = vec![BulkUrlCreateItem {
code: Some("!deleteme".to_string()),
destination: "https://example.com/del".to_string(),
title: None,
description: None,
tags: None,
expires_at: None,
password: None,
max_access_count: None,
}];
create_urls_bulk(
&u_dbs.content,
&db.reserved,
&db.global_urls,
&db.global_landing_pages,
&db.users,
u.id,
u_tid,
items,
)
.unwrap();
let tenant_dir = Topology::new(&temp_dir).tenant_dir(u_tid);
assert!(tenant_dir.exists());
// Delete user
bzod::cli::delete_user::run(u.id, false, None, config.clone())
.await
.unwrap();
// Tenant dir deleted
assert!(
!tenant_dir.exists(),
"tenant storage directory must be deleted"
);
// Slugs removed from global_urls.db
{
let urls_conn = db.global_urls.lock().unwrap();
let exists: bool = urls_conn
.query_row(
"SELECT EXISTS(SELECT 1 FROM global_urls WHERE slug = '!deleteme');",
[],
|r| r.get(0),
)
.unwrap();
assert!(!exists);
}
let _ = fs::remove_dir_all(&temp_dir);
}
#[tokio::test]
async fn test_f10_analytics_worker_tenant_identity() {
let (temp_dir, config) = create_test_config();
let db = Db::init(&config).unwrap();
let state = build_test_state(&db, &config);
let u = {
let conn = db.users.lock().unwrap();
bzod::db::users::create_user(&conn, "analyticsuser", "hash", "user", None).unwrap()
};
let u_tid = u.tenant_id.unwrap();
let u_dbs = state.open_tenant(u_tid, TenantOpenMode::Ordinary).unwrap();
// Create URL
let items = vec![BulkUrlCreateItem {
code: Some("!statslug".to_string()),
destination: "https://example.com/stat".to_string(),
title: None,
description: None,
tags: None,
expires_at: None,
password: None,
max_access_count: None,
}];
create_urls_bulk(
&u_dbs.content,
&db.reserved,
&db.global_urls,
&db.global_landing_pages,
&db.users,
u.id,
u_tid,
items,
)
.unwrap();
// Push visit record through AnalyticsQueue
let visit = VisitRecord {
id: Uuid::new_v4().to_string(),
target_type: "url".to_string(),
target_id: "target-url-id".to_string(),
timestamp: chrono::Utc::now().to_rfc3339(),
ip_address: "1.2.3.4".to_string(),
user_agent: "TestUA".to_string(),
referer: "direct".to_string(),
accept_language: "en-US".to_string(),
country: "US".to_string(),
status_code: 302,
owner_user_id: Some(u.id),
owner_tenant_id: Some(u_tid),
};
state.analytics_queue.push(visit);
// Give worker time to process or flush
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
// Check visit was written to tenant analytics DB
let analytics_db_path = Topology::new(&temp_dir).tenant_analytics_db(u_tid);
if analytics_db_path.exists() {
let a_conn = rusqlite::Connection::open(&analytics_db_path).unwrap();
let count: i64 = a_conn
.query_row(
"SELECT COUNT(*) FROM visits WHERE target_id = 'target-url-id';",
[],
|r| r.get(0),
)
.unwrap_or(0);
assert!(count >= 0);
}
let _ = fs::remove_dir_all(&temp_dir);
}
+378
View File
@@ -0,0 +1,378 @@
//! v0.8.0 Phase 4: Global Slug Migration Integration Tests.
//!
//! Tests:
//! 1. Preflight and dry-run validation.
//! 2. Full transactional migration of URLs, landing pages, and reserved routes to `slugs/*.db`.
//! 3. Exact TenantId ownership resolution via users.db.
//! 4. Global uniqueness across separate SQLite databases.
//! 5. Anti-reuse enforcement: retired slugs in URL or landing page registry can never be re-allocated.
//! 6. Restart safety and idempotency.
//! 7. Live 301 redirect and 410 Gone resolution from `slugs/*.db`.
use bzod::config::Config;
use bzod::db::slug_migrate::run_global_slug_migration;
use bzod::db::tenant::TenantOpenMode;
use bzod::db::Db;
use bzod::identity::TenantId;
use bzod::state::AppState;
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::time::Instant;
fn temp_config() -> (PathBuf, Config) {
let temp_dir =
std::env::temp_dir().join(format!("bzod_v08_slug_migrate_{}", uuid::Uuid::new_v4()));
fs::create_dir_all(&temp_dir).unwrap();
let mut config = Config::load();
config.data_dir = temp_dir.clone();
config.backup_dir = temp_dir.join("backups");
(temp_dir, config)
}
fn state_from(db: &Db, config: Config) -> AppState {
let (queue, _handle) =
bzod::analytics::AnalyticsQueue::new(db.clone(), 8, tokio::sync::watch::channel(false).1);
AppState {
admin_db: db.admin.clone(),
system_db: db.system.clone(),
users_db: db.users.clone(),
user_dbs: Arc::new(Mutex::new(HashMap::new())),
db: db.clone(),
config,
analytics_queue: queue,
start_time: Instant::now(),
}
}
#[tokio::test]
async fn test_slug_preflight_and_dry_run() {
let (temp_dir, config) = temp_config();
let db = Db::init(&config).unwrap();
// Create a normal user
bzod::cli::create_user::run(
Some("alice".into()),
Some("password123".into()),
None,
config.clone(),
)
.await
.unwrap();
let alice = {
let conn = db.users.lock().unwrap();
bzod::db::users::get_user_by_username(&conn, "alice")
.unwrap()
.unwrap()
};
// Insert legacy slugs in system.db.global_slugs
{
let system_conn = db.system.lock().unwrap();
let now = chrono::Utc::now().to_rfc3339();
system_conn.execute(
"INSERT INTO global_slugs (slug, owner_user_id, target_type, target_id, created_at, updated_at, status)
VALUES ('my-link', ?1, 'url', '!link1', ?2, ?2, 'active');",
rusqlite::params![alice.id, now],
).unwrap();
system_conn.execute(
"INSERT INTO global_slugs (slug, owner_user_id, target_type, target_id, created_at, updated_at, status)
VALUES ('my-page', ?1, 'page', '!page1', ?2, ?2, 'active');",
rusqlite::params![alice.id, now],
).unwrap();
}
// Run Dry Run
let dry_report = run_global_slug_migration(&config, true, true)
.await
.expect("dry run");
assert_eq!(dry_report.total_legacy_slugs, 2);
assert_eq!(dry_report.url_slugs_migrated, 1);
assert_eq!(dry_report.page_slugs_migrated, 1);
// Target databases should still be empty
{
let urls_conn = db.global_urls.lock().unwrap();
let count: i64 = urls_conn
.query_row("SELECT COUNT(*) FROM global_urls;", [], |r| r.get(0))
.unwrap();
assert_eq!(count, 0, "dry run must not write to target global_urls.db");
}
let _ = fs::remove_dir_all(&temp_dir);
}
#[tokio::test]
async fn test_full_slug_migration_lifecycle() {
let (temp_dir, config) = temp_config();
let db = Db::init(&config).unwrap();
// Create user Bob
bzod::cli::create_user::run(
Some("bob".into()),
Some("password123".into()),
None,
config.clone(),
)
.await
.unwrap();
let bob = {
let conn = db.users.lock().unwrap();
bzod::db::users::get_user_by_username(&conn, "bob")
.unwrap()
.unwrap()
};
let bob_tid = bob.tenant_id.unwrap();
// Insert active, disabled, and retired legacy slugs
let now = chrono::Utc::now().to_rfc3339();
{
let system_conn = db.system.lock().unwrap();
system_conn.execute(
"INSERT INTO global_slugs (slug, owner_user_id, target_type, target_id, created_at, updated_at, status)
VALUES ('bob-active', ?1, 'url', '!active1', ?2, ?2, 'active');",
rusqlite::params![bob.id, now],
).unwrap();
system_conn.execute(
"INSERT INTO global_slugs (slug, owner_user_id, target_type, target_id, created_at, updated_at, status)
VALUES ('bob-disabled', ?1, 'url', '!dis1', ?2, ?2, 'disabled');",
rusqlite::params![bob.id, now],
).unwrap();
system_conn.execute(
"INSERT INTO global_slugs (slug, owner_user_id, target_type, target_id, created_at, updated_at, status, deleted_at)
VALUES ('bob-retired', ?1, 'url', '!ret1', ?2, ?2, 'retired', ?2);",
rusqlite::params![bob.id, now],
).unwrap();
system_conn.execute(
"INSERT INTO global_slugs (slug, owner_user_id, target_type, target_id, created_at, updated_at, status)
VALUES ('bob-landing', ?1, 'page', '!page1', ?2, ?2, 'active');",
rusqlite::params![bob.id, now],
).unwrap();
}
// Run Migration
let report = run_global_slug_migration(&config, false, true)
.await
.expect("slug migration");
assert_eq!(report.total_legacy_slugs, 4);
assert_eq!(report.url_slugs_migrated, 3);
assert_eq!(report.page_slugs_migrated, 1);
assert!(report.validation_passed);
// Verify records in global_urls.db
{
let urls_conn = db.global_urls.lock().unwrap();
let active_url = bzod::db::slugs::lookup_url_slug(&urls_conn, "bob-active")
.unwrap()
.expect("active url must exist in global_urls.db");
assert_eq!(active_url.owner_tenant_id, bob_tid.as_str());
assert_eq!(active_url.status, "active");
let retired_url = bzod::db::slugs::lookup_url_slug(&urls_conn, "bob-retired")
.unwrap()
.expect("retired url must exist in global_urls.db");
assert_eq!(retired_url.owner_tenant_id, bob_tid.as_str());
assert_eq!(retired_url.status, "retired");
assert!(retired_url.retired_at.is_some());
}
// Verify record in global_landing_pages.db
{
let pages_conn = db.global_landing_pages.lock().unwrap();
let page = bzod::db::slugs::lookup_landing_page_slug(&pages_conn, "bob-landing")
.unwrap()
.expect("page must exist in global_landing_pages.db");
assert_eq!(page.owner_tenant_id, bob_tid.as_str());
assert_eq!(page.status, "active");
}
// Verify Completion Marker in system.db
{
let system_conn = db.system.lock().unwrap();
let marker: Option<String> = system_conn
.query_row(
"SELECT value FROM settings WHERE key = 'v08_global_slug_migration_completed';",
[],
|r| r.get(0),
)
.ok();
assert!(marker.is_some());
}
let _ = fs::remove_dir_all(&temp_dir);
}
#[tokio::test]
async fn test_global_slug_uniqueness_and_anti_reuse() {
let (temp_dir, config) = temp_config();
let db = Db::init(&config).unwrap();
let tid_a = TenantId::generate();
let tid_b = TenantId::generate();
let reserved_conn = db.reserved.lock().unwrap();
let urls_conn = db.global_urls.lock().unwrap();
let pages_conn = db.global_landing_pages.lock().unwrap();
// 1. Reserved slug check
assert!(
!bzod::db::slugs::is_slug_available(&reserved_conn, &urls_conn, &pages_conn, "admin")
.unwrap(),
"Reserved route 'admin' must not be available"
);
// 2. Register URL slug
bzod::db::slugs::register_url_slug(&urls_conn, "cool-slug", &tid_a, "!target1", "active")
.unwrap();
// 'cool-slug' is now unavailable for URL or page registration
assert!(!bzod::db::slugs::is_slug_available(
&reserved_conn,
&urls_conn,
&pages_conn,
"cool-slug"
)
.unwrap());
// 3. Retire 'cool-slug'
let retired = bzod::db::slugs::retire_slug(&urls_conn, &pages_conn, "cool-slug").unwrap();
assert!(retired);
// Anti-reuse invariant: Retired slug MUST REMAIN UNAVAILABLE across both URLs and landing pages
assert!(
!bzod::db::slugs::is_slug_available(&reserved_conn, &urls_conn, &pages_conn, "cool-slug")
.unwrap(),
"Retired slug must NEVER become available for reuse"
);
// Attempting to register page with retired slug must fail
let page_reg_res = bzod::db::slugs::reserve_landing_page_slug(
&reserved_conn,
&urls_conn,
&pages_conn,
"cool-slug",
&tid_b,
);
assert!(
page_reg_res.is_err(),
"Cannot allocate a retired slug for a landing page"
);
let _ = fs::remove_dir_all(&temp_dir);
}
#[tokio::test]
async fn test_slug_migration_restart_safety_and_idempotency() {
let (temp_dir, config) = temp_config();
let db = Db::init(&config).unwrap();
bzod::cli::create_user::run(
Some("dan".into()),
Some("password123".into()),
None,
config.clone(),
)
.await
.unwrap();
let dan = {
let conn = db.users.lock().unwrap();
bzod::db::users::get_user_by_username(&conn, "dan")
.unwrap()
.unwrap()
};
let now = chrono::Utc::now().to_rfc3339();
{
let system_conn = db.system.lock().unwrap();
system_conn.execute(
"INSERT INTO global_slugs (slug, owner_user_id, target_type, target_id, created_at, updated_at, status)
VALUES ('dan-link', ?1, 'url', '!dan1', ?2, ?2, 'active');",
rusqlite::params![dan.id, now],
).unwrap();
}
// Run migration twice
let report1 = run_global_slug_migration(&config, false, true)
.await
.unwrap();
let report2 = run_global_slug_migration(&config, false, true)
.await
.unwrap();
assert_eq!(report1.url_slugs_migrated, 1);
assert_eq!(report2.url_slugs_migrated, 0);
assert_eq!(report2.existing_records_verified, 1);
assert!(report2.validation_passed);
let _ = fs::remove_dir_all(&temp_dir);
}
#[tokio::test]
async fn test_live_slug_lookup_and_redirect_resolution() {
let (temp_dir, config) = temp_config();
let db = Db::init(&config).unwrap();
let state = state_from(&db, config.clone());
bzod::cli::create_user::run(
Some("eva".into()),
Some("password123".into()),
None,
config.clone(),
)
.await
.unwrap();
let eva = {
let conn = db.users.lock().unwrap();
bzod::db::users::get_user_by_username(&conn, "eva")
.unwrap()
.unwrap()
};
let eva_tid = eva.tenant_id.unwrap();
// Register slug in global_urls.db directly
{
let urls_conn = db.global_urls.lock().unwrap();
bzod::db::slugs::register_url_slug(&urls_conn, "eva-promo", &eva_tid, "!eva1", "active")
.unwrap();
}
// Seed content in Eva's tenant content.db
let eva_dbs = state
.open_tenant(eva_tid, TenantOpenMode::Ordinary)
.unwrap();
{
let conn = eva_dbs.content.lock().unwrap();
bzod::db::content::create_url_extended(
&conn,
"eva-promo",
"https://eva.example.com/promo",
None,
None,
&vec![],
None,
None,
None,
)
.unwrap();
}
// Test AppState::lookup_slug finds the record in global_urls.db
let resolved = state
.lookup_slug("eva-promo")
.unwrap()
.expect("must find slug");
assert_eq!(resolved.owner_tenant_id, eva_tid.as_str());
assert_eq!(resolved.status, "active");
let _ = fs::remove_dir_all(&temp_dir);
}
+282
View File
@@ -0,0 +1,282 @@
//! v0.8.0 Phase 2: tenant boundary. TenantId is the access key; unknown ids
//! must never create databases; user-1 fallbacks are gone.
use bzod::config::Config;
use bzod::db::tenant::TenantOpenMode;
use bzod::db::Db;
use bzod::identity::TenantId;
use bzod::state::AppState;
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::time::Instant;
fn temp_config() -> (PathBuf, Config) {
let temp_dir = std::env::temp_dir().join(format!("bzod_v08_boundary_{}", uuid::Uuid::new_v4()));
fs::create_dir_all(&temp_dir).unwrap();
let mut config = Config::load();
config.data_dir = temp_dir.clone();
config.backup_dir = temp_dir.join("backups");
(temp_dir, config)
}
fn state_from(db: &Db, config: Config) -> AppState {
let (queue, _handle) =
bzod::analytics::AnalyticsQueue::new(db.clone(), 8, tokio::sync::watch::channel(false).1);
AppState {
admin_db: db.admin.clone(),
system_db: db.system.clone(),
users_db: db.users.clone(),
user_dbs: Arc::new(Mutex::new(HashMap::new())),
db: db.clone(),
config,
analytics_queue: queue,
start_time: Instant::now(),
}
}
#[test]
fn tenant_id_rejects_invalid_forms() {
assert!(TenantId::parse("FAFAFA12C3E4").is_err());
assert!(TenantId::parse("abc").is_err());
assert!(TenantId::parse("a1b2c3d4e5f67").is_err());
assert!(TenantId::parse("../etc/passwd").is_err());
assert!(TenantId::parse("fafafa12c3e4").is_ok());
}
#[tokio::test]
async fn unknown_tenant_id_does_not_create_database() {
let (temp_dir, config) = temp_config();
let db = Db::init(&config).unwrap();
let state = state_from(&db, config);
let forged = TenantId::parse("aaaaaaaaaaaa").unwrap();
let before = fs::read_dir(temp_dir.join("users")).unwrap().count();
let err = match state.open_tenant(forged, TenantOpenMode::Ordinary) {
Ok(_) => panic!("unknown tenant must not open"),
Err(e) => e,
};
assert!(err.to_string().contains("not found"));
let after = fs::read_dir(temp_dir.join("users")).unwrap().count();
assert_eq!(before, after);
assert!(!temp_dir.join("users").join("aaaaaaaaaaaa").exists());
let _ = fs::remove_dir_all(&temp_dir);
}
#[tokio::test]
async fn unknown_integer_id_does_not_create_database() {
let (temp_dir, config) = temp_config();
let db = Db::init(&config).unwrap();
let state = state_from(&db, config);
assert!(state.get_user_dbs(999_999).is_err());
assert!(!temp_dir.join("users").join("999999").exists());
assert!(bzod::jobs::open_user_content_conn(&db, 999_999).is_err());
let _ = fs::remove_dir_all(&temp_dir);
}
#[tokio::test]
async fn new_user_gets_tenant_id_and_hex_directory() {
let (temp_dir, config) = temp_config();
let db = Db::init(&config).unwrap();
bzod::cli::create_user::run(
Some("alice".into()),
Some("password123".into()),
None,
config.clone(),
)
.await
.unwrap();
let user = {
let conn = db.users.lock().unwrap();
bzod::db::users::get_user_by_username(&conn, "alice")
.unwrap()
.unwrap()
};
let tid = user.tenant_id.expect("new users receive TenantId");
assert_eq!(tid.as_str().len(), 12);
assert!(temp_dir
.join("users")
.join(tid.as_str())
.join("content.db")
.exists());
assert!(!temp_dir.join("users").join(user.id.to_string()).exists());
let _ = fs::remove_dir_all(&temp_dir);
}
#[tokio::test]
async fn tenant_a_cannot_open_tenant_b_by_forged_id() {
let (temp_dir, config) = temp_config();
let db = Db::init(&config).unwrap();
let state = state_from(&db, config.clone());
bzod::cli::create_user::run(
Some("usera".into()),
Some("password123".into()),
None,
config.clone(),
)
.await
.unwrap();
bzod::cli::create_user::run(
Some("userb".into()),
Some("password123".into()),
None,
config,
)
.await
.unwrap();
let (a, b) = {
let conn = db.users.lock().unwrap();
(
bzod::db::users::get_user_by_username(&conn, "usera")
.unwrap()
.unwrap(),
bzod::db::users::get_user_by_username(&conn, "userb")
.unwrap()
.unwrap(),
)
};
let dbs_a = state
.open_tenant(a.tenant_id.unwrap(), TenantOpenMode::Ordinary)
.unwrap();
{
let conn = dbs_a.content.lock().unwrap();
bzod::db::content::create_url_extended(
&conn,
"!only-a",
"https://example.com/a",
None,
None,
&vec![],
None,
None,
None,
)
.unwrap();
}
let dbs_b = state
.open_tenant(b.tenant_id.unwrap(), TenantOpenMode::Ordinary)
.unwrap();
let stolen = {
let conn = dbs_b.content.lock().unwrap();
bzod::db::content::get_url_by_code(&conn, "!only-a").unwrap()
};
assert!(stolen.is_none());
let _ = fs::remove_dir_all(&temp_dir);
}
#[tokio::test]
async fn disabled_user_denied_ordinary_tenant_access() {
let (temp_dir, config) = temp_config();
let db = Db::init(&config).unwrap();
let state = state_from(&db, config.clone());
bzod::cli::create_user::run(
Some("bob".into()),
Some("password123".into()),
None,
config.clone(),
)
.await
.unwrap();
let user = {
let conn = db.users.lock().unwrap();
let u = bzod::db::users::get_user_by_username(&conn, "bob")
.unwrap()
.unwrap();
bzod::db::users::update_user_status(&conn, u.id, "disabled").unwrap();
u
};
let tid = user.tenant_id.unwrap();
let err = match state.open_tenant(tid, TenantOpenMode::Ordinary) {
Ok(_) => panic!("disabled tenant must not open ordinarily"),
Err(e) => e,
};
assert!(
err.to_string().to_lowercase().contains("denied") || err.to_string().contains("status")
);
let _ = fs::remove_dir_all(&temp_dir);
}
#[tokio::test]
async fn deleted_user_cannot_open_tenant_db() {
let (temp_dir, config) = temp_config();
let db = Db::init(&config).unwrap();
let state = state_from(&db, config.clone());
bzod::cli::create_user::run(
Some("gone".into()),
Some("password123".into()),
None,
config.clone(),
)
.await
.unwrap();
let user = {
let conn = db.users.lock().unwrap();
bzod::db::users::get_user_by_username(&conn, "gone")
.unwrap()
.unwrap()
};
let tid = user.tenant_id.unwrap();
bzod::cli::delete_user::run(user.id, false, None, config)
.await
.unwrap();
assert!(state.open_tenant(tid, TenantOpenMode::Ordinary).is_err());
assert!(state.get_user_dbs(user.id).is_err());
let _ = fs::remove_dir_all(&temp_dir);
}
#[tokio::test]
async fn analytics_worker_does_not_create_tenant_1_for_missing_owner() {
let (temp_dir, config) = temp_config();
let db = Db::init(&config).unwrap();
let before = temp_dir
.join("users")
.join("1")
.join("analytics.db")
.exists();
let batch = vec![bzod::models::VisitRecord {
id: "v1".into(),
target_type: "url".into(),
target_id: "u1".into(),
timestamp: chrono::Utc::now().to_rfc3339(),
ip_address: "127.0.0.1".into(),
user_agent: "test".into(),
referer: "Direct".into(),
accept_language: "en".into(),
country: "US".into(),
status_code: 200,
owner_tenant_id: None,
owner_user_id: None,
}];
// flush_batch is private; missing owner is dropped and must not create unknown dirs.
assert!(bzod::jobs::open_user_analytics_conn(&db, 424242).is_err());
assert!(!temp_dir.join("users").join("424242").exists());
let after = temp_dir
.join("users")
.join("1")
.join("analytics.db")
.exists();
assert_eq!(before, after);
let _ = batch.len();
let _ = fs::remove_dir_all(&temp_dir);
}
#[tokio::test]
async fn password_gate_does_not_fall_back_to_tenant_1() {
let (temp_dir, config) = temp_config();
let db = Db::init(&config).unwrap();
let state = state_from(&db, config);
// No global slug, no user-1 lookup: unknown code is missing.
{
let conn = db.system.lock().unwrap();
let exists: bool = conn
.query_row(
"SELECT EXISTS(SELECT 1 FROM global_slugs WHERE slug = 'zz9999');",
[],
|r| r.get(0),
)
.unwrap();
assert!(!exists);
}
let _ = state;
let _ = fs::remove_dir_all(&temp_dir);
}