//! v0.8.0 Phase 5 Correction: Admin Core-Only Capability Boundary Tests //! //! Required Test Cases: //! 1. Admin cannot create URL through POST /admin/urls/create (403 Forbidden). //! 2. Admin cannot create landing page through POST /admin/pages/create (403 Forbidden). //! 3. Admin cannot create URL through API (403 Forbidden). //! 4. Admin cannot create landing page through API (403 Forbidden). //! 5. Admin cannot bulk-create URLs through API (403 Forbidden). //! 6. Admin GET /admin/urls renders inspection-only registry (no creation form). //! 7. Admin GET /admin/pages renders inspection-only registry (no creation form). //! 8. Admin resource inspection still works on global URL registry. //! 9. Admin resource inspection still works on global page registry. //! 10. Admin moderation still works. //! 11. Admin transfer still works. //! 12. Normal tenant can still create URLs (via UI and API). //! 13. Normal tenant can still create landing pages (via UI and API). //! 14. Normal tenant creation uses its own TenantId. //! 15. No test or creation path uses users/1 as an application tenant. 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::state::AppState; use bzod::web::create_router; use sha2::{Digest, Sha256}; #[allow(dead_code)] struct TestHarness { temp_dir: PathBuf, config: Config, db: Db, base_url: String, admin_client: reqwest::Client, user_client: reqwest::Client, admin_api_key: String, bob_user_id: i64, bob_tenant_id: bzod::identity::TenantId, bob_token_secret: String, } impl TestHarness { async fn setup() -> Self { let temp_dir = std::env::temp_dir().join(format!("bzod_admin_boundary_{}", uuid::Uuid::new_v4())); fs::create_dir_all(&temp_dir).unwrap(); let mut config = Config::load_with_cli(Some(&temp_dir)).unwrap(); config.backup_dir = temp_dir.join("backups"); config.base_url = Some("http://localhost:8080".to_string()); let db = Db::init(&config).expect("Db::init failed"); let (queue, _) = AnalyticsQueue::new(db.clone(), 10, tokio::sync::watch::channel(false).1); let state = 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(), }; let router = create_router(state.clone()); 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 admin_client = reqwest::Client::builder() .cookie_store(true) .redirect(reqwest::redirect::Policy::none()) .build() .unwrap(); let user_client = reqwest::Client::builder() .cookie_store(true) .redirect(reqwest::redirect::Policy::none()) .build() .unwrap(); // 1. Create Admin let _ = bzod::cli::create_admin::run( Some("core_admin".to_string()), Some("AdminPass123!".to_string()), None, config.clone(), ) .await .unwrap(); let admin_user = { let conn = db.users.lock().unwrap(); bzod::db::users::get_user_by_username(&conn, "core_admin") .unwrap() .expect("Admin must exist") }; // 2. Create Normal Tenant let _ = bzod::cli::create_user::run( Some("tenant_bob".to_string()), Some("UserPass123!".to_string()), None, config.clone(), ) .await .unwrap(); let (bob_user_id, bob_tenant_id) = { let conn = db.users.lock().unwrap(); let u = bzod::db::users::get_user_by_username(&conn, "tenant_bob") .unwrap() .expect("Bob must exist"); (u.id, u.tenant_id.expect("Bob must have a TenantId")) }; // 3. Admin login let admin_csrf = extract_csrf(&admin_client, &format!("{}/admin/login", base_url)).await; let mut admin_login_params = HashMap::new(); admin_login_params.insert("username", "core_admin"); admin_login_params.insert("password", "AdminPass123!"); admin_login_params.insert("csrf_token", admin_csrf.as_str()); let admin_login_res = admin_client .post(format!("{}/admin/login", base_url)) .form(&admin_login_params) .send() .await .unwrap(); assert_eq!(admin_login_res.status(), reqwest::StatusCode::SEE_OTHER); // 4. User login let user_csrf = extract_csrf(&user_client, &format!("{}/login", base_url)).await; let mut user_login_params = HashMap::new(); user_login_params.insert("username", "tenant_bob"); user_login_params.insert("password", "UserPass123!"); user_login_params.insert("csrf_token", user_csrf.as_str()); let user_login_res = user_client .post(format!("{}/login", base_url)) .form(&user_login_params) .send() .await .unwrap(); assert_eq!(user_login_res.status(), reqwest::StatusCode::SEE_OTHER); // 5. Create API tokens let admin_key_secret = format!("bzo_{}", bzod::utils::generate_token(16)); { let mut hasher = Sha256::new(); hasher.update(admin_key_secret.as_bytes()); let hashed_key = hex::encode(hasher.finalize()); let conn = db.admin.lock().unwrap(); let _ = bzod::db::admin::create_api_key( &conn, &admin_user.id.to_string(), "Admin Test Key", &hashed_key, ) .unwrap(); } let bob_token_secret = format!("bzou_{}", bzod::utils::generate_token(16)); { let mut hasher = Sha256::new(); hasher.update(bob_token_secret.as_bytes()); let hashed_token = hex::encode(hasher.finalize()); let conn = db.users.lock().unwrap(); let _ = bzod::db::users::create_user_api_token(&conn, bob_user_id, &hashed_token).unwrap(); } TestHarness { temp_dir, config, db, base_url, admin_client, user_client, admin_api_key: admin_key_secret, bob_user_id, bob_tenant_id, bob_token_secret, } } } impl Drop for TestHarness { fn drop(&mut self) { let _ = fs::remove_dir_all(&self.temp_dir); } } async fn extract_csrf(client: &reqwest::Client, url: &str) -> String { let html = client.get(url).send().await.unwrap().text().await.unwrap(); html.split("name=\"csrf_token\" value=\"") .nth(1) .and_then(|s| s.split('"').next()) .unwrap_or_default() .to_string() } #[tokio::test] async fn test_01_admin_cannot_create_url_post() { let h = TestHarness::setup().await; let csrf = extract_csrf(&h.admin_client, &format!("{}/admin/urls", h.base_url)).await; let mut form = HashMap::new(); form.insert("destination", "https://fail.com"); form.insert("code", "!fail"); form.insert("csrf_token", csrf.as_str()); let res = h .admin_client .post(format!("{}/admin/urls/create", h.base_url)) .form(&form) .send() .await .unwrap(); assert_eq!( res.status(), reqwest::StatusCode::FORBIDDEN, "Admin POST /admin/urls/create must return 403" ); } #[tokio::test] async fn test_02_admin_cannot_create_landing_page_post() { let h = TestHarness::setup().await; let csrf = extract_csrf(&h.admin_client, &format!("{}/admin/pages", h.base_url)).await; let mut form = HashMap::new(); form.insert("title", "Admin Page"); form.insert("slug", "admin-page"); form.insert("code", "a1b2"); form.insert("state", "published"); form.insert("html_content", "

Admin

"); form.insert("csrf_token", csrf.as_str()); let res = h .admin_client .post(format!("{}/admin/pages/create", h.base_url)) .form(&form) .send() .await .unwrap(); assert_eq!( res.status(), reqwest::StatusCode::FORBIDDEN, "Admin POST /admin/pages/create must return 403" ); } #[tokio::test] async fn test_03_admin_cannot_create_url_api() { let h = TestHarness::setup().await; let api_client = reqwest::Client::new(); let res = api_client .post(format!("{}/api/v1/urls", h.base_url)) .header("Authorization", format!("Bearer {}", h.admin_api_key)) .json(&serde_json::json!({ "destination": "https://admin-api-fail.com", "code": "!admin_api" })) .send() .await .unwrap(); assert_eq!( res.status(), reqwest::StatusCode::FORBIDDEN, "Admin API POST /api/v1/urls must return 403" ); } #[tokio::test] async fn test_04_admin_cannot_create_landing_page_api() { let h = TestHarness::setup().await; let api_client = reqwest::Client::new(); let res = api_client .post(format!("{}/api/v1/pages", h.base_url)) .header("Authorization", format!("Bearer {}", h.admin_api_key)) .json(&serde_json::json!({ "title": "Admin Page API", "slug": "admin-page-api", "html_content": "

Admin API

", "state": "published" })) .send() .await .unwrap(); assert_eq!( res.status(), reqwest::StatusCode::FORBIDDEN, "Admin API POST /api/v1/pages must return 403" ); } #[tokio::test] async fn test_05_admin_cannot_bulk_create_urls_api() { let h = TestHarness::setup().await; let api_client = reqwest::Client::new(); let res = api_client .post(format!("{}/api/v1/bulk/url", h.base_url)) .header("Authorization", format!("Bearer {}", h.admin_api_key)) .json(&serde_json::json!([ { "destination": "https://bulk1.com", "code": "!b1" }, { "destination": "https://bulk2.com", "code": "!b2" } ])) .send() .await .unwrap(); assert_eq!( res.status(), reqwest::StatusCode::FORBIDDEN, "Admin API POST /api/v1/bulk/url must return 403" ); } #[tokio::test] async fn test_06_admin_urls_registry_renders_no_creation_form() { let h = TestHarness::setup().await; let html = h .admin_client .get(format!("{}/admin/urls", h.base_url)) .send() .await .unwrap() .text() .await .unwrap(); assert!( !html.contains("Shorten a New URL"), "Admin /admin/urls must NOT contain 'Shorten a New URL'" ); assert!( !html.contains("action=\"/admin/urls/create\""), "Admin /admin/urls must NOT contain creation form action" ); assert!( html.contains("Global URL Registry"), "Admin /admin/urls must render Global URL Registry" ); } #[tokio::test] async fn test_07_admin_pages_registry_renders_no_creation_form() { let h = TestHarness::setup().await; let html = h .admin_client .get(format!("{}/admin/pages", h.base_url)) .send() .await .unwrap() .text() .await .unwrap(); assert!( !html.contains("Create a New Landing Page"), "Admin /admin/pages must NOT contain 'Create a New Landing Page'" ); assert!( !html.contains("action=\"/admin/pages/create\""), "Admin /admin/pages must NOT contain creation form action" ); assert!( html.contains("Global Landing Page Registry"), "Admin /admin/pages must render Global Landing Page Registry" ); } #[tokio::test] async fn test_08_and_09_admin_inspection_of_global_registries() { let h = TestHarness::setup().await; let api_client = reqwest::Client::new(); // Bob creates URL let res = api_client .post(format!("{}/api/v1/urls", h.base_url)) .header("Authorization", format!("Bearer {}", h.bob_token_secret)) .json(&serde_json::json!({ "destination": "https://bob-portfolio.org", "code": "b0b001" })) .send() .await .unwrap(); assert_eq!(res.status(), reqwest::StatusCode::CREATED); // Bob creates Landing Page via UI let bob_pages_csrf = extract_csrf(&h.user_client, &format!("{}/user/pages", h.base_url)).await; let mut bob_page_form = HashMap::new(); bob_page_form.insert("title", "Bob Landing Page"); bob_page_form.insert("slug", "bob-page"); bob_page_form.insert("code", "a1b2"); bob_page_form.insert("custom_slug", ""); bob_page_form.insert("state", "published"); bob_page_form.insert("html_content", "

Welcome to Bob's Page

"); bob_page_form.insert("csrf_token", bob_pages_csrf.as_str()); let res = h .user_client .post(format!("{}/user/pages/create", h.base_url)) .form(&bob_page_form) .send() .await .unwrap(); assert_eq!(res.status(), reqwest::StatusCode::SEE_OTHER); // Test 8: Admin can inspect URL registry let admin_urls_inspected = h .admin_client .get(format!("{}/admin/urls", h.base_url)) .send() .await .unwrap() .text() .await .unwrap(); assert!( admin_urls_inspected.contains("b0b001"), "Admin URL registry must display tenant URLs" ); assert!( admin_urls_inspected.contains("tenant_bob"), "Admin URL registry must display owner username" ); assert!( admin_urls_inspected.contains(h.bob_tenant_id.as_str()), "Admin URL registry must display owner TenantId" ); // Test 9: Admin can inspect Landing Page registry let admin_pages_inspected = h .admin_client .get(format!("{}/admin/pages", h.base_url)) .send() .await .unwrap() .text() .await .unwrap(); assert!( admin_pages_inspected.contains("Bob Landing Page"), "Admin Page registry must display tenant landing pages" ); assert!( admin_pages_inspected.contains("tenant_bob"), "Admin Page registry must display owner username" ); assert!( admin_pages_inspected.contains(h.bob_tenant_id.as_str()), "Admin Page registry must display owner TenantId" ); } #[tokio::test] async fn test_10_admin_moderation_functional() { let h = TestHarness::setup().await; let api_client = reqwest::Client::new(); let _ = api_client .post(format!("{}/api/v1/urls", h.base_url)) .header("Authorization", format!("Bearer {}", h.bob_token_secret)) .json(&serde_json::json!({ "destination": "https://spam.org", "code": "!spam-link" })) .send() .await .unwrap(); let urls_conn = h.db.global_urls.lock().unwrap(); let pages_conn = h.db.global_landing_pages.lock().unwrap(); let retired = bzod::db::slugs::retire_slug(&urls_conn, &pages_conn, "!spam-link").unwrap(); assert!(retired, "Admin moderation (retire slug) must succeed"); } #[tokio::test] async fn test_11_admin_transfer_functional() { let h = TestHarness::setup().await; let api_client = reqwest::Client::new(); let _ = api_client .post(format!("{}/api/v1/urls", h.base_url)) .header("Authorization", format!("Bearer {}", h.bob_token_secret)) .json(&serde_json::json!({ "destination": "https://transfer-target.org", "code": "!transfer-link" })) .send() .await .unwrap(); let _ = bzod::cli::create_user::run( Some("tenant_alice".to_string()), Some("AlicePass123!".to_string()), None, h.config.clone(), ) .await .unwrap(); let alice_tenant_id = { let conn = h.db.users.lock().unwrap(); let u = bzod::db::users::get_user_by_username(&conn, "tenant_alice") .unwrap() .expect("Alice must exist"); u.tenant_id.expect("Alice must have a TenantId") }; let urls_conn = h.db.global_urls.lock().unwrap(); let pages_conn = h.db.global_landing_pages.lock().unwrap(); let transferred = bzod::db::slugs::transfer_slug_owner( &urls_conn, &pages_conn, "!transfer-link", &alice_tenant_id, "new_target_id", ) .unwrap(); assert!(transferred, "Admin slug transfer to Alice must succeed"); let lookup = bzod::db::slugs::lookup_slug(&urls_conn, &pages_conn, "!transfer-link") .unwrap() .unwrap(); assert_eq!( lookup.owner_tenant_id, alice_tenant_id.as_str(), "Slug owner must now be Alice's TenantId" ); } #[tokio::test] async fn test_12_13_14_15_normal_tenant_creation_and_topology() { let h = TestHarness::setup().await; // Verify User UI contains creation form let user_urls_html = h .user_client .get(format!("{}/user/urls", h.base_url)) .send() .await .unwrap() .text() .await .unwrap(); assert!( user_urls_html.contains("Create a New URL"), "User /user/urls MUST contain creation form" ); assert!( user_urls_html.contains("action=\"/user/urls/create\""), "User /user/urls MUST post to /user/urls/create" ); // UI URL creation let bob_urls_csrf = extract_csrf(&h.user_client, &format!("{}/user/urls", h.base_url)).await; let mut bob_url_form = HashMap::new(); bob_url_form.insert("destination", "https://bob-portfolio.org"); bob_url_form.insert("code", "b0b001"); bob_url_form.insert("title", "Bob's Portfolio"); bob_url_form.insert("csrf_token", bob_urls_csrf.as_str()); let res = h .user_client .post(format!("{}/user/urls/create", h.base_url)) .form(&bob_url_form) .send() .await .unwrap(); assert_eq!( res.status(), reqwest::StatusCode::SEE_OTHER, "User URL creation must succeed and redirect" ); // UI Landing page creation let bob_pages_csrf = extract_csrf(&h.user_client, &format!("{}/user/pages", h.base_url)).await; let mut bob_page_form = HashMap::new(); bob_page_form.insert("title", "Bob Landing Page"); bob_page_form.insert("slug", "bob-page"); bob_page_form.insert("code", "a1b2"); bob_page_form.insert("custom_slug", ""); bob_page_form.insert("state", "published"); bob_page_form.insert("html_content", "

Welcome to Bob's Page

"); bob_page_form.insert("csrf_token", bob_pages_csrf.as_str()); let res = h .user_client .post(format!("{}/user/pages/create", h.base_url)) .form(&bob_page_form) .send() .await .unwrap(); assert_eq!( res.status(), reqwest::StatusCode::SEE_OTHER, "User landing page creation must succeed and redirect" ); // Test 14: Verify content is stored under users//content.db let topology = Topology::new(&h.temp_dir); let bob_tenant_dir = topology.user_dir(h.bob_tenant_id.as_str()).unwrap(); assert!( bob_tenant_dir.join("content.db").exists(), "Bob's content.db must exist" ); let bob_conn = rusqlite::Connection::open(bob_tenant_dir.join("content.db")).unwrap(); let url_count: i64 = bob_conn .query_row("SELECT COUNT(*) FROM urls;", [], |r| r.get(0)) .unwrap(); assert_eq!( url_count, 1, "Bob's content.db must hold exactly 1 URL created by Bob" ); let page_count: i64 = bob_conn .query_row("SELECT COUNT(*) FROM landing_pages;", [], |r| r.get(0)) .unwrap(); assert_eq!( page_count, 1, "Bob's content.db must hold exactly 1 landing page created by Bob" ); // Test 15: No creation path uses users/1 or integer paths let legacy_int_dir = h.temp_dir.join("users").join("1"); assert!(!legacy_int_dir.exists(), "users/1 must NOT exist anywhere"); }