use super::{Directory, fixture}; mod auth; mod database; mod http; mod job; mod run; mod tenant; mod transport; mod upload; use dever::compiler::{build, project::Project}; use std::{ path::Path, process::{Command, Output}, }; fn execute(binary: &Path, name: &str, input: &str) -> Output { Command::new(binary).args([name, input]).output().unwrap() } #[test] fn application_commands_execute_with_strict_typed_json_and_original_faults() { let directory = Directory::new(); let project = Project::load(&fixture().with_file_name("application")).unwrap(); let program = project.check().unwrap(); assert_eq!(program.commands.len(), 9); let binary = build::build(&program, &project.sources, "main", &directory.0.join("app")).unwrap(); for (name, input, data) in [ ("greet", r#"{"name":"雪"}"#, r#""[Hello, 雪]""#), ("double", r#"{"value":21}"#, "42"), ("pair", r#"{"value":41}"#, r#"{"first":41,"second":42}"#), ("empty", "{}", "null"), ("positive", r#"{"value":7}"#, "7"), ("divide", r#"{"value":6,"divisor":2}"#, "3.0"), ("maybe", "{}", "null"), ("maybe", r#"{"value":null}"#, "null"), ("maybe", r#"{"value":7}"#, "7"), ( "maybe", r#"{"value":-9223372036854775808}"#, "-9223372036854775808", ), ( "maybe", r#"{"value":9223372036854775807}"#, "9223372036854775807", ), ( "echo", r#"{"value":{"id":"a","name":"雪","scores":[1,2]}}"#, r#"{"id":"a","name":"雪","scores":[1,2],"ratio":null}"#, ), ( "many", r#"{"rows":[{"id":"a","name":"雪","scores":[],"ratio":0.5}]}"#, r#"[{"id":"a","name":"雪","scores":[],"ratio":0.5}]"#, ), ] { let result = execute(&binary, &format!("demo.greeting.{name}"), input); assert!( result.status.success(), "{name}: {}", String::from_utf8_lossy(&result.stderr) ); assert_eq!( String::from_utf8(result.stdout).unwrap(), format!("{{\"code\":0,\"message\":\"ok\",\"data\":{data}}}\n") ); } let name = "雪".repeat(24000); let input = format!(r#"{{"rows":[{{"id":"a","name":"{name}","scores":[1,2]}}]}}"#); let result = execute(&binary, "demo.greeting.many", &input); assert!( result.status.success(), "{}", String::from_utf8_lossy(&result.stderr) ); assert_eq!( String::from_utf8(result.stdout).unwrap(), format!( "{{\"code\":0,\"message\":\"ok\",\"data\":[{{\"id\":\"a\",\"name\":\"{name}\",\"scores\":[1,2],\"ratio\":null}}]}}\n" ) ); let input = format!(r#"{{"rows":[{{"id":"a","name":"{name}","na\u006de":"duplicate","scores":[]}}]}}"#); let result = execute(&binary, "demo.greeting.many", &input); assert!(!result.status.success()); assert!(result.stdout.is_empty()); for (name, input) in [ ("double", "{}"), ("double", r#"{"value":1,"value":2}"#), ("double", r#"{"value":1,"extra":2}"#), ("double", "[1]"), ("double", r#"{"value":1.5}"#), ("double", r#"{"value":9223372036854775808}"#), ("double", r#"{"value":"1"}"#), ("double", r#"{"value":null}"#), ("maybe", r#"{"value":"7"}"#), ("maybe", r#"{"value":null,"value":7}"#), ("double", r#"{"value":1} {}"#), ("echo", r#"{"value":["a","b",[],null]}"#), ("echo", r#"{"value":{"id":1,"name":"a","scores":[]}}"#), ( "echo", r#"{"value":{"id":"a","name":"b","name":"c","scores":[]}}"#, ), ( "echo", r#"{"value":{"id":"a","name":"b","scores":[],"extra":0}}"#, ), ("many", r#"{"rows":[["a","b",[],null]]}"#), ( "many", r#"{"rows":[{"id":"a","name":"b","scores":[],"na\u006de":"c"}]}"#, ), ( "many", r#"{"rows":[{"id":"a","name":"b","scores":[],"ratio":null,"ratio":0.5}]}"#, ), ( "many", r#"{"rows":[{"id":"a","name":"b","scores":[],"extra":{}}]}"#, ), ("empty", "[]"), ("divide", r#"{"value":1e400,"divisor":2}"#), ] { let result = execute(&binary, &format!("demo.greeting.{name}"), input); assert!(!result.status.success(), "accepted {name}: {input}"); assert!(result.stdout.is_empty()); assert!( String::from_utf8_lossy(&result.stderr).contains("invalid command input"), "{name} {input}: {}", String::from_utf8_lossy(&result.stderr) ); } for (name, input, message) in [ ( "double", r#"{"value":9223372036854775807}"#, "integer arithmetic fault", ), ("positive", r#"{"value":-1}"#, "Problem.Rejected"), ] { let result = execute(&binary, &format!("demo.greeting.{name}"), input); assert!(!result.status.success()); assert!(result.stdout.is_empty()); let error = String::from_utf8(result.stderr).unwrap(); assert!( error.contains("domain.dever:") && error.contains(message), "{error}" ); } let result = execute( &binary, "demo.greeting.divide", r#"{"value":1,"divisor":0}"#, ); assert!(!result.status.success()); assert!(result.stdout.is_empty()); assert!(String::from_utf8_lossy(&result.stderr).contains("non-finite Float")); for arguments in [ vec![], vec!["demo.greeting.empty"], vec!["missing", "{}"], vec!["demo.greeting.empty", "{}", "extra"], ] { let result = Command::new(&binary).args(arguments).output().unwrap(); assert!(!result.status.success()); assert!(result.stdout.is_empty()); } let rejected = directory.0.join("bypass"); assert!( build::build( &program, &project.sources, "demo.utility.decorate", &rejected ) .is_err() ); assert!(!rejected.exists()); } fn source(root: &Path, relative: &str) { let path = root.join(relative); std::fs::create_dir_all(path.parent().unwrap()).unwrap(); std::fs::write(path, "layout validation precedes parsing").unwrap(); } #[test] fn application_layout_checks_roles_topics_and_project_root() { for (files, needle) in [ ( vec!["module/shop/account/main.dever"], "expected component/domain/role", ), ( vec!["module/shop/account/test.dever"], "role 'test' is not implemented yet", ), ( vec![ "module/shop/account/app.dever", "module/shop/account/app/read.dever", ], "not both", ), ( vec!["module/shop/account/domain/rules/read.dever"], "must be flat", ), ( vec!["module/shop/account/app/utils.dever"], "generic application", ), ] { let directory = Directory::new(); for path in files { source(&directory.0, path); } let error = Project::load(&directory.0) .err() .expect("invalid application layout accepted"); assert!(error.contains(needle), "{error}"); } let directory = Directory::new(); for path in [ "module/shop/account/app/read.dever", "module/shop/account/app/write.dever", "module/shop/account/api.dever", "module/shop/account/job.dever", "module/shop/account/api/admin/read.dever", "module/shop/account/model.dever", "module/shop/account/model/session.dever", ] { source(&directory.0, path); } assert!(Project::load(&directory.0).is_ok()); for relative in [ "module", "module/shop", "module/shop/account/app", "module/shop/account/app/read.dever", ] { let error = Project::load(&directory.0.join(relative)) .err() .expect("project root bypass accepted"); assert!(error.contains("project root"), "{error}"); } let linked = Directory::new(); std::os::unix::fs::symlink(directory.0.join("module"), linked.0.join("module")).unwrap(); assert!( Project::load(&linked.0) .err() .unwrap() .contains("not a link") ); }