use std::collections::BTreeMap; use picloud_executor_core::{Engine, ExecError, ExecRequest, InvocationType, Limits, LogLevel}; use picloud_shared::{ AppId, ExecutionId, KvEventOp, RequestId, ScriptId, ScriptSandbox, Services, TriggerEvent, }; use serde_json::json; fn req(body: serde_json::Value) -> ExecRequest { let execution_id = ExecutionId::new(); ExecRequest { execution_id, request_id: RequestId::new(), script_id: ScriptId::new(), script_name: "test".into(), invocation_type: InvocationType::Http, path: "/test".into(), headers: BTreeMap::new(), body, params: BTreeMap::new(), query: BTreeMap::new(), rest: String::new(), sandbox_overrides: ScriptSandbox::default(), app_id: AppId::new(), principal: None, trigger_depth: 0, root_execution_id: execution_id, is_dead_letter_handler: false, event: None, } } fn engine() -> Engine { Engine::new(Limits::default(), Services::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, Services::default()); // 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_params_query_rest() { let engine = engine(); let mut r = req(json!(null)); r.params.insert("name".into(), "alice".into()); r.params.insert("post".into(), "42".into()); r.query.insert("tab".into(), "details".into()); r.rest = "extra/path".into(); let src = r" #{ statusCode: 200, body: #{ name: ctx.request.params.name, post: ctx.request.params.post, tab: ctx.request.query.tab, rest: ctx.request.rest } } "; let resp = engine.execute(src, r).unwrap(); assert_eq!( resp.body, json!({ "name": "alice", "post": "42", "tab": "details", "rest": "extra/path" }) ); } #[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); } #[test] fn ctx_event_absent_for_direct_invocations() { // Scripts not fired through the triggers framework see no // `ctx.event` key — they can use `"event" in ctx` to detect. let src = r#" if "event" in ctx { #{ statusCode: 500, body: "should be absent" } } else { "absent" } "#; let resp = engine().execute(src, req(json!(null))).unwrap(); assert_eq!(resp.body, json!("absent")); } #[test] fn ctx_event_kv_shape_matches_design_notes() { // Build an ExecRequest mimicking what the dispatcher hands a // KV-triggered handler — `event = Some(TriggerEvent::Kv { … })`. let mut r = req(json!(null)); r.event = Some(TriggerEvent::Kv { op: KvEventOp::Insert, collection: "widgets".into(), key: "k1".into(), value: Some(json!({ "n": 1 })), }); let src = r" #{ source: ctx.event.source, op: ctx.event.op, collection: ctx.event.kv.collection, key: ctx.event.kv.key, value: ctx.event.kv.value } "; let resp = engine().execute(src, r).unwrap(); assert_eq!( resp.body, json!({ "source": "kv", "op": "insert", "collection": "widgets", "key": "k1", "value": { "n": 1 } }) ); } #[test] fn ctx_event_kv_delete_has_unit_value() { let mut r = req(json!(null)); r.event = Some(TriggerEvent::Kv { op: KvEventOp::Delete, collection: "widgets".into(), key: "k1".into(), value: None, }); let src = r" #{ op: ctx.event.op, value_is_unit: ctx.event.kv.value == () } "; let resp = engine().execute(src, r).unwrap(); assert_eq!(resp.body, json!({ "op": "delete", "value_is_unit": true })); }