#![cfg(feature = "constructors-wasm")]
use std::path::PathBuf;
use std::sync::OnceLock;
use std::time::Duration;
use cloacina::packaging::constructor_provider::{
package_constructor_provider, ProviderPackageOptions,
};
use cloacina::registry::loader::constructor_loader::{
load_constructor, load_trigger_constructor, ConstructorBinding, TriggerBinding,
};
use cloacina::registry::loader::{grants::ResolvedGrants, unpack_provider_archive};
use cloacina::{Context, Runtime, TaskNamespace};
use serde::Serialize;
#[derive(Serialize)]
struct TriggerConfig {
should_fire: bool,
message: String,
}
#[derive(Serialize)]
struct TaskConfig {
prefix: String,
}
fn examples_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../examples/constructor-contract")
}
fn stage_into(work: &tempfile::TempDir, providers: &std::path::Path, fixture: &str) {
let archive = work.path().join(format!("{fixture}.cloacina"));
let opts = ProviderPackageOptions {
crate_dir: examples_dir().join(fixture),
output: Some(archive.clone()),
sign_key: None,
manifest_bin: "emit_manifest".to_string(),
runtime: cloacina_constructor_contract::ProviderRuntime::Wasm,
release: true,
};
package_constructor_provider(&opts).expect("package_constructor_provider");
unpack_provider_archive(&archive, providers, &[]).expect("unpack provider archive");
}
fn providers_dir() -> &'static PathBuf {
static PROVIDERS: OnceLock<(tempfile::TempDir, PathBuf)> = OnceLock::new();
&PROVIDERS
.get_or_init(|| {
let work = tempfile::TempDir::new().unwrap();
let providers = work.path().join("providers");
std::fs::create_dir_all(&providers).unwrap();
stage_into(&work, &providers, "trigger-constructor-macro-fixture");
stage_into(&work, &providers, "task-constructor-macro-fixture");
(work, providers)
})
.1
}
#[tokio::test]
async fn non_trigger_primitive_fails_closed() {
let result = load_trigger_constructor(
providers_dir(),
"prefix",
"prefix",
&TaskConfig { prefix: "x".into() },
TriggerBinding::default(),
&ResolvedGrants::deny_all(),
);
match result {
Ok(_) => panic!("a Task member must not load as a Trigger"),
Err(err) => assert!(format!("{err}").contains("not Trigger"), "got: {err}"),
}
}
#[tokio::test]
async fn load_constructor_registers_trigger_into_runtime() {
let runtime = Runtime::empty();
assert!(runtime.get_trigger("heartbeat").is_none());
load_constructor(
&runtime,
providers_dir(),
"heartbeat",
"heartbeat",
&TriggerConfig {
should_fire: true,
message: "registered fire".into(),
},
ConstructorBinding::Trigger(TriggerBinding {
poll_interval: Duration::from_secs(30),
allow_concurrent: false,
workflow_name: "wf".into(),
cron_expression: None,
}),
&ResolvedGrants::deny_all(),
)
.expect("load_constructor (trigger)");
assert_eq!(runtime.trigger_names(), vec!["heartbeat".to_string()]);
let trigger = runtime
.get_trigger("heartbeat")
.expect("registered trigger");
assert_eq!(trigger.poll_interval(), Duration::from_secs(30));
let result = trigger.poll().await.expect("poll registered trigger");
let ctx = result.into_context().expect("fire");
assert_eq!(
ctx.get("reason"),
Some(&serde_json::json!("registered fire"))
);
}
#[tokio::test]
async fn load_constructor_registers_task_into_runtime() {
let runtime = Runtime::empty();
let ns = TaskNamespace::new("public", "prefix", "ops", "prefix");
assert!(runtime.get_task(&ns).is_none());
load_constructor(
&runtime,
providers_dir(),
"prefix",
"prefix",
&TaskConfig {
prefix: "hello, ".into(),
},
ConstructorBinding::Task {
namespace: ns.clone(),
},
&ResolvedGrants::deny_all(),
)
.expect("load_constructor (task)");
assert!(runtime.trigger_names().is_empty());
let task = runtime.get_task(&ns).expect("registered task");
let mut ctx = Context::new();
ctx.insert("name", serde_json::json!("world")).unwrap();
let out = task.execute(ctx).await.expect("execute registered task");
assert_eq!(out.get("result"), Some(&serde_json::json!("hello, world")));
}
#[tokio::test]
async fn load_constructor_rejects_mismatched_binding() {
let runtime = Runtime::empty();
let result = load_constructor(
&runtime,
providers_dir(),
"prefix",
"prefix",
&TaskConfig { prefix: "x".into() },
ConstructorBinding::Trigger(TriggerBinding::default()),
&ResolvedGrants::deny_all(),
);
match result {
Ok(_) => panic!("mismatched binding must fail closed"),
Err(err) => assert!(format!("{err}").contains("does not match"), "got: {err}"),
}
assert!(runtime.trigger_names().is_empty());
}