use super::*; mod defaults; mod migration; #[cfg(feature = "postgres")] pub(super) mod postgres; mod query; mod read; mod relation; mod scalar; mod text; #[test] fn database_source_compiles_and_persists_with_entry_rollback() { let directory = Directory::new(); let project = Project::load(&fixture().with_file_name("database")).unwrap(); let program = project.check().unwrap(); let binary = build::build( &program, &project.sources, "main", &directory.0.join("database"), ) .unwrap(); std::fs::create_dir(directory.0.join("config")).unwrap(); std::fs::write(directory.0.join("config/setting.json"), r#"{"database":{"default":{"type":"sqlite","path":"data/account.db","max_connections":1,"max_page_size":20}}}"#).unwrap(); let run = |name: &str, input: &str| { let result = execute(&binary, &format!("demo.account.{name}"), input); assert!( result.status.success(), "{name}: {}", String::from_utf8_lossy(&result.stderr) ); serde_json::from_slice::(&result.stdout).unwrap()["data"].clone() }; let created = run("create", r#"{"name":"Dever 中文"}"#); assert_eq!(created["id"], 1); assert_eq!(created["name"], "Dever 中文"); assert!(created["note"].is_null()); assert!(created["created_at"].as_str().unwrap().ends_with('Z')); assert_eq!(run("get", r#"{"id":1}"#), created); for input in [ r#"{"name":"Dever 中文"}"#.to_owned(), r#"{"name":""}"#.to_owned(), serde_json::json!({"name": "雪".repeat(65)}).to_string(), ] { assert!( !execute(&binary, "demo.account.create", &input) .status .success() ); } assert_eq!( run("rename", r#"{"id":1,"name":"updated"}"#)["name"], "updated" ); let page = run("search", r#"{"name":"updated","page":1,"size":2}"#); assert_eq!(page["items"][0]["id"], 1); assert_eq!(page["total"], 1); assert_eq!(page["pages"], 1); assert_eq!( run("search", r#"{"name":"updated","page":2,"size":2}"#)["items"], serde_json::json!([]) ); assert_eq!(run("find", "{}")["id"], 1); assert!(run("find", r#"{"note":"missing"}"#).is_null()); assert!( !execute( &binary, "demo.account.search", r#"{"name":"updated","page":1,"size":21}"# ) .status .success() ); let failed = execute( &binary, "demo.account.explode", r#"{"name":"rollback","value":9223372036854775807}"#, ); assert!(!failed.status.success()); assert!(String::from_utf8_lossy(&failed.stderr).contains("integer arithmetic fault")); assert_eq!(run("count", "{}"), 1); assert_eq!(run("remove", r#"{"id":1}"#), 1); assert_eq!(run("count", "{}"), 0); let embedded_nul = format!("{}\0", "雪".repeat(63)); let nul_record = run( "create", &serde_json::json!({"name": embedded_nul}).to_string(), ); assert_eq!(nul_record["name"], embedded_nul); assert!( !execute( &binary, "demo.account.create", &serde_json::json!({"name": format!("{}\0", "雪".repeat(64))}).to_string() ) .status .success() ); assert_eq!( run( "remove", &serde_json::json!({"id": nul_record["id"]}).to_string() ), 1 ); let missing = execute(&binary, "demo.account.get", r#"{"id":1}"#); assert!(!missing.status.success()); assert!(String::from_utf8_lossy(&missing.stderr).contains("database operation failed")); assert!( !execute( &binary, "demo.account.stage", r#"{"name":"committed stage","value":9223372036854775807}"# ) .status .success() ); assert_eq!(run("count", "{}"), 1); let stage = run("find", "{}"); assert_eq!(stage["name"], "committed stage"); run( "remove", &serde_json::json!({"id": stage["id"]}).to_string(), ); http_round_trip(&binary, &directory.0); } fn http_round_trip(binary: &Path, directory: &Path) { use super::http::{Server, request}; use std::{ net::{TcpListener, TcpStream}, time::{Duration, Instant}, }; let listener = TcpListener::bind("127.0.0.1:0").unwrap(); let port = listener.local_addr().unwrap().port(); drop(listener); let config = directory.join("setting-http.json"); std::fs::write(&config, format!(r#"{{"http":{{"host":"127.0.0.1","port":{port}}},"sites":{{"public":{{"path":"","hosts":["fixture.test"]}}}},"database":{{"default":{{"type":"sqlite","path":"data/account.db","max_connections":1,"max_page_size":20}}}}}}"#)).unwrap(); let mut server = Server( Command::new(binary) .args(["--config", config.to_str().unwrap()]) .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()); assert!(Instant::now() < deadline); std::thread::sleep(Duration::from_millis(10)); } let (status, body) = request( port, "POST", "/demo/account/create", "fixture.test", "Content-Type: application/json\r\n", r#"{"name":"HTTP 中文"}"#, ); assert_eq!(status, 200, "{body}"); let created: serde_json::Value = serde_json::from_str(&body).unwrap(); let id = created["data"]["id"].as_i64().unwrap(); let (status, body) = request( port, "GET", &format!("/demo/account/get?id={id}"), "fixture.test", "", "", ); assert_eq!(status, 200, "{body}"); assert_eq!( serde_json::from_str::(&body).unwrap(), created ); assert_eq!( request( port, "POST", "/demo/account/explode", "fixture.test", "Content-Type: application/json\r\n", r#"{"name":"rollback","value":9223372036854775807}"# ) .0, 500 ); assert_eq!( request( port, "GET", "/demo/account/get?id=99999", "fixture.test", "", "" ) .0, 500 ); let count = execute(binary, "demo.account.count", "{}"); assert!(count.status.success()); assert_eq!( serde_json::from_slice::(&count.stdout).unwrap()["data"], 1 ); assert_eq!( request( port, "POST", "/demo/account/stage", "fixture.test", "Content-Type: application/json\r\n", r#"{"name":"HTTP committed stage","value":9223372036854775807}"# ) .0, 500 ); let count = execute(binary, "demo.account.count", "{}"); assert!(count.status.success()); assert_eq!( serde_json::from_slice::(&count.stdout).unwrap()["data"], 2 ); assert!( Command::new("sh") .args([ "-c", "kill -TERM \"$1\"", "dever-test", &server.0.id().to_string() ]) .status() .unwrap() .success() ); let deadline = Instant::now() + Duration::from_secs(5); loop { if let Some(status) = server.0.try_wait().unwrap() { assert!(status.success()); break; } assert!(Instant::now() < deadline); std::thread::sleep(Duration::from_millis(10)); } }