#![allow(clippy::unwrap_used, clippy::panic)]
use std::{path::PathBuf, process::Command};
use tempfile::TempDir;
fn fixture_module_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/functions")
}
fn cli() -> Command {
Command::new(env!("CARGO_BIN_EXE_fraiseql-cli"))
}
fn write_schema(dir: &TempDir) -> PathBuf {
let module_dir = fixture_module_dir();
let schema = serde_json::json!({
"types": [],
"queries": [],
"mutations": [],
"functions": {
"module_dir": module_dir,
"definitions": [
{
"name": "notify_approved",
"trigger": "after:mutation:Order:update",
"runtime": "Deno",
"timeout_ms": null,
"when": [ { "field": "status", "changed_to": "approved" } ],
"re_runnable": false
}
]
}
});
let path = dir.path().join("schema.compiled.json");
std::fs::write(&path, serde_json::to_string_pretty(&schema).unwrap()).unwrap();
path
}
fn write_json(dir: &TempDir, name: &str, value: &serde_json::Value) -> PathBuf {
let path = dir.path().join(name);
std::fs::write(&path, serde_json::to_string(value).unwrap()).unwrap();
path
}
#[test]
fn matching_payload_runs_and_reports_result_and_host_ops() {
let dir = TempDir::new().unwrap();
let schema = write_schema(&dir);
let payload = write_json(
&dir,
"payload.json",
&serde_json::json!({
"event_kind": "update",
"old": { "id": "o-1", "status": "pending" },
"new": { "id": "o-1", "status": "approved" }
}),
);
let mock_query = write_json(
&dir,
"query.json",
&serde_json::json!([ { "response": { "data": { "markNotified": { "id": "o-1" } } } } ]),
);
let out = cli()
.args(["functions", "invoke", "notify_approved"])
.arg("--payload")
.arg(&payload)
.arg("--schema")
.arg(&schema)
.arg("--mock-query")
.arg(&mock_query)
.args(["--idempotency-token", "tok-123"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
let code = out.status.code().unwrap_or(-1);
assert_eq!(code, 0, "a matching payload runs (exit 0); stdout:\n{stdout}");
assert!(stdout.contains("notified"), "the guest's result is printed:\n{stdout}");
assert!(stdout.contains("tok-123"), "the injected token is visible:\n{stdout}");
assert!(stdout.contains("host ops"), "host-op calls are reported:\n{stdout}");
assert!(stdout.contains("query"), "the guest's query op is recorded:\n{stdout}");
}
#[test]
fn non_matching_predicate_exits_predicate_no_match() {
let dir = TempDir::new().unwrap();
let schema = write_schema(&dir);
let payload = write_json(
&dir,
"payload.json",
&serde_json::json!({
"event_kind": "update",
"old": { "id": "o-2", "status": "approved" },
"new": { "id": "o-2", "status": "approved" }
}),
);
let out = cli()
.args(["functions", "invoke", "notify_approved"])
.arg("--payload")
.arg(&payload)
.arg("--schema")
.arg(&schema)
.arg("--explain")
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
let code = out.status.code().unwrap_or(-1);
assert_eq!(code, 3, "a non-matching predicate exits 3 (no isolate); stdout:\n{stdout}");
assert!(stdout.contains("NO MATCH"), "--explain shows why it did not fire:\n{stdout}");
}
#[test]
fn mock_query_miss_fails_loud_and_the_guest_errors() {
let dir = TempDir::new().unwrap();
let schema = write_schema(&dir);
let payload = write_json(
&dir,
"payload.json",
&serde_json::json!({
"event_kind": "update",
"old": { "id": "o-3", "status": "pending" },
"new": { "id": "o-3", "status": "approved" }
}),
);
let mock_query = write_json(
&dir,
"query.json",
&serde_json::json!([ { "query_contains": "somethingElse", "response": {} } ]),
);
let out = cli()
.args(["functions", "invoke", "notify_approved"])
.arg("--payload")
.arg(&payload)
.arg("--schema")
.arg(&schema)
.arg("--mock-query")
.arg(&mock_query)
.output()
.unwrap();
let code = out.status.code().unwrap_or(-1);
assert_eq!(code, 4, "an unmatched mock fails the op loud → guest error (exit 4)");
}
#[test]
fn unknown_function_is_a_config_error() {
let dir = TempDir::new().unwrap();
let schema = write_schema(&dir);
let payload = write_json(&dir, "payload.json", &serde_json::json!({ "id": "x" }));
let out = cli()
.args(["functions", "invoke", "does_not_exist"])
.arg("--payload")
.arg(&payload)
.arg("--schema")
.arg(&schema)
.output()
.unwrap();
let code = out.status.code().unwrap_or(-1);
assert_eq!(code, 1, "an unknown function name is a config error (exit 1)");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("does_not_exist") || stderr.to_lowercase().contains("no function"),
"the error names the missing function:\n{stderr}"
);
}