use toasty::Db;
use topcoat::Result;
use super::*;
use crate::{
Ability, ResourceDef, lens,
panel::test_support::{Dummy, dummy_table, mount, panel_for},
};
#[test]
fn parse_bulk_ids_dedupes_and_trims() {
assert!(parse_bulk_ids("", MAX_BULK_IDS).is_empty());
assert_eq!(
parse_bulk_ids("a, b ,a,, c", MAX_BULK_IDS),
vec!["a", "b", "c"]
);
assert_eq!(parse_bulk_ids("a,b,c,d,e", 3).len(), 4);
}
#[tokio::test]
async fn bulk_delete_caps_ids_and_checks_the_token() {
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 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 ok = router
.handle(
http::Request::builder()
.uri("/admin/dummies/bulk-delete")
.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(format!(
"ids={}&confirm=1&csrf_token={token}",
row.id
)))
.unwrap(),
)
.await;
assert!(
ok.status().is_redirection(),
"a bulk delete by primary key succeeds, got {}",
ok.status()
);
let big = (0..(MAX_BULK_IDS + 1))
.map(|i| format!("00000000-0000-0000-0000-{:012}", i))
.collect::<Vec<_>>()
.join(",");
let capped = router
.handle(
http::Request::builder()
.uri("/admin/dummies/bulk-delete")
.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(format!(
"ids={big}&confirm=1&csrf_token={token}"
)))
.unwrap(),
)
.await;
assert_eq!(capped.status(), http::StatusCode::BAD_REQUEST);
let no_token = router
.handle(
http::Request::builder()
.uri("/admin/dummies/bulk-delete")
.method(http::Method::POST)
.header(
http::header::CONTENT_TYPE,
"application/x-www-form-urlencoded",
)
.body(Body::from(format!("ids={}", row.id)))
.unwrap(),
)
.await;
assert_eq!(no_token.status(), http::StatusCode::FORBIDDEN);
}
#[tokio::test]
async fn bulk_delete_mid_loop_failure_deletes_zero_rows() {
use crate::resource::Resource;
struct FlakyBulkResource;
impl Resource for FlakyBulkResource {
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(dummy_table())
}
async fn bulk_delete_records(
_cx: &Cx,
records: &[Dummy],
ex: &mut dyn toasty::Executor,
) -> Result<()> {
let first = &records[0];
Dummy::filter(Dummy::fields().id().eq(first.id))
.delete()
.exec(&mut *ex)
.await
.map_err(topcoat::Error::from)?;
Err(std::io::Error::other("boom").into())
}
}
let mut db = Db::builder()
.models(toasty::models!(Dummy))
.connect("sqlite::memory:")
.await
.unwrap();
db.push_schema().await.unwrap();
for name in ["one", "two"] {
toasty::create!(Dummy {
name: name.to_string(),
})
.exec(&mut db)
.await
.unwrap();
}
let mut db_ids = db.clone();
let rows = Dummy::all().exec(&mut db_ids).await.unwrap();
assert_eq!(rows.len(), 2);
let ids = rows
.iter()
.map(|r| r.id.to_string())
.collect::<Vec<_>>()
.join(",");
let router = mount(db.clone(), panel_for::<FlakyBulkResource>()).expect("panel builds");
let token = uuid::Uuid::new_v4().to_string();
let resp = router
.handle(
http::Request::builder()
.uri("/admin/dummies/bulk-delete")
.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(format!(
"ids={ids}&confirm=1&csrf_token={token}"
)))
.unwrap(),
)
.await;
assert!(
resp.status().is_server_error(),
"mid-loop failure must error, got {}",
resp.status()
);
let rows = Dummy::all().exec(&mut db_ids).await.unwrap();
assert_eq!(
rows.len(),
2,
"rollback must leave zero rows deleted, got {}",
2 - rows.len()
);
}