use dever_core::source::SourceMap; use dever_runtime::api::{Inputs, QueryValue}; use dever_runtime::bytes::Bytes; use dever_runtime::config::RuntimeProfile; use dever_runtime::http::{Header, Request}; use std::fs; use std::process::Command; #[path = "support/process.rs"] #[allow(dead_code)] mod process; #[path = "support/temp.rs"] mod temp; fn source(files: &[(&str, &str)]) -> SourceMap { let mut sources = SourceMap::default(); for (path, text) in files { sources.add(*path, *text); } sources } #[cfg(feature = "sqlite")] #[test] fn command_releases_short_transactions_before_waiting_and_preserves_committed_stages() { let sources = source(&[ ( "content/item/model.dever", "global type Item { name: Text unique }", ), ( "content/item/app.dever", "transaction store(name: Text) (value: Text) { saved = model.create({ name = name })\nvalue = saved.name }\ncount() (total: Int) { total = model.count() }\nperform() (total: Int) { saved = store(\"first\")\ntask = run(count())\ntotal = wait(task) }\nfail_later() (value: Text) { committed = store(\"second\")\nvalue = store(committed) }", ), ( "content/item/api.dever", "long cmd perform = app.perform\nlong cmd fail_later = app.fail_later\ncmd fail_atomic = app.fail_later\ncmd count = app.count", ), ]); let program = dever_core::check(&sources).unwrap_or_else(|errors| panic!("{errors:?}")); let rustc = std::env::var_os("RUSTC").unwrap_or_else(|| "rustc".into()); let native = dever_core::native::compile_project( &program, &sources, &rustc, RuntimeProfile { sqlite: true, postgres: false, }, ) .unwrap(); let root = native.executable().parent().unwrap(); fs::create_dir_all(root.join("config")).unwrap(); fs::write( root.join("config/setting.json"), r#"{"database":{"default":{"type":"sqlite","path":"data/db/app.db","max_connections":1}}}"#, ) .unwrap(); let invoke = |operation: &str| { let mut child = Command::new(native.executable()) .current_dir(root) .args([operation, "{}"]) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) .spawn() .unwrap(); process::wait(&mut child, std::time::Duration::from_secs(10)).unwrap(); child.wait_with_output().unwrap() }; let first = invoke("content.item.perform"); assert!( first.status.success(), "{}", String::from_utf8_lossy(&first.stderr) ); let first: serde_json::Value = serde_json::from_slice(&first.stdout).unwrap(); assert_eq!(first["data"], 1); let atomic = invoke("content.item.fail_atomic"); assert!(!atomic.status.success()); assert!(String::from_utf8_lossy(&atomic.stderr).contains("UNIQUE")); let retained = invoke("content.item.count"); assert!(retained.status.success()); let retained: serde_json::Value = serde_json::from_slice(&retained.stdout).unwrap(); assert_eq!(retained["data"], 1); let failed = invoke("content.item.fail_later"); assert!(!failed.status.success()); assert!(String::from_utf8_lossy(&failed.stderr).contains("UNIQUE")); let count = invoke("content.item.count"); assert!( count.status.success(), "{}", String::from_utf8_lossy(&count.stderr) ); let count: serde_json::Value = serde_json::from_slice(&count.stdout).unwrap(); assert_eq!(count["data"], 2); } #[test] fn project_entry_collects_api_and_command_without_main() { let sources = source(&[ ( "user/account/app.dever", "greet(name: Text) (value: Text) { value = text.trim(name) }", ), ( "user/account/api.dever", "get greeting = app.greet\ncmd greeting = app.greet", ), ]); let program = dever_core::check(&sources).unwrap(); let code = dever_core::native::emit_project(&program, &sources, RuntimeProfile::default()).unwrap(); assert!(code.contains("dever_runtime::api::serve(api_route).await")); assert!(code.contains("\"user.account.greeting\" => {")); assert!(code.contains("let service_start = args.is_empty()")); assert!(code.contains("cmd_dispatch(&args[0], &args[1]).await")); assert!(!code.contains("main.main")); } #[test] fn project_entry_can_embed_verified_external_resources() { let sources = source(&[ ( "user/account/app.dever", "greet() (value: Text) { value = \"ok\" }", ), ("user/account/api.dever", "cmd greeting = app.greet"), ]); let program = dever_core::check(&sources).unwrap(); let code = dever_core::native::emit_project_with_resources( &program, &sources, RuntimeProfile { sqlite: true, postgres: false, }, &[dever_core::native::EmbeddedResource { path: "worker.bin".into(), bytes: b"worker".to_vec(), sha256: "87eba76e7f3164534045ba922e7770fb58bbd14ad732bbf5ba6f11cc56989e6e".into(), executable: true, }], ) .unwrap(); assert!(code.contains("dever_runtime::external::prepare")); assert!(code.contains("DEVER_EXTERNAL_RESOURCES")); assert!(code.contains("include_bytes!(\"external-resource-0.bin\")")); } #[test] fn large_external_resources_do_not_expand_generated_source_and_reject_forged_digests() { use sha2::{Digest, Sha256}; let sources = source(&[ ( "user/account/app.dever", "greet() (value: Text) { value = \"ok\" }", ), ("user/account/api.dever", "cmd greeting = app.greet"), ]); let program = dever_core::check(&sources).unwrap(); let baseline = dever_core::native::emit_project(&program, &sources, RuntimeProfile::default()).unwrap(); let bytes = vec![157; 1024 * 1024]; let mut resource = dever_core::native::EmbeddedResource { path: "runtime.bin".into(), sha256: Sha256::digest(&bytes) .iter() .map(|byte| format!("{byte:02x}")) .collect(), bytes, executable: true, }; let code = dever_core::native::emit_project_with_resources( &program, &sources, RuntimeProfile::default(), std::slice::from_ref(&resource), ) .unwrap(); assert!( code.len().saturating_sub(baseline.len()) < 8 * 1024, "resource bytes must not become Rust literals" ); assert!(code.contains("include_bytes!(\"external-resource-0.bin\")")); let mut copy = resource.clone(); copy.path = "runtime-copy.bin".into(); copy.executable = false; let shared = dever_core::native::emit_project_with_resources( &program, &sources, RuntimeProfile::default(), &[resource.clone(), copy], ) .unwrap(); assert_eq!(shared.matches("include_bytes!(").count(), 1); assert!(!shared.contains("external-resource-1.bin")); assert!(shared.contains("runtime-copy.bin")); assert_eq!(shared.matches("bytes: DEVER_EXTERNAL_BYTES_0").count(), 2); resource.bytes[512 * 1024] ^= 1; assert!( dever_core::native::emit_project_with_resources( &program, &sources, RuntimeProfile::default(), &[resource], ) .unwrap_err() .contains("invalid digest") ); } #[test] fn api_and_command_inputs_share_strict_json_validation() { assert!(Inputs::from_json_object("{\"profile\":{\"name\":\"a\",\"name\":\"b\"}}").is_err()); let mut inputs = Inputs::from_json_object("{}").unwrap(); assert!(inputs.raw_json("required").is_err()); assert_eq!(inputs.optional_raw_json("optional").unwrap(), None); assert!(inputs.json("required", QueryValue::Text).is_err()); assert_eq!( inputs.optional_json("optional", QueryValue::Text).unwrap(), "null" ); let request = Request { method: "PUT".into(), target: "/user/account/profile".into(), headers: vec![Header { name: "content-type".into(), value: Bytes::from_text("application/json"), }], body: Bytes::from_text("{\"count\":12}"), }; let mut inputs = Inputs::from_request(&request).unwrap(); assert_eq!(inputs.json("count", QueryValue::Int).unwrap(), "12"); inputs.finish().unwrap(); } #[test] fn generated_write_entry_rejects_detached_concurrency() { let sources = source(&[ ( "content/article/model.dever", "type Article { title: Text(1, 32) }", ), ( "content/article/app.dever", "write() () { article = model.create({ title = \"published\" }) }\npublish() (value: Text) { task = run(write())\nwait(task)\nvalue = \"done\" }", ), ("content/article/api.dever", "post publish = app.publish"), ]); let errors = dever_core::check(&sources).unwrap_err(); let rendered = errors .iter() .map(|error| error.render(&sources)) .collect::(); assert!( rendered.contains("automatic entry transactions cannot contain"), "{rendered}" ); } #[test] fn generated_long_entry_keeps_password_work_outside_its_write_transaction() { let sources = source(&[ ( "user/account/model.dever", "type Account { email: Text(1, 254)\nprivate password_hash: Text(255) }", ), ( "user/account/app.dever", r#"register(password: Secret) (id: model.id) { hash = blocking(dever.crypto.password_hash(password)) id = persist(hash) } transaction persist(hash: Text) (id: model.id) { stored = model.create({ email = "reader@example.com" password_hash = hash }) id = stored.id }"#, ), ( "user/account/api.dever", "long post register = app.register", ), ]); let program = dever_core::check(&sources).unwrap_or_else(|errors| { panic!( "{}", errors .iter() .map(|error| error.render(&sources)) .collect::() ) }); let code = dever_core::native::emit_project(&program, &sources, RuntimeProfile::default()).unwrap(); assert!( code.contains("transaction_database.begin().await"), "{code}" ); assert!(code.contains("task::blocking_typed"), "{code}"); } #[test] fn http_request_capabilities_are_not_available_to_commands() { let sources = source(&[ ( "user/account/app.dever", "current() (value: Text) { value = dever.api.request_id() }", ), ("user/account/api.dever", "cmd current = app.current"), ]); let errors = dever_core::check(&sources).unwrap_err(); let rendered = errors .iter() .map(|error| error.render(&sources)) .collect::(); assert!( rendered.contains("HTTP request capabilities are not available from CMD or Job"), "{rendered}" ); } #[test] fn http_request_capabilities_cannot_escape_into_detached_tasks() { let sources = source(&[ ( "user/account/app.dever", "read() (value: Text) { value = dever.api.request_id() }\ncurrent() (value: Text) { task = run(read())\nvalue = wait(task) }", ), ("user/account/api.dever", "get current = app.current"), ]); let errors = dever_core::check(&sources).unwrap_err(); let rendered = errors .iter() .map(|error| error.render(&sources)) .collect::(); assert!(rendered.contains("HTTP request, identity, and site capabilities cannot be used inside detached or parallel work"), "{rendered}"); } #[test] fn generated_http_context_capabilities_compile_without_a_source_context_value() { let sources = source(&[ ( "user/account/app.dever", "current() (value: Text) { value = dever.api.request_id()\ndever.api.set_header(\"x-request-id\", value)\noptions = dever.api.secure_cookie_options()\ndever.api.set_cookie(\"theme\", \"dark\", options) }", ), ("user/account/api.dever", "get current = app.current"), ]); let program = dever_core::check(&sources).unwrap_or_else(|errors| { panic!( "{}", errors .iter() .map(|error| error.render(&sources)) .collect::() ) }); let rustc = std::env::var_os("RUSTC").unwrap_or_else(|| "rustc".into()); let native = dever_core::native::compile_project(&program, &sources, &rustc, RuntimeProfile::default()) .unwrap(); assert!(native.executable().is_file()); } #[test] fn generated_authenticated_route_compiles_with_safe_standard_error_mapping() { let directory = temp::TemporaryDirectory::new(); fs::create_dir(directory.path().join("config")).unwrap(); fs::write( directory.path().join("config/setting.json"), r#"{ "auth": { "providers": { "session": { "verify": "user.account.verify", "jwtSecret": "0123456789abcdef0123456789abcdef" } } }, "sites": { "admin": {"path": "admin", "auth": "session"} } }"#, ) .unwrap(); let settings = dever_runtime::config::Settings::load_project(directory.path()).unwrap(); let sources = source(&[ ( "user/account/model.dever", "global type Account { email: Text(1, 254) }", ), ( "platform/tenant/model.dever", "global type Tenant { name: Text(1, 64) }", ), ( "user/account/app.dever", r#"type Identity { id: Text user_id: user.account.model.id? tenant_id: platform.tenant.model.id? } verify(claims: dever.auth.Claims) (identity: Identity) { identity = Identity { id = claims.subject user_id = null tenant_id = null } } profile() (response: Text) { fail(dever.api.Error.Forbidden) } permissions() (response: List) { response = dever.auth.permissions() } manage_role() (ok: Bool) { dever.auth.save_role("reader", "Reader", false, ["user.account.admin.profile"]) dever.auth.grant_role(1, "reader") dever.auth.revoke_role(1, "reader") dever.auth.disable_role("reader") ok = true } type Work { value: Text } submit() (id: Id) { id = dever.job.enqueue(job.deliver, Work { value = "queued" }, "profile") } deliver(input: Work) () { news.article.deliver(input.value) }"#, ), ( "user/account/api/admin/profile.dever", "get profile = app.profile\nget permissions = app.permissions\npost manage_role = app.manage_role\npost submit = app.submit", ), ( "user/account/job.dever", "database default\njob deliver(input: app.Work) () retry(2) timeout(1000) { app.deliver(input) }", ), ( "news/article/model.dever", "type Article { title: Text(1, 128) }", ), ( "news/article/app.dever", "deliver(title: Text) () { stored = model.create({ title = title }) }", ), ( "gateway/session/app.dever", "login() (response: Text) { token = dever.auth.issue(\"account:1\", \"session:1\", null)\noptions = dever.api.secure_cookie_options()\ndever.api.set_secret_cookie(\"cms_session\", token, options)\nresponse = \"ok\" }", ), ( "gateway/session/api/admin/login.dever", "public post login = app.login", ), ]); let program = dever_core::check_with_settings(&sources, &settings).unwrap_or_else(|errors| { panic!( "{}", errors .iter() .map(|error| error.render(&sources)) .collect::() ) }); let generated = dever_core::native::emit_project( &program, &sources, RuntimeProfile { sqlite: true, postgres: false, }, ) .unwrap(); assert!(generated.contains("dever_runtime::api::standard_error(403)")); assert!(generated.contains("dever_runtime::auth::validate_job_source(&provider, &site)")); assert!(generated.contains( "dever_runtime::auth::validate_job_identity(&verified_claims, &verified_identity)" )); assert!(generated.contains("dever_runtime::auth::store::authorize")); assert!(generated.contains("AUTHORIZATION_PERMISSIONS.iter().any")); assert!(generated.contains("permission_key")); assert!(generated.contains("let auth_claims =")); assert!(generated.contains("job_dispatch_verified(claim).await")); let dispatch = generated.split_once("async fn job_dispatch(").unwrap().1; let verify = dispatch.find("let identity:").unwrap(); let permission = dispatch .find("AUTHORIZATION_PERMISSIONS.iter().any") .unwrap(); let components = permission + dispatch[permission..] .find("job_require_components(&claim).await") .unwrap(); let role = components + dispatch[components..] .find("dever_runtime::auth::store::authorize") .unwrap(); assert!(verify < permission && permission < components && components < role); let rustc = std::env::var_os("RUSTC").unwrap_or_else(|| "rustc".into()); let native = dever_core::native::compile_project( &program, &sources, &rustc, RuntimeProfile { sqlite: true, postgres: false, }, ) .unwrap(); assert!(native.executable().is_file()); } #[test] fn command_only_entry_requires_explicit_selection() { let sources = source(&[ ( "user/account/app.dever", "greet() (value: Text) { value = \"ok\" }", ), ("user/account/api.dever", "cmd greeting = app.greet"), ]); let program = dever_core::check(&sources).unwrap(); let code = dever_core::native::emit_project(&program, &sources, RuntimeProfile::default()).unwrap(); assert!(code.contains("application has no service entry; select a CMD and JSON input")); assert!(code.contains("\"user.account.greeting\" => Some(false)")); assert!(!code.contains("dever_runtime::api::serve(api_route)")); } #[test] fn application_build_rejects_unreachable_app_and_private_forwarder() { let sources = source(&[ ( "user/account/domain.dever", "base(value: Text) (result: Text) pure { result = text.trim(value) }\nalias(value: Text) (result: Text) pure { result = base(value) }", ), ( "user/account/app.dever", "greet(value: Text) (result: Text) { result = domain.alias(value) }\nunused() (result: Text) { result = \"unused\" }", ), ("user/account/api.dever", "get greeting = app.greet"), ]); let program = dever_core::check(&sources).unwrap(); let errors = program.application_errors().unwrap(); let rendered = errors .iter() .map(|error| error.render(&sources)) .collect::(); assert!( rendered.contains("unreachable from API, CMD, or Job"), "{rendered}" ); assert!( rendered.contains("only forwards unchanged inputs"), "{rendered}" ); } #[test] fn generated_api_and_command_binary_compiles() { let sources = source(&[ ( "user/account/app.dever", "greet(name: Text) (value: Text) { value = text.trim(name) }", ), ( "user/account/api.dever", "get greeting = app.greet\npost greeting = app.greet\ncmd greeting = app.greet", ), ]); let program = dever_core::check(&sources).unwrap(); assert!(program.application_errors().unwrap().is_empty()); let rustc = std::env::var_os("RUSTC").unwrap_or_else(|| "rustc".into()); let binary = dever_core::native::compile_project(&program, &sources, &rustc, RuntimeProfile::default()) .unwrap(); assert!(binary.executable().is_file()); let directory = temp::TemporaryDirectory::new(); fs::create_dir(directory.path().join("config")).unwrap(); fs::write(directory.path().join("config/setting.json"), "{}\n").unwrap(); let executable = directory.path().join("app"); binary.save(&executable).unwrap(); let run = Command::new(&executable) .args(["user.account.greeting", r#"{"name":" Ada "}"#]) .output() .unwrap(); assert!( run.status.success(), "{}", String::from_utf8_lossy(&run.stderr) ); assert_eq!( String::from_utf8(run.stdout).unwrap().trim(), r#"{"code":0,"message":"ok","data":"Ada"}"# ); } #[test] fn generated_job_entry_wraps_model_writes_in_transaction() { let sources = source(&[ ( "content/article/model.dever", "type Article { title: Text(1, 32) }", ), ( "content/article/app.dever", "apply() () { article = model.create({ title = \"published\" }) }", ), ( "content/article/job.dever", "database default\njob publish() () retry(2) timeout(1000) { app.apply() }", ), ]); let program = dever_core::check(&sources).unwrap(); assert!(program.application_errors().unwrap().is_empty()); let code = dever_core::native::emit_project( &program, &sources, RuntimeProfile { sqlite: true, postgres: false, }, ) .unwrap(); assert!(code.contains("job_serve().await")); assert!(code.contains("Some(&transaction)")); assert!(code.contains("transaction.commit().await")); assert!(code.contains("transaction.rollback().await")); let rustc = std::env::var_os("RUSTC").unwrap_or_else(|| "rustc".into()); let binary = dever_core::native::compile_project( &program, &sources, &rustc, RuntimeProfile { sqlite: true, postgres: false, }, ) .unwrap(); assert!(binary.executable().is_file()); } #[test] fn postgres_profile_compiles_without_connecting() { let sources = source(&[ ( "content/article/model.dever", "global type Article { title: Text(1, 32) }", ), ( "content/article/app.dever", "count() (total: Int) { total = model.count() }", ), ("content/article/api.dever", "cmd count = app.count"), ]); let program = dever_core::check(&sources).unwrap(); assert!(program.application_errors().unwrap().is_empty()); let rustc = std::env::var_os("RUSTC").unwrap_or_else(|| "rustc".into()); let binary = dever_core::native::compile_project( &program, &sources, &rustc, RuntimeProfile { sqlite: false, postgres: true, }, ) .unwrap(); assert!(binary.executable().is_file()); } #[cfg(feature = "sqlite")] #[test] fn generated_tenant_migration_is_explicit_idempotent_and_never_falls_back() { use dever_runtime::orm::Value; use dever_runtime::sqlite::Database; let directory = temp::TemporaryDirectory::new(); fs::create_dir(directory.path().join("config")).unwrap(); fs::write( directory.path().join("config/setting.json"), r#"{ "tenant": { "database": "default", "max_pools": 4, "idle_timeout_ms": 60000 }, "database": { "default": { "type": "sqlite", "path": "data/platform.db", "tenant_directory": "data/tenants", "max_connections": 2 } } }"#, ) .unwrap(); let settings = dever_runtime::config::Settings::load_project(directory.path()).unwrap(); let sources = source(&[ ( "content/article/model.dever", "type Article { title: Text(1, 160) }", ), ( "content/article/app.dever", "count() (total: Int) { total = model.count() }", ), ("content/article/api.dever", "cmd count = app.count"), ]); let program = dever_core::check_with_settings(&sources, &settings).unwrap_or_else(|errors| { panic!( "{}", errors .iter() .map(|error| error.render(&sources)) .collect::() ) }); assert!(program.application_errors().unwrap().is_empty()); let rustc = std::env::var_os("RUSTC").unwrap_or_else(|| "rustc".into()); let binary = dever_core::native::compile_project( &program, &sources, &rustc, RuntimeProfile { sqlite: true, postgres: false, }, ) .unwrap(); let executable = directory.path().join("app"); binary.save(&executable).unwrap(); let tenant_database = directory.path().join("data/tenants/tenant_7.db"); let unscoped = Command::new(&executable) .args(["content.article.count", "{}"]) .output() .unwrap(); assert!(!unscoped.status.success()); assert!( String::from_utf8_lossy(&unscoped.stderr).contains("requires an explicit tenant"), "{}", String::from_utf8_lossy(&unscoped.stderr), ); assert!(!tenant_database.exists()); for _ in 0..2 { let migrated = Command::new(&executable) .args(["--dever-tenant-migrate", "7"]) .output() .unwrap(); assert!( migrated.status.success(), "{}", String::from_utf8_lossy(&migrated.stderr), ); } assert!(tenant_database.is_file()); let scoped = Command::new(&executable) .args(["--dever-tenant-cmd", "7", "content.article.count", "{}"]) .output() .unwrap(); assert!( scoped.status.success(), "{}", String::from_utf8_lossy(&scoped.stderr) ); assert_eq!( String::from_utf8(scoped.stdout).unwrap().trim(), r#"{"code":0,"message":"ok","data":0}"#, ); let disabled = Command::new(&executable) .args(["--dever-tenant-component", "7", "disable", "content"]) .output() .unwrap(); assert!( disabled.status.success(), "{}", String::from_utf8_lossy(&disabled.stderr) ); let blocked = Command::new(&executable) .args(["--dever-tenant-cmd", "7", "content.article.count", "{}"]) .output() .unwrap(); assert!(!blocked.status.success()); assert!( String::from_utf8_lossy(&blocked.stderr).contains("tenant component 'content' is disabled"), "{}", String::from_utf8_lossy(&blocked.stderr), ); let enabled = Command::new(&executable) .args(["--dever-tenant-component", "7", "enable", "content"]) .output() .unwrap(); assert!( enabled.status.success(), "{}", String::from_utf8_lossy(&enabled.stderr) ); let restored = Command::new(&executable) .args(["--dever-tenant-cmd", "7", "content.article.count", "{}"]) .output() .unwrap(); assert!( restored.status.success(), "{}", String::from_utf8_lossy(&restored.stderr) ); let runtime = tokio::runtime::Builder::new_current_thread() .enable_all() .build() .unwrap(); runtime.block_on(async { let tenant = Database::open(&tenant_database, 1, 100).unwrap(); let marker = tenant .query( "SELECT tenant_id FROM _dever_tenant_ready WHERE marker = 1", Vec::new(), ) .await .unwrap(); assert_eq!(marker[0].get(0).unwrap(), &Value::Int(7)); tenant .query( "SELECT id, title, created_at FROM article LIMIT 1", Vec::new(), ) .await .unwrap(); let platform = Database::open(directory.path().join("data/platform.db"), 1, 100).unwrap(); assert!( platform .query( "SELECT id, title, created_at FROM article LIMIT 1", Vec::new() ) .await .is_err() ); }); } #[test] fn generated_command_rolls_back_when_output_encoding_fails() { let sources = source(&[ ( "content/article/model.dever", "type Article { title: Text(1, 32) }", ), ( "content/article/app.dever", "create() (article: model.Article) { article = model.create({ title = \"published\" }) }", ), ("content/article/api.dever", "cmd create = app.create"), ]); let program = dever_core::check(&sources).unwrap(); let code = dever_core::native::emit_project( &program, &sources, RuntimeProfile { sqlite: true, postgres: false, }, ) .unwrap(); assert!(code.contains("let encoded = match cmd_output_0_encode(&value)")); assert!(code.contains("Err(error) => { let rollback = transaction.rollback().await;")); }