From ddf155b9019cf842bb246f812da2bbed42bf5291 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 23 Sep 2021 23:51:50 +0200 Subject: [PATCH] WIP: generate sample template contexts for testing --- crates/core/src/storage/user.rs | 11 ++ crates/core/src/templates/context.rs | 147 +++++++++++++++++++++++++-- crates/core/src/templates/macros.rs | 32 ++++++ crates/core/src/templates/mod.rs | 12 +++ 4 files changed, 192 insertions(+), 10 deletions(-) diff --git a/crates/core/src/storage/user.rs b/crates/core/src/storage/user.rs index b35414f7..35fae3a4 100644 --- a/crates/core/src/storage/user.rs +++ b/crates/core/src/storage/user.rs @@ -69,6 +69,17 @@ impl SessionInfo { self.active = false; Ok(self) } + + pub(crate) fn samples() -> Vec { + vec![SessionInfo { + id: 1, + user_id: 2, + username: "john".to_string(), + active: true, + created_at: Utc::now(), + last_authd_at: Some(Utc::now()), + }] + } } #[derive(Debug, Error)] diff --git a/crates/core/src/templates/context.rs b/crates/core/src/templates/context.rs index 5dd5ef74..a9623f82 100644 --- a/crates/core/src/templates/context.rs +++ b/crates/core/src/templates/context.rs @@ -15,7 +15,7 @@ //! Contexts used in templates use oauth2_types::errors::OAuth2Error; -use serde::Serialize; +use serde::{ser::SerializeStruct, Serialize}; use url::Url; use crate::{errors::ErroredForm, filters::CsrfToken, storage::SessionInfo}; @@ -51,15 +51,11 @@ pub trait TemplateContext { inner: self, } } -} -impl TemplateContext for EmptyContext {} -impl TemplateContext for IndexContext {} -impl TemplateContext for LoginContext {} -impl TemplateContext for FormPostContext {} -impl TemplateContext for WithSession {} -impl TemplateContext for WithOptionalSession {} -impl TemplateContext for WithCsrf {} + fn sample() -> Vec + where + Self: Sized; +} /// Context with a CSRF token in it #[derive(Serialize)] @@ -70,6 +66,21 @@ pub struct WithCsrf { inner: T, } +impl TemplateContext for WithCsrf { + fn sample() -> Vec + where + Self: Sized, + { + T::sample() + .into_iter() + .map(|inner| WithCsrf { + csrf_token: "fake_csrf_token".into(), + inner, + }) + .collect() + } +} + /// Context with a user session in it #[derive(Serialize)] pub struct WithSession { @@ -79,6 +90,23 @@ pub struct WithSession { inner: T, } +impl TemplateContext for WithSession { + fn sample() -> Vec + where + Self: Sized, + { + SessionInfo::samples() + .into_iter() + .flat_map(|session| { + T::sample().into_iter().map(move |inner| WithSession { + current_session: session.clone(), + inner, + }) + }) + .collect() + } +} + /// Context with an optional user session in it #[derive(Serialize)] pub struct WithOptionalSession { @@ -88,10 +116,52 @@ pub struct WithOptionalSession { inner: T, } +impl TemplateContext for WithOptionalSession { + fn sample() -> Vec + where + Self: Sized, + { + SessionInfo::samples() + .into_iter() + .map(Some) // Wrap all samples in an Option + .chain(std::iter::once(None)) // Add the "None" option + .flat_map(|session| { + T::sample() + .into_iter() + .map(move |inner| WithOptionalSession { + current_session: session.clone(), + inner, + }) + }) + .collect() + } +} + /// An empty context used for composition -#[derive(Serialize)] pub struct EmptyContext; +impl Serialize for EmptyContext { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut s = serializer.serialize_struct("EmptyContext", 0)?; + // FIXME: for some reason, serde seems to not like struct flattening with empty + // stuff + s.serialize_field("__UNUSED", &())?; + s.end() + } +} + +impl TemplateContext for EmptyContext { + fn sample() -> Vec + where + Self: Sized, + { + vec![EmptyContext] + } +} + /// Context used by the `index.html` template #[derive(Serialize)] pub struct IndexContext { @@ -105,6 +175,19 @@ impl IndexContext { } } +impl TemplateContext for IndexContext { + fn sample() -> Vec + where + Self: Sized, + { + vec![Self { + discovery_url: "https://example.com/.well-known/openid-configuration" + .parse() + .unwrap(), + }] + } +} + #[derive(Serialize, Debug, Clone, Copy, Hash, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub enum LoginFormField { @@ -118,6 +201,18 @@ pub struct LoginContext { form: ErroredForm, } +impl TemplateContext for LoginContext { + fn sample() -> Vec + where + Self: Sized, + { + // TODO: samples with errors + vec![LoginContext { + form: ErroredForm::default(), + }] + } +} + impl LoginContext { #[must_use] pub fn with_form_error(form: ErroredForm) -> Self { @@ -140,6 +235,22 @@ pub struct FormPostContext { params: T, } +impl TemplateContext for FormPostContext { + fn sample() -> Vec + where + Self: Sized, + { + let sample_params = T::sample(); + sample_params + .into_iter() + .map(|params| FormPostContext { + redirect_uri: "https://example.com/callback".parse().unwrap(), + params, + }) + .collect() + } +} + impl FormPostContext { pub fn new(redirect_uri: Url, params: T) -> Self { Self { @@ -157,6 +268,22 @@ pub struct ErrorContext { details: Option, } +impl TemplateContext for ErrorContext { + fn sample() -> Vec + where + Self: Sized, + { + vec![ + Self::new() + .with_code("sample_error") + .with_description("A fancy description".into()) + .with_details("Something happened".into()), + Self::new().with_code("another_error"), + Self::new(), + ] + } +} + impl ErrorContext { #[must_use] pub fn new() -> Self { diff --git a/crates/core/src/templates/macros.rs b/crates/core/src/templates/macros.rs index 0905ca53..c39ec440 100644 --- a/crates/core/src/templates/macros.rs +++ b/crates/core/src/templates/macros.rs @@ -32,6 +32,10 @@ macro_rules! register_templates { extra = { $( $extra_template:expr ),* }; )? + $( + generics = { $( $generic:ident = $sample:ty ),* }; + )? + $( // Match any attribute on the function, such as #[doc], #[allow(dead_code)], etc. $( #[ $attr:meta ] )* @@ -72,6 +76,34 @@ macro_rules! register_templates { .map_err(|source| TemplateError::Render { template: $template, source }) } )* + + pub fn check_render(&self) -> Result<(), TemplateError> { + self.check_render_inner $( ::< $( $sample ),+ > )? () + } + + fn check_render_inner + $( < $( $generic ),+ > )? + (&self) -> Result<(), TemplateError> + $( where + $( $generic : TemplateContext + Serialize, )+ + )? + + { + $( + { + let samples: Vec< $param > = TemplateContext::sample(); + + let name = $template; + for sample in samples { + ::tracing::info!(name, "Rendering template"); + self. $name (&sample)?; + } + } + )* + + + Ok(()) + } } }; } diff --git a/crates/core/src/templates/mod.rs b/crates/core/src/templates/mod.rs index 48dfbe7d..bf485b14 100644 --- a/crates/core/src/templates/mod.rs +++ b/crates/core/src/templates/mod.rs @@ -167,6 +167,7 @@ impl Reject for TemplateError {} register_templates! { extra = { "base.html" }; + generics = { T = EmptyContext }; /// Render the login page pub fn render_login(WithCsrf) { "login.html" } @@ -186,3 +187,14 @@ register_templates! { /// Render the HTML error page pub fn render_error(ErrorContext) { "error.html" } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn check_all_templates() { + let templates = Templates::load(None, true).unwrap(); + templates.check_render().unwrap(); + } +}