refactor: complete v0.8 core architecture
This commit is contained in:
1 parent
f138a645f8
commit
d7e0ac7679
98 files changed
+8413
-3044
No files matched your search
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user