Files
windmill/backend/windmill-api-integration-tests/tests/run_inline.rs
Diego Imbert 7d9d16a6a3 feat: runScript inline for path and hash (#8019)
* runScript inline for path and hash

* Update backend/windmill-api/src/jobs.rs

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>

* refactor: unify inline script param structs and deduplicate closures

- Replace RunInlineScriptByPathFnParams and RunInlineScriptByHashFnParams
  with a single RunInlineScriptFnParams using InlineScriptTarget enum
- Collapse two nearly-identical closures in worker.rs into one
- Merge duplicate InlineByPath/InlineByHash into InlineScriptArgs
- Extract shared run_inline_script_inner helper in API handler
- Add missing check_scopes to run_inline_script_by_hash endpoint
- Fix duplicate lines from prior commit in run_inline_script_by_path
- Change tag from "inline_preview" to "inline" for deployed scripts

Co-authored-by: Diego Imbert <diegoimbert@users.noreply.github.com>

* Integration tests

* rm

* rename feature to run_inline

* Run inline integration tests

* Fix tests

* check path scope

* openapi fix

* nits

* remove register_potential_assets_on_inline_execution

* unused variable

* refactor

* Pass user_db to check script permission

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Diego Imbert <diegoimbert@users.noreply.github.com>
2026-02-27 13:59:14 +01:00

259 lines
7.9 KiB
Rust

use serde_json::json;
use sqlx::{Pool, Postgres};
use windmill_test_utils::*;
#[cfg(feature = "run_inline")]
async fn init_inline_utils(port: u16) -> anyhow::Result<()> {
use std::sync::Once;
static INIT: Once = Once::new();
INIT.call_once(|| {
let (killpill_tx, killpill_rx) = windmill_common::KillpillSender::new(1);
let base_internal_url = format!("http://localhost:{}", port);
windmill_worker::init_worker_internal_server_inline_utils(killpill_rx, base_internal_url)
.expect("Failed to initialize inline utils");
// Keep killpill_tx alive for the test duration
std::mem::forget(killpill_tx);
});
Ok(())
}
fn run_inline_url(port: u16, endpoint: &str) -> String {
format!("http://localhost:{port}/api/w/test-workspace/jobs/run_inline/{endpoint}")
}
fn client() -> reqwest::Client {
reqwest::Client::new()
}
fn authed(builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
builder.header("Authorization", "Bearer SECRET_TOKEN")
}
fn new_script(
path: &str,
summary: &str,
content: &str,
language: &str,
schema_properties: serde_json::Value,
) -> serde_json::Value {
json!({
"path": path,
"summary": summary,
"description": "",
"content": content,
"language": language,
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": schema_properties,
"required": []
}
})
}
#[cfg(feature = "run_inline")]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_run_inline_by_path(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
let base = format!("http://localhost:{port}/api/w/test-workspace");
// Initialize inline utils for script execution
init_inline_utils(port).await?;
// Create a DuckDB script (one of the languages that supports inline execution)
// DuckDB requires parameter declarations in comments: -- $param_name (type)
let script_path = "u/test-user/inline_test";
let script_content = "-- $x (integer)
-- $y (integer)
SELECT $x + $y as result";
let resp = authed(client().post(format!("{base}/scripts/create")))
.json(&new_script(
script_path,
"Inline test script",
script_content,
"duckdb",
json!({
"x": {"type": "integer"},
"y": {"type": "integer"}
}),
))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 201, "create script: {}", resp.text().await?);
// Test run_inline by path with args
let resp = authed(client().post(run_inline_url(port, &format!("p/{script_path}"))))
.json(&json!({
"args": {
"x": 5,
"y": 15
}
}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200, "run_inline by path with args failed");
let result = resp.json::<serde_json::Value>().await?;
// DuckDB query should return array with one row containing result field
// The result structure is [{"result": 20}]
assert!(result.is_array(), "expected array result, got: {}", result);
let rows = result.as_array().unwrap();
assert_eq!(rows.len(), 1, "expected 1 row, got: {}", rows.len());
assert_eq!(
rows[0]["result"],
json!(20),
"expected result 20, got: {}",
rows[0]
);
Ok(())
}
#[cfg(feature = "run_inline")]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_run_inline_by_hash(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
let base = format!("http://localhost:{port}/api/w/test-workspace");
// Initialize inline utils for script execution
init_inline_utils(port).await?;
// Create a DuckDB script and get its hash
let script_path = "u/test-user/inline_hash_test";
let script_content = "-- $a (integer)
-- $b (integer)
SELECT $a * $b as product";
let resp = authed(client().post(format!("{base}/scripts/create")))
.json(&new_script(
script_path,
"Inline hash test script",
script_content,
"duckdb",
json!({
"a": {"type": "integer"},
"b": {"type": "integer"}
}),
))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 201, "create script: {}", resp.text().await?);
// Get the script to retrieve its hash
let resp = authed(client().get(format!("{base}/scripts/get/p/{script_path}")))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
let script_data = resp.json::<serde_json::Value>().await?;
let hash = script_data["hash"]
.as_str()
.expect("hash should be present");
// Test run_inline by hash with args
let resp = authed(client().post(run_inline_url(port, &format!("h/{hash}"))))
.json(&json!({
"args": {
"a": 7,
"b": 3
}
}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200, "run_inline by hash with args failed");
let result = resp.json::<serde_json::Value>().await?;
// Should return array with one row: [{"product": 21}]
assert!(result.is_array(), "expected array result");
let rows = result.as_array().unwrap();
assert_eq!(rows.len(), 1, "expected 1 row");
assert_eq!(
rows[0]["product"],
json!(21),
"expected product 21, got: {}",
rows[0]
);
Ok(())
}
#[cfg(feature = "run_inline")]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_run_inline_preview(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
// Initialize inline utils for script execution
init_inline_utils(port).await?;
// Test run_inline preview with direct DuckDB content
let resp = authed(client().post(run_inline_url(port, "preview")))
.json(&json!({
"content": "-- $msg (text)\nSELECT 'Hello, ' || $msg || '!' as greeting",
"language": "duckdb",
"args": {
"msg": "World"
}
}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200, "run_inline preview failed");
let result = resp.json::<serde_json::Value>().await?;
// Should return array with one row: [{"greeting": "Hello, World!"}]
assert!(result.is_array(), "expected array result");
let rows = result.as_array().unwrap();
assert_eq!(rows.len(), 1, "expected 1 row");
assert_eq!(
rows[0]["greeting"],
json!("Hello, World!"),
"expected 'Hello, World!', got: {}",
rows[0]
);
Ok(())
}
#[cfg(feature = "run_inline")]
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_run_inline_nonexistent_script(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
// Initialize inline utils
init_inline_utils(port).await?;
// Test run_inline by path with non-existent script - should return an error
let resp = authed(client().post(run_inline_url(port, "p/u/test-user/nonexistent_script")))
.json(&json!({
"args": null
}))
.send()
.await
.unwrap();
// Should return an error (script not found)
assert!(
resp.status().is_client_error() || resp.status().is_server_error(),
"expected error status for nonexistent script, got: {}",
resp.status()
);
Ok(())
}