Phase 4-lite C5. Two `pic` journeys:
* `group_script_is_inherited_with_cow_and_isolation` — a group endpoint
`shared` is invoked by name from an app under the group; the app's own
`shared` then shadows it (CoW); an app outside the group cannot reach it.
* `manifest_binds_route_to_inherited_group_script_idempotently` — a manifest
that declares no `greet` script binds a route to the inherited group
`greet`; plan shows a route create (no script create), apply succeeds, and
re-plan is a clean no-op.
Adds a `ScriptGuard` (deletes a script by id on drop) since a group-owned
script blocks its group's deletion (ON DELETE RESTRICT) — it must drop before
the GroupGuard.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
125 lines
3.2 KiB
Rust
125 lines
3.2 KiB
Rust
//! RAII guards that delete server-side resources on `Drop`.
|
|
//!
|
|
//! Each guard owns the minimum it needs to issue a single DELETE: the
|
|
//! base URL, an admin bearer token, and the resource identifier.
|
|
//! Failures are swallowed because Drop runs during teardown — a panic
|
|
//! here would just mask the real failure that the test was reporting.
|
|
|
|
pub struct AppGuard {
|
|
url: String,
|
|
token: String,
|
|
slug: String,
|
|
}
|
|
|
|
impl AppGuard {
|
|
pub fn new(url: &str, token: &str, slug: &str) -> Self {
|
|
Self {
|
|
url: url.to_string(),
|
|
token: token.to_string(),
|
|
slug: slug.to_string(),
|
|
}
|
|
}
|
|
|
|
pub fn slug(&self) -> &str {
|
|
&self.slug
|
|
}
|
|
}
|
|
|
|
impl Drop for AppGuard {
|
|
fn drop(&mut self) {
|
|
let client = reqwest::blocking::Client::new();
|
|
let _ = client
|
|
.delete(format!(
|
|
"{}/api/v1/admin/apps/{}?force=true",
|
|
self.url, self.slug
|
|
))
|
|
.bearer_auth(&self.token)
|
|
.send();
|
|
}
|
|
}
|
|
|
|
/// Deletes a script by id on drop (best-effort). Phase 4: a group-owned
|
|
/// script blocks its group's deletion (`ON DELETE RESTRICT`), so register the
|
|
/// `ScriptGuard` *after* the owning `GroupGuard` — the script drops (deletes)
|
|
/// first, leaving the group removable.
|
|
pub struct ScriptGuard {
|
|
url: String,
|
|
token: String,
|
|
id: String,
|
|
}
|
|
|
|
impl ScriptGuard {
|
|
pub fn new(url: &str, token: &str, id: &str) -> Self {
|
|
Self {
|
|
url: url.to_string(),
|
|
token: token.to_string(),
|
|
id: id.to_string(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Drop for ScriptGuard {
|
|
fn drop(&mut self) {
|
|
let client = reqwest::blocking::Client::new();
|
|
let _ = client
|
|
.delete(format!("{}/api/v1/admin/scripts/{}", self.url, self.id))
|
|
.bearer_auth(&self.token)
|
|
.send();
|
|
}
|
|
}
|
|
|
|
pub struct UserGuard {
|
|
url: String,
|
|
token: String,
|
|
user_id: String,
|
|
}
|
|
|
|
/// Deletes a group on drop (best-effort). The group must be empty by then
|
|
/// — register an `AppGuard`/child `GroupGuard` *after* this one so the
|
|
/// child drops (deletes) first, leaving an empty node here.
|
|
pub struct GroupGuard {
|
|
url: String,
|
|
token: String,
|
|
slug: String,
|
|
}
|
|
|
|
impl GroupGuard {
|
|
pub fn new(url: &str, token: &str, slug: &str) -> Self {
|
|
Self {
|
|
url: url.to_string(),
|
|
token: token.to_string(),
|
|
slug: slug.to_string(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Drop for GroupGuard {
|
|
fn drop(&mut self) {
|
|
let client = reqwest::blocking::Client::new();
|
|
let _ = client
|
|
.delete(format!("{}/api/v1/admin/groups/{}", self.url, self.slug))
|
|
.bearer_auth(&self.token)
|
|
.send();
|
|
}
|
|
}
|
|
|
|
impl UserGuard {
|
|
pub fn new(url: &str, token: &str, user_id: &str) -> Self {
|
|
Self {
|
|
url: url.to_string(),
|
|
token: token.to_string(),
|
|
user_id: user_id.to_string(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Drop for UserGuard {
|
|
fn drop(&mut self) {
|
|
let client = reqwest::blocking::Client::new();
|
|
let _ = client
|
|
.delete(format!("{}/api/v1/admin/admins/{}", self.url, self.user_id))
|
|
.bearer_auth(&self.token)
|
|
.send();
|
|
}
|
|
}
|