mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
8a96e3a4ec
* fix: raw apps with no stylesheet were permanently un-deployable Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: keep js strict when defaulting the raw app bundle css Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test: drop ephemeral narration from raw app bundle regression test Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test: pin the extension each raw app bundle half is fetched under Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
172 lines
5.2 KiB
Rust
172 lines
5.2 KiB
Rust
//! Raw app bundles whose stylesheet is absent rather than empty.
|
|
//!
|
|
//! `create_app_raw` stores the `css` blob only when the multipart field is
|
|
//! present, so an app built from sources with no styles has no `css` row at
|
|
//! all. Cross-workspace deploy re-fetches both bundle parts and treats a
|
|
//! non-200 as fatal, so 404ing the stylesheet makes such an app un-deployable:
|
|
//! absent means empty.
|
|
//!
|
|
//! This test pins down:
|
|
//! - a bundle stored without a `css` part serves an empty stylesheet,
|
|
//! - a bundle that has styles serves them byte-for-byte, so the tolerance
|
|
//! never blanks a real stylesheet, and
|
|
//! - a missing `js` blob still 404s — the tolerance must not leak to the
|
|
//! half of the bundle whose absence really is broken.
|
|
|
|
use reqwest::multipart;
|
|
use sqlx::{Pool, Postgres};
|
|
use windmill_test_utils::*;
|
|
|
|
fn client() -> reqwest::Client {
|
|
reqwest::Client::new()
|
|
}
|
|
|
|
fn authed(builder: reqwest::RequestBuilder, token: &str) -> reqwest::RequestBuilder {
|
|
builder.header("Authorization", format!("Bearer {}", token))
|
|
}
|
|
|
|
fn app_part(path: &str) -> anyhow::Result<multipart::Part> {
|
|
let app = serde_json::json!({
|
|
"path": path,
|
|
"summary": "",
|
|
"value": {},
|
|
"policy": { "execution_mode": "publisher" },
|
|
"raw_app": true,
|
|
});
|
|
Ok(multipart::Part::text(app.to_string()).mime_str("application/json")?)
|
|
}
|
|
|
|
#[sqlx::test(fixtures("base"))]
|
|
async fn test_raw_app_without_stylesheet_stays_readable(db: Pool<Postgres>) -> anyhow::Result<()> {
|
|
initialize_tracing().await;
|
|
|
|
let server = ApiServer::start(db.clone()).await?;
|
|
let port = server.addr.port();
|
|
let base = format!("http://localhost:{port}/api/w/test-workspace");
|
|
|
|
let js_body = "console.log('app')";
|
|
|
|
// 1. Create a raw app with NO css part at all — what a client that omits
|
|
// the field produces.
|
|
let form = multipart::Form::new()
|
|
.part("app", app_part("u/test-user/nocss")?)
|
|
.part("js", multipart::Part::text(js_body));
|
|
let resp = authed(
|
|
client().post(format!("{base}/apps/create_raw")),
|
|
"SECRET_TOKEN",
|
|
)
|
|
.multipart(form)
|
|
.send()
|
|
.await?;
|
|
assert_eq!(
|
|
resp.status(),
|
|
201,
|
|
"creating a raw app without a css part should succeed: {}",
|
|
resp.text().await?
|
|
);
|
|
|
|
let secret = authed(
|
|
client().get(format!(
|
|
"{base}/apps/secret_of_latest_version/u/test-user/nocss"
|
|
)),
|
|
"SECRET_TOKEN",
|
|
)
|
|
.send()
|
|
.await?
|
|
.text()
|
|
.await?;
|
|
|
|
// 2. The stylesheet is absent, not broken: serve it empty so deploy — which
|
|
// re-fetches both parts — can carry the app to another workspace.
|
|
let resp = authed(
|
|
client().get(format!("{base}/apps/get_data/v/{secret}.css")),
|
|
"SECRET_TOKEN",
|
|
)
|
|
.send()
|
|
.await?;
|
|
assert_eq!(
|
|
resp.status(),
|
|
200,
|
|
"a bundle with no stylesheet must serve an empty one, not 404"
|
|
);
|
|
assert_eq!(resp.text().await?, "", "the empty stylesheet must be empty");
|
|
|
|
// 3. The JavaScript half is untouched by the fallback.
|
|
let resp = authed(
|
|
client().get(format!("{base}/apps/get_data/v/{secret}.js")),
|
|
"SECRET_TOKEN",
|
|
)
|
|
.send()
|
|
.await?;
|
|
assert_eq!(resp.status(), 200);
|
|
assert_eq!(resp.text().await?, js_body);
|
|
|
|
// 4. An app that does have styles must still serve them unchanged — the
|
|
// fallback must never blank a real stylesheet.
|
|
let css_body = "body { color: red }";
|
|
let form = multipart::Form::new()
|
|
.part("app", app_part("u/test-user/withcss")?)
|
|
.part("js", multipart::Part::text(js_body))
|
|
.part("css", multipart::Part::text(css_body));
|
|
let resp = authed(
|
|
client().post(format!("{base}/apps/create_raw")),
|
|
"SECRET_TOKEN",
|
|
)
|
|
.multipart(form)
|
|
.send()
|
|
.await?;
|
|
assert_eq!(resp.status(), 201, "{}", resp.text().await?);
|
|
|
|
let styled_secret = authed(
|
|
client().get(format!(
|
|
"{base}/apps/secret_of_latest_version/u/test-user/withcss"
|
|
)),
|
|
"SECRET_TOKEN",
|
|
)
|
|
.send()
|
|
.await?
|
|
.text()
|
|
.await?;
|
|
|
|
let resp = authed(
|
|
client().get(format!("{base}/apps/get_data/v/{styled_secret}.css")),
|
|
"SECRET_TOKEN",
|
|
)
|
|
.send()
|
|
.await?;
|
|
assert_eq!(resp.status(), 200);
|
|
assert_eq!(
|
|
resp.text().await?,
|
|
css_body,
|
|
"an app with styles must round-trip them intact"
|
|
);
|
|
|
|
// 5. A missing `js` blob is a genuinely broken bundle and must still 404.
|
|
// The API cannot create this state (js is mandatory on write), so drop
|
|
// the row directly.
|
|
let version_id: i64 = sqlx::query_scalar(
|
|
"SELECT versions[array_upper(versions, 1)] FROM app
|
|
WHERE path = 'u/test-user/nocss' AND workspace_id = 'test-workspace'",
|
|
)
|
|
.fetch_one(&db)
|
|
.await?;
|
|
sqlx::query("DELETE FROM app_bundles WHERE app_version_id = $1 AND file_type = 'js'")
|
|
.bind(version_id)
|
|
.execute(&db)
|
|
.await?;
|
|
|
|
let resp = authed(
|
|
client().get(format!("{base}/apps/get_data/v/{secret}.js")),
|
|
"SECRET_TOKEN",
|
|
)
|
|
.send()
|
|
.await?;
|
|
assert_eq!(
|
|
resp.status(),
|
|
404,
|
|
"a missing js bundle must still fail loudly — the css tolerance must not leak to it"
|
|
);
|
|
|
|
Ok(())
|
|
}
|