use toasty::Db;
use topcoat::{
Result,
context::Cx,
router::Body,
view::{View, view},
};
use crate::{
DeclarationError, DeclarationErrorKind, Page, PageDef, Panel, SegmentFault, Site,
panel::test_support::{Dummy, dummy_table, mount, mount_without_db, refusal, response_html},
resource::{Resource, ResourceDef},
test_support::memory_db,
};
struct Dashboard;
impl Page for Dashboard {
async fn render(cx: &Cx) -> Result<impl View> {
Ok(view! { cx => <h1>"Welcome home"</h1> })
}
}
struct ReportsPage;
impl Page for ReportsPage {
fn declare() -> PageDef {
PageDef::new().navigation_order(-1)
}
async fn render(cx: &Cx) -> Result<impl View> {
Ok(view! { cx => <h1>"Quarterly reports"</h1> })
}
}
struct DummyResource;
impl Resource for DummyResource {
type Model = Dummy;
type Form = crate::NoForm<Self::Model>;
fn declare() -> ResourceDef<Self> {
ResourceDef::new().table(dummy_table())
}
}
async fn db() -> Db {
memory_db(toasty::models!(Dummy)).await
}
async fn get(router: &topcoat::router::Router, uri: &str) -> (http::StatusCode, String) {
let request = http::Request::builder()
.uri(uri)
.body(Body::empty())
.unwrap();
let response = router.handle(request).await;
(response.status(), response_html(response).await)
}
#[tokio::test]
async fn pages_mount_under_the_prefix_with_their_sidebar_entries() {
let panel = || {
Panel::new("backoffice")
.auth(crate::Auth::disabled())
.resource::<DummyResource>()
.page::<ReportsPage>()
.home::<Dashboard>()
};
let registered = panel().registered();
let entries: Vec<_> = registered
.nav_items
.iter()
.map(|entry| {
(
entry.item.label.as_str(),
entry.item.url(),
entry.item.order,
)
})
.collect();
assert_eq!(
entries,
[
("Dashboard", Some("/backoffice"), 0),
("Dummies", Some("/backoffice/dummies"), 0),
("Reports", Some("/backoffice/reports"), -1),
]
);
let router = mount(db().await, panel()).expect("the panel builds");
let (status, html) = get(&router, "/backoffice").await;
assert_eq!(
status,
http::StatusCode::OK,
"the home page replaces the redirect"
);
assert!(
html.contains("Welcome home"),
"home renders at the prefix: {html}"
);
let (status, html) = get(&router, "/backoffice/reports").await;
assert_eq!(status, http::StatusCode::OK);
assert!(
html.contains("Quarterly reports"),
"the page renders: {html}"
);
}
#[test]
fn a_page_slug_that_a_resource_holds_does_not_build() {
struct DummiesPage;
impl Page for DummiesPage {
async fn render(cx: &Cx) -> Result<impl View> {
Ok(view! { cx => "dummies" })
}
}
let duplicate = || DeclarationErrorKind::DuplicateSlug {
slug: "dummies".to_string(),
};
let errors = refusal(mount_without_db(
Panel::new("admin")
.resource::<DummyResource>()
.page::<DummiesPage>(),
));
assert_eq!(
errors[0],
DeclarationError::of::<DummiesPage>(Site::Registration, duplicate())
);
let errors = refusal(mount_without_db(
Panel::new("admin")
.page::<DummiesPage>()
.resource::<DummyResource>(),
));
assert_eq!(
errors[0],
DeclarationError::of::<DummyResource>(Site::Registration, duplicate())
);
}
#[tokio::test]
async fn page_with_adjusts_the_page_for_its_panel_only() {
let panel = |prefix: &str| {
Panel::new(prefix)
.auth(crate::Auth::disabled())
.resource::<DummyResource>()
};
let portal = mount(
db().await,
panel("portal").page_with::<ReportsPage>(|def| def.slug("figures").access(|_| false)),
)
.expect("the portal builds");
assert_eq!(
get(&portal, "/portal/figures").await.0,
http::StatusCode::FORBIDDEN
);
assert_eq!(
get(&portal, "/portal/reports").await.0,
http::StatusCode::NOT_FOUND
);
let admin = mount(db().await, panel("admin").page::<ReportsPage>()).expect("the admin builds");
assert_eq!(get(&admin, "/admin/reports").await.0, http::StatusCode::OK);
}
#[test]
fn a_page_slug_that_is_not_one_segment_does_not_build() {
struct NestedPage;
impl Page for NestedPage {
fn declare() -> PageDef {
PageDef::new().slug("reports/q3")
}
async fn render(cx: &Cx) -> Result<impl View> {
Ok(view! { cx => "nested" })
}
}
let errors = refusal(mount_without_db(Panel::new("admin").page::<NestedPage>()));
assert_eq!(
errors[0].kind,
DeclarationErrorKind::InvalidSegment {
item: "PageDef::slug",
segment: "reports/q3".to_string(),
fault: SegmentFault::Char('/'),
}
);
}
#[test]
fn a_second_home_page_does_not_build() {
let errors = refusal(mount_without_db(
Panel::new("admin")
.home::<Dashboard>()
.home::<ReportsPage>(),
));
assert_eq!(
errors[0],
DeclarationError::of::<ReportsPage>(Site::Registration, DeclarationErrorKind::SecondHome)
);
}
#[test]
fn a_page_registered_twice_does_not_build() {
let duplicate = DeclarationError::of::<ReportsPage>(
Site::Registration,
DeclarationErrorKind::DuplicatePage,
);
let errors = refusal(mount_without_db(
Panel::new("admin")
.page::<ReportsPage>()
.page_with::<ReportsPage>(|def| def.slug("figures")),
));
assert_eq!(errors[0], duplicate);
let errors = refusal(mount_without_db(
Panel::new("admin")
.home::<ReportsPage>()
.page::<ReportsPage>(),
));
assert_eq!(errors[0], duplicate);
}
#[tokio::test]
async fn can_open_answers_for_the_requests_panel() {
struct ClosedPage;
impl Page for ClosedPage {
fn declare() -> PageDef {
PageDef::new().access(|_| false)
}
async fn render(cx: &Cx) -> Result<impl View> {
Ok(view! { cx => "closed" })
}
}
struct Probe;
impl Page for Probe {
async fn render(cx: &Cx) -> Result<impl View> {
let answers = format!(
"reports={} closed={}",
crate::can_open::<ReportsPage>(cx),
crate::can_open::<ClosedPage>(cx),
);
Ok(view! { cx => <p>(answers)</p> })
}
}
let panel = |prefix: &str| {
Panel::new(prefix)
.auth(crate::Auth::disabled())
.resource::<DummyResource>()
.home::<Probe>()
};
let admin = mount(
db().await,
panel("admin").page::<ReportsPage>().page::<ClosedPage>(),
)
.expect("the admin builds");
let (_, html) = get(&admin, "/admin").await;
assert!(html.contains("reports=true closed=false"), "{html}");
let portal = mount(db().await, panel("portal")).expect("the portal builds");
let (_, html) = get(&portal, "/portal").await;
assert!(html.contains("reports=false closed=false"), "{html}");
}
#[test]
fn a_page_slug_the_panel_routes_itself_does_not_build() {
struct LoginPage;
impl Page for LoginPage {
async fn render(cx: &Cx) -> Result<impl View> {
Ok(view! { cx => "login" })
}
}
let errors = refusal(mount_without_db(Panel::new("admin").page::<LoginPage>()));
assert_eq!(
errors[0],
DeclarationError::of::<LoginPage>(
Site::Registration,
DeclarationErrorKind::ReservedSlug {
slug: "login".to_string(),
},
)
);
}
#[tokio::test]
async fn without_a_home_page_the_root_redirects_to_the_first_resource() {
let router = mount(
db().await,
Panel::new("admin")
.auth(crate::Auth::disabled())
.resource::<DummyResource>()
.page::<ReportsPage>(),
)
.expect("the panel builds");
let request = http::Request::builder()
.uri("/admin")
.body(Body::empty())
.unwrap();
let response = router.handle(request).await;
assert_eq!(response.status(), http::StatusCode::TEMPORARY_REDIRECT);
assert_eq!(
response.headers().get(http::header::LOCATION).unwrap(),
"/admin/dummies"
);
assert!(
response
.headers()
.get(http::header::CONTENT_SECURITY_POLICY)
.and_then(|value| value.to_str().ok())
.is_some_and(|policy| policy.contains("frame-ancestors")),
"the root redirect must carry the directive"
);
}
#[tokio::test]
async fn a_page_redirects_to_login_without_a_resolved_user() {
use topcoat::{context::CxTestBuilder, router::response::IntoResponse, view::ViewExt};
let (parts, ()) = http::Request::builder()
.uri("/admin/reports")
.body(())
.unwrap()
.into_parts();
let cx = CxTestBuilder::new()
.request_context(parts)
.request_context(crate::panel::test_support::current_panel(
crate::panel::test_support::panel_state("/admin", crate::Auth::password()),
))
.build();
let Err(error) = super::page_handler::<ReportsPage>(&cx, Body::empty())
.single()
.await
else {
panic!("a page must not render without a resolved user");
};
let response = error.into_response(&cx).expect("the gate redirect renders");
let location = response
.headers()
.get(http::header::LOCATION)
.and_then(|value| value.to_str().ok())
.unwrap_or_default();
assert!(
location.starts_with("/admin/login"),
"an anonymous page request redirects to login, got {location}"
);
}
#[tokio::test]
async fn the_sidebar_lists_only_what_the_user_may_open() {
struct SecretPage;
impl Page for SecretPage {
fn declare() -> PageDef {
PageDef::new().access(|_| false)
}
async fn render(cx: &Cx) -> Result<impl View> {
Ok(view! { cx => <h1>"Secret"</h1> })
}
}
struct OpenResource;
impl Resource for OpenResource {
type Model = Dummy;
type Form = crate::NoForm<Self::Model>;
fn declare() -> ResourceDef<Self> {
ResourceDef::new()
.slug("open")
.policy(crate::policy::ReadOnly)
.table(dummy_table())
}
}
let router = mount(
db().await,
Panel::new("admin")
.auth(crate::Auth::disabled())
.home::<Dashboard>()
.resource::<OpenResource>()
.resource::<DummyResource>()
.page::<ReportsPage>()
.page::<SecretPage>(),
)
.expect("the panel builds");
let (status, _) = get(&router, "/admin/secret").await;
assert_eq!(status, http::StatusCode::FORBIDDEN);
let (status, html) = get(&router, "/admin").await;
assert_eq!(status, http::StatusCode::OK);
for (href, listed) in [
("/admin/reports", true),
("/admin/open", true),
("/admin/secret", false),
("/admin/dummies", false),
] {
assert_eq!(
html.contains(&format!("href=\"{href}\"")),
listed,
"{href} listed: {listed}, got {html}"
);
}
}