2026-03-25 22:32:07 +00:00
|
|
|
#![cfg(feature = "deno")]
|
|
|
|
|
|
|
|
|
|
use std::collections::HashMap;
|
2026-03-25 23:02:51 +00:00
|
|
|
use std::io::Write;
|
2026-03-25 22:32:07 +00:00
|
|
|
|
|
|
|
|
use wfe_yaml::executors::deno::config::{DenoConfig, DenoPermissions};
|
2026-03-25 23:02:51 +00:00
|
|
|
use wfe_yaml::executors::deno::module_loader::WfeModuleLoader;
|
2026-03-25 22:32:07 +00:00
|
|
|
use wfe_yaml::executors::deno::permissions::PermissionChecker;
|
2026-03-25 23:02:51 +00:00
|
|
|
use wfe_yaml::executors::deno::runtime::{create_runtime, would_auto_add_esm_sh};
|
2026-03-25 22:32:07 +00:00
|
|
|
use wfe_yaml::executors::deno::step::DenoStep;
|
|
|
|
|
|
|
|
|
|
use wfe_core::models::execution_pointer::ExecutionPointer;
|
|
|
|
|
use wfe_core::models::workflow_definition::WorkflowStep;
|
|
|
|
|
use wfe_core::models::workflow_instance::WorkflowInstance;
|
|
|
|
|
use wfe_core::traits::step::{StepBody, StepExecutionContext};
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Helpers
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
fn default_config(script: &str) -> DenoConfig {
|
|
|
|
|
DenoConfig {
|
|
|
|
|
script: Some(script.to_string()),
|
|
|
|
|
file: None,
|
|
|
|
|
permissions: DenoPermissions::default(),
|
|
|
|
|
modules: vec![],
|
|
|
|
|
env: HashMap::new(),
|
|
|
|
|
timeout_ms: None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn make_context<'a>(
|
|
|
|
|
step: &'a WorkflowStep,
|
|
|
|
|
workflow: &'a WorkflowInstance,
|
|
|
|
|
pointer: &'a ExecutionPointer,
|
|
|
|
|
) -> StepExecutionContext<'a> {
|
|
|
|
|
StepExecutionContext {
|
|
|
|
|
item: None,
|
|
|
|
|
execution_pointer: pointer,
|
|
|
|
|
persistence_data: None,
|
|
|
|
|
step,
|
|
|
|
|
workflow,
|
|
|
|
|
cancellation_token: tokio_util::sync::CancellationToken::new(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn make_test_fixtures(
|
|
|
|
|
data: serde_json::Value,
|
|
|
|
|
) -> (WorkflowStep, WorkflowInstance, ExecutionPointer) {
|
|
|
|
|
let mut step = WorkflowStep::new(0, "deno");
|
|
|
|
|
step.name = Some("test-deno-step".to_string());
|
|
|
|
|
let workflow = WorkflowInstance::new("test-workflow", 1, data);
|
|
|
|
|
let pointer = ExecutionPointer::new(0);
|
|
|
|
|
(step, workflow, pointer)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Integration tests
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn deno_inline_script_completes() {
|
|
|
|
|
let mut step = DenoStep::new(default_config("1 + 1;"));
|
|
|
|
|
let (ws, wf, ptr) = make_test_fixtures(serde_json::json!({}));
|
|
|
|
|
let ctx = make_context(&ws, &wf, &ptr);
|
|
|
|
|
let result = step.run(&ctx).await.unwrap();
|
|
|
|
|
assert!(result.proceed);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn deno_output_captured() {
|
|
|
|
|
let mut step = DenoStep::new(default_config(r#"output("greeting", "hello");"#));
|
|
|
|
|
let (ws, wf, ptr) = make_test_fixtures(serde_json::json!({}));
|
|
|
|
|
let ctx = make_context(&ws, &wf, &ptr);
|
|
|
|
|
let result = step.run(&ctx).await.unwrap();
|
|
|
|
|
assert!(result.proceed);
|
|
|
|
|
let data = result.output_data.unwrap();
|
|
|
|
|
assert_eq!(data["greeting"], serde_json::json!("hello"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn deno_inputs_available() {
|
|
|
|
|
let script = r#"
|
|
|
|
|
const data = inputs();
|
|
|
|
|
output("got_name", data.name);
|
|
|
|
|
"#;
|
|
|
|
|
let mut step = DenoStep::new(default_config(script));
|
|
|
|
|
let (ws, wf, ptr) = make_test_fixtures(serde_json::json!({"name": "alice"}));
|
|
|
|
|
let ctx = make_context(&ws, &wf, &ptr);
|
|
|
|
|
let result = step.run(&ctx).await.unwrap();
|
|
|
|
|
let data = result.output_data.unwrap();
|
|
|
|
|
assert_eq!(data["got_name"], serde_json::json!("alice"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn deno_script_error_fails_step() {
|
|
|
|
|
let mut step = DenoStep::new(default_config("throw new Error('boom');"));
|
|
|
|
|
let (ws, wf, ptr) = make_test_fixtures(serde_json::json!({}));
|
|
|
|
|
let ctx = make_context(&ws, &wf, &ptr);
|
|
|
|
|
let err = step.run(&ctx).await.unwrap_err();
|
|
|
|
|
let msg = err.to_string();
|
|
|
|
|
assert!(msg.contains("boom") || msg.contains("Error"), "got: {msg}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn deno_timeout_kills_execution() {
|
|
|
|
|
let mut config = default_config("while (true) {}");
|
|
|
|
|
config.timeout_ms = Some(100);
|
|
|
|
|
let mut step = DenoStep::new(config);
|
|
|
|
|
let (ws, wf, ptr) = make_test_fixtures(serde_json::json!({}));
|
|
|
|
|
let ctx = make_context(&ws, &wf, &ptr);
|
|
|
|
|
let err = step.run(&ctx).await.unwrap_err();
|
|
|
|
|
let msg = err.to_string();
|
|
|
|
|
assert!(msg.contains("timed out"), "got: {msg}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn deno_multiple_outputs() {
|
|
|
|
|
let script = r#"
|
|
|
|
|
output("a", 1);
|
|
|
|
|
output("b", "two");
|
|
|
|
|
output("c", true);
|
|
|
|
|
"#;
|
|
|
|
|
let mut step = DenoStep::new(default_config(script));
|
|
|
|
|
let (ws, wf, ptr) = make_test_fixtures(serde_json::json!({}));
|
|
|
|
|
let ctx = make_context(&ws, &wf, &ptr);
|
|
|
|
|
let result = step.run(&ctx).await.unwrap();
|
|
|
|
|
let data = result.output_data.unwrap();
|
|
|
|
|
assert_eq!(data["a"], serde_json::json!(1));
|
|
|
|
|
assert_eq!(data["b"], serde_json::json!("two"));
|
|
|
|
|
assert_eq!(data["c"], serde_json::json!(true));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn deno_log_works() {
|
|
|
|
|
let mut step = DenoStep::new(default_config(r#"log("hello from deno");"#));
|
|
|
|
|
let (ws, wf, ptr) = make_test_fixtures(serde_json::json!({}));
|
|
|
|
|
let ctx = make_context(&ws, &wf, &ptr);
|
|
|
|
|
// Should not crash.
|
|
|
|
|
let result = step.run(&ctx).await.unwrap();
|
|
|
|
|
assert!(result.proceed);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn deno_empty_script_completes() {
|
|
|
|
|
let mut step = DenoStep::new(default_config(""));
|
|
|
|
|
let (ws, wf, ptr) = make_test_fixtures(serde_json::json!({}));
|
|
|
|
|
let ctx = make_context(&ws, &wf, &ptr);
|
|
|
|
|
let result = step.run(&ctx).await.unwrap();
|
|
|
|
|
assert!(result.proceed);
|
|
|
|
|
assert!(result.output_data.is_none());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Permission integration tests
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn permission_net_allowed() {
|
|
|
|
|
let perms = DenoPermissions {
|
|
|
|
|
net: vec!["api.example.com".to_string()],
|
|
|
|
|
..Default::default()
|
|
|
|
|
};
|
|
|
|
|
let checker = PermissionChecker::from_config(&perms);
|
|
|
|
|
assert!(checker.check_net("api.example.com").is_ok());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn permission_net_denied() {
|
|
|
|
|
let perms = DenoPermissions::default();
|
|
|
|
|
let checker = PermissionChecker::from_config(&perms);
|
|
|
|
|
assert!(checker.check_net("evil.com").is_err());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn permission_env_denied() {
|
|
|
|
|
let perms = DenoPermissions {
|
|
|
|
|
env: vec!["SAFE_VAR".to_string()],
|
|
|
|
|
..Default::default()
|
|
|
|
|
};
|
|
|
|
|
let checker = PermissionChecker::from_config(&perms);
|
|
|
|
|
assert!(checker.check_env("SECRET_KEY").is_err());
|
|
|
|
|
assert!(checker.check_env("SAFE_VAR").is_ok());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Runtime creation tests
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn runtime_creation_succeeds() {
|
|
|
|
|
let config = default_config("1");
|
|
|
|
|
let rt = create_runtime(&config, serde_json::json!({"x": 1}), "test");
|
|
|
|
|
assert!(rt.is_ok());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn runtime_op_state_populated() {
|
|
|
|
|
let config = default_config("1");
|
|
|
|
|
let rt = create_runtime(&config, serde_json::json!({"foo": "bar"}), "my-step").unwrap();
|
|
|
|
|
let state = rt.op_state();
|
|
|
|
|
let state = state.borrow();
|
|
|
|
|
let inputs = state.borrow::<wfe_yaml::executors::deno::ops::WorkflowInputs>();
|
|
|
|
|
assert_eq!(inputs.data, serde_json::json!({"foo": "bar"}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Validation tests
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn validation_deno_step_missing_config() {
|
|
|
|
|
let yaml = r#"
|
|
|
|
|
workflow:
|
|
|
|
|
id: test
|
|
|
|
|
version: 1
|
|
|
|
|
steps:
|
|
|
|
|
- name: run_js
|
|
|
|
|
type: deno
|
|
|
|
|
"#;
|
|
|
|
|
let config = HashMap::new();
|
|
|
|
|
let result = wfe_yaml::load_workflow_from_str(yaml, &config);
|
|
|
|
|
assert!(result.is_err());
|
|
|
|
|
let msg = result.err().unwrap().to_string();
|
|
|
|
|
assert!(
|
|
|
|
|
msg.contains("config") || msg.contains("Deno"),
|
|
|
|
|
"got: {msg}"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn validation_deno_step_missing_script_and_file() {
|
|
|
|
|
let yaml = r#"
|
|
|
|
|
workflow:
|
|
|
|
|
id: test
|
|
|
|
|
version: 1
|
|
|
|
|
steps:
|
|
|
|
|
- name: run_js
|
|
|
|
|
type: deno
|
|
|
|
|
config:
|
|
|
|
|
env:
|
|
|
|
|
FOO: bar
|
|
|
|
|
"#;
|
|
|
|
|
let config = HashMap::new();
|
|
|
|
|
let result = wfe_yaml::load_workflow_from_str(yaml, &config);
|
|
|
|
|
assert!(result.is_err());
|
|
|
|
|
let msg = result.err().unwrap().to_string();
|
|
|
|
|
assert!(
|
|
|
|
|
msg.contains("script") || msg.contains("file"),
|
|
|
|
|
"got: {msg}"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn compilation_deno_step_produces_factory() {
|
|
|
|
|
let yaml = r#"
|
|
|
|
|
workflow:
|
|
|
|
|
id: test-deno
|
|
|
|
|
version: 1
|
|
|
|
|
steps:
|
|
|
|
|
- name: js_step
|
|
|
|
|
type: deno
|
|
|
|
|
config:
|
|
|
|
|
script: "output('key', 'val');"
|
|
|
|
|
"#;
|
|
|
|
|
let config = HashMap::new();
|
|
|
|
|
let compiled = wfe_yaml::load_workflow_from_str(yaml, &config).unwrap();
|
|
|
|
|
assert!(!compiled.step_factories.is_empty());
|
|
|
|
|
let (key, _factory) = &compiled.step_factories[0];
|
|
|
|
|
assert!(key.contains("deno"), "factory key should contain 'deno', got: {key}");
|
|
|
|
|
}
|
2026-03-25 23:02:51 +00:00
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Phase 4: HTTP op integration tests
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
fn config_with_net(script: &str, net_hosts: &[&str]) -> DenoConfig {
|
|
|
|
|
DenoConfig {
|
|
|
|
|
script: Some(script.to_string()),
|
|
|
|
|
file: None,
|
|
|
|
|
permissions: DenoPermissions {
|
|
|
|
|
net: net_hosts.iter().map(|s| s.to_string()).collect(),
|
|
|
|
|
..Default::default()
|
|
|
|
|
},
|
|
|
|
|
modules: vec![],
|
|
|
|
|
env: HashMap::new(),
|
|
|
|
|
timeout_ms: Some(10000),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn deno_fetch_allowed_host() {
|
|
|
|
|
let server = wiremock::MockServer::start().await;
|
|
|
|
|
wiremock::Mock::given(wiremock::matchers::method("GET"))
|
|
|
|
|
.respond_with(wiremock::ResponseTemplate::new(200).set_body_string("ok"))
|
|
|
|
|
.mount(&server)
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
let script = format!(
|
|
|
|
|
r#"
|
|
|
|
|
const resp = await fetch("{}");
|
|
|
|
|
output("status", resp.status);
|
|
|
|
|
output("body", await resp.text());
|
|
|
|
|
"#,
|
|
|
|
|
server.uri()
|
|
|
|
|
);
|
|
|
|
|
let mut step = DenoStep::new(config_with_net(&script, &["127.0.0.1"]));
|
|
|
|
|
let (ws, wf, ptr) = make_test_fixtures(serde_json::json!({}));
|
|
|
|
|
let ctx = make_context(&ws, &wf, &ptr);
|
|
|
|
|
let result = step.run(&ctx).await.unwrap();
|
|
|
|
|
let data = result.output_data.unwrap();
|
|
|
|
|
assert_eq!(data["status"], serde_json::json!(200));
|
|
|
|
|
assert_eq!(data["body"], serde_json::json!("ok"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn deno_fetch_denied_host() {
|
|
|
|
|
let server = wiremock::MockServer::start().await;
|
|
|
|
|
wiremock::Mock::given(wiremock::matchers::method("GET"))
|
|
|
|
|
.respond_with(wiremock::ResponseTemplate::new(200).set_body_string("ok"))
|
|
|
|
|
.mount(&server)
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
// Do NOT add 127.0.0.1 to net permissions.
|
|
|
|
|
let script = format!(
|
|
|
|
|
r#"
|
|
|
|
|
try {{
|
|
|
|
|
await fetch("{}");
|
|
|
|
|
output("result", "should_not_reach");
|
|
|
|
|
}} catch (e) {{
|
|
|
|
|
output("error", e.message || String(e));
|
|
|
|
|
}}
|
|
|
|
|
"#,
|
|
|
|
|
server.uri()
|
|
|
|
|
);
|
|
|
|
|
let mut step = DenoStep::new(config_with_net(&script, &[]));
|
|
|
|
|
let (ws, wf, ptr) = make_test_fixtures(serde_json::json!({}));
|
|
|
|
|
let ctx = make_context(&ws, &wf, &ptr);
|
|
|
|
|
let result = step.run(&ctx).await.unwrap();
|
|
|
|
|
let data = result.output_data.unwrap();
|
|
|
|
|
assert!(
|
|
|
|
|
data.get("error").is_some(),
|
|
|
|
|
"expected permission error, got: {data:?}"
|
|
|
|
|
);
|
|
|
|
|
let err_msg = data["error"].as_str().unwrap();
|
|
|
|
|
assert!(
|
|
|
|
|
err_msg.contains("Permission denied"),
|
|
|
|
|
"got: {err_msg}"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn deno_fetch_returns_json() {
|
|
|
|
|
let server = wiremock::MockServer::start().await;
|
|
|
|
|
wiremock::Mock::given(wiremock::matchers::method("GET"))
|
|
|
|
|
.respond_with(
|
|
|
|
|
wiremock::ResponseTemplate::new(200)
|
|
|
|
|
.set_body_json(serde_json::json!({"name": "alice", "age": 30})),
|
|
|
|
|
)
|
|
|
|
|
.mount(&server)
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
let script = format!(
|
|
|
|
|
r#"
|
|
|
|
|
const resp = await fetch("{}");
|
|
|
|
|
const data = await resp.json();
|
|
|
|
|
output("name", data.name);
|
|
|
|
|
output("age", data.age);
|
|
|
|
|
"#,
|
|
|
|
|
server.uri()
|
|
|
|
|
);
|
|
|
|
|
let mut step = DenoStep::new(config_with_net(&script, &["127.0.0.1"]));
|
|
|
|
|
let (ws, wf, ptr) = make_test_fixtures(serde_json::json!({}));
|
|
|
|
|
let ctx = make_context(&ws, &wf, &ptr);
|
|
|
|
|
let result = step.run(&ctx).await.unwrap();
|
|
|
|
|
let data = result.output_data.unwrap();
|
|
|
|
|
assert_eq!(data["name"], serde_json::json!("alice"));
|
|
|
|
|
assert_eq!(data["age"], serde_json::json!(30));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn deno_fetch_post_with_body() {
|
|
|
|
|
let server = wiremock::MockServer::start().await;
|
|
|
|
|
wiremock::Mock::given(wiremock::matchers::method("POST"))
|
|
|
|
|
.and(wiremock::matchers::header("content-type", "application/json"))
|
|
|
|
|
.and(wiremock::matchers::body_json(serde_json::json!({"key": "val"})))
|
|
|
|
|
.respond_with(wiremock::ResponseTemplate::new(201).set_body_string("created"))
|
|
|
|
|
.mount(&server)
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
let script = format!(
|
|
|
|
|
r#"
|
|
|
|
|
const resp = await fetch("{}", {{
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: {{ "content-type": "application/json" }},
|
|
|
|
|
body: JSON.stringify({{ key: "val" }})
|
|
|
|
|
}});
|
|
|
|
|
output("status", resp.status);
|
|
|
|
|
output("body", await resp.text());
|
|
|
|
|
"#,
|
|
|
|
|
server.uri()
|
|
|
|
|
);
|
|
|
|
|
let mut step = DenoStep::new(config_with_net(&script, &["127.0.0.1"]));
|
|
|
|
|
let (ws, wf, ptr) = make_test_fixtures(serde_json::json!({}));
|
|
|
|
|
let ctx = make_context(&ws, &wf, &ptr);
|
|
|
|
|
let result = step.run(&ctx).await.unwrap();
|
|
|
|
|
let data = result.output_data.unwrap();
|
|
|
|
|
assert_eq!(data["status"], serde_json::json!(201));
|
|
|
|
|
assert_eq!(data["body"], serde_json::json!("created"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn deno_fetch_no_permissions_denies_all() {
|
|
|
|
|
let server = wiremock::MockServer::start().await;
|
|
|
|
|
wiremock::Mock::given(wiremock::matchers::any())
|
|
|
|
|
.respond_with(wiremock::ResponseTemplate::new(200))
|
|
|
|
|
.mount(&server)
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
// Empty net allowlist should deny everything.
|
|
|
|
|
let script = format!(
|
|
|
|
|
r#"
|
|
|
|
|
try {{
|
|
|
|
|
await fetch("{}");
|
|
|
|
|
output("result", "should_not_reach");
|
|
|
|
|
}} catch (e) {{
|
|
|
|
|
output("denied", true);
|
|
|
|
|
}}
|
|
|
|
|
"#,
|
|
|
|
|
server.uri()
|
|
|
|
|
);
|
|
|
|
|
let mut step = DenoStep::new(config_with_net(&script, &[]));
|
|
|
|
|
let (ws, wf, ptr) = make_test_fixtures(serde_json::json!({}));
|
|
|
|
|
let ctx = make_context(&ws, &wf, &ptr);
|
|
|
|
|
let result = step.run(&ctx).await.unwrap();
|
|
|
|
|
let data = result.output_data.unwrap();
|
|
|
|
|
assert_eq!(data["denied"], serde_json::json!(true));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Phase 5: Module loader integration tests
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn deno_module_loader_resolves_npm_to_esm_sh() {
|
|
|
|
|
let result = WfeModuleLoader::resolve_npm_specifier("npm:lodash@4.17.21");
|
|
|
|
|
assert_eq!(result, Some("https://esm.sh/lodash@4.17.21".to_string()));
|
|
|
|
|
|
|
|
|
|
let scoped = WfeModuleLoader::resolve_npm_specifier("npm:@scope/pkg@1.0.0");
|
|
|
|
|
assert_eq!(scoped, Some("https://esm.sh/@scope/pkg@1.0.0".to_string()));
|
|
|
|
|
|
|
|
|
|
// Non-npm specifiers return None.
|
|
|
|
|
assert!(WfeModuleLoader::resolve_npm_specifier("https://cdn.com/mod.js").is_none());
|
|
|
|
|
assert!(WfeModuleLoader::resolve_npm_specifier("./local.js").is_none());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn deno_esm_sh_auto_added_to_net_allowlist() {
|
|
|
|
|
// When modules are declared, esm.sh should be auto-added.
|
|
|
|
|
let config = DenoConfig {
|
|
|
|
|
script: Some("1".to_string()),
|
|
|
|
|
file: None,
|
|
|
|
|
permissions: DenoPermissions::default(),
|
|
|
|
|
modules: vec!["npm:lodash@4".to_string()],
|
|
|
|
|
env: HashMap::new(),
|
|
|
|
|
timeout_ms: None,
|
|
|
|
|
};
|
|
|
|
|
assert!(would_auto_add_esm_sh(&config));
|
|
|
|
|
|
|
|
|
|
// When no modules, esm.sh is not added.
|
|
|
|
|
let config_no_modules = DenoConfig {
|
|
|
|
|
script: Some("1".to_string()),
|
|
|
|
|
file: None,
|
|
|
|
|
permissions: DenoPermissions::default(),
|
|
|
|
|
modules: vec![],
|
|
|
|
|
env: HashMap::new(),
|
|
|
|
|
timeout_ms: None,
|
|
|
|
|
};
|
|
|
|
|
assert!(!would_auto_add_esm_sh(&config_no_modules));
|
|
|
|
|
|
|
|
|
|
// When esm.sh already present, not duplicated.
|
|
|
|
|
let config_already = DenoConfig {
|
|
|
|
|
script: Some("1".to_string()),
|
|
|
|
|
file: None,
|
|
|
|
|
permissions: DenoPermissions {
|
|
|
|
|
net: vec!["esm.sh".to_string()],
|
|
|
|
|
..Default::default()
|
|
|
|
|
},
|
|
|
|
|
modules: vec!["npm:lodash@4".to_string()],
|
|
|
|
|
env: HashMap::new(),
|
|
|
|
|
timeout_ms: None,
|
|
|
|
|
};
|
|
|
|
|
assert!(!would_auto_add_esm_sh(&config_already));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn deno_import_local_file() {
|
|
|
|
|
// Create a temp JS file to import.
|
|
|
|
|
let dir = tempfile::tempdir().unwrap();
|
|
|
|
|
let helper_path = dir.path().join("helper.js");
|
|
|
|
|
let mut f = std::fs::File::create(&helper_path).unwrap();
|
|
|
|
|
writeln!(f, "export function greet(name) {{ return `hello ${{name}}`; }}").unwrap();
|
|
|
|
|
drop(f);
|
|
|
|
|
|
|
|
|
|
let main_path = dir.path().join("main.js");
|
|
|
|
|
let main_code = format!(
|
|
|
|
|
r#"import {{ greet }} from "file://{}";
|
|
|
|
|
output("greeting", greet("world"));"#,
|
|
|
|
|
helper_path.to_str().unwrap()
|
|
|
|
|
);
|
|
|
|
|
std::fs::write(&main_path, &main_code).unwrap();
|
|
|
|
|
|
|
|
|
|
let config = DenoConfig {
|
|
|
|
|
script: None,
|
|
|
|
|
file: Some(main_path.to_str().unwrap().to_string()),
|
|
|
|
|
permissions: DenoPermissions {
|
|
|
|
|
read: vec![dir.path().to_str().unwrap().to_string()],
|
|
|
|
|
..Default::default()
|
|
|
|
|
},
|
|
|
|
|
modules: vec![],
|
|
|
|
|
env: HashMap::new(),
|
|
|
|
|
timeout_ms: Some(5000),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let mut step = DenoStep::new(config);
|
|
|
|
|
let (ws, wf, ptr) = make_test_fixtures(serde_json::json!({}));
|
|
|
|
|
let ctx = make_context(&ws, &wf, &ptr);
|
|
|
|
|
let result = step.run(&ctx).await.unwrap();
|
|
|
|
|
let data = result.output_data.unwrap();
|
|
|
|
|
assert_eq!(data["greeting"], serde_json::json!("hello world"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn deno_dynamic_import_denied() {
|
|
|
|
|
let server = wiremock::MockServer::start().await;
|
|
|
|
|
wiremock::Mock::given(wiremock::matchers::any())
|
|
|
|
|
.respond_with(
|
|
|
|
|
wiremock::ResponseTemplate::new(200)
|
|
|
|
|
.set_body_string("export const x = 1;"),
|
|
|
|
|
)
|
|
|
|
|
.mount(&server)
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
// dynamic_import defaults to false.
|
|
|
|
|
let script = format!(
|
|
|
|
|
r#"
|
|
|
|
|
try {{
|
|
|
|
|
await import("{}/mod.js");
|
|
|
|
|
output("result", "should_not_reach");
|
|
|
|
|
}} catch (e) {{
|
|
|
|
|
output("error", e.message || String(e));
|
|
|
|
|
}}
|
|
|
|
|
"#,
|
|
|
|
|
server.uri()
|
|
|
|
|
);
|
|
|
|
|
let mut step = DenoStep::new(config_with_net(&script, &["127.0.0.1"]));
|
|
|
|
|
let (ws, wf, ptr) = make_test_fixtures(serde_json::json!({}));
|
|
|
|
|
let ctx = make_context(&ws, &wf, &ptr);
|
|
|
|
|
let result = step.run(&ctx).await.unwrap();
|
|
|
|
|
let data = result.output_data.unwrap();
|
|
|
|
|
assert!(
|
|
|
|
|
data.get("error").is_some(),
|
|
|
|
|
"expected dynamic import to be denied, got: {data:?}"
|
|
|
|
|
);
|
|
|
|
|
let err_msg = data["error"].as_str().unwrap();
|
|
|
|
|
assert!(
|
|
|
|
|
err_msg.contains("Dynamic import") || err_msg.contains("not allowed"),
|
|
|
|
|
"expected dynamic import denied error, got: {err_msg}"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn deno_import_npm_module() {
|
|
|
|
|
// Use a tiny npm package via esm.sh.
|
|
|
|
|
let script = r#"
|
|
|
|
|
import isNumber from "npm:is-number@7.0.0";
|
|
|
|
|
output("result", isNumber(42));
|
|
|
|
|
"#;
|
|
|
|
|
let config = DenoConfig {
|
|
|
|
|
script: Some(script.to_string()),
|
|
|
|
|
file: None,
|
|
|
|
|
permissions: DenoPermissions {
|
|
|
|
|
net: vec!["esm.sh".to_string()],
|
|
|
|
|
..Default::default()
|
|
|
|
|
},
|
|
|
|
|
modules: vec!["npm:is-number@7.0.0".to_string()],
|
|
|
|
|
env: HashMap::new(),
|
|
|
|
|
timeout_ms: Some(30000),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let mut step = DenoStep::new(config);
|
|
|
|
|
let (ws, wf, ptr) = make_test_fixtures(serde_json::json!({}));
|
|
|
|
|
let ctx = make_context(&ws, &wf, &ptr);
|
|
|
|
|
let result = step.run(&ctx).await.unwrap();
|
|
|
|
|
let data = result.output_data.unwrap();
|
|
|
|
|
assert_eq!(data["result"], serde_json::json!(true));
|
|
|
|
|
}
|