#![allow(clippy::unwrap_used)]
use chrono::Utc;
use crate::{
EventPayload, FunctionModule, ResourceLimits, RuntimeType, host::NoopHostContext,
observer::FunctionObserver,
};
fn test_event() -> EventPayload {
EventPayload {
trigger_type: "test".to_string(),
entity: "Test".to_string(),
event_kind: "created".to_string(),
data: serde_json::json!({"value": 42}),
timestamp: Utc::now(),
}
}
#[tokio::test]
#[cfg(feature = "runtime-deno")]
async fn test_function_observer_dispatches_js_to_deno() {
let source = "export default async (event) => event;".to_string();
let module = FunctionModule::from_source("test_js".to_string(), source, RuntimeType::Deno);
let mut observer = FunctionObserver::new();
let deno_runtime =
crate::runtime::deno::DenoRuntime::new(&crate::runtime::deno::DenoConfig::default())
.unwrap();
observer.register_runtime(RuntimeType::Deno, deno_runtime);
let event = test_event();
let event_data = event.data.clone();
let result = observer
.invoke(&module, event.clone(), &NoopHostContext::new(event), ResourceLimits::default())
.await;
assert!(result.is_ok(), "Observer should dispatch to Deno runtime");
let result = result.unwrap();
assert_eq!(result.value, Some(event_data));
}
#[tokio::test]
#[cfg(feature = "runtime-deno")]
async fn test_function_observer_dispatches_ts_to_deno() {
let source = "export default async (event) => event;".to_string();
let module = FunctionModule::from_source("test_ts".to_string(), source, RuntimeType::Deno);
let mut observer = FunctionObserver::new();
let deno_runtime =
crate::runtime::deno::DenoRuntime::new(&crate::runtime::deno::DenoConfig::default())
.unwrap();
observer.register_runtime(RuntimeType::Deno, deno_runtime);
let event = test_event();
let event_data = event.data.clone();
let result = observer
.invoke(&module, event.clone(), &NoopHostContext::new(event), ResourceLimits::default())
.await;
assert!(result.is_ok(), "Observer should dispatch to Deno runtime for TypeScript");
let result = result.unwrap();
assert_eq!(result.value, Some(event_data));
}
#[tokio::test]
#[cfg(all(feature = "runtime-wasm", feature = "runtime-deno"))]
async fn test_function_observer_wasm_and_deno_coexist() {
let js_source = "export default async (event) => event;".to_string();
let js_module =
FunctionModule::from_source("test_js".to_string(), js_source, RuntimeType::Deno);
let mut observer = FunctionObserver::new();
let deno_runtime =
crate::runtime::deno::DenoRuntime::new(&crate::runtime::deno::DenoConfig::default())
.unwrap();
observer.register_runtime(RuntimeType::Deno, deno_runtime);
let wasm_runtime =
crate::runtime::wasm::WasmRuntime::new(&crate::runtime::wasm::WasmConfig::default())
.unwrap();
observer.register_runtime(RuntimeType::Wasm, wasm_runtime);
let event = test_event();
let event_data = event.data.clone();
let js_result = observer
.invoke(
&js_module,
event.clone(),
&NoopHostContext::new(event),
ResourceLimits::default(),
)
.await;
assert!(js_result.is_ok(), "Observer should dispatch JS to Deno runtime");
let js_result = js_result.unwrap();
assert_eq!(js_result.value, Some(event_data));
}
#[tokio::test]
async fn test_function_observer_unknown_runtime_returns_error() {
let source = "export default async (event) => event;".to_string();
let module = FunctionModule::from_source("test_unknown".to_string(), source, RuntimeType::Deno);
let observer = FunctionObserver::new();
let event = test_event();
let result = observer
.invoke(&module, event.clone(), &NoopHostContext::new(event), ResourceLimits::default())
.await;
assert!(result.is_err(), "Observer should return error for unregistered runtime");
let err = result.unwrap_err();
assert!(
matches!(err, fraiseql_error::FraiseQLError::Unsupported { .. }),
"Error should be Unsupported, got: {:?}",
err
);
}
#[test]
fn test_dispatch_entity_event_no_triggers_returns_empty() {
use std::collections::HashMap;
use crate::triggers::{TriggerRegistry, mutation::EntityEvent};
let observer = FunctionObserver::new();
let registry = TriggerRegistry::new();
let modules: HashMap<String, FunctionModule> = HashMap::new();
let event = EntityEvent {
entity: "User".to_string(),
event_kind: crate::triggers::mutation::EventKind::Insert,
old: None,
new: Some(serde_json::json!({ "id": 1, "name": "Alice" })),
timestamp: Utc::now(),
};
let matching = observer.find_after_mutation_triggers(®istry, &event);
assert!(matching.is_empty(), "empty registry → no matching triggers");
let _ = modules; }
#[test]
fn test_dispatch_entity_event_finds_matching_triggers() {
use crate::{
FunctionDefinition,
triggers::{
TriggerRegistry,
mutation::{EntityEvent, EventKind},
},
};
let defs = vec![FunctionDefinition::new(
"onUserCreated",
"after:mutation:User:insert",
RuntimeType::Deno,
)];
let registry = TriggerRegistry::load_from_definitions(&defs).unwrap();
let observer = FunctionObserver::new();
let insert_event = EntityEvent {
entity: "User".to_string(),
event_kind: EventKind::Insert,
old: None,
new: Some(serde_json::json!({ "id": 1 })),
timestamp: Utc::now(),
};
let matching = observer.find_after_mutation_triggers(®istry, &insert_event);
assert_eq!(matching.len(), 1, "should match 1 trigger for User insert");
let update_event = EntityEvent {
entity: "User".to_string(),
event_kind: EventKind::Update,
old: Some(serde_json::json!({ "id": 1, "name": "Old" })),
new: Some(serde_json::json!({ "id": 1, "name": "New" })),
timestamp: Utc::now(),
};
let matching = observer.find_after_mutation_triggers(®istry, &update_event);
assert!(matching.is_empty(), "update event should not match insert-only trigger");
}