Holistic Phase-1 review found the "a disabled script can't execute via any path" guarantee (§4.3) held only for the sync user-route, the execute-by-id bypass, and the trigger outbox arm — three paths still ran disabled scripts: - Queue arm: `list_active_queue_consumers` filtered the trigger's `enabled` but not the bound script's. Add `JOIN scripts … AND s.enabled = TRUE` so a disabled script's queue trigger stops consuming. - Async-HTTP (202) + queued invoke() arms: `build_http_request` / `build_invoke_request` hardcoded `active: true`. Set it to `script.enabled`, and MOVE the dispatcher's fire-time `active` drop to after the source-kind match so it covers all three arms uniformly (previously trigger-only). - `invoke()` (script-to-script): `resolve_id`/`resolve_name` checked cross-app isolation but not `enabled`. A disabled target now resolves to NotFound (indistinguishable from absent), matching the data plane. Also (review LOW/§4.7): - The route-on/script-off 404 returned `NotFound(script_id)`, leaking the internal id and distinguishable from absent. Return the same flat "no route matches" 404 as the unmatched case. - Add the §4.7 "enabled endpoint with no route and no trigger" reachability warning (was unimplemented; only disabled-target shipped). Tested: manager-core lib 368 + orchestrator 75 + 22 project-tool journeys (incl. a new enabled-route→disabled-script flat-404 e2e) green; clippy -D warnings clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
227 lines
7.5 KiB
Rust
227 lines
7.5 KiB
Rust
//! `enabled` three-state lifecycle, end to end: disabling a script via the
|
|
//! manifest makes it non-invocable (404 on the execute-by-id bypass), and
|
|
//! re-enabling restores it — proving the data path + runtime honoring.
|
|
|
|
use std::fs;
|
|
use std::path::Path;
|
|
|
|
use serde_json::Value;
|
|
use tempfile::TempDir;
|
|
|
|
use crate::common;
|
|
use crate::common::cleanup::AppGuard;
|
|
|
|
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
|
|
#[test]
|
|
fn disabling_a_script_makes_it_uninvocable_then_reenable() {
|
|
let Some(fx) = common::fixture_or_skip() else {
|
|
return;
|
|
};
|
|
let env = common::admin_env(fx);
|
|
let slug = common::unique_slug("enabled");
|
|
common::pic_as(&env)
|
|
.args(["apps", "create", &slug])
|
|
.assert()
|
|
.success();
|
|
let _guard = AppGuard::new(&env.url, &env.token, &slug);
|
|
|
|
let dir = TempDir::new().unwrap();
|
|
fs::create_dir_all(dir.path().join("scripts")).unwrap();
|
|
fs::write(dir.path().join("scripts/hello.rhai"), "\"hi\"").unwrap();
|
|
let manifest_path = dir.path().join("picloud.toml");
|
|
let manifest = |enabled_line: &str| {
|
|
format!(
|
|
"[app]\nslug = \"{slug}\"\nname = \"Enabled\"\n\n\
|
|
[[scripts]]\nname = \"hello\"\nfile = \"scripts/hello.rhai\"\n{enabled_line}"
|
|
)
|
|
};
|
|
|
|
// Active → invocable.
|
|
fs::write(&manifest_path, manifest("")).unwrap();
|
|
apply(&env, &manifest_path);
|
|
let id = script_id(&env, &slug, "hello");
|
|
assert_eq!(invoke_status(&env, &id), 200, "active script must invoke");
|
|
|
|
// Disabled → 404 (not invocable), but still deployed (re-pull would show it).
|
|
fs::write(&manifest_path, manifest("enabled = false\n")).unwrap();
|
|
apply(&env, &manifest_path);
|
|
assert_eq!(
|
|
invoke_status(&env, &id),
|
|
404,
|
|
"disabled script must 404 on execute-by-id"
|
|
);
|
|
|
|
// Re-enabled → invocable again.
|
|
fs::write(&manifest_path, manifest("enabled = true\n")).unwrap();
|
|
apply(&env, &manifest_path);
|
|
assert_eq!(
|
|
invoke_status(&env, &id),
|
|
200,
|
|
"re-enabled script must invoke"
|
|
);
|
|
}
|
|
|
|
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
|
|
#[test]
|
|
fn disabling_a_route_makes_it_404_then_reenable() {
|
|
let Some(fx) = common::fixture_or_skip() else {
|
|
return;
|
|
};
|
|
let env = common::admin_env(fx);
|
|
let slug = common::unique_slug("enbl-route");
|
|
common::pic_as(&env)
|
|
.args(["apps", "create", &slug])
|
|
.assert()
|
|
.success();
|
|
let _guard = AppGuard::new(&env.url, &env.token, &slug);
|
|
|
|
// The app must claim a Host before its routes are reachable (two-phase
|
|
// dispatch: Host → app → route). A unique strict host avoids colliding
|
|
// with other tests' instance-global claims.
|
|
let host = format!("{slug}.test");
|
|
common::pic_as(&env)
|
|
.args(["apps", "domains", "add", &slug, &host])
|
|
.assert()
|
|
.success();
|
|
|
|
let dir = TempDir::new().unwrap();
|
|
fs::create_dir_all(dir.path().join("scripts")).unwrap();
|
|
fs::write(dir.path().join("scripts/hello.rhai"), "\"hi\"").unwrap();
|
|
let manifest_path = dir.path().join("picloud.toml");
|
|
let manifest = |enabled_line: &str| {
|
|
format!(
|
|
"[app]\nslug = \"{slug}\"\nname = \"EnabledRoute\"\n\n\
|
|
[[scripts]]\nname = \"hello\"\nfile = \"scripts/hello.rhai\"\n\n\
|
|
[[routes]]\nscript = \"hello\"\nmethod = \"GET\"\n\
|
|
host_kind = \"any\"\npath_kind = \"exact\"\npath = \"/hello\"\n{enabled_line}"
|
|
)
|
|
};
|
|
|
|
// Active → the route serves.
|
|
fs::write(&manifest_path, manifest("")).unwrap();
|
|
apply(&env, &manifest_path);
|
|
assert_eq!(
|
|
route_status(&env, &host, "/hello"),
|
|
200,
|
|
"active route serves"
|
|
);
|
|
|
|
// Disabled → 404, indistinguishable from absent.
|
|
fs::write(&manifest_path, manifest("enabled = false\n")).unwrap();
|
|
apply(&env, &manifest_path);
|
|
assert_eq!(
|
|
route_status(&env, &host, "/hello"),
|
|
404,
|
|
"disabled route must 404"
|
|
);
|
|
|
|
// Re-enabled → serves again.
|
|
fs::write(&manifest_path, manifest("enabled = true\n")).unwrap();
|
|
apply(&env, &manifest_path);
|
|
assert_eq!(
|
|
route_status(&env, &host, "/hello"),
|
|
200,
|
|
"re-enabled route serves"
|
|
);
|
|
}
|
|
|
|
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
|
|
#[test]
|
|
fn enabled_route_to_disabled_script_404s_flatly() {
|
|
let Some(fx) = common::fixture_or_skip() else {
|
|
return;
|
|
};
|
|
let env = common::admin_env(fx);
|
|
let slug = common::unique_slug("enbl-os");
|
|
common::pic_as(&env)
|
|
.args(["apps", "create", &slug])
|
|
.assert()
|
|
.success();
|
|
let _guard = AppGuard::new(&env.url, &env.token, &slug);
|
|
let host = format!("{slug}.test");
|
|
common::pic_as(&env)
|
|
.args(["apps", "domains", "add", &slug, &host])
|
|
.assert()
|
|
.success();
|
|
|
|
let dir = TempDir::new().unwrap();
|
|
fs::create_dir_all(dir.path().join("scripts")).unwrap();
|
|
fs::write(dir.path().join("scripts/hello.rhai"), "\"hi\"").unwrap();
|
|
let manifest_path = dir.path().join("picloud.toml");
|
|
// Route stays enabled; the SCRIPT it binds is disabled (route-on/script-off).
|
|
fs::write(
|
|
&manifest_path,
|
|
format!(
|
|
"[app]\nslug = \"{slug}\"\nname = \"OS\"\n\n\
|
|
[[scripts]]\nname = \"hello\"\nfile = \"scripts/hello.rhai\"\nenabled = false\n\n\
|
|
[[routes]]\nscript = \"hello\"\nmethod = \"GET\"\n\
|
|
host_kind = \"any\"\npath_kind = \"exact\"\npath = \"/hello\"\n"
|
|
),
|
|
)
|
|
.unwrap();
|
|
apply(&env, &manifest_path);
|
|
|
|
// 404, and the body must be the flat "no route matches" form — never the
|
|
// internal script id (no info leak; indistinguishable from absent).
|
|
let client = reqwest::blocking::Client::new();
|
|
let resp = client
|
|
.get(format!("{}/hello", env.url))
|
|
.header(reqwest::header::HOST, &host)
|
|
.send()
|
|
.unwrap();
|
|
assert_eq!(resp.status().as_u16(), 404);
|
|
let body = resp.text().unwrap();
|
|
assert!(body.contains("no route matches"), "flat 404 body: {body}");
|
|
}
|
|
|
|
fn apply(env: &common::TestEnv, manifest_path: &Path) {
|
|
common::pic_as(env)
|
|
.args(["apply", "--file"])
|
|
.arg(manifest_path)
|
|
.assert()
|
|
.success();
|
|
}
|
|
|
|
/// GET a user route under an explicit `Host` (the claimed domain) and return
|
|
/// the HTTP status code.
|
|
fn route_status(env: &common::TestEnv, host: &str, path: &str) -> u16 {
|
|
let client = reqwest::blocking::Client::new();
|
|
client
|
|
.get(format!("{}{path}", env.url))
|
|
.header(reqwest::header::HOST, host)
|
|
.send()
|
|
.unwrap()
|
|
.status()
|
|
.as_u16()
|
|
}
|
|
|
|
/// Resolve a script's id via the admin API (the manifest carries no ids).
|
|
fn script_id(env: &common::TestEnv, slug: &str, name: &str) -> String {
|
|
let client = reqwest::blocking::Client::new();
|
|
let scripts: Vec<Value> = client
|
|
.get(format!("{}/api/v1/admin/scripts?app={slug}", env.url))
|
|
.bearer_auth(&env.token)
|
|
.send()
|
|
.unwrap()
|
|
.json()
|
|
.unwrap();
|
|
scripts
|
|
.into_iter()
|
|
.find(|s| s["name"] == name)
|
|
.and_then(|s| s["id"].as_str().map(String::from))
|
|
.expect("script id")
|
|
}
|
|
|
|
/// POST the execute-by-id bypass and return the HTTP status code.
|
|
fn invoke_status(env: &common::TestEnv, id: &str) -> u16 {
|
|
let client = reqwest::blocking::Client::new();
|
|
client
|
|
.post(format!("{}/api/v1/execute/{id}", env.url))
|
|
.bearer_auth(&env.token)
|
|
.body("{}")
|
|
.send()
|
|
.unwrap()
|
|
.status()
|
|
.as_u16()
|
|
}
|