use super::{ auth::{KEY, tenant_token}, http::{Server, request}, *, }; use serde_json::{Value, json}; use std::{ net::{TcpListener, TcpStream}, time::{Duration, Instant}, }; #[cfg(feature = "postgres")] mod postgres; fn command(binary: &Path, tenant: Option, name: &str, input: Value) -> Output { let mut command = Command::new(binary); if let Some(tenant) = tenant { command.args(["--tenant", &tenant.to_string()]); } let output = command.args([name, &input.to_string()]).output().unwrap(); if !output.status.success() { eprintln!( "{name} tenant={tenant:?}: {}", String::from_utf8_lossy(&output.stderr) ); } output } fn success(output: Output) -> Value { assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); serde_json::from_slice::(&output.stdout).unwrap()["data"].clone() } fn get(port: u16, tenant: i64, action: &str) -> (u16, Value) { let name = format!("tenant-{tenant}"); let token = tenant_token(&name, "session", Some(&name)); let (status, body) = request( port, "GET", &format!("/news/item/admin/entry/{action}"), "fixture.test", &format!("Authorization: Bearer {token}\r\n"), "", ); ( status, serde_json::from_str::(&body).unwrap()["data"].clone(), ) } fn post(port: u16, tenant: i64, action: &str, input: Value) -> (u16, Value) { let name = format!("tenant-{tenant}"); let token = tenant_token(&name, "session", Some(&name)); let (status, body) = request( port, "POST", &format!("/news/item/admin/entry/{action}"), "fixture.test", &format!("Authorization: Bearer {token}\r\nContent-Type: application/json\r\n"), &input.to_string(), ); ( status, serde_json::from_str::(&body).unwrap()["data"].clone(), ) } fn start(binary: &Path, port: u16) -> Server { let mut server = Server( Command::new(binary) .stdout(std::process::Stdio::null()) .spawn() .unwrap(), ); let deadline = Instant::now() + Duration::from_secs(5); while TcpStream::connect(("127.0.0.1", port)).is_err() { assert!( server.0.try_wait().unwrap().is_none(), "server exited before listen" ); assert!(Instant::now() < deadline, "server did not listen"); std::thread::sleep(Duration::from_millis(10)); } server } #[test] fn tenants_execute_dever_http_rest_capabilities_and_persistent_jobs() { let directory = Directory::new(); verify( &directory, json!({"type":"sqlite","path":"data/control.db", "tenant_directory":"data/tenant", "max_connections":4}), ); } fn verify(directory: &Directory, connection: Value) { let project = Project::load(&fixture().with_file_name("tenant")).unwrap(); let program = project.check().unwrap(); program.check_tenant_entries().unwrap(); let binary = build::build( &program, &project.sources, "main", &directory.0.join("tenant"), ) .unwrap(); let listener = TcpListener::bind("127.0.0.1:0").unwrap(); let port = listener.local_addr().unwrap().port(); drop(listener); std::fs::create_dir(directory.0.join("config")).unwrap(); let mut settings = json!({ "http":{"host":"127.0.0.1","port":port}, "sites":{"admin":{"path":"admin","auth":"session","hosts":["fixture.test"]}}, "auth":{"providers":{"session":{"verify":"platform.account.verify","jwtSecret":KEY,"ttlSeconds":60}}}, "database":{"default":connection}, "tenant":{"database":"default","max_pools":4,"idle_timeout_ms":1000}, "runtime":{"mode":"api"}, "job":{"workers":1,"poll_ms":20,"lease_ms":3000,"retry_base_ms":100,"retry_max_ms":1000} }); let config = directory.0.join("config/setting.json"); std::fs::write(&config, settings.to_string()).unwrap(); for tenant in [1, 2] { assert_eq!( success(command( &binary, None, "platform.account.seed", json!({"subject":format!("tenant-{tenant}"),"session":"session"}) )), tenant ); } assert!( !command(&binary, None, "news.item.count", json!({})) .status .success() ); assert!( !command(&binary, Some(1), "news.item.count", json!({})) .status .success() ); for tenant in [1, 2] { success(command( &binary, Some(tenant), "dever.tenant.migrate", Value::Null, )); success(command( &binary, Some(tenant), "dever.tenant.migrate", Value::Null, )); success(command( &binary, Some(tenant), "dever.auth.owner", json!({"site":"admin","user_id":tenant}), )); assert_eq!( success(command( &binary, Some(tenant), "news.item.create", json!({"title":"same-title"}) )), 1 ); } let server = start(&binary, port); for tenant in [1, 2] { assert_eq!(get(port, tenant, "count"), (200, json!(1))); } let wrong = tenant_token("tenant-1", "session", Some("tenant-2")); assert_eq!( request( port, "GET", "/news/item/admin/entry/count", "fixture.test", &format!("Authorization: Bearer {wrong}\r\n"), "" ) .0, 401 ); let id = "01900000-0000-7000-8000-000000000001"; let (status, key) = post(port, 1, "register", json!({"id":id})); assert_eq!(status, 200, "{key}"); assert_eq!(key, format!("capability:news.item.admin.{id}")); assert!( get(port, 1, "permissions") .1 .as_array() .unwrap() .iter() .any(|value| value["key"] == key) ); assert!( !get(port, 2, "permissions") .1 .as_array() .unwrap() .iter() .any(|value| value["key"] == key) ); assert_eq!(post(port, 1, "remove", json!({"id":id})).0, 200); assert!( !get(port, 1, "permissions") .1 .as_array() .unwrap() .iter() .any(|value| value["key"] == key) ); let first = tenant_token("tenant-1", "session", Some("tenant-1")); let (status, body) = request( port, "GET", "/news/item/admin/resource?page=1&size=10", "fixture.test", &format!("Authorization: Bearer {first}\r\n"), "", ); assert_eq!(status, 200, "{body}"); assert!(body.contains("same-title")); success(command( &binary, Some(1), "dever.tenant.disable", json!("news"), )); assert_eq!(get(port, 1, "count").0, 403); assert_eq!(get(port, 2, "count").0, 200); assert!( !command(&binary, Some(1), "news.item.count", json!({})) .status .success() ); assert_eq!( request( port, "GET", "/news/item/admin/entry/health", "fixture.test", "", "" ) .0, 200 ); success(command( &binary, Some(1), "dever.tenant.enable", json!("news"), )); assert!( !command(&binary, Some(1), "dever.tenant.disable", json!("platform")) .status .success() ); for tenant in [1, 2] { let input = json!({"title":"system-work","key":"same-key"}); let first = success(command( &binary, Some(tenant), "news.item.submit", input.clone(), )); let second = success(command(&binary, Some(tenant), "news.item.submit", input)); assert_eq!(first, second); assert_eq!( post( port, tenant, "submit", json!({"title":"user-work","key":"user-key"}) ) .0, 200 ); } settings["runtime"]["mode"] = "worker".into(); std::fs::write(&config, settings.to_string()).unwrap(); let mut worker = Server( Command::new(&binary) .stdout(std::process::Stdio::null()) .spawn() .unwrap(), ); let deadline = Instant::now() + Duration::from_secs(10); loop { assert!(worker.0.try_wait().unwrap().is_none(), "worker exited"); let counts = [1, 2] .map(|tenant| success(command(&binary, Some(tenant), "news.item.count", json!({})))); if counts == [json!(3), json!(3)] { break; } assert!( Instant::now() < deadline, "tasks did not finish: {counts:?}" ); std::thread::sleep(Duration::from_millis(30)); } drop(worker); drop(server); }