diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 37204316ca..bef3fe26f7 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -15778,6 +15778,7 @@ dependencies = [ "windmill-indexer", "windmill-queue", "windmill-runtime-nativets", + "windmill-test-utils", "windmill-worker", "windows-service", "windows-sys 0.52.0", @@ -16138,6 +16139,26 @@ dependencies = [ "windmill-common", ] +[[package]] +name = "windmill-api-integration-tests" +version = "1.631.1" +dependencies = [ + "anyhow", + "async-nats", + "rand 0.9.0", + "rdkafka", + "reqwest 0.13.1", + "rumqttc", + "serde", + "serde_json", + "sqlx", + "tokio", + "uuid", + "windmill-api-client", + "windmill-common", + "windmill-test-utils", +] + [[package]] name = "windmill-api-jobs" version = "1.631.1" @@ -16969,6 +16990,29 @@ dependencies = [ "windmill-oauth", ] +[[package]] +name = "windmill-test-utils" +version = "1.631.1" +dependencies = [ + "anyhow", + "axum 0.7.9", + "chrono", + "futures", + "serde", + "serde_json", + "sqlx", + "tokio", + "tracing", + "uuid", + "windmill-api", + "windmill-api-agent-workers", + "windmill-api-client", + "windmill-common", + "windmill-queue", + "windmill-runtime-nativets", + "windmill-worker", +] + [[package]] name = "windmill-trigger" version = "1.631.1" diff --git a/backend/Cargo.toml b/backend/Cargo.toml index b8ae9f8c1d..9c22b21604 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -67,7 +67,9 @@ members = [ "./parsers/windmill-parser-py", "./parsers/windmill-parser-py-imports", "./parsers/windmill-sql-datatype-parser-wasm", - "./parsers/windmill-parser-yaml", "windmill-macros", "parsers/windmill-parser-nu" + "./parsers/windmill-parser-yaml", "windmill-macros", "parsers/windmill-parser-nu", + "./windmill-test-utils", + "./windmill-api-integration-tests", ] exclude = ["./windmill-duckdb-ffi-internal"] @@ -240,6 +242,7 @@ serde_json.workspace = true reqwest.workspace = true windmill-queue = { workspace = true, features = ["failpoints"] } windmill-dep-map.workspace = true +windmill-test-utils.workspace = true axum.workspace = true serde.workspace = true windmill-api-client.workspace = true @@ -317,6 +320,7 @@ windmill-parser-php = { path = "./parsers/windmill-parser-php" } windmill-jseval = { path = "./windmill-jseval" } windmill-runtime-nativets = { path = "./windmill-runtime-nativets" } windmill-api-client = { path = "./windmill-api-client" } +windmill-test-utils = { path = "./windmill-test-utils" } reqwest-retry = "^0" reqwest-middleware = { version = "^0", features = ["json"] } diff --git a/backend/tests/agent_workers.rs b/backend/tests/agent_workers.rs index 26c4003212..b6414dc24e 100644 --- a/backend/tests/agent_workers.rs +++ b/backend/tests/agent_workers.rs @@ -1,8 +1,6 @@ #![cfg(all(feature = "private", feature = "agent_worker_server"))] -mod common; - -use common::*; +use windmill_test_utils::*; use serde_json::json; use sqlx::{Pool, Postgres}; use windmill_common::{ diff --git a/backend/tests/bun_jobs.rs b/backend/tests/bun_jobs.rs index cf3ce4d350..534d530095 100644 --- a/backend/tests/bun_jobs.rs +++ b/backend/tests/bun_jobs.rs @@ -1,5 +1,4 @@ -mod common; -use crate::common::*; +use windmill_test_utils::*; use sqlx::postgres::Postgres; use sqlx::Pool; use windmill_common::jobs::{JobPayload, RawCode}; @@ -860,7 +859,7 @@ export function main() { // ============================================================================ mod dedicated_worker_protocol { - use crate::common::{parse_dedicated_worker_line, DedicatedWorkerResult}; + use windmill_test_utils::{parse_dedicated_worker_line, DedicatedWorkerResult}; use std::io::{BufRead, BufReader, Write}; use std::process::{Command, Stdio}; use windmill_worker::{ diff --git a/backend/tests/dependency_map.rs b/backend/tests/dependency_map.rs index d217b710d6..9113e8f165 100644 --- a/backend/tests/dependency_map.rs +++ b/backend/tests/dependency_map.rs @@ -2,8 +2,7 @@ use sqlx::{Pool, Postgres}; use tokio_stream::StreamExt; use windmill_api_client::types::NewScript; -mod common; -use common::{in_test_worker, init_client, listen_for_completed_jobs, ApiServer}; +use windmill_test_utils::{in_test_worker, init_client, listen_for_completed_jobs, ApiServer}; mod dependency_map { use super::*; diff --git a/backend/tests/error_handler.rs b/backend/tests/error_handler.rs index cad9336ab7..b117cbfba3 100644 --- a/backend/tests/error_handler.rs +++ b/backend/tests/error_handler.rs @@ -1,7 +1,6 @@ use sqlx::{Pool, Postgres}; -mod common; -use common::*; +use windmill_test_utils::*; /// Test that workspace error handler can be set and removed via database operations #[cfg(feature = "deno_core")] diff --git a/backend/tests/flow_engine_parity.rs b/backend/tests/flow_engine_parity.rs index 875897094e..58b76dc50d 100644 --- a/backend/tests/flow_engine_parity.rs +++ b/backend/tests/flow_engine_parity.rs @@ -25,8 +25,7 @@ use windmill_common::{ scripts::ScriptLang, }; -mod common; -use common::*; +use windmill_test_utils::*; /// Helper to create a FlowModule with default fields fn flow_module(id: &str, value: FlowModuleValue) -> FlowModule { diff --git a/backend/tests/job_payload.rs b/backend/tests/job_payload.rs index 2321db5bcf..87253807be 100644 --- a/backend/tests/job_payload.rs +++ b/backend/tests/job_payload.rs @@ -1,5 +1,3 @@ -mod common; - mod job_payload { use serde_json::json; use sqlx::{Pool, Postgres}; @@ -8,7 +6,7 @@ mod job_payload { use windmill_common::scripts::{ScriptHash, ScriptLang}; use windmill_common::flow_status::RestartedFrom; - use crate::common::*; + use windmill_test_utils::*; use windmill_common::min_version::{ MIN_VERSION, MIN_VERSION_IS_AT_LEAST_1_427, MIN_VERSION_IS_AT_LEAST_1_432, MIN_VERSION_IS_AT_LEAST_1_440, diff --git a/backend/tests/list_jobs.rs b/backend/tests/list_jobs.rs index a3d87385f9..65451bcbb7 100644 --- a/backend/tests/list_jobs.rs +++ b/backend/tests/list_jobs.rs @@ -7,8 +7,7 @@ use windmill_common::{ scripts::ScriptLang, }; -mod common; -use common::*; +use windmill_test_utils::*; #[derive(Debug, Deserialize)] struct ListJobsResponse { diff --git a/backend/tests/nativets_jobs.rs b/backend/tests/nativets_jobs.rs index c654882589..eaf3a96652 100644 --- a/backend/tests/nativets_jobs.rs +++ b/backend/tests/nativets_jobs.rs @@ -14,10 +14,7 @@ */ #[cfg(feature = "deno_core")] -mod common; - -#[cfg(feature = "deno_core")] -use common::*; +use windmill_test_utils::*; #[cfg(feature = "deno_core")] use futures::StreamExt; diff --git a/backend/tests/nativets_stress.rs b/backend/tests/nativets_stress.rs index cf3710d86c..9717403830 100644 --- a/backend/tests/nativets_stress.rs +++ b/backend/tests/nativets_stress.rs @@ -11,10 +11,7 @@ */ #[cfg(feature = "deno_core")] -mod common; - -#[cfg(feature = "deno_core")] -use common::*; +use windmill_test_utils::*; #[cfg(feature = "deno_core")] use std::time::Instant; diff --git a/backend/tests/python_jobs.rs b/backend/tests/python_jobs.rs index 47a554afd5..dd494de007 100644 --- a/backend/tests/python_jobs.rs +++ b/backend/tests/python_jobs.rs @@ -1,5 +1,4 @@ -mod common; -use crate::common::*; +use windmill_test_utils::*; use sqlx::postgres::Postgres; use sqlx::Pool; use windmill_common::scripts::ScriptLang; diff --git a/backend/tests/retry.rs b/backend/tests/retry.rs index 60b81573db..cb77e4546b 100644 --- a/backend/tests/retry.rs +++ b/backend/tests/retry.rs @@ -1,8 +1,6 @@ -mod common; - #[cfg(feature = "deno_core")] mod retry { - use crate::common::*; + use windmill_test_utils::*; use serde_json::json; use sqlx::{Pool, Postgres}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; diff --git a/backend/tests/success_handler.rs b/backend/tests/success_handler.rs index e3c733100e..2f580e4dcb 100644 --- a/backend/tests/success_handler.rs +++ b/backend/tests/success_handler.rs @@ -1,7 +1,6 @@ use sqlx::{Pool, Postgres}; -mod common; -use common::*; +use windmill_test_utils::*; /// Test that the workspace success handler cache works correctly with 60s TTL #[cfg(feature = "deno_core")] diff --git a/backend/tests/suspend_resume.rs b/backend/tests/suspend_resume.rs index a3e4cc456f..f2347af57a 100644 --- a/backend/tests/suspend_resume.rs +++ b/backend/tests/suspend_resume.rs @@ -1,11 +1,9 @@ -mod common; - mod suspend_resume { #[cfg(feature = "deno_core")] use serde_json::json; #[cfg(feature = "deno_core")] - use crate::common::*; + use windmill_test_utils::*; #[cfg(feature = "deno_core")] use futures::{Stream, StreamExt}; diff --git a/backend/tests/worker.rs b/backend/tests/worker.rs index f98e1e3ad7..d084a0177c 100644 --- a/backend/tests/worker.rs +++ b/backend/tests/worker.rs @@ -25,8 +25,7 @@ use windmill_common::{ jobs::{JobPayload, RawCode}, scripts::ScriptLang, }; -mod common; -use common::*; +use windmill_test_utils::*; #[cfg(feature = "enterprise")] use futures::StreamExt; diff --git a/backend/tests/workspace_dependencies.rs b/backend/tests/workspace_dependencies.rs index 124e727e91..d8e705d0c3 100644 --- a/backend/tests/workspace_dependencies.rs +++ b/backend/tests/workspace_dependencies.rs @@ -1,9 +1,8 @@ -mod common; mod workspace_dependencies { - use crate::common::in_test_worker; - use crate::common::init_client; - use crate::common::listen_for_completed_jobs; + use windmill_test_utils::in_test_worker; + use windmill_test_utils::init_client; + use windmill_test_utils::listen_for_completed_jobs; use sqlx::{Pool, Postgres}; use tokio_stream::StreamExt; use windmill_common::scripts::ScriptLang; diff --git a/backend/windmill-api-integration-tests/Cargo.toml b/backend/windmill-api-integration-tests/Cargo.toml new file mode 100644 index 0000000000..8caa178d3d --- /dev/null +++ b/backend/windmill-api-integration-tests/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "windmill-api-integration-tests" +version.workspace = true +authors.workspace = true +edition.workspace = true + +[lib] +name = "windmill_api_integration_tests" +path = "src/lib.rs" + +[features] +default = [] +private = ["windmill-test-utils/private"] +enterprise = ["windmill-test-utils/enterprise"] + +[dependencies] +windmill-test-utils.workspace = true +windmill-api-client.workspace = true +windmill-common = { workspace = true, default-features = false } +sqlx.workspace = true +serde_json.workspace = true +serde.workspace = true +reqwest.workspace = true +tokio.workspace = true +anyhow.workspace = true +uuid.workspace = true +rand.workspace = true +rumqttc.workspace = true +rdkafka.workspace = true +async-nats.workspace = true diff --git a/backend/windmill-api-integration-tests/src/lib.rs b/backend/windmill-api-integration-tests/src/lib.rs new file mode 100644 index 0000000000..00682b90bc --- /dev/null +++ b/backend/windmill-api-integration-tests/src/lib.rs @@ -0,0 +1,2 @@ +// This crate exists only to host integration tests. +// See tests/ directory for the actual test files. diff --git a/backend/tests/apps.rs b/backend/windmill-api-integration-tests/tests/apps.rs similarity index 98% rename from backend/tests/apps.rs rename to backend/windmill-api-integration-tests/tests/apps.rs index 869858a265..55c85711ae 100644 --- a/backend/tests/apps.rs +++ b/backend/windmill-api-integration-tests/tests/apps.rs @@ -1,8 +1,7 @@ use serde_json::json; use sqlx::{Pool, Postgres}; -mod common; -use common::*; +use windmill_test_utils::*; fn app_url(port: u16, endpoint: &str, path: &str) -> String { format!("http://localhost:{port}/api/w/test-workspace/apps/{endpoint}/{path}") @@ -40,7 +39,7 @@ fn new_app(path: &str, summary: &str) -> serde_json::Value { }) } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_app_endpoints(db: Pool) -> anyhow::Result<()> { initialize_tracing().await; let server = ApiServer::start(db.clone()).await?; diff --git a/backend/tests/drafts.rs b/backend/windmill-api-integration-tests/tests/drafts.rs similarity index 97% rename from backend/tests/drafts.rs rename to backend/windmill-api-integration-tests/tests/drafts.rs index e479d0b97c..d86c91b6a1 100644 --- a/backend/tests/drafts.rs +++ b/backend/windmill-api-integration-tests/tests/drafts.rs @@ -1,8 +1,7 @@ use serde_json::json; use sqlx::{Pool, Postgres}; -mod common; -use common::*; +use windmill_test_utils::*; fn client() -> reqwest::Client { reqwest::Client::new() @@ -12,7 +11,7 @@ fn authed(builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder { builder.header("Authorization", "Bearer SECRET_TOKEN") } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_draft_endpoints(db: Pool) -> anyhow::Result<()> { initialize_tracing().await; let server = ApiServer::start(db.clone()).await?; diff --git a/backend/windmill-api-integration-tests/tests/fixtures/base.sql b/backend/windmill-api-integration-tests/tests/fixtures/base.sql new file mode 100644 index 0000000000..7db9918fba --- /dev/null +++ b/backend/windmill-api-integration-tests/tests/fixtures/base.sql @@ -0,0 +1,146 @@ +-- used for backend automated testing +-- https://docs.rs/sqlx/latest/sqlx/attr.test.html + +INSERT INTO workspace + (id, name, owner) + VALUES ('test-workspace', 'test-workspace', 'test-user'); + +INSERT INTO usr(workspace_id, email, username, is_admin, role) VALUES + ('test-workspace', 'test@windmill.dev', 'test-user', true, 'Admin'); + +INSERT INTO workspace_key(workspace_id, kind, key) VALUES + ('test-workspace', 'cloud', 'test-key'); + + +INSERT INTO workspace_settings (workspace_id) VALUES + ('test-workspace'); + +INSERT INTO group_ (workspace_id, name, summary, extra_perms) VALUES + ('test-workspace', 'all', 'All users', '{}'); + +INSERT INTO password(email, password_hash, login_type, super_admin, verified, name, username) + VALUES ('test@windmill.dev', 'not-a-real-hash', 'password', true, true, 'Test User', 'test-user'); + +INSERT INTO password(email, password_hash, login_type, super_admin, verified, name) + VALUES ('test2@windmill.dev', 'not-a-real-hash', 'password', false, true, 'Test User 2'); + +INSERT INTO password(email, password_hash, login_type, super_admin, verified, name) + VALUES ('test3@windmill.dev', 'not-a-real-hash', 'password', false, true, 'Test User 3'); + +INSERT INTO usr(workspace_id, email, username, is_admin, role) VALUES + ('test-workspace', 'test2@windmill.dev', 'test-user-2', false, 'User'); + +INSERT INTO usr(workspace_id, email, username, is_admin, role) VALUES + ('test-workspace', 'test3@windmill.dev', 'test-user-3', false, 'User'); + +insert INTO token(token, email, label, super_admin) VALUES ('SECRET_TOKEN', 'test@windmill.dev', 'test token', true); +insert INTO token(token, email, label, super_admin) VALUES ('SECRET_TOKEN_2', 'test2@windmill.dev', 'test token 2', false); +insert INTO token(token, email, label, super_admin) VALUES ('SECRET_TOKEN_3', 'test3@windmill.dev', 'test token 3', false); + +GRANT ALL PRIVILEGES ON TABLE workspace_key TO windmill_admin; +GRANT ALL PRIVILEGES ON TABLE workspace_key TO windmill_user; + +CREATE FUNCTION "notify_insert_on_completed_job" () +RETURNS TRIGGER AS $$ +BEGIN + PERFORM pg_notify('completed', NEW.id::text); + RETURN NEW; +END; +$$ LANGUAGE PLPGSQL; + + CREATE TRIGGER "notify_insert_on_completed_job" + AFTER INSERT ON "v2_job_completed" + FOR EACH ROW +EXECUTE FUNCTION "notify_insert_on_completed_job" (); + + +CREATE FUNCTION "notify_queue" () +RETURNS TRIGGER AS $$ +BEGIN + PERFORM pg_notify('queued', NEW.id::text); + RETURN NEW; +END; +$$ LANGUAGE PLPGSQL; + + CREATE TRIGGER "notify_queue_after_insert" + AFTER INSERT ON "v2_job_queue" + FOR EACH ROW +EXECUTE FUNCTION "notify_queue" (); + + CREATE TRIGGER "notify_queue_after_flow_status_update" + AFTER UPDATE ON "v2_job_status" + FOR EACH ROW + WHEN (NEW.flow_status IS DISTINCT FROM OLD.flow_status) +EXECUTE FUNCTION "notify_queue" (); + +-- Apply phase 4: +DROP FUNCTION IF EXISTS v2_job_after_update CASCADE; +DROP FUNCTION IF EXISTS v2_job_completed_before_insert CASCADE; +DROP FUNCTION IF EXISTS v2_job_completed_before_update CASCADE; +DROP FUNCTION IF EXISTS v2_job_queue_after_insert CASCADE; +DROP FUNCTION IF EXISTS v2_job_queue_before_insert CASCADE; +DROP FUNCTION IF EXISTS v2_job_queue_before_update CASCADE; +DROP FUNCTION IF EXISTS v2_job_runtime_before_insert CASCADE; +DROP FUNCTION IF EXISTS v2_job_runtime_before_update CASCADE; +DROP FUNCTION IF EXISTS v2_job_status_before_insert CASCADE; +DROP FUNCTION IF EXISTS v2_job_status_before_update CASCADE; + +DROP VIEW IF EXISTS completed_job, completed_job_view, job, queue, queue_view CASCADE; + +ALTER TABLE v2_job_queue + DROP COLUMN IF EXISTS __parent_job CASCADE, + DROP COLUMN IF EXISTS __created_by CASCADE, + DROP COLUMN IF EXISTS __script_hash CASCADE, + DROP COLUMN IF EXISTS __script_path CASCADE, + DROP COLUMN IF EXISTS __args CASCADE, + DROP COLUMN IF EXISTS __logs CASCADE, + DROP COLUMN IF EXISTS __raw_code CASCADE, + DROP COLUMN IF EXISTS __canceled CASCADE, + DROP COLUMN IF EXISTS __last_ping CASCADE, + DROP COLUMN IF EXISTS __job_kind CASCADE, + DROP COLUMN IF EXISTS __env_id CASCADE, + DROP COLUMN IF EXISTS __schedule_path CASCADE, + DROP COLUMN IF EXISTS __permissioned_as CASCADE, + DROP COLUMN IF EXISTS __flow_status CASCADE, + DROP COLUMN IF EXISTS __raw_flow CASCADE, + DROP COLUMN IF EXISTS __is_flow_step CASCADE, + DROP COLUMN IF EXISTS __language CASCADE, + DROP COLUMN IF EXISTS __same_worker CASCADE, + DROP COLUMN IF EXISTS __raw_lock CASCADE, + DROP COLUMN IF EXISTS __pre_run_error CASCADE, + DROP COLUMN IF EXISTS __email CASCADE, + DROP COLUMN IF EXISTS __visible_to_owner CASCADE, + DROP COLUMN IF EXISTS __mem_peak CASCADE, + DROP COLUMN IF EXISTS __root_job CASCADE, + DROP COLUMN IF EXISTS __leaf_jobs CASCADE, + DROP COLUMN IF EXISTS __concurrent_limit CASCADE, + DROP COLUMN IF EXISTS __concurrency_time_window_s CASCADE, + DROP COLUMN IF EXISTS __timeout CASCADE, + DROP COLUMN IF EXISTS __flow_step_id CASCADE, + DROP COLUMN IF EXISTS __cache_ttl CASCADE; + +LOCK TABLE v2_job_queue IN ACCESS EXCLUSIVE MODE; +ALTER TABLE v2_job_completed + DROP COLUMN IF EXISTS __parent_job CASCADE, + DROP COLUMN IF EXISTS __created_by CASCADE, + DROP COLUMN IF EXISTS __created_at CASCADE, + DROP COLUMN IF EXISTS __success CASCADE, + DROP COLUMN IF EXISTS __script_hash CASCADE, + DROP COLUMN IF EXISTS __script_path CASCADE, + DROP COLUMN IF EXISTS __args CASCADE, + DROP COLUMN IF EXISTS __logs CASCADE, + DROP COLUMN IF EXISTS __raw_code CASCADE, + DROP COLUMN IF EXISTS __canceled CASCADE, + DROP COLUMN IF EXISTS __job_kind CASCADE, + DROP COLUMN IF EXISTS __env_id CASCADE, + DROP COLUMN IF EXISTS __schedule_path CASCADE, + DROP COLUMN IF EXISTS __permissioned_as CASCADE, + DROP COLUMN IF EXISTS __raw_flow CASCADE, + DROP COLUMN IF EXISTS __is_flow_step CASCADE, + DROP COLUMN IF EXISTS __language CASCADE, + DROP COLUMN IF EXISTS __is_skipped CASCADE, + DROP COLUMN IF EXISTS __raw_lock CASCADE, + DROP COLUMN IF EXISTS __email CASCADE, + DROP COLUMN IF EXISTS __visible_to_owner CASCADE, + DROP COLUMN IF EXISTS __tag CASCADE, + DROP COLUMN IF EXISTS __priority CASCADE; diff --git a/backend/windmill-api-integration-tests/tests/fixtures/permissions_test.sql b/backend/windmill-api-integration-tests/tests/fixtures/permissions_test.sql new file mode 100644 index 0000000000..a9006f5233 --- /dev/null +++ b/backend/windmill-api-integration-tests/tests/fixtures/permissions_test.sql @@ -0,0 +1,238 @@ +-- Permissions test fixture +-- This sets up a comprehensive permission testing scenario + +-- ============================================ +-- USERS +-- ============================================ + +-- Admin user (may already exist from base.sql, use ON CONFLICT) +INSERT INTO usr (workspace_id, email, username, is_admin, role) +VALUES ('test-workspace', 'admin@windmill.dev', 'admin', true, 'Admin') +ON CONFLICT (workspace_id, username) DO NOTHING; + +-- Regular users (non-admin) +INSERT INTO usr (workspace_id, email, username, is_admin, role) +VALUES + ('test-workspace', 'alice@windmill.dev', 'alice', false, 'Developer'), + ('test-workspace', 'bob@windmill.dev', 'bob', false, 'Developer'), + ('test-workspace', 'charlie@windmill.dev', 'charlie', false, 'Developer'); + +-- Operator user (can execute but cannot create/update scripts, flows, apps) +INSERT INTO usr (workspace_id, email, username, is_admin, operator, role) +VALUES + ('test-workspace', 'operator@windmill.dev', 'operator', false, true, 'Operator'); + +-- Add users to password table (use ON CONFLICT since admin may exist from base.sql) +INSERT INTO password (email, password_hash, login_type, super_admin, verified, name) +VALUES + ('admin@windmill.dev', 'dummy_hash', 'password', false, true, 'Admin User'), + ('alice@windmill.dev', 'dummy_hash', 'password', false, true, 'Alice'), + ('bob@windmill.dev', 'dummy_hash', 'password', false, true, 'Bob'), + ('charlie@windmill.dev', 'dummy_hash', 'password', false, true, 'Charlie'), + ('operator@windmill.dev', 'dummy_hash', 'password', false, true, 'Operator') +ON CONFLICT (email) DO NOTHING; + +-- ============================================ +-- TOKENS for authentication +-- ============================================ + +-- Tokens associated with emails (workspace-scoped) +-- The auth system will look up the user by email in the usr table +-- Note: tokens must be at least 10 characters (TOKEN_PREFIX_LEN) +INSERT INTO token (token, email, label, super_admin, owner, workspace_id) +VALUES + ('ADMIN_TOKEN_TEST', 'admin@windmill.dev', 'Admin token', false, 'u/admin', 'test-workspace'), + ('ALICE_TOKEN_TEST', 'alice@windmill.dev', 'Alice token', false, 'u/alice', 'test-workspace'), + ('BOB_TOKEN_TEST12', 'bob@windmill.dev', 'Bob token', false, 'u/bob', 'test-workspace'), + ('CHARLIE_TOKEN_01', 'charlie@windmill.dev', 'Charlie token', false, 'u/charlie', 'test-workspace'), + ('OPERATOR_TOKEN_1', 'operator@windmill.dev', 'Operator token', false, 'u/operator', 'test-workspace'); + +-- ============================================ +-- GROUPS +-- ============================================ + +-- 'developers' group - Charlie is a member +INSERT INTO group_ (workspace_id, name, summary, extra_perms) +VALUES ('test-workspace', 'developers', 'Developer group', '{}'); + +-- 'editors' group - Bob is a member (has write access to some folders) +INSERT INTO group_ (workspace_id, name, summary, extra_perms) +VALUES ('test-workspace', 'editors', 'Editor group', '{}'); + +-- Group memberships +INSERT INTO usr_to_group (workspace_id, group_, usr) +VALUES + ('test-workspace', 'developers', 'charlie'), + ('test-workspace', 'editors', 'bob'); + +-- ============================================ +-- FOLDERS +-- ============================================ + +-- 'shared' folder: +-- - Alice has read-only access (extra_perms: u/alice -> false) +-- - Bob has write access (extra_perms: u/bob -> true) +INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms, created_by) +VALUES ('test-workspace', 'shared', 'Shared Folder', '{"u/admin"}', + '{"u/alice": false, "u/bob": true}', 'admin'); + +-- 'team' folder: +-- - 'developers' group has read-only access +INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms, created_by) +VALUES ('test-workspace', 'team', 'Team Folder', '{"u/admin"}', + '{"g/developers": false}', 'admin'); + +-- 'editable' folder: +-- - 'editors' group has write access +INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms, created_by) +VALUES ('test-workspace', 'editable', 'Editable Folder', '{"u/admin"}', + '{"g/editors": true}', 'admin'); + +-- 'admin_only' folder: +-- - Only admin has access (but we'll add item-level perms for specific items) +INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms, created_by) +VALUES ('test-workspace', 'admin_only', 'Admin Only Folder', '{"u/admin"}', + '{}', 'admin'); + +-- 'alice_owned' folder: +-- - Alice is an owner (owners must also be in extra_perms for get_folders_for_user to find them) +INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms, created_by) +VALUES ('test-workspace', 'alice_owned', 'Alice Owned Folder', '{"u/alice"}', + '{"u/alice": true}', 'alice'); + +-- ============================================ +-- SCRIPTS +-- ============================================ + +-- Alice's personal script +INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock, extra_perms) +VALUES ('test-workspace', 1001, 'u/alice/my_script', + 'export function main() { return "alice script"; }', + 'deno', 'script', 'alice', '{}', 'Alice script', '', '', '{}'); + +-- Bob's personal script +INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock, extra_perms) +VALUES ('test-workspace', 1002, 'u/bob/my_script', + 'export function main() { return "bob script"; }', + 'deno', 'script', 'bob', '{}', 'Bob script', '', '', '{}'); + +-- Script in shared folder (accessible by Alice read-only, Bob write) +INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock, extra_perms) +VALUES ('test-workspace', 1003, 'f/shared/public_script', + 'export function main() { return "public"; }', + 'deno', 'script', 'admin', '{}', 'Public script', '', '', '{}'); + +-- Script in team folder (accessible by developers group) +INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock, extra_perms) +VALUES ('test-workspace', 1004, 'f/team/team_script', + 'export function main() { return "team"; }', + 'deno', 'script', 'admin', '{}', 'Team script', '', '', '{}'); + +-- Script in admin_only folder but with explicit share to Alice +INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock, extra_perms) +VALUES ('test-workspace', 1005, 'f/admin_only/shared_with_alice', + 'export function main() { return "shared"; }', + 'deno', 'script', 'admin', '{}', 'Shared with Alice', '', '', + '{"u/alice": false}'); + +-- Script in alice_owned folder +INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock, extra_perms) +VALUES ('test-workspace', 1006, 'f/alice_owned/owner_script', + 'export function main() { return "owner"; }', + 'deno', 'script', 'alice', '{}', 'Owner script', '', '', '{}'); + +-- Alice's script with extra_perms sharing to Bob (read-only) +INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock, extra_perms) +VALUES ('test-workspace', 1007, 'u/alice/extra_shared_script', + 'export function main() { return "extra shared"; }', + 'deno', 'script', 'alice', '{}', 'Extra shared script', '', '', + '{"u/bob": false}'); + +-- ============================================ +-- FLOWS (with flow_versions) +-- ============================================ + +-- Create flows first (without versions) +INSERT INTO flow (workspace_id, path, summary, description, value, edited_by, edited_at, schema, extra_perms) +VALUES + ('test-workspace', 'u/alice/my_flow', 'Alice flow', '', '{"modules": []}', 'alice', NOW(), '{}', '{}'), + ('test-workspace', 'u/bob/my_flow', 'Bob flow', '', '{"modules": []}', 'bob', NOW(), '{}', '{}'), + ('test-workspace', 'f/shared/shared_flow', 'Shared flow', '', '{"modules": []}', 'admin', NOW(), '{}', '{}'); + +-- Create flow versions +INSERT INTO flow_version (id, workspace_id, path, value, schema, created_by, created_at) +VALUES + (1001, 'test-workspace', 'u/alice/my_flow', '{"modules": []}', '{}', 'alice', NOW()), + (1002, 'test-workspace', 'u/bob/my_flow', '{"modules": []}', '{}', 'bob', NOW()), + (1003, 'test-workspace', 'f/shared/shared_flow', '{"modules": []}', '{}', 'admin', NOW()); + +-- Update flows with version references +UPDATE flow SET versions = ARRAY[1001::bigint] WHERE path = 'u/alice/my_flow' AND workspace_id = 'test-workspace'; +UPDATE flow SET versions = ARRAY[1002::bigint] WHERE path = 'u/bob/my_flow' AND workspace_id = 'test-workspace'; +UPDATE flow SET versions = ARRAY[1003::bigint] WHERE path = 'f/shared/shared_flow' AND workspace_id = 'test-workspace'; + +-- ============================================ +-- RESOURCES +-- ============================================ + +-- Alice's personal resource +INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by) +VALUES ('test-workspace', 'u/alice/my_resource', '{"key": "alice_value"}', + 'Alice resource', 'object', '{}', 'alice'); + +-- Bob's personal resource +INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by) +VALUES ('test-workspace', 'u/bob/my_resource', '{"key": "bob_value"}', + 'Bob resource', 'object', '{}', 'bob'); + +-- ============================================ +-- VARIABLES +-- ============================================ + +-- Alice's personal variable (non-secret for testing permissions, not encryption) +INSERT INTO variable (workspace_id, path, value, is_secret, description, extra_perms) +VALUES ('test-workspace', 'u/alice/my_variable', 'alice_value', false, + 'Alice variable', '{}'); + +-- Bob's personal variable (non-secret for testing permissions, not encryption) +INSERT INTO variable (workspace_id, path, value, is_secret, description, extra_perms) +VALUES ('test-workspace', 'u/bob/my_variable', 'bob_value', false, + 'Bob variable', '{}'); + +-- ============================================ +-- SCHEDULES +-- ============================================ + +-- Alice's personal schedule +INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, enabled, script_path, args, is_flow, email, timezone, extra_perms) +VALUES ('test-workspace', 'u/alice/my_schedule', 'alice', NOW(), '0 * * * *', false, + 'u/alice/my_script', '{}', false, 'alice@windmill.dev', 'UTC', '{}'); + +-- Bob's personal schedule +INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, enabled, script_path, args, is_flow, email, timezone, extra_perms) +VALUES ('test-workspace', 'u/bob/my_schedule', 'bob', NOW(), '0 * * * *', false, + 'u/bob/my_script', '{}', false, 'bob@windmill.dev', 'UTC', '{}'); + +-- ============================================ +-- APPS (with app_versions) +-- ============================================ + +-- Alice's personal app +INSERT INTO app (id, workspace_id, path, summary, versions, policy, extra_perms) +VALUES (2001, 'test-workspace', 'u/alice/my_app', 'Alice app', '{}', + '{"on_behalf_of": "u/alice", "on_behalf_of_email": "alice@windmill.dev", "execution_mode": "viewer"}', '{}'); + +-- Shared folder app +INSERT INTO app (id, workspace_id, path, summary, versions, policy, extra_perms) +VALUES (2002, 'test-workspace', 'f/shared/shared_app', 'Shared app', '{}', + '{"on_behalf_of": "u/admin", "on_behalf_of_email": "admin@windmill.dev", "execution_mode": "viewer"}', '{}'); + +-- Create app versions +INSERT INTO app_version (id, app_id, value, created_by, created_at) +VALUES + (2001, 2001, '{"grid": []}', 'alice', NOW()), + (2002, 2002, '{"grid": []}', 'admin', NOW()); + +-- Update apps with version references +UPDATE app SET versions = ARRAY[2001::bigint] WHERE id = 2001; +UPDATE app SET versions = ARRAY[2002::bigint] WHERE id = 2002; diff --git a/backend/windmill-api-integration-tests/tests/fixtures/resources_test.sql b/backend/windmill-api-integration-tests/tests/fixtures/resources_test.sql new file mode 100644 index 0000000000..1d267256cd --- /dev/null +++ b/backend/windmill-api-integration-tests/tests/fixtures/resources_test.sql @@ -0,0 +1,69 @@ +-- Combined fixture for resource endpoint tests + +-- === resource type test data === + +INSERT INTO resource_type (workspace_id, name, schema, description, created_by) +VALUES ('test-workspace', 'test_db', '{"type": "object", "properties": {"host": {"type": "string"}}}', + 'Test DB type', 'test-user'); + +-- === get_value_interpolated test data === + +INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by) +VALUES ('test-workspace', 'u/test-user/simple_resource', '{"host": "localhost", "port": 5432}', + 'Simple resource', 'object', '{}', 'test-user'); + +INSERT INTO variable (workspace_id, path, value, is_secret, description, extra_perms) +VALUES ('test-workspace', 'u/test-user/db_password', 'hunter2', false, 'DB password', '{}'); + +INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by) +VALUES ('test-workspace', 'u/test-user/resource_with_var', '{"host": "localhost", "password": "$var:u/test-user/db_password"}', + 'Resource with var ref', 'object', '{}', 'test-user'); + +INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by) +VALUES ('test-workspace', 'u/test-user/db_credentials', '{"user": "admin", "password": "secret123"}', + 'DB credentials', 'object', '{}', 'test-user'); + +INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by) +VALUES ('test-workspace', 'u/test-user/resource_with_res', '{"host": "localhost", "credentials": "$res:u/test-user/db_credentials"}', + 'Resource with res ref', 'object', '{}', 'test-user'); + +INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by) +VALUES ('test-workspace', 'u/test-user/resource_mixed', '{"host": "localhost", "password": "$var:u/test-user/db_password", "credentials": "$res:u/test-user/db_credentials"}', + 'Resource with mixed refs', 'object', '{}', 'test-user'); + +INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by) +VALUES ('test-workspace', 'u/test-user/null_resource', null, + 'Null resource', 'object', '{}', 'test-user'); + +INSERT INTO variable (workspace_id, path, value, is_secret, description, extra_perms) +VALUES ('test-workspace', 'u/test-user/api_key', 'sk-abc123', false, 'API key', '{}'); + +INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by) +VALUES ('test-workspace', 'u/test-user/inner_resource', '{"key": "$var:u/test-user/api_key"}', + 'Inner resource', 'object', '{}', 'test-user'); + +INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by) +VALUES ('test-workspace', 'u/test-user/chained_resource', '{"service": "myapi", "auth": "$res:u/test-user/inner_resource"}', + 'Chained resource', 'object', '{}', 'test-user'); + +INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by) +VALUES ('test-workspace', 'u/test-user/resource_with_array', '{"hosts": ["host1", "host2"], "port": 5432}', + 'Resource with array', 'object', '{}', 'test-user'); + +INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by) +VALUES ('test-workspace', 'u/test-user/scalar_var_resource', '"$var:u/test-user/db_password"', + 'Scalar var ref', 'string', '{}', 'test-user'); + +-- === mcp_tools test data === + +INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by) +VALUES ('test-workspace', 'u/test-user/mcp_valid', '{"name": "test-mcp", "url": "http://127.0.0.1:19999/mcp"}', + 'Valid MCP resource (unreachable)', 'mcp_server', '{}', 'test-user'); + +INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by) +VALUES ('test-workspace', 'u/test-user/mcp_invalid_format', '{"host": "localhost", "port": 5432}', + 'Not an MCP resource', 'object', '{}', 'test-user'); + +INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by) +VALUES ('test-workspace', 'u/test-user/mcp_null', null, + 'Null MCP resource', 'mcp_server', '{}', 'test-user'); diff --git a/backend/windmill-api-integration-tests/tests/fixtures/variables_test.sql b/backend/windmill-api-integration-tests/tests/fixtures/variables_test.sql new file mode 100644 index 0000000000..0d3e99e313 --- /dev/null +++ b/backend/windmill-api-integration-tests/tests/fixtures/variables_test.sql @@ -0,0 +1,10 @@ +-- Fixture for variable endpoint tests + +INSERT INTO variable (workspace_id, path, value, is_secret, description, extra_perms) +VALUES ('test-workspace', 'u/test-user/plain_var', 'hello world', false, 'A plain variable', '{}'); + +INSERT INTO variable (workspace_id, path, value, is_secret, description, extra_perms) +VALUES ('test-workspace', 'u/test-user/secret_var', 'supersecret', true, 'A secret variable', '{}'); + +INSERT INTO variable (workspace_id, path, value, is_secret, description, extra_perms) +VALUES ('test-workspace', 'u/test-user/another_var', 'foobar', false, 'Another variable', '{}'); diff --git a/backend/tests/flows.rs b/backend/windmill-api-integration-tests/tests/flows.rs similarity index 99% rename from backend/tests/flows.rs rename to backend/windmill-api-integration-tests/tests/flows.rs index 848231f2fc..ff3f86bf2d 100644 --- a/backend/tests/flows.rs +++ b/backend/windmill-api-integration-tests/tests/flows.rs @@ -1,8 +1,7 @@ use serde_json::json; use sqlx::{Pool, Postgres}; -mod common; -use common::*; +use windmill_test_utils::*; fn flow_url(port: u16, endpoint: &str, path: &str) -> String { format!("http://localhost:{port}/api/w/test-workspace/flows/{endpoint}/{path}") @@ -40,7 +39,7 @@ fn new_flow(path: &str, summary: &str) -> serde_json::Value { }) } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_flow_endpoints(db: Pool) -> anyhow::Result<()> { initialize_tracing().await; let server = ApiServer::start(db.clone()).await?; diff --git a/backend/tests/folders.rs b/backend/windmill-api-integration-tests/tests/folders.rs similarity index 98% rename from backend/tests/folders.rs rename to backend/windmill-api-integration-tests/tests/folders.rs index 075eea69eb..3d2ff5b4dd 100644 --- a/backend/tests/folders.rs +++ b/backend/windmill-api-integration-tests/tests/folders.rs @@ -1,8 +1,7 @@ use serde_json::json; use sqlx::{Pool, Postgres}; -mod common; -use common::*; +use windmill_test_utils::*; fn folder_url(port: u16, endpoint: &str, name: &str) -> String { format!("http://localhost:{port}/api/w/test-workspace/folders/{endpoint}/{name}") @@ -16,7 +15,7 @@ fn authed(builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder { builder.header("Authorization", "Bearer SECRET_TOKEN") } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_folder_endpoints(db: Pool) -> anyhow::Result<()> { initialize_tracing().await; let server = ApiServer::start(db.clone()).await?; diff --git a/backend/tests/groups.rs b/backend/windmill-api-integration-tests/tests/groups.rs similarity index 99% rename from backend/tests/groups.rs rename to backend/windmill-api-integration-tests/tests/groups.rs index 453a15916e..0fd8ee7e08 100644 --- a/backend/tests/groups.rs +++ b/backend/windmill-api-integration-tests/tests/groups.rs @@ -1,8 +1,7 @@ use serde_json::json; use sqlx::{Pool, Postgres}; -mod common; -use common::*; +use windmill_test_utils::*; fn group_url(port: u16, endpoint: &str, name: &str) -> String { format!("http://localhost:{port}/api/w/test-workspace/groups/{endpoint}/{name}") @@ -16,7 +15,7 @@ fn authed(builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder { builder.header("Authorization", "Bearer SECRET_TOKEN") } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_group_endpoints(db: Pool) -> anyhow::Result<()> { initialize_tracing().await; let server = ApiServer::start(db.clone()).await?; diff --git a/backend/tests/permissions.rs b/backend/windmill-api-integration-tests/tests/permissions.rs similarity index 98% rename from backend/tests/permissions.rs rename to backend/windmill-api-integration-tests/tests/permissions.rs index 0b1604fbb1..329ed5e11d 100644 --- a/backend/tests/permissions.rs +++ b/backend/windmill-api-integration-tests/tests/permissions.rs @@ -1,8 +1,7 @@ use serde_json::json; use sqlx::{Pool, Postgres}; -mod common; -use common::*; +use windmill_test_utils::*; /// Helper to create a client authenticated as a specific user async fn create_client_for_user(_port: u16, token: &str) -> reqwest::Client { @@ -44,7 +43,7 @@ async fn can_write(client: &reqwest::Client, url: &str, body: serde_json::Value) /// `cargo test --features deno_core permissions -- --ignored` #[ignore] #[cfg(feature = "deno_core")] -#[sqlx::test(fixtures("base", "permissions_test"))] +#[sqlx::test(migrations = "../migrations", fixtures("base", "permissions_test"))] async fn test_permissions_exhaustive(db: Pool) -> anyhow::Result<()> { initialize_tracing().await; let server = ApiServer::start(db.clone()).await?; @@ -477,7 +476,7 @@ async fn test_permissions_exhaustive(db: Pool) -> anyhow::Result<()> { /// Additional test for verifying group permission inheritance #[ignore] #[cfg(feature = "deno_core")] -#[sqlx::test(fixtures("base", "permissions_test"))] +#[sqlx::test(migrations = "../migrations", fixtures("base", "permissions_test"))] async fn test_group_permission_inheritance(db: Pool) -> anyhow::Result<()> { initialize_tracing().await; let server = ApiServer::start(db.clone()).await?; @@ -547,7 +546,7 @@ async fn test_group_permission_inheritance(db: Pool) -> anyhow::Result /// Test that permissions work correctly for all item types #[ignore] #[cfg(feature = "deno_core")] -#[sqlx::test(fixtures("base", "permissions_test"))] +#[sqlx::test(migrations = "../migrations", fixtures("base", "permissions_test"))] async fn test_all_item_types_permissions(db: Pool) -> anyhow::Result<()> { initialize_tracing().await; let server = ApiServer::start(db.clone()).await?; @@ -588,7 +587,7 @@ async fn test_all_item_types_permissions(db: Pool) -> anyhow::Result<( /// Operators have limited permissions - they can execute but cannot manage resources #[ignore] #[cfg(feature = "deno_core")] -#[sqlx::test(fixtures("base", "permissions_test"))] +#[sqlx::test(migrations = "../migrations", fixtures("base", "permissions_test"))] async fn test_operator_cannot_create_update(db: Pool) -> anyhow::Result<()> { initialize_tracing().await; let server = ApiServer::start(db.clone()).await?; diff --git a/backend/tests/protection_rules.rs b/backend/windmill-api-integration-tests/tests/protection_rules.rs similarity index 98% rename from backend/tests/protection_rules.rs rename to backend/windmill-api-integration-tests/tests/protection_rules.rs index 31198f9c9c..9469554999 100644 --- a/backend/tests/protection_rules.rs +++ b/backend/windmill-api-integration-tests/tests/protection_rules.rs @@ -7,8 +7,7 @@ use serde_json::json; use sqlx::{Pool, Postgres}; use windmill_common::workspaces::invalidate_protection_rules_cache; -mod common; -use common::*; +use windmill_test_utils::*; fn client() -> reqwest::Client { reqwest::Client::new() @@ -51,7 +50,7 @@ fn new_flow(path: &str, summary: &str) -> serde_json::Value { /// Comprehensive test for protection rules functionality. /// Tests all essential cases in a single test to avoid cache interference. -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_protection_rules(db: Pool) -> anyhow::Result<()> { initialize_tracing().await; invalidate_protection_rules_cache("test-workspace"); diff --git a/backend/tests/resources.rs b/backend/windmill-api-integration-tests/tests/resources.rs similarity index 98% rename from backend/tests/resources.rs rename to backend/windmill-api-integration-tests/tests/resources.rs index a81daf029e..6294c67308 100644 --- a/backend/tests/resources.rs +++ b/backend/windmill-api-integration-tests/tests/resources.rs @@ -1,8 +1,7 @@ use serde_json::json; use sqlx::{Pool, Postgres}; -mod common; -use common::*; +use windmill_test_utils::*; fn resource_url(port: u16, endpoint: &str, path: &str) -> String { format!("http://localhost:{port}/api/w/test-workspace/resources/{endpoint}/{path}") @@ -23,7 +22,7 @@ async fn authed_get(port: u16, endpoint: &str, path: &str) -> reqwest::Response .unwrap() } -#[sqlx::test(fixtures("base", "resources_test"))] +#[sqlx::test(migrations = "../migrations", fixtures("base", "resources_test"))] async fn test_resource_endpoints(db: Pool) -> anyhow::Result<()> { initialize_tracing().await; let server = ApiServer::start(db.clone()).await?; @@ -400,7 +399,7 @@ async fn test_resource_endpoints(db: Pool) -> anyhow::Result<()> { } #[cfg(feature = "mcp")] -#[sqlx::test(fixtures("base", "resources_test"))] +#[sqlx::test(migrations = "../migrations", fixtures("base", "resources_test"))] async fn test_mcp_tools(db: Pool) -> anyhow::Result<()> { initialize_tracing().await; let server = ApiServer::start(db.clone()).await?; diff --git a/backend/tests/schedules.rs b/backend/windmill-api-integration-tests/tests/schedules.rs similarity index 98% rename from backend/tests/schedules.rs rename to backend/windmill-api-integration-tests/tests/schedules.rs index ee66264c5f..89ba0ec2cc 100644 --- a/backend/tests/schedules.rs +++ b/backend/windmill-api-integration-tests/tests/schedules.rs @@ -1,8 +1,7 @@ use serde_json::json; use sqlx::{Pool, Postgres}; -mod common; -use common::*; +use windmill_test_utils::*; fn schedule_url(port: u16, endpoint: &str, path: &str) -> String { format!("http://localhost:{port}/api/w/test-workspace/schedules/{endpoint}/{path}") @@ -23,7 +22,7 @@ async fn authed_get(port: u16, endpoint: &str, path: &str) -> reqwest::Response .unwrap() } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_schedule_endpoints(db: Pool) -> anyhow::Result<()> { initialize_tracing().await; let server = ApiServer::start(db.clone()).await?; diff --git a/backend/tests/scripts.rs b/backend/windmill-api-integration-tests/tests/scripts.rs similarity index 99% rename from backend/tests/scripts.rs rename to backend/windmill-api-integration-tests/tests/scripts.rs index 3f55f36a64..74fd9c8611 100644 --- a/backend/tests/scripts.rs +++ b/backend/windmill-api-integration-tests/tests/scripts.rs @@ -1,8 +1,7 @@ use serde_json::json; use sqlx::{Pool, Postgres}; -mod common; -use common::*; +use windmill_test_utils::*; fn script_url(port: u16, endpoint: &str, path: &str) -> String { format!("http://localhost:{port}/api/w/test-workspace/scripts/{endpoint}/{path}") @@ -39,7 +38,7 @@ fn new_script(path: &str, summary: &str, content: &str) -> serde_json::Value { }) } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_script_endpoints(db: Pool) -> anyhow::Result<()> { initialize_tracing().await; let server = ApiServer::start(db.clone()).await?; diff --git a/backend/tests/trigger_e2e.rs b/backend/windmill-api-integration-tests/tests/trigger_e2e.rs similarity index 98% rename from backend/tests/trigger_e2e.rs rename to backend/windmill-api-integration-tests/tests/trigger_e2e.rs index 7bfb735a5c..3b0e32749a 100644 --- a/backend/tests/trigger_e2e.rs +++ b/backend/windmill-api-integration-tests/tests/trigger_e2e.rs @@ -24,8 +24,7 @@ use serde_json::json; use sqlx::{Pool, Postgres}; use std::time::Duration; -mod common; -use common::*; +use windmill_test_utils::*; // ============================================================================ // Helpers @@ -137,7 +136,7 @@ async fn insert_resource( /// -- --ignored --nocapture /// ``` #[ignore = "requires running MQTT broker on localhost:1883"] -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_mqtt_e2e(db: Pool) -> anyhow::Result<()> { initialize_tracing().await; @@ -230,7 +229,7 @@ async fn test_mqtt_e2e(db: Pool) -> anyhow::Result<()> { /// -- --ignored --nocapture /// ``` #[ignore = "requires running WebSocket echo server on localhost:8765"] -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_websocket_e2e(db: Pool) -> anyhow::Result<()> { initialize_tracing().await; @@ -286,7 +285,7 @@ async fn test_websocket_e2e(db: Pool) -> anyhow::Result<()> { /// -- --ignored --nocapture /// ``` #[ignore = "requires PostgreSQL with wal_level=logical"] -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_postgres_e2e(db: Pool) -> anyhow::Result<()> { initialize_tracing().await; @@ -387,7 +386,7 @@ async fn test_postgres_e2e(db: Pool) -> anyhow::Result<()> { /// ``` #[cfg(all(feature = "enterprise", feature = "private"))] #[ignore = "requires running Kafka broker on localhost:9092"] -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_kafka_e2e(db: Pool) -> anyhow::Result<()> { initialize_tracing().await; @@ -478,7 +477,7 @@ async fn test_kafka_e2e(db: Pool) -> anyhow::Result<()> { /// ``` #[cfg(all(feature = "enterprise", feature = "private"))] #[ignore = "requires running NATS server on localhost:4222"] -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_nats_e2e(db: Pool) -> anyhow::Result<()> { initialize_tracing().await; @@ -553,7 +552,7 @@ async fn test_nats_e2e(db: Pool) -> anyhow::Result<()> { /// ``` #[cfg(all(feature = "enterprise", feature = "private"))] #[ignore = "requires LocalStack SQS on localhost:4566"] -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_sqs_e2e(db: Pool) -> anyhow::Result<()> { initialize_tracing().await; @@ -641,7 +640,7 @@ async fn test_sqs_e2e(db: Pool) -> anyhow::Result<()> { /// ``` #[cfg(all(feature = "enterprise", feature = "private"))] #[ignore = "requires GCP Pub/Sub emulator on localhost:8085"] -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_gcp_e2e(db: Pool) -> anyhow::Result<()> { initialize_tracing().await; diff --git a/backend/tests/triggers.rs b/backend/windmill-api-integration-tests/tests/triggers.rs similarity index 95% rename from backend/tests/triggers.rs rename to backend/windmill-api-integration-tests/tests/triggers.rs index ff7d34decd..46854d566e 100644 --- a/backend/tests/triggers.rs +++ b/backend/windmill-api-integration-tests/tests/triggers.rs @@ -12,14 +12,13 @@ use serde::Deserialize; use serde_json::json; use sqlx::{Pool, Postgres}; -mod common; -use common::*; +use windmill_test_utils::*; // ============================================================================ // Capture Config Tests (direct DB) // ============================================================================ -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_capture_config_insert_and_query(db: Pool) -> anyhow::Result<()> { sqlx::query!( r#" @@ -56,7 +55,7 @@ async fn test_capture_config_insert_and_query(db: Pool) -> anyhow::Res Ok(()) } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_capture_config_upsert(db: Pool) -> anyhow::Result<()> { sqlx::query!( r#" @@ -104,7 +103,7 @@ async fn test_capture_config_upsert(db: Pool) -> anyhow::Result<()> { Ok(()) } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_capture_config_ping_updates_timestamp(db: Pool) -> anyhow::Result<()> { sqlx::query!( r#" @@ -159,7 +158,7 @@ async fn test_capture_config_ping_updates_timestamp(db: Pool) -> anyho // Capture Payload Tests (direct DB) // ============================================================================ -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_capture_insert_and_list(db: Pool) -> anyhow::Result<()> { for i in 0..2 { sqlx::query!( @@ -192,7 +191,7 @@ async fn test_capture_insert_and_list(db: Pool) -> anyhow::Result<()> Ok(()) } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_capture_delete(db: Pool) -> anyhow::Result<()> { let id = sqlx::query_scalar!( r#" @@ -227,7 +226,7 @@ async fn test_capture_delete(db: Pool) -> anyhow::Result<()> { Ok(()) } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_capture_filter_by_trigger_kind(db: Pool) -> anyhow::Result<()> { sqlx::query!( r#" @@ -304,7 +303,7 @@ struct CaptureResponse { main_args: serde_json::Value, } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_capture_api_set_config_and_list(db: Pool) -> anyhow::Result<()> { initialize_tracing().await; @@ -348,7 +347,7 @@ async fn test_capture_api_set_config_and_list(db: Pool) -> anyhow::Res Ok(()) } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_capture_api_list_captures(db: Pool) -> anyhow::Result<()> { initialize_tracing().await; @@ -394,7 +393,7 @@ async fn test_capture_api_list_captures(db: Pool) -> anyhow::Result<() Ok(()) } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_capture_api_get_single(db: Pool) -> anyhow::Result<()> { initialize_tracing().await; @@ -441,7 +440,7 @@ async fn test_capture_api_get_single(db: Pool) -> anyhow::Result<()> { Ok(()) } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_capture_api_delete(db: Pool) -> anyhow::Result<()> { initialize_tracing().await; @@ -493,7 +492,7 @@ async fn test_capture_api_delete(db: Pool) -> anyhow::Result<()> { Ok(()) } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_capture_api_pagination(db: Pool) -> anyhow::Result<()> { initialize_tracing().await; @@ -555,7 +554,7 @@ async fn test_capture_api_pagination(db: Pool) -> anyhow::Result<()> { // HTTP Trigger Tests (direct DB) // ============================================================================ -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_http_trigger_insert_and_query(db: Pool) -> anyhow::Result<()> { sqlx::query!( r#" @@ -610,7 +609,7 @@ async fn test_http_trigger_insert_and_query(db: Pool) -> anyhow::Resul Ok(()) } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_http_trigger_multiple_methods(db: Pool) -> anyhow::Result<()> { let methods = ["get", "post", "put", "delete", "patch"]; @@ -657,7 +656,7 @@ async fn test_http_trigger_multiple_methods(db: Pool) -> anyhow::Resul Ok(()) } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_http_trigger_authentication_methods(db: Pool) -> anyhow::Result<()> { let auth_methods = ["none", "windmill", "api_key", "basic_http", "signature"]; @@ -708,7 +707,7 @@ async fn test_http_trigger_authentication_methods(db: Pool) -> anyhow: Ok(()) } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_http_trigger_update(db: Pool) -> anyhow::Result<()> { sqlx::query!( r#" @@ -761,7 +760,7 @@ async fn test_http_trigger_update(db: Pool) -> anyhow::Result<()> { Ok(()) } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_http_trigger_delete(db: Pool) -> anyhow::Result<()> { sqlx::query!( r#" @@ -813,7 +812,7 @@ async fn test_http_trigger_delete(db: Pool) -> anyhow::Result<()> { Ok(()) } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_http_trigger_mode_filtering(db: Pool) -> anyhow::Result<()> { let modes = ["enabled", "disabled", "suspended"]; @@ -870,7 +869,7 @@ async fn test_http_trigger_mode_filtering(db: Pool) -> anyhow::Result< // Other Trigger Types Tests (DB schema validation) // ============================================================================ -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_websocket_trigger_insert(db: Pool) -> anyhow::Result<()> { sqlx::query!( r#" @@ -909,7 +908,7 @@ async fn test_websocket_trigger_insert(db: Pool) -> anyhow::Result<()> Ok(()) } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_kafka_trigger_insert(db: Pool) -> anyhow::Result<()> { sqlx::query!( r#" @@ -952,7 +951,7 @@ async fn test_kafka_trigger_insert(db: Pool) -> anyhow::Result<()> { Ok(()) } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_postgres_trigger_insert(db: Pool) -> anyhow::Result<()> { sqlx::query!( r#" @@ -995,7 +994,7 @@ async fn test_postgres_trigger_insert(db: Pool) -> anyhow::Result<()> Ok(()) } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_nats_trigger_insert(db: Pool) -> anyhow::Result<()> { sqlx::query!( r#" @@ -1038,7 +1037,7 @@ async fn test_nats_trigger_insert(db: Pool) -> anyhow::Result<()> { Ok(()) } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_sqs_trigger_insert(db: Pool) -> anyhow::Result<()> { sqlx::query!( r#" @@ -1089,7 +1088,7 @@ async fn test_sqs_trigger_insert(db: Pool) -> anyhow::Result<()> { // Cross-trigger tests // ============================================================================ -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_trigger_server_state_tracking(db: Pool) -> anyhow::Result<()> { sqlx::query!( r#" @@ -1144,7 +1143,7 @@ async fn test_trigger_server_state_tracking(db: Pool) -> anyhow::Resul Ok(()) } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_trigger_mode_filtering(db: Pool) -> anyhow::Result<()> { let modes = ["enabled", "disabled", "enabled"]; @@ -1191,7 +1190,7 @@ async fn test_trigger_mode_filtering(db: Pool) -> anyhow::Result<()> { Ok(()) } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_multiple_capture_configs_per_path(db: Pool) -> anyhow::Result<()> { let trigger_kinds = ["webhook", "email", "kafka"]; @@ -1229,7 +1228,7 @@ async fn test_multiple_capture_configs_per_path(db: Pool) -> anyhow::R // Schedule Tests (DB-level) // ============================================================================ -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_schedule_insert_and_query(db: Pool) -> anyhow::Result<()> { sqlx::query!( r#" @@ -1277,7 +1276,7 @@ async fn test_schedule_insert_and_query(db: Pool) -> anyhow::Result<() // MQTT Trigger Tests (DB-level) // ============================================================================ -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_mqtt_trigger_insert(db: Pool) -> anyhow::Result<()> { sqlx::query( r#" @@ -1321,7 +1320,7 @@ async fn test_mqtt_trigger_insert(db: Pool) -> anyhow::Result<()> { Ok(()) } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_mqtt_trigger_update(db: Pool) -> anyhow::Result<()> { sqlx::query( r#" @@ -1366,7 +1365,7 @@ async fn test_mqtt_trigger_update(db: Pool) -> anyhow::Result<()> { Ok(()) } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_mqtt_trigger_delete(db: Pool) -> anyhow::Result<()> { sqlx::query( r#" @@ -1414,7 +1413,7 @@ async fn test_mqtt_trigger_delete(db: Pool) -> anyhow::Result<()> { // GCP Trigger Tests (DB-level) // ============================================================================ -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_gcp_trigger_insert_pull(db: Pool) -> anyhow::Result<()> { sqlx::query( r#" @@ -1465,7 +1464,7 @@ async fn test_gcp_trigger_insert_pull(db: Pool) -> anyhow::Result<()> Ok(()) } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_gcp_trigger_insert_push(db: Pool) -> anyhow::Result<()> { sqlx::query( r#" @@ -1515,7 +1514,7 @@ async fn test_gcp_trigger_insert_push(db: Pool) -> anyhow::Result<()> Ok(()) } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_gcp_trigger_unique_constraint(db: Pool) -> anyhow::Result<()> { let insert_query = r#" INSERT INTO gcp_trigger ( @@ -1569,7 +1568,7 @@ async fn test_gcp_trigger_unique_constraint(db: Pool) -> anyhow::Resul // Email Trigger Tests (DB-level) // ============================================================================ -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_email_trigger_insert(db: Pool) -> anyhow::Result<()> { sqlx::query!( r#" @@ -1612,7 +1611,7 @@ async fn test_email_trigger_insert(db: Pool) -> anyhow::Result<()> { Ok(()) } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_email_trigger_update(db: Pool) -> anyhow::Result<()> { sqlx::query!( r#" @@ -1658,7 +1657,7 @@ async fn test_email_trigger_update(db: Pool) -> anyhow::Result<()> { Ok(()) } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_email_trigger_delete(db: Pool) -> anyhow::Result<()> { sqlx::query!( r#" diff --git a/backend/tests/users.rs b/backend/windmill-api-integration-tests/tests/users.rs similarity index 99% rename from backend/tests/users.rs rename to backend/windmill-api-integration-tests/tests/users.rs index 352b3cc5c6..61e174a002 100644 --- a/backend/tests/users.rs +++ b/backend/windmill-api-integration-tests/tests/users.rs @@ -1,8 +1,7 @@ use serde_json::json; use sqlx::{Pool, Postgres}; -mod common; -use common::*; +use windmill_test_utils::*; fn user_url(port: u16, endpoint: &str, name: &str) -> String { format!("http://localhost:{port}/api/w/test-workspace/users/{endpoint}/{name}") @@ -16,7 +15,7 @@ fn authed(builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder { builder.header("Authorization", "Bearer SECRET_TOKEN") } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_user_endpoints(db: Pool) -> anyhow::Result<()> { initialize_tracing().await; let server = ApiServer::start(db.clone()).await?; diff --git a/backend/tests/variables.rs b/backend/windmill-api-integration-tests/tests/variables.rs similarity index 98% rename from backend/tests/variables.rs rename to backend/windmill-api-integration-tests/tests/variables.rs index 22892e522a..0d4edaff91 100644 --- a/backend/tests/variables.rs +++ b/backend/windmill-api-integration-tests/tests/variables.rs @@ -1,8 +1,7 @@ use serde_json::json; use sqlx::{Pool, Postgres}; -mod common; -use common::*; +use windmill_test_utils::*; fn variable_url(port: u16, endpoint: &str, path: &str) -> String { format!("http://localhost:{port}/api/w/test-workspace/variables/{endpoint}/{path}") @@ -23,7 +22,7 @@ async fn authed_get(port: u16, endpoint: &str, path: &str) -> reqwest::Response .unwrap() } -#[sqlx::test(fixtures("base", "variables_test"))] +#[sqlx::test(migrations = "../migrations", fixtures("base", "variables_test"))] async fn test_variable_endpoints(db: Pool) -> anyhow::Result<()> { initialize_tracing().await; let server = ApiServer::start(db.clone()).await?; diff --git a/backend/tests/workspace_comparison.rs b/backend/windmill-api-integration-tests/tests/workspace_comparison.rs similarity index 99% rename from backend/tests/workspace_comparison.rs rename to backend/windmill-api-integration-tests/tests/workspace_comparison.rs index 5761d6e051..f9cb6037c5 100644 --- a/backend/tests/workspace_comparison.rs +++ b/backend/windmill-api-integration-tests/tests/workspace_comparison.rs @@ -1,8 +1,7 @@ use serde_json::json; use sqlx::{Pool, Postgres}; -mod common; -use common::*; +use windmill_test_utils::*; /// Comprehensive integration test for the compare_workspaces endpoint. /// @@ -12,7 +11,7 @@ use common::*; /// 3. Making various changes in both workspaces (new items, modifications, conflicts, deletions, renames) /// 4. Populating the workspace_diff table to simulate Git sync tracking /// 5. Calling compare_workspaces and verifying all aspects of the comparison -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_compare_workspaces_comprehensive(db: Pool) -> anyhow::Result<()> { initialize_tracing().await; diff --git a/backend/tests/workspaces.rs b/backend/windmill-api-integration-tests/tests/workspaces.rs similarity index 99% rename from backend/tests/workspaces.rs rename to backend/windmill-api-integration-tests/tests/workspaces.rs index 4a7507b155..58c07965dc 100644 --- a/backend/tests/workspaces.rs +++ b/backend/windmill-api-integration-tests/tests/workspaces.rs @@ -1,8 +1,7 @@ use serde_json::json; use sqlx::{Pool, Postgres}; -mod common; -use common::*; +use windmill_test_utils::*; fn client() -> reqwest::Client { reqwest::Client::new() @@ -12,7 +11,7 @@ fn authed(builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder { builder.header("Authorization", "Bearer SECRET_TOKEN") } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_workspace_endpoints(db: Pool) -> anyhow::Result<()> { initialize_tracing().await; let server = ApiServer::start(db.clone()).await?; diff --git a/backend/windmill-common/tests/fixtures/base.sql b/backend/windmill-common/tests/fixtures/base.sql new file mode 100644 index 0000000000..7db9918fba --- /dev/null +++ b/backend/windmill-common/tests/fixtures/base.sql @@ -0,0 +1,146 @@ +-- used for backend automated testing +-- https://docs.rs/sqlx/latest/sqlx/attr.test.html + +INSERT INTO workspace + (id, name, owner) + VALUES ('test-workspace', 'test-workspace', 'test-user'); + +INSERT INTO usr(workspace_id, email, username, is_admin, role) VALUES + ('test-workspace', 'test@windmill.dev', 'test-user', true, 'Admin'); + +INSERT INTO workspace_key(workspace_id, kind, key) VALUES + ('test-workspace', 'cloud', 'test-key'); + + +INSERT INTO workspace_settings (workspace_id) VALUES + ('test-workspace'); + +INSERT INTO group_ (workspace_id, name, summary, extra_perms) VALUES + ('test-workspace', 'all', 'All users', '{}'); + +INSERT INTO password(email, password_hash, login_type, super_admin, verified, name, username) + VALUES ('test@windmill.dev', 'not-a-real-hash', 'password', true, true, 'Test User', 'test-user'); + +INSERT INTO password(email, password_hash, login_type, super_admin, verified, name) + VALUES ('test2@windmill.dev', 'not-a-real-hash', 'password', false, true, 'Test User 2'); + +INSERT INTO password(email, password_hash, login_type, super_admin, verified, name) + VALUES ('test3@windmill.dev', 'not-a-real-hash', 'password', false, true, 'Test User 3'); + +INSERT INTO usr(workspace_id, email, username, is_admin, role) VALUES + ('test-workspace', 'test2@windmill.dev', 'test-user-2', false, 'User'); + +INSERT INTO usr(workspace_id, email, username, is_admin, role) VALUES + ('test-workspace', 'test3@windmill.dev', 'test-user-3', false, 'User'); + +insert INTO token(token, email, label, super_admin) VALUES ('SECRET_TOKEN', 'test@windmill.dev', 'test token', true); +insert INTO token(token, email, label, super_admin) VALUES ('SECRET_TOKEN_2', 'test2@windmill.dev', 'test token 2', false); +insert INTO token(token, email, label, super_admin) VALUES ('SECRET_TOKEN_3', 'test3@windmill.dev', 'test token 3', false); + +GRANT ALL PRIVILEGES ON TABLE workspace_key TO windmill_admin; +GRANT ALL PRIVILEGES ON TABLE workspace_key TO windmill_user; + +CREATE FUNCTION "notify_insert_on_completed_job" () +RETURNS TRIGGER AS $$ +BEGIN + PERFORM pg_notify('completed', NEW.id::text); + RETURN NEW; +END; +$$ LANGUAGE PLPGSQL; + + CREATE TRIGGER "notify_insert_on_completed_job" + AFTER INSERT ON "v2_job_completed" + FOR EACH ROW +EXECUTE FUNCTION "notify_insert_on_completed_job" (); + + +CREATE FUNCTION "notify_queue" () +RETURNS TRIGGER AS $$ +BEGIN + PERFORM pg_notify('queued', NEW.id::text); + RETURN NEW; +END; +$$ LANGUAGE PLPGSQL; + + CREATE TRIGGER "notify_queue_after_insert" + AFTER INSERT ON "v2_job_queue" + FOR EACH ROW +EXECUTE FUNCTION "notify_queue" (); + + CREATE TRIGGER "notify_queue_after_flow_status_update" + AFTER UPDATE ON "v2_job_status" + FOR EACH ROW + WHEN (NEW.flow_status IS DISTINCT FROM OLD.flow_status) +EXECUTE FUNCTION "notify_queue" (); + +-- Apply phase 4: +DROP FUNCTION IF EXISTS v2_job_after_update CASCADE; +DROP FUNCTION IF EXISTS v2_job_completed_before_insert CASCADE; +DROP FUNCTION IF EXISTS v2_job_completed_before_update CASCADE; +DROP FUNCTION IF EXISTS v2_job_queue_after_insert CASCADE; +DROP FUNCTION IF EXISTS v2_job_queue_before_insert CASCADE; +DROP FUNCTION IF EXISTS v2_job_queue_before_update CASCADE; +DROP FUNCTION IF EXISTS v2_job_runtime_before_insert CASCADE; +DROP FUNCTION IF EXISTS v2_job_runtime_before_update CASCADE; +DROP FUNCTION IF EXISTS v2_job_status_before_insert CASCADE; +DROP FUNCTION IF EXISTS v2_job_status_before_update CASCADE; + +DROP VIEW IF EXISTS completed_job, completed_job_view, job, queue, queue_view CASCADE; + +ALTER TABLE v2_job_queue + DROP COLUMN IF EXISTS __parent_job CASCADE, + DROP COLUMN IF EXISTS __created_by CASCADE, + DROP COLUMN IF EXISTS __script_hash CASCADE, + DROP COLUMN IF EXISTS __script_path CASCADE, + DROP COLUMN IF EXISTS __args CASCADE, + DROP COLUMN IF EXISTS __logs CASCADE, + DROP COLUMN IF EXISTS __raw_code CASCADE, + DROP COLUMN IF EXISTS __canceled CASCADE, + DROP COLUMN IF EXISTS __last_ping CASCADE, + DROP COLUMN IF EXISTS __job_kind CASCADE, + DROP COLUMN IF EXISTS __env_id CASCADE, + DROP COLUMN IF EXISTS __schedule_path CASCADE, + DROP COLUMN IF EXISTS __permissioned_as CASCADE, + DROP COLUMN IF EXISTS __flow_status CASCADE, + DROP COLUMN IF EXISTS __raw_flow CASCADE, + DROP COLUMN IF EXISTS __is_flow_step CASCADE, + DROP COLUMN IF EXISTS __language CASCADE, + DROP COLUMN IF EXISTS __same_worker CASCADE, + DROP COLUMN IF EXISTS __raw_lock CASCADE, + DROP COLUMN IF EXISTS __pre_run_error CASCADE, + DROP COLUMN IF EXISTS __email CASCADE, + DROP COLUMN IF EXISTS __visible_to_owner CASCADE, + DROP COLUMN IF EXISTS __mem_peak CASCADE, + DROP COLUMN IF EXISTS __root_job CASCADE, + DROP COLUMN IF EXISTS __leaf_jobs CASCADE, + DROP COLUMN IF EXISTS __concurrent_limit CASCADE, + DROP COLUMN IF EXISTS __concurrency_time_window_s CASCADE, + DROP COLUMN IF EXISTS __timeout CASCADE, + DROP COLUMN IF EXISTS __flow_step_id CASCADE, + DROP COLUMN IF EXISTS __cache_ttl CASCADE; + +LOCK TABLE v2_job_queue IN ACCESS EXCLUSIVE MODE; +ALTER TABLE v2_job_completed + DROP COLUMN IF EXISTS __parent_job CASCADE, + DROP COLUMN IF EXISTS __created_by CASCADE, + DROP COLUMN IF EXISTS __created_at CASCADE, + DROP COLUMN IF EXISTS __success CASCADE, + DROP COLUMN IF EXISTS __script_hash CASCADE, + DROP COLUMN IF EXISTS __script_path CASCADE, + DROP COLUMN IF EXISTS __args CASCADE, + DROP COLUMN IF EXISTS __logs CASCADE, + DROP COLUMN IF EXISTS __raw_code CASCADE, + DROP COLUMN IF EXISTS __canceled CASCADE, + DROP COLUMN IF EXISTS __job_kind CASCADE, + DROP COLUMN IF EXISTS __env_id CASCADE, + DROP COLUMN IF EXISTS __schedule_path CASCADE, + DROP COLUMN IF EXISTS __permissioned_as CASCADE, + DROP COLUMN IF EXISTS __raw_flow CASCADE, + DROP COLUMN IF EXISTS __is_flow_step CASCADE, + DROP COLUMN IF EXISTS __language CASCADE, + DROP COLUMN IF EXISTS __is_skipped CASCADE, + DROP COLUMN IF EXISTS __raw_lock CASCADE, + DROP COLUMN IF EXISTS __email CASCADE, + DROP COLUMN IF EXISTS __visible_to_owner CASCADE, + DROP COLUMN IF EXISTS __tag CASCADE, + DROP COLUMN IF EXISTS __priority CASCADE; diff --git a/backend/windmill-common/tests/fixtures/instance_group_auto_add.sql b/backend/windmill-common/tests/fixtures/instance_group_auto_add.sql new file mode 100644 index 0000000000..599abd7ea2 --- /dev/null +++ b/backend/windmill-common/tests/fixtures/instance_group_auto_add.sql @@ -0,0 +1,76 @@ +-- Fixture for instance group auto-add tests +-- Sets up test data for testing the interaction between instance groups and workspace auto-add + +-- Create additional workspaces for testing +INSERT INTO workspace (id, name, owner) +VALUES + ('ws-with-auto-add', 'Workspace with Auto Add', 'admin'), + ('ws-no-auto-add', 'Workspace without Auto Add', 'admin'), + ('ws-multi-group', 'Workspace with Multiple Groups', 'admin') +ON CONFLICT DO NOTHING; + +INSERT INTO workspace_settings (workspace_id) +VALUES + ('ws-with-auto-add'), + ('ws-no-auto-add'), + ('ws-multi-group') +ON CONFLICT DO NOTHING; + +INSERT INTO workspace_key(workspace_id, kind, key) +VALUES + ('ws-with-auto-add', 'cloud', 'key-auto-add'), + ('ws-no-auto-add', 'cloud', 'key-no-auto'), + ('ws-multi-group', 'cloud', 'key-multi') +ON CONFLICT DO NOTHING; + +-- Create instance groups +INSERT INTO instance_group (name, summary, id) +VALUES + ('engineering', 'Engineering team', 'eng-uuid-001'), + ('sales', 'Sales team', 'sales-uuid-002'), + ('admins', 'Admin group', 'admin-uuid-003') +ON CONFLICT DO NOTHING; + +-- Create users in the password table (required for auto-add eligibility) +INSERT INTO password (email, password_hash, login_type, super_admin, verified, name) +VALUES + ('alice@example.com', 'hash1', 'password', false, true, 'Alice'), + ('bob@example.com', 'hash2', 'password', false, true, 'Bob'), + ('charlie@example.com', 'hash3', 'password', false, true, 'Charlie'), + ('dave@example.com', 'hash4', 'password', false, true, 'Dave'), + ('admin@windmill.dev', 'hash5', 'password', true, true, 'Admin') +ON CONFLICT DO NOTHING; + +-- Add users to instance groups +INSERT INTO email_to_igroup (email, igroup) +VALUES + ('alice@example.com', 'engineering'), + ('bob@example.com', 'engineering'), + ('bob@example.com', 'sales'), + ('charlie@example.com', 'sales'), + ('dave@example.com', 'admins') +ON CONFLICT DO NOTHING; + +-- Create admin user in workspaces for running tests +INSERT INTO usr (workspace_id, username, email, is_admin, operator) +VALUES + ('ws-with-auto-add', 'admin', 'admin@windmill.dev', true, false), + ('ws-no-auto-add', 'admin', 'admin@windmill.dev', true, false), + ('ws-multi-group', 'admin', 'admin@windmill.dev', true, false) +ON CONFLICT DO NOTHING; + +-- Create the 'all' group for each workspace (required for user auto-add) +INSERT INTO group_ (workspace_id, name, summary, extra_perms) +VALUES + ('ws-with-auto-add', 'all', 'All users', '{}'), + ('ws-no-auto-add', 'all', 'All users', '{}'), + ('ws-multi-group', 'all', 'All users', '{}') +ON CONFLICT DO NOTHING; + +-- Add admin to the 'all' group +INSERT INTO usr_to_group (workspace_id, usr, group_) +VALUES + ('ws-with-auto-add', 'admin', 'all'), + ('ws-no-auto-add', 'admin', 'all'), + ('ws-multi-group', 'admin', 'all') +ON CONFLICT DO NOTHING; diff --git a/backend/windmill-common/tests/fixtures/secret_backend.sql b/backend/windmill-common/tests/fixtures/secret_backend.sql new file mode 100644 index 0000000000..1ebbc13e9b --- /dev/null +++ b/backend/windmill-common/tests/fixtures/secret_backend.sql @@ -0,0 +1,31 @@ +-- Fixture for secret backend migration tests +-- Sets up test secrets in the variable table + +-- Create a second workspace for testing workspace isolation +INSERT INTO workspace (id, name, owner) +VALUES ('test-workspace-2', 'test-workspace-2', 'test-user') +ON CONFLICT DO NOTHING; + +INSERT INTO workspace_settings (workspace_id) +VALUES ('test-workspace-2') +ON CONFLICT DO NOTHING; + +INSERT INTO workspace_key(workspace_id, kind, key) +VALUES ('test-workspace-2', 'cloud', 'test-key-2') +ON CONFLICT DO NOTHING; + +-- Insert test variables with placeholder values. +-- Secret values are encrypted by the test setup using build_crypt + encrypt +-- with the workspace key, matching production behavior. +INSERT INTO variable (workspace_id, path, value, is_secret, description, extra_perms) +VALUES + ('test-workspace', 'u/test-user/db_password', 'PLACEHOLDER', true, 'Database password', '{}'), + ('test-workspace', 'u/test-user/api_key', 'PLACEHOLDER', true, 'API key for external service', '{}'), + ('test-workspace', 'u/test-user/public_var', 'not-a-secret', false, 'A non-secret variable', '{}') +ON CONFLICT DO NOTHING; + +-- Insert test secrets for workspace 2 (to test isolation) +INSERT INTO variable (workspace_id, path, value, is_secret, description, extra_perms) +VALUES + ('test-workspace-2', 'u/test-user/other_secret', 'PLACEHOLDER', true, 'Secret in workspace 2', '{}') +ON CONFLICT DO NOTHING; diff --git a/backend/tests/instance_group_auto_add.rs b/backend/windmill-common/tests/instance_group_auto_add.rs similarity index 96% rename from backend/tests/instance_group_auto_add.rs rename to backend/windmill-common/tests/instance_group_auto_add.rs index 60ebd3c922..e8a6c841c6 100644 --- a/backend/tests/instance_group_auto_add.rs +++ b/backend/windmill-common/tests/instance_group_auto_add.rs @@ -31,7 +31,7 @@ mod tests { /// Test that configuring instance groups for a workspace auto-adds existing group members #[ignore = "requires database setup - run with --ignored flag"] - #[sqlx::test(fixtures("base", "instance_group_auto_add"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "instance_group_auto_add"))] async fn test_configure_instance_groups_adds_existing_members(db: Pool) { // Configure workspace to auto-add users from 'engineering' group with 'developer' role let groups = vec!["engineering".to_string()]; @@ -158,7 +158,7 @@ mod tests { /// Test role assignment based on instance group configuration #[ignore = "requires database setup - run with --ignored flag"] - #[sqlx::test(fixtures("base", "instance_group_auto_add"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "instance_group_auto_add"))] async fn test_role_assignment_admin(db: Pool) { // Configure workspace with admins group having admin role let groups = vec!["admins".to_string()]; @@ -209,7 +209,7 @@ mod tests { /// Test role assignment for operator #[ignore = "requires database setup - run with --ignored flag"] - #[sqlx::test(fixtures("base", "instance_group_auto_add"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "instance_group_auto_add"))] async fn test_role_assignment_operator(db: Pool) { // Configure workspace with sales group having operator role let groups = vec!["sales".to_string()]; @@ -260,7 +260,7 @@ mod tests { /// Test role precedence when user is in multiple instance groups #[ignore = "requires database setup - run with --ignored flag"] - #[sqlx::test(fixtures("base", "instance_group_auto_add"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "instance_group_auto_add"))] async fn test_role_precedence_multiple_groups(db: Pool) { // Configure workspace with multiple groups: engineering (admin), sales (operator) // Bob is in both groups, should get admin role (highest precedence) @@ -322,7 +322,7 @@ mod tests { /// Test removing user from instance group removes them from workspace #[ignore = "requires database setup - run with --ignored flag"] - #[sqlx::test(fixtures("base", "instance_group_auto_add"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "instance_group_auto_add"))] async fn test_remove_user_from_instance_group(db: Pool) { // First, add alice to the workspace via engineering group let added_via = json!({"source": "instance_group", "group": "engineering"}); @@ -416,7 +416,7 @@ mod tests { /// Test that users added via domain are not affected by instance group removal #[ignore = "requires database setup - run with --ignored flag"] - #[sqlx::test(fixtures("base", "instance_group_auto_add"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "instance_group_auto_add"))] async fn test_domain_added_users_not_affected_by_group_removal(db: Pool) { // Add alice via domain (not instance group) let added_via = json!({"source": "domain", "domain": "example.com"}); @@ -469,7 +469,7 @@ mod tests { /// Test cleanup when instance group is removed from workspace configuration #[ignore = "requires database setup - run with --ignored flag"] - #[sqlx::test(fixtures("base", "instance_group_auto_add"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "instance_group_auto_add"))] async fn test_cleanup_removed_instance_groups(db: Pool) { // First, add users via engineering group for (username, email) in &[("alice", "alice@example.com"), ("bob", "bob@example.com")] { @@ -588,7 +588,7 @@ mod tests { /// Test that users are not duplicated if already in workspace #[ignore = "requires database setup - run with --ignored flag"] - #[sqlx::test(fixtures("base", "instance_group_auto_add"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "instance_group_auto_add"))] async fn test_no_duplicate_users(db: Pool) { // Add alice to workspace first (without instance group tracking) sqlx::query!( @@ -636,7 +636,7 @@ mod tests { /// Test workspace without auto-add configured is not affected #[ignore = "requires database setup - run with --ignored flag"] - #[sqlx::test(fixtures("base", "instance_group_auto_add"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "instance_group_auto_add"))] async fn test_workspace_without_auto_add_not_affected(db: Pool) { // ws-no-auto-add has no instance_groups configured @@ -672,7 +672,7 @@ mod tests { /// Test querying workspaces configured with a specific instance group #[ignore = "requires database setup - run with --ignored flag"] - #[sqlx::test(fixtures("base", "instance_group_auto_add"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "instance_group_auto_add"))] async fn test_query_workspaces_with_instance_group(db: Pool) { // Configure ws-with-auto-add to use engineering group let groups = vec!["engineering".to_string()]; diff --git a/backend/tests/notify_events.rs b/backend/windmill-common/tests/notify_events.rs similarity index 95% rename from backend/tests/notify_events.rs rename to backend/windmill-common/tests/notify_events.rs index a85a2f1c5a..36302b8e66 100644 --- a/backend/tests/notify_events.rs +++ b/backend/windmill-common/tests/notify_events.rs @@ -11,8 +11,6 @@ use sqlx::{Pool, Postgres}; use windmill_common::notify_events::{cleanup_old_events, get_latest_event_id, poll_notify_events}; -mod common; - /// Helper to insert a test event directly async fn insert_test_event(db: &Pool, channel: &str, payload: &str) -> i64 { sqlx::query_scalar::<_, i64>( @@ -39,7 +37,7 @@ async fn count_events_for_channel(db: &Pool, channel: &str) -> i64 { // Basic Functionality Tests // ============================================================================ -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_get_latest_event_id_returns_valid_id(db: Pool) { // Get current latest id let latest_id = get_latest_event_id(&db).await.expect("Should get latest event id"); @@ -51,7 +49,7 @@ async fn test_get_latest_event_id_returns_valid_id(db: Pool) { assert!(new_latest_id >= new_id, "Latest id should be >= new event id"); } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_get_latest_event_id_with_events(db: Pool) { let _id1 = insert_test_event(&db, "test_channel_1", "payload1").await; let _id2 = insert_test_event(&db, "test_channel_2", "payload2").await; @@ -61,7 +59,7 @@ async fn test_get_latest_event_id_with_events(db: Pool) { assert!(latest_id >= id3, "Latest id should be >= last inserted id"); } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_poll_notify_events_no_new_events(db: Pool) { // Get latest id first let latest_id = get_latest_event_id(&db).await.unwrap(); @@ -71,7 +69,7 @@ async fn test_poll_notify_events_no_new_events(db: Pool) { assert!(events.is_empty(), "Should return empty vec when polling from latest id"); } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_poll_notify_events_returns_new_events(db: Pool) { let before_id = get_latest_event_id(&db).await.unwrap(); @@ -92,7 +90,7 @@ async fn test_poll_notify_events_returns_new_events(db: Pool) { assert!(our_events[0].id < our_events[1].id, "Events should be ordered by id ascending"); } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_poll_notify_events_respects_last_event_id(db: Pool) { let id1 = insert_test_event(&db, "test_respect_id", "payload1").await; let _id2 = insert_test_event(&db, "test_respect_id", "payload2").await; @@ -109,7 +107,7 @@ async fn test_poll_notify_events_respects_last_event_id(db: Pool) { assert!(our_events.iter().all(|e| e.id > id1), "All events should have id > id1"); } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_cleanup_old_events(db: Pool) { // Use unique channel names to avoid interference from other tests let old_channel = format!("test_cleanup_old_{}", uuid::Uuid::new_v4()); @@ -156,7 +154,7 @@ async fn test_cleanup_old_events(db: Pool) { // Database Trigger Tests - Verify triggers insert events correctly // ============================================================================ -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_trigger_notify_config_change(db: Pool) { let before_id = get_latest_event_id(&db).await.unwrap(); @@ -178,7 +176,7 @@ async fn test_trigger_notify_config_change(db: Pool) { assert!(!config_events.is_empty(), "Should have notify_config_change event"); } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_trigger_notify_global_setting_change_insert(db: Pool) { let before_id = get_latest_event_id(&db).await.unwrap(); @@ -201,7 +199,7 @@ async fn test_trigger_notify_global_setting_change_insert(db: Pool) { assert!(!setting_events.is_empty(), "Should have notify_global_setting_change event on insert"); } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_trigger_notify_global_setting_change_update(db: Pool) { // Use a unique setting name for testing let setting_name = format!("test_setting_update_{}", uuid::Uuid::new_v4()); @@ -231,7 +229,7 @@ async fn test_trigger_notify_global_setting_change_update(db: Pool) { assert!(!setting_events.is_empty(), "Should have notify_global_setting_change event on update"); } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_trigger_notify_global_setting_change_delete(db: Pool) { // Use a unique setting name for testing let setting_name = format!("test_setting_delete_{}", uuid::Uuid::new_v4()); @@ -261,7 +259,7 @@ async fn test_trigger_notify_global_setting_change_delete(db: Pool) { assert!(!setting_events.is_empty(), "Should have notify_global_setting_change event on delete"); } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_trigger_notify_workspace_envs_change(db: Pool) { let before_id = get_latest_event_id(&db).await.unwrap(); @@ -283,7 +281,7 @@ async fn test_trigger_notify_workspace_envs_change(db: Pool) { assert!(!env_events.is_empty(), "Should have notify_workspace_envs_change event"); } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_trigger_notify_workspace_key_change(db: Pool) { let before_id = get_latest_event_id(&db).await.unwrap(); @@ -305,7 +303,7 @@ async fn test_trigger_notify_workspace_key_change(db: Pool) { assert!(!key_events.is_empty(), "Should have notify_workspace_key_change event"); } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_trigger_notify_token_invalidation(db: Pool) { // First insert a session token let token = format!("test_token_{}", uuid::Uuid::new_v4()); @@ -336,7 +334,7 @@ async fn test_trigger_notify_token_invalidation(db: Pool) { assert!(!token_events.is_empty(), "Should have notify_token_invalidation event"); } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_trigger_notify_webhook_change(db: Pool) { let before_id = get_latest_event_id(&db).await.unwrap(); @@ -355,7 +353,7 @@ async fn test_trigger_notify_webhook_change(db: Pool) { assert!(!webhook_events.is_empty(), "Should have notify_webhook_change event"); } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_trigger_notify_workspace_premium_change(db: Pool) { let before_id = get_latest_event_id(&db).await.unwrap(); @@ -378,7 +376,7 @@ async fn test_trigger_notify_workspace_premium_change(db: Pool) { // HTTP Trigger Tests // ============================================================================ -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_trigger_notify_http_trigger_change(db: Pool) { let before_id = get_latest_event_id(&db).await.unwrap(); @@ -408,7 +406,7 @@ async fn test_trigger_notify_http_trigger_change(db: Pool) { // Script/Flow Version Change Tests // ============================================================================ -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_trigger_notify_runnable_version_change_script(db: Pool) { // First create a script without lock let script_path = format!("f/test/script_{}", uuid::Uuid::new_v4()); @@ -449,7 +447,7 @@ async fn test_trigger_notify_runnable_version_change_script(db: Pool) assert_eq!(parts[1], "script", "Second part should be 'script'"); } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_trigger_notify_runnable_version_change_flow(db: Pool) { // First create a flow with empty versions array let flow_path = format!("f/test/flow_{}", uuid::Uuid::new_v4()); @@ -494,7 +492,7 @@ async fn test_trigger_notify_runnable_version_change_flow(db: Pool) { // Concurrent Access Tests // ============================================================================ -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_concurrent_event_insertion(db: Pool) { // Use a unique channel name for this test run let channel = format!("test_concurrent_{}", uuid::Uuid::new_v4()); @@ -536,7 +534,7 @@ async fn test_concurrent_event_insertion(db: Pool) { assert_eq!(ids.len(), 10, "All events should have unique IDs"); } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_polling_isolation(db: Pool) { // Use a unique channel name for this test let channel = format!("test_isolation_{}", uuid::Uuid::new_v4()); @@ -591,7 +589,7 @@ async fn test_polling_isolation(db: Pool) { // Edge Case Tests // ============================================================================ -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_empty_payload(db: Pool) { let before_id = get_latest_event_id(&db).await.unwrap(); @@ -607,7 +605,7 @@ async fn test_empty_payload(db: Pool) { assert_eq!(empty_events[0].payload, "", "Payload should be empty string"); } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_large_payload(db: Pool) { let before_id = get_latest_event_id(&db).await.unwrap(); @@ -625,7 +623,7 @@ async fn test_large_payload(db: Pool) { assert_eq!(large_events[0].payload.len(), 1024, "Payload should be preserved"); } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_special_characters_in_payload(db: Pool) { let before_id = get_latest_event_id(&db).await.unwrap(); @@ -642,7 +640,7 @@ async fn test_special_characters_in_payload(db: Pool) { assert_eq!(special_events[0].payload, special_payload, "Special characters should be preserved"); } -#[sqlx::test(fixtures("base"))] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_cleanup_with_no_old_events(db: Pool) { // Use a unique channel name for this test let channel = format!("test_no_old_{}", uuid::Uuid::new_v4()); diff --git a/backend/tests/secret_backend_integration.rs b/backend/windmill-common/tests/secret_backend_integration.rs similarity index 96% rename from backend/tests/secret_backend_integration.rs rename to backend/windmill-common/tests/secret_backend_integration.rs index f9f0fe9f5f..817a8d0e65 100644 --- a/backend/tests/secret_backend_integration.rs +++ b/backend/windmill-common/tests/secret_backend_integration.rs @@ -114,7 +114,7 @@ mod tests { // ==================== Static Token Tests ==================== /// Test Vault connection with static token - #[sqlx::test(fixtures("base", "secret_backend"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "secret_backend"))] async fn test_vault_connection_static_token(db: Pool) { skip_if_no_vault!(); @@ -132,7 +132,7 @@ mod tests { } /// Test basic CRUD operations with static token - #[sqlx::test(fixtures("base", "secret_backend"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "secret_backend"))] async fn test_vault_crud_static_token(_db: Pool) { skip_if_no_vault!(); @@ -193,7 +193,7 @@ mod tests { /// Test Vault connection with JWT authentication #[cfg(feature = "openidconnect")] - #[sqlx::test(fixtures("base", "secret_backend"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "secret_backend"))] async fn test_vault_connection_jwt(db: Pool) { skip_if_no_vault!(); setup_base_url().await; @@ -215,7 +215,7 @@ mod tests { /// Test basic CRUD operations with JWT authentication #[cfg(feature = "openidconnect")] - #[sqlx::test(fixtures("base", "secret_backend"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "secret_backend"))] async fn test_vault_crud_jwt(db: Pool) { skip_if_no_vault!(); setup_base_url().await; @@ -260,7 +260,7 @@ mod tests { // ==================== Migration Tests ==================== /// Test migration from database to Vault - #[sqlx::test(fixtures("base", "secret_backend"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "secret_backend"))] async fn test_migrate_db_to_vault(db: Pool) { skip_if_no_vault!(); @@ -314,7 +314,7 @@ mod tests { } /// Test migration from Vault back to database - #[sqlx::test(fixtures("base", "secret_backend"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "secret_backend"))] async fn test_migrate_vault_to_db(db: Pool) { skip_if_no_vault!(); @@ -373,7 +373,7 @@ mod tests { // ==================== Variable Rename Tests ==================== /// Test renaming a variable path in Vault - #[sqlx::test(fixtures("base", "secret_backend"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "secret_backend"))] async fn test_variable_rename(db: Pool) { skip_if_no_vault!(); let _ = &db; // suppress unused warning @@ -445,7 +445,7 @@ mod tests { // ==================== Full Round Trip Test ==================== /// Test full round-trip: DB -> Vault -> DB with verification - #[sqlx::test(fixtures("base", "secret_backend"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "secret_backend"))] async fn test_full_round_trip(db: Pool) { skip_if_no_vault!(); @@ -501,7 +501,7 @@ mod tests { // ==================== Workspace Isolation Test ==================== /// Test that workspace isolation is maintained - #[sqlx::test(fixtures("base", "secret_backend"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "secret_backend"))] async fn test_workspace_isolation(db: Pool) { skip_if_no_vault!(); diff --git a/backend/tests/secret_backend_migration.rs b/backend/windmill-common/tests/secret_backend_migration.rs similarity index 96% rename from backend/tests/secret_backend_migration.rs rename to backend/windmill-common/tests/secret_backend_migration.rs index d78da8603c..baa2940319 100644 --- a/backend/tests/secret_backend_migration.rs +++ b/backend/windmill-common/tests/secret_backend_migration.rs @@ -27,8 +27,6 @@ use windmill_common::secret_backend::{ SecretBackend, VaultSettings, }; -mod common; - fn test_vault_settings() -> VaultSettings { VaultSettings { address: std::env::var("VAULT_ADDR").unwrap_or_else(|_| "http://127.0.0.1:8200".to_string()), @@ -42,7 +40,7 @@ fn test_vault_settings() -> VaultSettings { } /// Test that we can connect to Vault -#[sqlx::test(fixtures("base", "secret_backend"))] +#[sqlx::test(migrations = "../migrations", fixtures("base", "secret_backend"))] #[ignore = "requires running Vault instance"] async fn test_vault_connection_works(db: Pool) { let settings = test_vault_settings(); @@ -53,7 +51,7 @@ async fn test_vault_connection_works(db: Pool) { } /// Test migration from database to Vault -#[sqlx::test(fixtures("base", "secret_backend"))] +#[sqlx::test(migrations = "../migrations", fixtures("base", "secret_backend"))] #[ignore = "requires running Vault instance"] async fn test_migrate_db_to_vault(db: Pool) { let settings = test_vault_settings(); @@ -119,7 +117,7 @@ async fn test_migrate_db_to_vault(db: Pool) { } /// Test migration from Vault to database -#[sqlx::test(fixtures("base", "secret_backend"))] +#[sqlx::test(migrations = "../migrations", fixtures("base", "secret_backend"))] #[ignore = "requires running Vault instance"] async fn test_migrate_vault_to_db(db: Pool) { let settings = test_vault_settings(); @@ -196,7 +194,7 @@ async fn test_migrate_vault_to_db(db: Pool) { } /// Test full round-trip migration: DB -> Vault -> DB -#[sqlx::test(fixtures("base", "secret_backend"))] +#[sqlx::test(migrations = "../migrations", fixtures("base", "secret_backend"))] #[ignore = "requires running Vault instance"] async fn test_full_round_trip_migration(db: Pool) { let settings = test_vault_settings(); @@ -276,7 +274,7 @@ async fn test_full_round_trip_migration(db: Pool) { } /// Test that workspace isolation is maintained during migration -#[sqlx::test(fixtures("base", "secret_backend"))] +#[sqlx::test(migrations = "../migrations", fixtures("base", "secret_backend"))] #[ignore = "requires running Vault instance"] async fn test_workspace_isolation(db: Pool) { let settings = test_vault_settings(); diff --git a/backend/windmill-queue/tests/fixtures/base.sql b/backend/windmill-queue/tests/fixtures/base.sql new file mode 100644 index 0000000000..7db9918fba --- /dev/null +++ b/backend/windmill-queue/tests/fixtures/base.sql @@ -0,0 +1,146 @@ +-- used for backend automated testing +-- https://docs.rs/sqlx/latest/sqlx/attr.test.html + +INSERT INTO workspace + (id, name, owner) + VALUES ('test-workspace', 'test-workspace', 'test-user'); + +INSERT INTO usr(workspace_id, email, username, is_admin, role) VALUES + ('test-workspace', 'test@windmill.dev', 'test-user', true, 'Admin'); + +INSERT INTO workspace_key(workspace_id, kind, key) VALUES + ('test-workspace', 'cloud', 'test-key'); + + +INSERT INTO workspace_settings (workspace_id) VALUES + ('test-workspace'); + +INSERT INTO group_ (workspace_id, name, summary, extra_perms) VALUES + ('test-workspace', 'all', 'All users', '{}'); + +INSERT INTO password(email, password_hash, login_type, super_admin, verified, name, username) + VALUES ('test@windmill.dev', 'not-a-real-hash', 'password', true, true, 'Test User', 'test-user'); + +INSERT INTO password(email, password_hash, login_type, super_admin, verified, name) + VALUES ('test2@windmill.dev', 'not-a-real-hash', 'password', false, true, 'Test User 2'); + +INSERT INTO password(email, password_hash, login_type, super_admin, verified, name) + VALUES ('test3@windmill.dev', 'not-a-real-hash', 'password', false, true, 'Test User 3'); + +INSERT INTO usr(workspace_id, email, username, is_admin, role) VALUES + ('test-workspace', 'test2@windmill.dev', 'test-user-2', false, 'User'); + +INSERT INTO usr(workspace_id, email, username, is_admin, role) VALUES + ('test-workspace', 'test3@windmill.dev', 'test-user-3', false, 'User'); + +insert INTO token(token, email, label, super_admin) VALUES ('SECRET_TOKEN', 'test@windmill.dev', 'test token', true); +insert INTO token(token, email, label, super_admin) VALUES ('SECRET_TOKEN_2', 'test2@windmill.dev', 'test token 2', false); +insert INTO token(token, email, label, super_admin) VALUES ('SECRET_TOKEN_3', 'test3@windmill.dev', 'test token 3', false); + +GRANT ALL PRIVILEGES ON TABLE workspace_key TO windmill_admin; +GRANT ALL PRIVILEGES ON TABLE workspace_key TO windmill_user; + +CREATE FUNCTION "notify_insert_on_completed_job" () +RETURNS TRIGGER AS $$ +BEGIN + PERFORM pg_notify('completed', NEW.id::text); + RETURN NEW; +END; +$$ LANGUAGE PLPGSQL; + + CREATE TRIGGER "notify_insert_on_completed_job" + AFTER INSERT ON "v2_job_completed" + FOR EACH ROW +EXECUTE FUNCTION "notify_insert_on_completed_job" (); + + +CREATE FUNCTION "notify_queue" () +RETURNS TRIGGER AS $$ +BEGIN + PERFORM pg_notify('queued', NEW.id::text); + RETURN NEW; +END; +$$ LANGUAGE PLPGSQL; + + CREATE TRIGGER "notify_queue_after_insert" + AFTER INSERT ON "v2_job_queue" + FOR EACH ROW +EXECUTE FUNCTION "notify_queue" (); + + CREATE TRIGGER "notify_queue_after_flow_status_update" + AFTER UPDATE ON "v2_job_status" + FOR EACH ROW + WHEN (NEW.flow_status IS DISTINCT FROM OLD.flow_status) +EXECUTE FUNCTION "notify_queue" (); + +-- Apply phase 4: +DROP FUNCTION IF EXISTS v2_job_after_update CASCADE; +DROP FUNCTION IF EXISTS v2_job_completed_before_insert CASCADE; +DROP FUNCTION IF EXISTS v2_job_completed_before_update CASCADE; +DROP FUNCTION IF EXISTS v2_job_queue_after_insert CASCADE; +DROP FUNCTION IF EXISTS v2_job_queue_before_insert CASCADE; +DROP FUNCTION IF EXISTS v2_job_queue_before_update CASCADE; +DROP FUNCTION IF EXISTS v2_job_runtime_before_insert CASCADE; +DROP FUNCTION IF EXISTS v2_job_runtime_before_update CASCADE; +DROP FUNCTION IF EXISTS v2_job_status_before_insert CASCADE; +DROP FUNCTION IF EXISTS v2_job_status_before_update CASCADE; + +DROP VIEW IF EXISTS completed_job, completed_job_view, job, queue, queue_view CASCADE; + +ALTER TABLE v2_job_queue + DROP COLUMN IF EXISTS __parent_job CASCADE, + DROP COLUMN IF EXISTS __created_by CASCADE, + DROP COLUMN IF EXISTS __script_hash CASCADE, + DROP COLUMN IF EXISTS __script_path CASCADE, + DROP COLUMN IF EXISTS __args CASCADE, + DROP COLUMN IF EXISTS __logs CASCADE, + DROP COLUMN IF EXISTS __raw_code CASCADE, + DROP COLUMN IF EXISTS __canceled CASCADE, + DROP COLUMN IF EXISTS __last_ping CASCADE, + DROP COLUMN IF EXISTS __job_kind CASCADE, + DROP COLUMN IF EXISTS __env_id CASCADE, + DROP COLUMN IF EXISTS __schedule_path CASCADE, + DROP COLUMN IF EXISTS __permissioned_as CASCADE, + DROP COLUMN IF EXISTS __flow_status CASCADE, + DROP COLUMN IF EXISTS __raw_flow CASCADE, + DROP COLUMN IF EXISTS __is_flow_step CASCADE, + DROP COLUMN IF EXISTS __language CASCADE, + DROP COLUMN IF EXISTS __same_worker CASCADE, + DROP COLUMN IF EXISTS __raw_lock CASCADE, + DROP COLUMN IF EXISTS __pre_run_error CASCADE, + DROP COLUMN IF EXISTS __email CASCADE, + DROP COLUMN IF EXISTS __visible_to_owner CASCADE, + DROP COLUMN IF EXISTS __mem_peak CASCADE, + DROP COLUMN IF EXISTS __root_job CASCADE, + DROP COLUMN IF EXISTS __leaf_jobs CASCADE, + DROP COLUMN IF EXISTS __concurrent_limit CASCADE, + DROP COLUMN IF EXISTS __concurrency_time_window_s CASCADE, + DROP COLUMN IF EXISTS __timeout CASCADE, + DROP COLUMN IF EXISTS __flow_step_id CASCADE, + DROP COLUMN IF EXISTS __cache_ttl CASCADE; + +LOCK TABLE v2_job_queue IN ACCESS EXCLUSIVE MODE; +ALTER TABLE v2_job_completed + DROP COLUMN IF EXISTS __parent_job CASCADE, + DROP COLUMN IF EXISTS __created_by CASCADE, + DROP COLUMN IF EXISTS __created_at CASCADE, + DROP COLUMN IF EXISTS __success CASCADE, + DROP COLUMN IF EXISTS __script_hash CASCADE, + DROP COLUMN IF EXISTS __script_path CASCADE, + DROP COLUMN IF EXISTS __args CASCADE, + DROP COLUMN IF EXISTS __logs CASCADE, + DROP COLUMN IF EXISTS __raw_code CASCADE, + DROP COLUMN IF EXISTS __canceled CASCADE, + DROP COLUMN IF EXISTS __job_kind CASCADE, + DROP COLUMN IF EXISTS __env_id CASCADE, + DROP COLUMN IF EXISTS __schedule_path CASCADE, + DROP COLUMN IF EXISTS __permissioned_as CASCADE, + DROP COLUMN IF EXISTS __raw_flow CASCADE, + DROP COLUMN IF EXISTS __is_flow_step CASCADE, + DROP COLUMN IF EXISTS __language CASCADE, + DROP COLUMN IF EXISTS __is_skipped CASCADE, + DROP COLUMN IF EXISTS __raw_lock CASCADE, + DROP COLUMN IF EXISTS __email CASCADE, + DROP COLUMN IF EXISTS __visible_to_owner CASCADE, + DROP COLUMN IF EXISTS __tag CASCADE, + DROP COLUMN IF EXISTS __priority CASCADE; diff --git a/backend/windmill-queue/tests/fixtures/schedule_push.sql b/backend/windmill-queue/tests/fixtures/schedule_push.sql new file mode 100644 index 0000000000..9312d0bbd9 --- /dev/null +++ b/backend/windmill-queue/tests/fixtures/schedule_push.sql @@ -0,0 +1,84 @@ +-- Fixture for schedule push tests +-- Sets up scripts, flows, users, and schedules needed to test push_scheduled_job + +-- Password entries for auth resolution +INSERT INTO password (email, password_hash, login_type, super_admin, verified, name) +VALUES + ('test@windmill.dev', 'dummy_hash', 'password', false, true, 'Test User'), + ('obo@windmill.dev', 'dummy_hash', 'password', false, true, 'OBO User') +ON CONFLICT (email) DO NOTHING; + +-- OBO user in workspace +INSERT INTO usr (workspace_id, email, username, is_admin, role) +VALUES ('test-workspace', 'obo@windmill.dev', 'obo-user', false, 'Developer') +ON CONFLICT (workspace_id, username) DO NOTHING; + +-- A simple script +INSERT INTO script (workspace_id, created_by, content, schema, summary, description, path, hash, language, lock, kind) +VALUES ( + 'test-workspace', 'test-user', + 'export async function main() { return "ok"; }', + '{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}', + 'Test script', '', 'f/system/test_script', 100001, 'deno', '', 'script' +); + +-- A script with on_behalf_of_email +INSERT INTO script (workspace_id, created_by, content, schema, summary, description, path, hash, language, lock, kind, on_behalf_of_email) +VALUES ( + 'test-workspace', 'test-user', + 'export async function main() { return "obo"; }', + '{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}', + 'OBO script', '', 'f/system/obo_script', 100002, 'deno', '', 'script', 'obo@windmill.dev' +); + +-- A script with a tag +INSERT INTO script (workspace_id, created_by, content, schema, summary, description, path, hash, language, lock, kind, tag) +VALUES ( + 'test-workspace', 'test-user', + 'export async function main() { return "tagged"; }', + '{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}', + 'Tagged script', '', 'f/system/tagged_script', 100003, 'deno', '', 'script', 'custom-tag' +); + +-- A script with timeout +INSERT INTO script (workspace_id, created_by, content, schema, summary, description, path, hash, language, lock, kind, timeout) +VALUES ( + 'test-workspace', 'test-user', + 'export async function main() { return "timeout"; }', + '{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}', + 'Timeout script', '', 'f/system/timeout_script', 100004, 'deno', '', 'script', 300 +); + +-- A flow +INSERT INTO flow (workspace_id, summary, description, path, versions, schema, value, edited_by) +VALUES ( + 'test-workspace', 'Test flow', '', 'f/system/test_flow', '{200001}', + '{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}', + '{"modules": [{"id": "a", "value": {"path": "f/system/test_script", "type": "script", "input_transforms": {}}}]}', + 'test-user' +); + +INSERT INTO flow_version (id, workspace_id, path, schema, value, created_by) +VALUES ( + 200001, 'test-workspace', 'f/system/test_flow', + '{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}', + '{"modules": [{"id": "a", "value": {"path": "f/system/test_script", "type": "script", "input_transforms": {}}}]}', + 'test-user' +); + +-- A flow with on_behalf_of_email +INSERT INTO flow (workspace_id, summary, description, path, versions, schema, value, edited_by, on_behalf_of_email) +VALUES ( + 'test-workspace', 'OBO flow', '', 'f/system/obo_flow', '{200002}', + '{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}', + '{"modules": [{"id": "a", "value": {"path": "f/system/test_script", "type": "script", "input_transforms": {}}}]}', + 'test-user', 'obo@windmill.dev' +); + +INSERT INTO flow_version (id, workspace_id, path, schema, value, created_by) +VALUES ( + 200002, 'test-workspace', 'f/system/obo_flow', + '{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}', + '{"modules": [{"id": "a", "value": {"path": "f/system/test_script", "type": "script", "input_transforms": {}}}]}', + 'test-user' +); diff --git a/backend/tests/job_cleanup_test.rs b/backend/windmill-queue/tests/job_cleanup_test.rs similarity index 100% rename from backend/tests/job_cleanup_test.rs rename to backend/windmill-queue/tests/job_cleanup_test.rs diff --git a/backend/tests/schedule_push.rs b/backend/windmill-queue/tests/schedule_push.rs similarity index 95% rename from backend/tests/schedule_push.rs rename to backend/windmill-queue/tests/schedule_push.rs index b26336b637..f946234670 100644 --- a/backend/tests/schedule_push.rs +++ b/backend/windmill-queue/tests/schedule_push.rs @@ -1,5 +1,3 @@ -mod common; - mod schedule_push { use chrono::Utc; use sqlx::{Pool, Postgres}; @@ -118,7 +116,7 @@ mod schedule_push { // push_scheduled_job: basic script schedule // ----------------------------------------------------------------------- - #[sqlx::test(fixtures("base", "schedule_push"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))] async fn test_push_script_schedule(db: Pool) -> anyhow::Result<()> { let schedule = make_schedule(|_| {}); let authed = make_authed(); @@ -140,7 +138,7 @@ mod schedule_push { // push_scheduled_job: flow schedule // ----------------------------------------------------------------------- - #[sqlx::test(fixtures("base", "schedule_push"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))] async fn test_push_flow_schedule(db: Pool) -> anyhow::Result<()> { let schedule = make_schedule(|s| { s.is_flow = true; @@ -164,7 +162,7 @@ mod schedule_push { // push_scheduled_job: on_behalf_of_email (script) // ----------------------------------------------------------------------- - #[sqlx::test(fixtures("base", "schedule_push"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))] async fn test_push_script_on_behalf_of_email(db: Pool) -> anyhow::Result<()> { let schedule = make_schedule(|s| { s.script_path = "f/system/obo_script".to_string(); @@ -191,7 +189,7 @@ mod schedule_push { // push_scheduled_job: on_behalf_of_email (flow) // ----------------------------------------------------------------------- - #[sqlx::test(fixtures("base", "schedule_push"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))] async fn test_push_flow_on_behalf_of_email(db: Pool) -> anyhow::Result<()> { let schedule = make_schedule(|s| { s.is_flow = true; @@ -218,7 +216,7 @@ mod schedule_push { // push_scheduled_job: with retry wraps in SingleStepFlow // ----------------------------------------------------------------------- - #[sqlx::test(fixtures("base", "schedule_push"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))] async fn test_push_script_with_retry(db: Pool) -> anyhow::Result<()> { let schedule = make_schedule(|s| { s.retry = Some(serde_json::json!({ @@ -247,7 +245,7 @@ mod schedule_push { // push_scheduled_job: duplicate detection (same schedule + time = skip) // ----------------------------------------------------------------------- - #[sqlx::test(fixtures("base", "schedule_push"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))] async fn test_push_duplicate_skipped(db: Pool) -> anyhow::Result<()> { let schedule = make_schedule(|_| {}); let authed = make_authed(); @@ -270,7 +268,7 @@ mod schedule_push { // push_scheduled_job: invalid timezone // ----------------------------------------------------------------------- - #[sqlx::test(fixtures("base", "schedule_push"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))] async fn test_push_invalid_timezone(db: Pool) -> anyhow::Result<()> { let schedule = make_schedule(|s| { s.timezone = "Invalid/Timezone".to_string(); @@ -288,7 +286,7 @@ mod schedule_push { // push_scheduled_job: invalid cron expression // ----------------------------------------------------------------------- - #[sqlx::test(fixtures("base", "schedule_push"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))] async fn test_push_invalid_cron(db: Pool) -> anyhow::Result<()> { let schedule = make_schedule(|s| { s.schedule = "not a cron".to_string(); @@ -306,7 +304,7 @@ mod schedule_push { // push_scheduled_job: invalid args (not a dict) // ----------------------------------------------------------------------- - #[sqlx::test(fixtures("base", "schedule_push"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))] async fn test_push_invalid_args(db: Pool) -> anyhow::Result<()> { let schedule = make_schedule(|s| { let raw = serde_json::value::RawValue::from_string("[1,2,3]".to_string()).unwrap(); @@ -325,7 +323,7 @@ mod schedule_push { // push_scheduled_job: with schedule args passed to job // ----------------------------------------------------------------------- - #[sqlx::test(fixtures("base", "schedule_push"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))] async fn test_push_with_args(db: Pool) -> anyhow::Result<()> { let schedule = make_schedule(|s| { let raw = @@ -353,7 +351,7 @@ mod schedule_push { // push_scheduled_job: script not found // ----------------------------------------------------------------------- - #[sqlx::test(fixtures("base", "schedule_push"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))] async fn test_push_script_not_found(db: Pool) -> anyhow::Result<()> { let schedule = make_schedule(|s| { s.script_path = "f/system/nonexistent".to_string(); @@ -371,7 +369,7 @@ mod schedule_push { // push_scheduled_job: flow not found // ----------------------------------------------------------------------- - #[sqlx::test(fixtures("base", "schedule_push"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))] async fn test_push_flow_not_found(db: Pool) -> anyhow::Result<()> { let schedule = make_schedule(|s| { s.is_flow = true; @@ -390,7 +388,7 @@ mod schedule_push { // push_scheduled_job: paused schedule (paused_until in future) // ----------------------------------------------------------------------- - #[sqlx::test(fixtures("base", "schedule_push"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))] async fn test_push_paused_schedule(db: Pool) -> anyhow::Result<()> { let schedule = make_schedule(|s| { s.paused_until = Some(Utc::now() + chrono::Duration::hours(1)); @@ -417,7 +415,7 @@ mod schedule_push { // push_scheduled_job: clock shift detection (now_cutoff >= now) // ----------------------------------------------------------------------- - #[sqlx::test(fixtures("base", "schedule_push"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))] async fn test_push_clock_shift(db: Pool) -> anyhow::Result<()> { let schedule = make_schedule(|_| {}); let authed = make_authed(); @@ -445,7 +443,7 @@ mod schedule_push { // try_schedule_next_job: disabled schedule does not push // ----------------------------------------------------------------------- - #[sqlx::test(fixtures("base", "schedule_push"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))] async fn test_handle_disabled_schedule(db: Pool) -> anyhow::Result<()> { let schedule = make_schedule(|s| { s.enabled = false; @@ -471,7 +469,7 @@ mod schedule_push { // try_schedule_next_job: script path mismatch does not push // ----------------------------------------------------------------------- - #[sqlx::test(fixtures("base", "schedule_push"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))] async fn test_handle_path_mismatch(db: Pool) -> anyhow::Result<()> { let schedule = make_schedule(|_| {}); let job = make_completed_job(&schedule); @@ -495,7 +493,7 @@ mod schedule_push { // try_schedule_next_job: enabled + matching path pushes next job // ----------------------------------------------------------------------- - #[sqlx::test(fixtures("base", "schedule_push"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))] async fn test_handle_enabled_pushes_next_job(db: Pool) -> anyhow::Result<()> { let schedule = make_schedule(|_| {}); let job = make_completed_job(&schedule); @@ -524,7 +522,7 @@ mod schedule_push { // try_schedule_next_job: on_behalf_of_email via handle path // ----------------------------------------------------------------------- - #[sqlx::test(fixtures("base", "schedule_push"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))] async fn test_handle_on_behalf_of_email(db: Pool) -> anyhow::Result<()> { let schedule = make_schedule(|s| { s.script_path = "f/system/obo_script".to_string(); @@ -559,7 +557,7 @@ mod schedule_push { // (caller is responsible for retry + eventual disable) // ----------------------------------------------------------------------- - #[sqlx::test(fixtures("base", "schedule_push"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))] async fn test_handle_push_failure_disables_schedule(db: Pool) -> anyhow::Result<()> { sqlx::query( "INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap) @@ -604,7 +602,7 @@ mod schedule_push { // If the caller commits, both the next tick and any prior writes persist. // ----------------------------------------------------------------------- - #[sqlx::test(fixtures("base", "schedule_push"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))] async fn test_success_atomic_with_commit(db: Pool) -> anyhow::Result<()> { sqlx::query( "INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap) @@ -646,7 +644,7 @@ mod schedule_push { // Ensures no next tick leaks when the outer tx is not committed. // ----------------------------------------------------------------------- - #[sqlx::test(fixtures("base", "schedule_push"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))] async fn test_success_rolls_back_on_drop(db: Pool) -> anyhow::Result<()> { let schedule = make_schedule(|_| {}); let job = make_completed_job(&schedule); @@ -675,7 +673,7 @@ mod schedule_push { // The schedule must stay enabled when the caller doesn't commit. // ----------------------------------------------------------------------- - #[sqlx::test(fixtures("base", "schedule_push"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))] async fn test_failure_disable_rolls_back_on_drop(db: Pool) -> anyhow::Result<()> { sqlx::query( "INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap) @@ -721,7 +719,7 @@ mod schedule_push { // The caller can perform additional writes on the returned tx. // ----------------------------------------------------------------------- - #[sqlx::test(fixtures("base", "schedule_push"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))] async fn test_tx_usable_after_success(db: Pool) -> anyhow::Result<()> { let schedule = make_schedule(|_| {}); let job = make_completed_job(&schedule); @@ -759,7 +757,7 @@ mod schedule_push { // The caller can still write on the returned tx after a schedule disable. // ----------------------------------------------------------------------- - #[sqlx::test(fixtures("base", "schedule_push"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))] async fn test_tx_usable_after_failure(db: Pool) -> anyhow::Result<()> { sqlx::query( "INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap) @@ -815,7 +813,7 @@ mod schedule_push { // try_schedule_next_job: flow schedule pushes next job // ----------------------------------------------------------------------- - #[sqlx::test(fixtures("base", "schedule_push"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))] async fn test_try_schedule_flow(db: Pool) -> anyhow::Result<()> { let schedule = make_schedule(|s| { s.is_flow = true; @@ -847,7 +845,7 @@ mod schedule_push { // try_schedule_next_job: script with retry wraps in SingleStepFlow // ----------------------------------------------------------------------- - #[sqlx::test(fixtures("base", "schedule_push"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))] async fn test_try_schedule_with_retry(db: Pool) -> anyhow::Result<()> { let schedule = make_schedule(|s| { s.retry = Some(serde_json::json!({ @@ -882,7 +880,7 @@ mod schedule_push { // try_schedule_next_job: push failure error message is stored on schedule // ----------------------------------------------------------------------- - #[sqlx::test(fixtures("base", "schedule_push"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))] async fn test_push_failure_stores_error_message(db: Pool) -> anyhow::Result<()> { sqlx::query( "INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap) @@ -927,7 +925,7 @@ mod schedule_push { // try_schedule_next_job: disabled schedule leaves no side effects // ----------------------------------------------------------------------- - #[sqlx::test(fixtures("base", "schedule_push"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))] async fn test_disabled_schedule_no_side_effects(db: Pool) -> anyhow::Result<()> { sqlx::query( "INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap) @@ -970,7 +968,7 @@ mod schedule_push { // try_schedule_next_job: path mismatch leaves schedule unchanged // ----------------------------------------------------------------------- - #[sqlx::test(fixtures("base", "schedule_push"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))] async fn test_path_mismatch_no_side_effects(db: Pool) -> anyhow::Result<()> { sqlx::query( "INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap) @@ -1012,7 +1010,7 @@ mod schedule_push { // doesn't error. The function should return (tx, None). // ----------------------------------------------------------------------- - #[sqlx::test(fixtures("base", "schedule_push"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))] async fn test_push_failure_schedule_not_in_db(db: Pool) -> anyhow::Result<()> { // Do NOT insert a schedule row — the disable UPDATE will match 0 rows let schedule = make_schedule(|s| { @@ -1044,7 +1042,7 @@ mod schedule_push { // unmodified enabled state are committed together. // ----------------------------------------------------------------------- - #[sqlx::test(fixtures("base", "schedule_push"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))] async fn test_invariant_success_means_tick_committed(db: Pool) -> anyhow::Result<()> { sqlx::query( "INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap) @@ -1089,7 +1087,7 @@ mod schedule_push { // enabled and no next tick. // ----------------------------------------------------------------------- - #[sqlx::test(fixtures("base", "schedule_push"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))] async fn test_invariant_failure_means_disabled_after_commit( db: Pool, ) -> anyhow::Result<()> { @@ -1139,7 +1137,7 @@ mod schedule_push { // enabled so that zombie retry can re-attempt. // ----------------------------------------------------------------------- - #[sqlx::test(fixtures("base", "schedule_push"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))] async fn test_invariant_rollback_preserves_schedule_for_retry( db: Pool, ) -> anyhow::Result<()> { @@ -1187,6 +1185,7 @@ mod schedule_push { // Failpoint tests — feature-gated, only compiled under `failpoints` // =================================================================== + #[cfg(feature = "failpoints")] mod failpoint_tests { use super::*; use windmill_queue::jobs::schedule_failpoints::{ScheduleFailPoint, ACTIVE}; @@ -1195,7 +1194,7 @@ mod schedule_push { // SavepointCreate failpoint → schedule disabled, 0 jobs // --------------------------------------------------------------- - #[sqlx::test(fixtures("base", "schedule_push"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))] async fn test_failpoint_savepoint_create_disables(db: Pool) -> anyhow::Result<()> { sqlx::query( "INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap) @@ -1232,7 +1231,7 @@ mod schedule_push { // Push failpoint → schedule disabled, 0 jobs // --------------------------------------------------------------- - #[sqlx::test(fixtures("base", "schedule_push"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))] async fn test_failpoint_push_disables(db: Pool) -> anyhow::Result<()> { sqlx::query( "INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap) @@ -1269,7 +1268,7 @@ mod schedule_push { // SavepointCommit failpoint → schedule disabled, 0 jobs // --------------------------------------------------------------- - #[sqlx::test(fixtures("base", "schedule_push"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))] async fn test_failpoint_savepoint_commit_disables(db: Pool) -> anyhow::Result<()> { sqlx::query( "INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap) @@ -1306,7 +1305,7 @@ mod schedule_push { // ScheduleDisable failpoint → returns Some(err), caller doesn't commit // --------------------------------------------------------------- - #[sqlx::test(fixtures("base", "schedule_push"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))] async fn test_failpoint_schedule_disable_returns_err(db: Pool) -> anyhow::Result<()> { sqlx::query( "INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap) @@ -1335,7 +1334,7 @@ mod schedule_push { // ScheduleDisable failpoint + tx drop → schedule stays enabled // --------------------------------------------------------------- - #[sqlx::test(fixtures("base", "schedule_push"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))] async fn test_failpoint_disable_failure_rollback(db: Pool) -> anyhow::Result<()> { sqlx::query( "INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap) @@ -1375,7 +1374,7 @@ mod schedule_push { // no error handler notification (QuotaExceeded is silenced) // --------------------------------------------------------------- - #[sqlx::test(fixtures("base", "schedule_push"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))] async fn test_failpoint_push_quota_exceeded_script(db: Pool) -> anyhow::Result<()> { sqlx::query( "INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap) @@ -1414,7 +1413,7 @@ mod schedule_push { // PushQuotaExceeded failpoint (flow) → schedule disabled, 0 jobs // --------------------------------------------------------------- - #[sqlx::test(fixtures("base", "schedule_push"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))] async fn test_failpoint_push_quota_exceeded_flow(db: Pool) -> anyhow::Result<()> { sqlx::query( "INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap) @@ -1470,7 +1469,7 @@ mod schedule_push { // 4. The schedule remains enabled for retry on next flow execution // ----------------------------------------------------------------------- - #[sqlx::test(fixtures("base", "schedule_push"))] + #[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))] async fn test_zombie_flow_after_schedule_push_failure_meets_restart_criteria( db: Pool, ) -> anyhow::Result<()> { diff --git a/backend/windmill-test-utils/Cargo.toml b/backend/windmill-test-utils/Cargo.toml new file mode 100644 index 0000000000..3d253a6772 --- /dev/null +++ b/backend/windmill-test-utils/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "windmill-test-utils" +version.workspace = true +authors.workspace = true +edition.workspace = true + +[lib] +name = "windmill_test_utils" +path = "src/lib.rs" + +[features] +default = [] +private = [] +enterprise = [] +python = ["windmill-common/python"] +deno_core = ["dep:windmill-runtime-nativets"] +agent_worker_server = ["dep:windmill-api-agent-workers"] + +[dependencies] +windmill-api = { workspace = true, default-features = false } +windmill-worker.workspace = true +windmill-queue.workspace = true +windmill-common = { workspace = true, default-features = false } +windmill-api-client.workspace = true +windmill-runtime-nativets = { workspace = true, optional = true } +windmill-api-agent-workers = { workspace = true, optional = true } + +sqlx.workspace = true +futures.workspace = true +serde.workspace = true +serde_json.workspace = true +tokio.workspace = true +uuid.workspace = true +chrono.workspace = true +axum.workspace = true +anyhow.workspace = true +tracing.workspace = true diff --git a/backend/tests/common/mod.rs b/backend/windmill-test-utils/src/lib.rs similarity index 99% rename from backend/tests/common/mod.rs rename to backend/windmill-test-utils/src/lib.rs index e377df5164..34858fac2e 100644 --- a/backend/tests/common/mod.rs +++ b/backend/windmill-test-utils/src/lib.rs @@ -574,7 +574,6 @@ pub async fn test_for_versions>( use futures::StreamExt; -// #[cfg(feature = "python")] pub async fn assert_lockfile( db: &Pool, script_content: String,