use std::collections::BTreeMap; use picloud_executor_core::{Engine, ExecError, ExecRequest, InvocationType, Limits, LogLevel}; use picloud_shared::{ExecutionId, RequestId, ScriptId, ScriptSandbox}; use serde_json::json; fn req(body: serde_json::Value) -> ExecRequest { ExecRequest { execution_id: ExecutionId::new(), request_id: RequestId::new(), script_id: ScriptId::new(), script_name: "test".into(), invocation_type: InvocationType::Http, path: "/test".into(), headers: BTreeMap::new(), body, sandbox_overrides: ScriptSandbox::default(), } } fn engine() -> Engine { Engine::new(Limits::default()) } #[test] fn validate_accepts_well_formed_script() { engine() .validate("let x = 1; #{ statusCode: 200, body: x }") .expect("valid script should validate"); } #[test] fn validate_rejects_syntax_errors() { let err = engine() .validate("this is not rhai @@@") .expect_err("invalid script should not validate"); assert!(matches!(err, ExecError::Parse(_))); } #[test] fn returns_unwrapped_value_as_200_body() { let resp = engine() .execute("42", req(json!(null))) .expect("should execute"); assert_eq!(resp.status_code, 200); assert_eq!(resp.body, json!(42)); assert!(resp.headers.is_empty()); } #[test] fn returns_structured_response_when_status_code_present() { let src = r#" #{ statusCode: 201, headers: #{ "x-test": "hello" }, body: #{ ok: true, msg: "created" } } "#; let resp = engine().execute(src, req(json!(null))).unwrap(); assert_eq!(resp.status_code, 201); assert_eq!( resp.headers.get("x-test").map(String::as_str), Some("hello") ); assert_eq!(resp.body, json!({ "ok": true, "msg": "created" })); } #[test] fn ctx_exposes_request_data() { let src = r" #{ statusCode: 200, body: #{ path: ctx.request.path, name: ctx.script_name, amount: ctx.request.body.amount } } "; let r = ExecRequest { path: "/payments".into(), body: json!({ "amount": 1234 }), script_name: "payments".into(), ..req(json!(null)) }; let resp = engine().execute(src, r).unwrap(); assert_eq!( resp.body, json!({ "path": "/payments", "name": "payments", "amount": 1234 }) ); } #[test] fn captures_log_calls() { let src = r#" log::info("starting"); log::warn("watch out", #{ count: 3 }); log::error("oops"); log::trace("deep diagnostic"); 42 "#; let resp = engine().execute(src, req(json!(null))).unwrap(); assert_eq!(resp.logs.len(), 4); let levels: Vec<_> = resp.logs.iter().map(|l| l.level).collect(); assert_eq!( levels, vec![ LogLevel::Info, LogLevel::Warn, LogLevel::Error, LogLevel::Trace ] ); assert_eq!(resp.logs[0].message, "starting"); assert_eq!(resp.logs[1].data, Some(json!({ "count": 3 }))); } #[test] fn enforces_operation_budget() { let limits = Limits { max_operations: 1_000, ..Limits::default() }; let engine = Engine::new(limits); // 10_000 iterations vastly exceeds 1_000 ops. let src = r"let n = 0; for i in 0..10000 { n += 1; } n"; let err = engine .execute(src, req(json!(null))) .expect_err("should exceed budget"); assert!(matches!(err, ExecError::OperationBudgetExceeded)); } #[test] fn per_request_sandbox_override_tightens_budget() { // Engine default is 1M ops — the script below would finish. // We override down to 500 ops on this single request; should fail. let engine = engine(); let src = r"let n = 0; for i in 0..10000 { n += 1; } n"; let r = ExecRequest { sandbox_overrides: ScriptSandbox { max_operations: Some(500), ..ScriptSandbox::default() }, ..req(json!(null)) }; let err = engine.execute(src, r).expect_err("override should tighten"); assert!(matches!(err, ExecError::OperationBudgetExceeded)); } #[test] fn override_only_replaces_specified_field() { // Tight string size, default everything else. Strings > 32 chars // should fail; loops up to default 1M ops should still pass. let engine = engine(); let small_string_ok = r#"let s = "hello"; #{ statusCode: 200, body: s }"#; let r1 = ExecRequest { sandbox_overrides: ScriptSandbox { max_string_size: Some(32), ..ScriptSandbox::default() }, ..req(json!(null)) }; let resp = engine.execute(small_string_ok, r1).unwrap(); assert_eq!(resp.body, json!("hello")); } #[test] fn runtime_error_is_mapped_to_runtime_variant() { let err = engine() .execute("1 / 0", req(json!(null))) .expect_err("division by zero should error"); assert!(matches!(err, ExecError::Runtime(_))); } #[test] fn module_import_is_blocked() { let err = engine() .execute(r#"import "evil" as e; 1"#, req(json!(null))) .expect_err("imports should be blocked"); // Module-not-found is reported as a runtime error via DummyModuleResolver. assert!(matches!(err, ExecError::Runtime(_) | ExecError::Parse(_))); } #[test] fn ctx_exposes_sdk_version() { let resp = engine() .execute("ctx.sdk_version", req(json!(null))) .unwrap(); // Whatever it is, it must look like "MAJOR.MINOR" — that's the // contract scripts feature-detect against. let v = resp.body.as_str().expect("sdk_version is a string"); let parts: Vec<&str> = v.split('.').collect(); assert_eq!(parts.len(), 2, "expected major.minor, got {v:?}"); assert!(parts[0].parse::().is_ok(), "major not numeric: {v:?}"); assert!(parts[1].parse::().is_ok(), "minor not numeric: {v:?}"); } #[test] fn body_passes_through_nested_json_round_trip() { let src = "#{ statusCode: 200, body: ctx.request.body }"; let body = json!({ "deep": { "list": [1, "two", 3.5, null, true, { "k": "v" }], "count": 6 } }); let resp = engine().execute(src, req(body.clone())).unwrap(); assert_eq!(resp.body, body); }