use toasty::Db;
use topcoat::Result;
use super::*;
use crate::{
Ability, ResourceDef, lens,
panel::test_support::{Dummy, dummy_table, mount, panel_for},
};
#[tokio::test]
async fn delete_and_bulk_delete_require_view() {
use crate::resource::Resource;
struct ViewDeniedResource;
impl Resource for ViewDeniedResource {
type Model = Dummy;
type Form = crate::NoForm<Self::Model>;
fn declare() -> ResourceDef<Self> {
ResourceDef::new()
.slug("dummies")
.policy(|_cx: &Cx, ability: Ability<'_, Dummy>| match ability {
Ability::View(_record) => false,
Ability::DeleteAny => true,
Ability::Delete(_) => true,
_ => false,
})
.table(dummy_table())
}
}
let mut db = Db::builder()
.models(toasty::models!(Dummy))
.connect("sqlite::memory:")
.await
.unwrap();
db.push_schema().await.unwrap();
let row = toasty::create!(Dummy {
name: "Ada".to_string(),
})
.exec(&mut db)
.await
.unwrap();
let router = mount(db, panel_for::<ViewDeniedResource>()).expect("panel builds");
let token = uuid::Uuid::new_v4().to_string();
let post = |uri: String, body: String| {
router.handle(
http::Request::builder()
.uri(uri)
.method(http::Method::POST)
.header(
http::header::CONTENT_TYPE,
"application/x-www-form-urlencoded",
)
.header(
http::header::COOKIE,
format!("{}={token}", crate::csrf::COOKIE_NAME),
)
.body(Body::from(body))
.unwrap(),
)
};
let single = post(
format!("/admin/dummies/{}/delete", row.id),
format!("confirm=1&csrf_token={token}"),
)
.await;
assert_eq!(
single.status(),
http::StatusCode::FORBIDDEN,
"view-denied single delete must 403, got {}",
single.status()
);
let bulk = post(
"/admin/dummies/bulk-delete".to_string(),
format!("ids={}&confirm=1&csrf_token={token}", row.id),
)
.await;
assert_eq!(
bulk.status(),
http::StatusCode::FORBIDDEN,
"view-denied bulk delete must 403, got {}",
bulk.status()
);
}
#[tokio::test]
async fn delete_and_bulk_delete_require_delete_any() {
use crate::resource::Resource;
struct RowOnlyResource;
impl Resource for RowOnlyResource {
type Model = Dummy;
type Form = crate::NoForm<Self::Model>;
fn declare() -> ResourceDef<Self> {
ResourceDef::new()
.slug("dummies")
.policy(|_cx: &Cx, ability: Ability<'_, Dummy>| {
matches!(ability, Ability::View(_) | Ability::Delete(_))
})
.table(dummy_table())
}
}
let mut db = Db::builder()
.models(toasty::models!(Dummy))
.connect("sqlite::memory:")
.await
.unwrap();
db.push_schema().await.unwrap();
let row = toasty::create!(Dummy {
name: "Ada".to_string(),
})
.exec(&mut db)
.await
.unwrap();
let router = mount(db.clone(), panel_for::<RowOnlyResource>()).expect("panel builds");
let token = uuid::Uuid::new_v4().to_string();
let post = |uri: String, body: String| {
router.handle(
http::Request::builder()
.uri(uri)
.method(http::Method::POST)
.header(
http::header::CONTENT_TYPE,
"application/x-www-form-urlencoded",
)
.header(
http::header::COOKIE,
format!("{}={token}", crate::csrf::COOKIE_NAME),
)
.body(Body::from(body))
.unwrap(),
)
};
let single = post(
format!("/admin/dummies/{}/delete", row.id),
format!("confirm=1&csrf_token={token}"),
)
.await;
assert_eq!(single.status(), http::StatusCode::FORBIDDEN);
let bulk = post(
"/admin/dummies/bulk-delete".to_string(),
format!("ids={}&confirm=1&csrf_token={token}", row.id),
)
.await;
assert_eq!(bulk.status(), http::StatusCode::FORBIDDEN);
let mut db = db;
let remaining = Dummy::all().exec(&mut db).await.unwrap();
assert_eq!(remaining.len(), 1, "a refused delete writes nothing");
}
#[tokio::test]
async fn delete_resolves_the_primary_key_only() {
use crate::resource::Resource;
struct KeyedResource;
impl Resource for KeyedResource {
type Model = Dummy;
type Form = crate::NoForm<Self::Model>;
fn declare() -> ResourceDef<Self> {
ResourceDef::new()
.slug("dummies")
.policy(|_cx: &Cx, ability: Ability<'_, Dummy>| {
matches!(
ability,
Ability::View(_) | Ability::DeleteAny | Ability::Delete(_)
)
})
.table(crate::table::Table::new(crate::table::TextColumn::new(
lens!(Dummy.name),
)))
}
async fn delete_record(
_cx: &Cx,
_record: &Dummy,
_ex: &mut dyn toasty::Executor,
) -> Result<()> {
Ok(())
}
async fn bulk_delete_records(
_cx: &Cx,
_records: &[Dummy],
_ex: &mut dyn toasty::Executor,
) -> Result<()> {
Ok(())
}
}
let mut db = Db::builder()
.models(toasty::models!(Dummy))
.connect("sqlite::memory:")
.await
.unwrap();
db.push_schema().await.unwrap();
let row = toasty::create!(Dummy {
name: "Ada".to_string(),
})
.exec(&mut db)
.await
.unwrap();
let router = mount(db, panel_for::<KeyedResource>()).expect("panel builds");
let token = uuid::Uuid::new_v4().to_string();
let post = |uri: String, body: String| {
router.handle(
http::Request::builder()
.uri(uri)
.method(http::Method::POST)
.header(
http::header::CONTENT_TYPE,
"application/x-www-form-urlencoded",
)
.header(
http::header::COOKIE,
format!("{}={token}", crate::csrf::COOKIE_NAME),
)
.body(Body::from(body))
.unwrap(),
)
};
let name_single = post(
"/admin/dummies/Ada/delete".to_string(),
format!("confirm=1&csrf_token={token}"),
)
.await;
assert_eq!(
name_single.status(),
http::StatusCode::NOT_FOUND,
"a non-key value must not resolve, got {}",
name_single.status()
);
let record_single = post(
format!("/admin/dummies/{}/delete", row.id),
format!("confirm=1&csrf_token={token}"),
)
.await;
assert!(
record_single.status().is_redirection(),
"the primary key must delete, got {}",
record_single.status()
);
let name_bulk = post(
"/admin/dummies/bulk-delete".to_string(),
format!("ids=Ada&confirm=1&csrf_token={token}"),
)
.await;
assert_eq!(
name_bulk.status(),
http::StatusCode::NOT_FOUND,
"a non-key value must not resolve in bulk, got {}",
name_bulk.status()
);
let record_bulk = post(
"/admin/dummies/bulk-delete".to_string(),
format!("ids={}&confirm=1&csrf_token={token}", row.id),
)
.await;
assert!(
record_bulk.status().is_redirection(),
"the primary key must bulk-delete, got {}",
record_bulk.status()
);
}