use super::*;
use crate::{
Ability, ResourceDef, Site,
panel::test_support::{Dummy, dummy_table, mount, panel_for, panel_state, refusal, serving},
test_support::tableless_db,
};
#[test]
fn serve_dir_accepts_only_a_catch_all_pattern() {
assert!(is_directory_pattern("/uploads/{*file}"));
assert!(is_directory_pattern("/{*file}"));
assert!(is_directory_pattern("/up loads/{*file}"));
assert!(!is_directory_pattern("/uploads"));
assert!(!is_directory_pattern("/uploads/"));
assert!(!is_directory_pattern("/uploads/{file}"));
assert!(!is_directory_pattern("/uploads/{*}"));
assert!(!is_directory_pattern("/{*file}/more"));
assert!(!is_directory_pattern("/uploads/{*file"));
assert!(!is_directory_pattern("/uploads//{*file}"));
assert!(!is_directory_pattern("/uploads/{*fi-le}"));
}
struct Unbindable;
impl crate::table::Column<Dummy> for Unbindable {
fn name(&self) -> &str {
"unbindable"
}
fn label(&self) -> crate::Text {
"Unbindable".into()
}
fn text(&self, _cx: &Cx, _row: &Dummy) -> String {
String::new()
}
fn internals(&self) -> &dyn crate::column::ColumnInternals<Dummy> {
self
}
}
impl crate::column::ColumnInternals<Dummy> for Unbindable {
fn misdeclared(&self) -> Option<DeclarationErrorKind> {
Some(DeclarationErrorKind::TraversalLens { steps: 2 })
}
}
#[tokio::test]
async fn a_detail_column_binding_no_field_refuses_the_mount() {
struct UnbindableView;
impl crate::resource::Resource for UnbindableView {
type Model = Dummy;
type Form = crate::NoForm<Self::Model>;
fn declare() -> ResourceDef<Self> {
ResourceDef::new()
.table(dummy_table())
.detail(crate::Detail::empty().column(Unbindable))
}
}
let db = tableless_db(toasty::models!(Dummy)).await;
assert_eq!(
refusal(mount(db, panel_for::<UnbindableView>())),
[DeclarationError::of::<UnbindableView>(
Site::Detail,
DeclarationErrorKind::TraversalLens { steps: 2 },
)]
);
}
#[tokio::test]
async fn panel_root_redirect_rechecks_auth_before_the_root_target() {
use topcoat::{context::Cx, router::response::IntoResponse};
let (parts, ()) = http::Request::builder()
.uri("/admin")
.body(())
.unwrap()
.into_parts();
let mut gated = panel_state("/admin", crate::Auth::password());
gated.root_redirect = Some("/admin/users".to_string());
let gated = Arc::new(gated);
let cx = serving(Cx::default().with(parts), &gated);
let err = match panel_root_redirect(&cx, Body::empty()).await {
Ok(_) => panic!("unauthenticated root must not read the root target"),
Err(err) => err,
};
let location = err
.into_response(&cx)
.expect("gate redirect renders")
.headers()
.get(http::header::LOCATION)
.expect("login redirect carries a location")
.to_str()
.unwrap()
.to_string();
assert!(
location.starts_with("/admin/login"),
"unauthenticated root must redirect to login, got {location}"
);
let user = crate::auth::SignedIn {
user: Arc::new(crate::auth::AdminUser {
id: uuid::Uuid::nil(),
email: "ada@example.com".to_string(),
password_hash: String::new(),
display_name: "Ada".to_string(),
active: true,
email_verified_at: None,
created_at: jiff::Timestamp::UNIX_EPOCH,
}),
realm: Arc::clone(&gated.realm),
tenant: None,
impersonator: None,
};
let (parts, ()) = http::Request::builder()
.uri("/admin")
.body(())
.unwrap()
.into_parts();
let cx = serving(Cx::default().with(parts), &gated).with(user);
let err = match panel_root_redirect(&cx, Body::empty()).await {
Ok(_) => panic!("the redirect is an Err response"),
Err(err) => err,
};
let location = err
.into_response(&cx)
.expect("root redirect renders")
.headers()
.get(http::header::LOCATION)
.expect("root redirect carries a location")
.to_str()
.unwrap()
.to_string();
assert_eq!(location, "/admin/users");
}
#[tokio::test]
async fn an_unmounted_resource_is_refused_not_rebuilt() {
use crate::resource::Resource;
struct UnmountedResource;
impl Resource for UnmountedResource {
type Model = Dummy;
type Form = crate::NoForm<Self::Model>;
fn declare() -> ResourceDef<Self> {
ResourceDef::new().policy(crate::Allow).table(dummy_table())
}
}
let db = tableless_db(toasty::models!(Dummy)).await;
let bare = topcoat::context::Cx::default().with(db.clone());
let panel = Panel::new("admin")
.handle(&db)
.expect("panel builds")
.context();
for cx in [bare, panel] {
assert!(!crate::can::<UnmountedResource>(&cx, Ability::ViewAny));
let refused = crate::scoped_query::<UnmountedResource>(&cx).expect_err("not mounted");
assert!(refused.to_string().contains("not mounted"), "{refused}");
}
}
#[tokio::test]
async fn a_panel_handle_answers_from_the_mounted_def() {
use std::sync::atomic::{AtomicUsize, Ordering};
use crate::resource::Resource;
static DECLARE_CALLS: AtomicUsize = AtomicUsize::new(0);
struct HandledResource;
impl Resource for HandledResource {
type Model = Dummy;
type Form = crate::NoForm<Self::Model>;
fn declare() -> ResourceDef<Self> {
DECLARE_CALLS.fetch_add(1, Ordering::SeqCst);
ResourceDef::new()
.policy(crate::ReadOnly)
.table(dummy_table())
}
}
let db = tableless_db(toasty::models!(Dummy)).await;
let builder = topcoat::router::Router::builder()
.with(db)
.panel(panel_for::<HandledResource>())
.expect("panel builds");
assert!(builder.panel_handle("portal").is_none(), "no panel there");
let handle = builder
.panel_handle("/admin/")
.expect("the panel at /admin");
for cx in [handle.context(), handle.clone().context()] {
assert!(crate::can::<HandledResource>(&cx, Ability::ViewAny));
assert!(!crate::can::<HandledResource>(&cx, Ability::Create));
}
assert_eq!(DECLARE_CALLS.load(Ordering::SeqCst), 1);
}
#[tokio::test]
async fn handle_refuses_a_misdeclared_resource() {
use crate::resource::Resource;
struct BadView;
impl Resource for BadView {
type Model = Dummy;
type Form = crate::NoForm<Self::Model>;
fn declare() -> ResourceDef<Self> {
ResourceDef::new()
.table(dummy_table())
.detail(crate::Detail::empty().column(Unbindable))
}
}
let db = tableless_db(toasty::models!(Dummy)).await;
assert_eq!(
refusal(panel_for::<BadView>().handle(&db)),
refusal(mount(db, panel_for::<BadView>()))
);
}