//! `pic scripts invoke` — body sources (inline, `@file`, `@-`), header //! propagation, exit-code semantics for non-2xx responses, and 404 //! handling for unknown ids. use predicates::prelude::*; use serde_json::Value; use crate::common; #[ignore = "needs DATABASE_URL pointing at a running Postgres"] #[test] fn invoke_with_inline_json_body_echoes() { let Some(fx) = common::fixture_or_skip() else { return; }; let env = common::admin_env(fx); let (id, _guard) = common::deploy_fixture(&env, "invoke-inline", "echo.rhai"); let out = common::pic_as(&env) .args(["scripts", "invoke", &id, "--body", r#"{"x":1}"#]) .output() .expect("invoke"); assert!(out.status.success(), "invoke failed: {out:?}"); let parsed: Value = serde_json::from_slice(&out.stdout).expect("stdout JSON"); assert_eq!(parsed["body"]["x"], 1, "echoed body: {parsed}"); } #[ignore = "needs DATABASE_URL pointing at a running Postgres"] #[test] fn invoke_with_file_body() { let Some(fx) = common::fixture_or_skip() else { return; }; let env = common::admin_env(fx); let (id, _guard) = common::deploy_fixture(&env, "invoke-file", "echo.rhai"); let tmp = tempfile::NamedTempFile::new().expect("tempfile"); std::fs::write(tmp.path(), r#"{"src":"file"}"#).unwrap(); let body_arg = format!("@{}", tmp.path().display()); let out = common::pic_as(&env) .args(["scripts", "invoke", &id, "--body", &body_arg]) .output() .expect("invoke"); assert!(out.status.success(), "invoke failed: {out:?}"); let parsed: Value = serde_json::from_slice(&out.stdout).expect("stdout JSON"); assert_eq!(parsed["body"]["src"], "file", "echoed body: {parsed}"); } #[ignore = "needs DATABASE_URL pointing at a running Postgres"] #[test] fn invoke_with_stdin_body() { let Some(fx) = common::fixture_or_skip() else { return; }; let env = common::admin_env(fx); let (id, _guard) = common::deploy_fixture(&env, "invoke-stdin", "echo.rhai"); let assert = common::pic_as(&env) .args(["scripts", "invoke", &id, "--body", "@-"]) .write_stdin(r#"{"src":"stdin"}"#) .assert() .success(); let out = assert.get_output(); let parsed: Value = serde_json::from_slice(&out.stdout).expect("stdout JSON"); assert_eq!(parsed["body"]["src"], "stdin", "echoed body: {parsed}"); } #[ignore = "needs DATABASE_URL pointing at a running Postgres"] #[test] fn invoke_propagates_headers() { let Some(fx) = common::fixture_or_skip() else { return; }; let env = common::admin_env(fx); let (id, _guard) = common::deploy_fixture(&env, "invoke-hdr", "echo.rhai"); let out = common::pic_as(&env) .args([ "scripts", "invoke", &id, "-H", "X-Foo: bar", "-H", "X-Baz=qux", ]) .output() .expect("invoke"); assert!(out.status.success(), "invoke failed: {out:?}"); let parsed: Value = serde_json::from_slice(&out.stdout).expect("stdout JSON"); // HTTP normalises header names to lowercase. assert_eq!(parsed["headers"]["x-foo"], "bar", "echoed: {parsed}"); assert_eq!(parsed["headers"]["x-baz"], "qux", "echoed: {parsed}"); } #[ignore = "needs DATABASE_URL pointing at a running Postgres"] #[test] fn invoke_unknown_script_id_errors() { let Some(fx) = common::fixture_or_skip() else { return; }; let env = common::admin_env(fx); // Any well-formed UUID that doesn't exist server-side. The // orchestrator's `/execute/{id}` handler returns 404 specifically // for unknown ids — tighten the predicate so a regressed 401 // wouldn't sneak through. let bogus = "00000000-0000-0000-0000-000000000000"; common::pic_as(&env) .args(["scripts", "invoke", bogus]) .assert() .failure() .stderr(predicate::str::contains("HTTP 404")); } /// `pic invoke ` (top-level alias) and `pic scripts invoke ` /// must hit the same handler and produce identical-shape stdout. #[ignore = "needs DATABASE_URL pointing at a running Postgres"] #[test] fn top_level_invoke_alias_works() { let Some(fx) = common::fixture_or_skip() else { return; }; let env = common::admin_env(fx); let (id, _guard) = common::deploy_fixture(&env, "inv-alias", "hello.rhai"); let nested = common::pic_as(&env) .args(["scripts", "invoke", &id]) .output() .expect("scripts invoke"); assert!(nested.status.success()); let nested_body: Value = serde_json::from_slice(&nested.stdout).unwrap(); let aliased = common::pic_as(&env) .args(["invoke", &id]) .output() .expect("invoke (top-level)"); assert!(aliased.status.success()); let aliased_body: Value = serde_json::from_slice(&aliased.stdout).unwrap(); assert_eq!( nested_body, aliased_body, "top-level alias should produce identical body to scripts invoke" ); } #[ignore = "needs DATABASE_URL pointing at a running Postgres"] #[test] fn invoke_non_2xx_exits_nonzero_but_prints_body() { let Some(fx) = common::fixture_or_skip() else { return; }; let env = common::admin_env(fx); let (id, _guard) = common::deploy_fixture(&env, "invoke-500", "boom.rhai"); let out = common::pic_as(&env) .args(["scripts", "invoke", &id]) .output() .expect("invoke"); assert!(!out.status.success(), "expected non-zero exit: {out:?}"); let stderr = String::from_utf8_lossy(&out.stderr); assert!( stderr.contains("<- HTTP 500"), "stderr should report HTTP 500: {stderr}" ); let parsed: Value = serde_json::from_slice(&out.stdout) .unwrap_or_else(|e| panic!("stdout was not JSON ({e}): {:?}", out.stdout)); assert_eq!(parsed["ok"], false, "boom body: {parsed}"); assert_eq!(parsed["why"], "intentional", "boom body: {parsed}"); }