Files
windmill/backend/tests/workspace_export.rs
Diego Imbert ae42cbf4a8 feat(backend): remove draft_only, move never-deployed items into the draft table
Never-deployed "draft only" scripts/flows/apps used to live as a stub row in
their own table (draft_only = true) alongside a draft row. They now live solely
in the `draft` table.

Migration `remove_draft_only`:
- ensures every draft_only stub has a matching draft row (ON CONFLICT DO
  NOTHING preserves the real draft; synthesises one matching the frontend
  draft JSON shape only for stubs that lost their draft), with email = NULL
- deletes the stub rows (FKs to *_version / dependency tables cascade)
- drops the draft_only column from script/flow/app
The down migration re-adds the (nullable) columns; it is fully reversible
schema-wise (deleted stubs are not resurrected — their content lives in draft).

Removes every draft_only / include_draft_only reference from the backend:
handlers, list filters (draft-only items no longer exist in those tables so the
filters are dropped), INSERT/SELECT column lists, NewScript/NewFlow and
*WithDraft structs, the deploy no-op comparison, the delete-permission check
(deleting always requires admin now), git-sync's draft-only skip, and openapi.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 16:12:12 +02:00

198 lines
6.4 KiB
Rust

use sqlx::postgres::Postgres;
use sqlx::Pool;
use windmill_test_utils::{initialize_tracing, ApiServer};
/// Integration test: exercises every explicit-column query in `tarball_workspace`.
///
/// Creates one entity of each type (folder, script, resource, resource_type,
/// variable, schedule, group) in the test workspace, then calls the tarball
/// export endpoint with all include_* flags enabled. Success means every
/// `SELECT col1, col2, ...` list matches the database schema.
///
/// Tables exercised (one explicit-column query each):
/// folder, script, resource, resource_type, variable, schedule, usr, group_
#[sqlx::test(fixtures("base"))]
async fn test_tarball_export_all_tables(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
let base_url = format!("http://localhost:{port}");
let client = windmill_api_client::create_client(&base_url, "SECRET_TOKEN".to_string());
let http = client.client();
// ---- folder ----
sqlx::query(
r#"INSERT INTO folder
(workspace_id, name, display_name, owners, extra_perms, summary)
VALUES ($1, $2, $3, $4, '{}'::jsonb, $5)"#,
)
.bind("test-workspace")
.bind("test_folder")
.bind("Test Folder")
.bind(vec!["u/test-user"])
.bind("a test folder")
.execute(&db)
.await?;
// ---- script (exercises the 30-column Script<ScriptRunnableSettingsHandle> query) ----
client
.create_script(
"test-workspace",
&windmill_api_client::types::NewScript {
content: "export function main() { return 42; }".to_string(),
language: windmill_api_client::types::ScriptLang::Bun,
path: "f/test_folder/test_script".to_string(),
summary: "test script".to_string(),
description: "script for export test".to_string(),
kind: Some("script".to_string()),
tag: Some("test".to_string()),
lock: None,
parent_hash: None,
schema: Default::default(),
is_template: None,
dedicated_worker: None,
ws_error_handler_muted: None,
priority: None,
cache_ttl: None,
concurrent_limit: None,
concurrency_time_window_s: None,
timeout: None,
delete_after_secs: None,
restart_unless_cancelled: None,
visible_to_runner_only: None,
auto_kind: None,
on_behalf_of_email: None,
has_preprocessor: None,
codebase: None,
envs: vec![],
deployment_message: None,
assets: vec![],
modules: None,
concurrency_key: None,
},
)
.await?;
// ---- resource ----
sqlx::query(
r#"INSERT INTO resource
(workspace_id, path, value, description, resource_type, created_by)
VALUES ($1, $2, $3, $4, $5, $6)"#,
)
.bind("test-workspace")
.bind("f/test_folder/test_res")
.bind(serde_json::json!({"url": "http://example.com"}))
.bind("test resource")
.bind("http")
.bind("test-user")
.execute(&db)
.await?;
// ---- resource_type ----
sqlx::query(
r#"INSERT INTO resource_type
(workspace_id, name, schema, description, created_by)
VALUES ($1, $2, $3, $4, $5)"#,
)
.bind("test-workspace")
.bind("http")
.bind(serde_json::json!({"type": "object"}))
.bind("HTTP resource type")
.bind("system")
.execute(&db)
.await?;
// ---- variable ----
sqlx::query(
r#"INSERT INTO variable
(workspace_id, path, value, is_secret, description, account)
VALUES ($1, $2, $3, $4, $5, $6)"#,
)
.bind("test-workspace")
.bind("f/test_folder/test_var")
.bind("test_value")
.bind(false)
.bind("test variable")
.bind(None::<i32>)
.execute(&db)
.await?;
// ---- schedule ----
client
.create_schedule(
"test-workspace",
&windmill_api_client::types::NewSchedule {
schedule: "0 0 0 * * *".to_string(),
script_path: "f/test_folder/test_script".to_string(),
path: "f/test_folder/test_schedule".to_string(),
is_flow: false,
timezone: "UTC".to_string(),
args: Default::default(),
enabled: Some(false),
description: Some("test schedule".to_string()),
summary: Some("test schedule".to_string()),
tag: None,
cron_version: Some("v2".to_string()),
on_failure: None,
on_failure_times: None,
on_failure_exact: None,
on_failure_extra_args: None,
on_recovery: None,
on_recovery_times: None,
on_recovery_extra_args: None,
on_success: None,
on_success_extra_args: None,
ws_error_handler_muted: None,
retry: None,
no_flow_overlap: None,
},
)
.await?;
// ---- group_ (base fixture already has "all", create one more for include_groups) ----
sqlx::query(
"INSERT INTO group_ (workspace_id, name, summary, extra_perms) VALUES ($1, $2, $3, '{}'::jsonb)",
)
.bind("test-workspace")
.bind("testgroup")
.bind("test group")
.execute(&db)
.await?;
// ---- tarball export: hits ALL explicit-column queries at once ----
let params = [
"archive_type=tar",
"include_schedules=true",
"include_users=true",
"include_groups=true",
"include_settings=true",
"include_workspace_dependencies=true",
"settings_version=v1",
];
let resp = http
.get(format!(
"{}/api/w/test-workspace/workspaces/tarball?{}",
base_url,
params.join("&")
))
.bearer_auth("SECRET_TOKEN")
.send()
.await?;
assert_eq!(
resp.status(),
200,
"tarball export failed: {}",
resp.text().await.unwrap_or_default()
);
// Verify we got actual bytes back
let body = resp.bytes().await?;
assert!(!body.is_empty(), "tarball export returned empty body");
Ok(())
}