From 2c7f204dd13e347e27ce0aadafe47d664ff46c3f Mon Sep 17 00:00:00 2001 From: Jade Ellis Date: Fri, 17 May 2024 15:35:34 +0100 Subject: [PATCH] test: Snapshot test HTML and text generation This commit also decouples the HTML and text functions from Axum to ease testing and reduce duplication. --- Cargo.lock | 17 ++++ Cargo.toml | 1 + fixtures/basic.html | 24 +++++ fixtures/basic.txt | 3 + fixtures/include.html | 24 +++++ fixtures/include.txt | 3 + src/render.rs | 161 +++++++++++++++++++-------------- src/serve.rs | 8 +- templates/basic.mjml | 6 +- templates/include.mjml | 2 +- templates/include_snippet.mjml | 1 + 11 files changed, 177 insertions(+), 73 deletions(-) create mode 100644 fixtures/basic.html create mode 100644 fixtures/basic.txt create mode 100644 fixtures/include.html create mode 100644 fixtures/include.txt create mode 100644 templates/include_snippet.mjml diff --git a/Cargo.lock b/Cargo.lock index 60d85ee..4bd51b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -264,6 +264,12 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dissimilar" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59f8e79d1fbf76bdfbde321e902714bf6c49df88a7dda6fc682fc2979226962d" + [[package]] name = "either" version = "1.12.0" @@ -276,6 +282,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "expect-test" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e0be0a561335815e06dab7c62e50353134c796e7a6155402a64bcff66b6a5e0" +dependencies = [ + "dissimilar", + "once_cell", +] + [[package]] name = "flate2" version = "1.0.30" @@ -636,6 +652,7 @@ name = "mb-mail-service" version = "0.1.0" dependencies = [ "axum", + "expect-test", "handlebars", "html2text", "listenfd", diff --git a/Cargo.toml b/Cargo.toml index 368c6dd..e866f19 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] axum = { version = "0.7.5", features = ["tracing", "macros"] } +expect-test = "1.5.0" handlebars = { version = "5.1.2", features = ["rust-embed"] } html2text = "0.12.4" listenfd = "1.0.1" diff --git a/fixtures/basic.html b/fixtures/basic.html new file mode 100644 index 0000000..b02ebde --- /dev/null +++ b/fixtures/basic.html @@ -0,0 +1,24 @@ + + + + +

Hello world!

\ No newline at end of file diff --git a/fixtures/basic.txt b/fixtures/basic.txt new file mode 100644 index 0000000..89cff2d --- /dev/null +++ b/fixtures/basic.txt @@ -0,0 +1,3 @@ +──────────── +Hello world! +──────────── diff --git a/fixtures/include.html b/fixtures/include.html new file mode 100644 index 0000000..701e210 --- /dev/null +++ b/fixtures/include.html @@ -0,0 +1,24 @@ + + + + +

Hello includes!

\ No newline at end of file diff --git a/fixtures/include.txt b/fixtures/include.txt new file mode 100644 index 0000000..039a73e --- /dev/null +++ b/fixtures/include.txt @@ -0,0 +1,3 @@ +─────────────── +Hello includes! +─────────────── diff --git a/src/render.rs b/src/render.rs index 4a9acff..6451ca2 100644 --- a/src/render.rs +++ b/src/render.rs @@ -30,21 +30,35 @@ pub(crate) enum EngineError { Parse(#[from] mrml::prelude::parser::Error), #[error("Failed to render template: {0}")] Render(#[from] mrml::prelude::render::Error), + #[error("Template not found: {0}")] + TemplateNotFound(String), + #[error("Failed to convert HTML to text: {0}")] + FailedTextConversion(#[from] html2text::Error), } impl IntoResponse for EngineError { fn into_response(self) -> axum::response::Response { + tracing::error!("{self}: {self:?}"); match self { - Self::Parse(ref inner) => tracing::error!("Unable to parse template: {inner:?}"), - Self::Render(ref inner) => tracing::error!("Unable to render template: {inner:?}"), - }; - ( - axum::http::StatusCode::INTERNAL_SERVER_ERROR, - format!("{self}"), - ) - .into_response() + EngineError::TemplateNotFound(_) => (StatusCode::NOT_FOUND, format!("{self}")), + _ => (StatusCode::INTERNAL_SERVER_ERROR, format!("{self}")), + } + .into_response() } } +pub async fn render_html(template_id: String) -> Result { + let path = template_id + ".mjml"; + let template = TemplateFiles::get(&path) + .map(|f| String::from_utf8(f.data.to_vec()).expect("Template was not valid UTF-8")) + .ok_or(EngineError::TemplateNotFound(path))?; + let opts = ParserOptions { + include_loader: Box::new(TemplateFiles), + }; + let root = mrml::parse_with_options(template, &opts)?; + let opts = mrml::prelude::render::RenderOptions::default(); + let content = root.render(&opts)?; + Ok(content) +} #[utoipa::path( get, @@ -57,22 +71,17 @@ impl IntoResponse for EngineError { ("template_id" = String, Path, description = "Template to render"), ) )] -pub async fn render_html(Path(template_id): Path) -> Result { - let path = template_id + ".mjml"; - if let Some(template) = TemplateFiles::get(&path) - .map(|f| String::from_utf8(f.data.to_vec()).expect("Template was not valid UTF-8")) - { - let opts = ParserOptions { - include_loader: Box::new(TemplateFiles), - }; - let root = mrml::parse_with_options(template, &opts)?; - let opts = mrml::prelude::render::RenderOptions::default(); - let content = root.render(&opts)?; - - Ok(([(header::CONTENT_TYPE, "text/html")], content).into_response()) - } else { - Ok((StatusCode::NOT_FOUND, format!("Not Found: {}", path)).into_response()) - } +pub async fn render_html_route(Path(template_id): Path) -> Result { + let content = render_html(template_id).await?; + + Ok(([(header::CONTENT_TYPE, "text/html")], content).into_response()) +} + +pub async fn render_text(template_id: String) -> Result { + let content = render_html(template_id).await?; + + let text = html2text::config::plain().string_from_read(content.as_bytes(), 50)?; + Ok(text) } #[utoipa::path( @@ -86,54 +95,72 @@ pub async fn render_html(Path(template_id): Path) -> Result) -> Result { - let path = template_id + ".mjml"; - if let Some(template) = TemplateFiles::get(&path) - .map(|f| String::from_utf8(f.data.to_vec()).expect("Template was not valid UTF-8")) - { - let opts = ParserOptions { - include_loader: Box::new(TemplateFiles), - }; - let root = mrml::parse_with_options(template, &opts)?; - let opts = mrml::prelude::render::RenderOptions::default(); - let content = root.render(&opts)?; - - Ok(( - [(header::CONTENT_TYPE, "text/plain; charset=UTF-8")], - html2text::config::plain() - .string_from_read(content.as_bytes(), 50) - .expect("Failed to convert to HTML"), - ) - .into_response()) - } else { - Ok((StatusCode::NOT_FOUND, format!("Not Found: {}", path)).into_response()) - } +pub async fn render_text_route(Path(template_id): Path) -> Result { + let content = render_text(template_id).await?; + + Ok(( + [(header::CONTENT_TYPE, "text/plain; charset=UTF-8")], + content, + ) + .into_response()) } +#[cfg(test)] +mod test { + use expect_test::expect_file; -#[test] -fn render() -> Result<(), Box> { - use handlebars::Handlebars; - use serde_json::Map; - use std::fs::File; + #[tokio::test] + async fn basic_template_html() { + let res: String = super::render_html("basic".to_string()).await.unwrap(); + let expected = expect_file!["../fixtures/basic.html"]; + expected.assert_eq(&res); + } - let mut handlebars = Handlebars::new(); + #[tokio::test] + async fn include_template_html() { + let res = super::render_html("include".to_string()).await.unwrap(); + let expected = expect_file!["../fixtures/include.html"]; + expected.assert_eq(&res); + } - handlebars - .register_embed_templates::() - .unwrap(); + #[tokio::test] + async fn basic_template_text() { + let res: String = super::render_text("basic".to_string()).await.unwrap(); + let expected = expect_file!["../fixtures/basic.txt"]; + expected.assert_eq(&res); + } - println!("Loaded templates"); + #[tokio::test] + async fn include_template_text() { + let res = super::render_text("include".to_string()).await.unwrap(); + let expected = expect_file!["../fixtures/include.txt"]; + expected.assert_eq(&res); + } - let mut data = Map::new(); - data.insert("bin_name".into(), env!("CARGO_BIN_NAME").into()); - let mut output_file = File::create("target/test.html")?; - handlebars.render_to_write("test.hbs", &data, &mut output_file)?; - println!("target/test.html generated"); + #[test] + fn render() -> Result<(), Box> { + use handlebars::Handlebars; + use serde_json::Map; + use std::fs::File; - Ok(()) -} + let mut handlebars = Handlebars::new(); -// TODO: Iterate over and prerender all the templates? -// fn render_mrml() { -// TemplateFiles::iter().map(f) -// } + handlebars + .register_embed_templates::() + .unwrap(); + + println!("Loaded templates"); + + let mut data = Map::new(); + data.insert("bin_name".into(), env!("CARGO_BIN_NAME").into()); + let mut output_file = File::create("target/test.html")?; + handlebars.render_to_write("test.hbs", &data, &mut output_file)?; + println!("target/test.html generated"); + + Ok(()) + } + + // TODO: Iterate over and prerender all the templates? + // fn render_mrml() { + // TemplateFiles::iter().map(f) + // } +} diff --git a/src/serve.rs b/src/serve.rs index ee5a46a..6095081 100644 --- a/src/serve.rs +++ b/src/serve.rs @@ -4,7 +4,7 @@ use utoipa_swagger_ui::SwaggerUi; #[derive(OpenApi)] #[openapi( paths( - crate::render::render_html, crate::render::render_text + crate::render::render_html_route, crate::render::render_text_route ), tags( (name = "mb-mail-service", description = "MusicBrains Mail Service API") @@ -21,7 +21,7 @@ use std::{ }; use tokio::{net::TcpListener, signal}; -use crate::render::{render_html, render_text}; +use crate::render::{render_html_route, render_text_route}; use axum::routing::get; pub(crate) async fn serve() { @@ -42,8 +42,8 @@ pub(crate) async fn serve() { // OpenAPI docs .merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", ApiDoc::openapi())) // Our routes - .route("/templates/:template_id/html", get(render_html)) - .route("/templates/:template_id/text", get(render_text)) + .route("/templates/:template_id/html", get(render_html_route)) + .route("/templates/:template_id/text", get(render_text_route)) .layer(( // Logging TraceLayer::new_for_http(), diff --git a/templates/basic.mjml b/templates/basic.mjml index 531acd0..a936c9c 100644 --- a/templates/basic.mjml +++ b/templates/basic.mjml @@ -1 +1,5 @@ -Hello \ No newline at end of file + + + Hello world! + + \ No newline at end of file diff --git a/templates/include.mjml b/templates/include.mjml index 03736e8..d544638 100644 --- a/templates/include.mjml +++ b/templates/include.mjml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/templates/include_snippet.mjml b/templates/include_snippet.mjml new file mode 100644 index 0000000..761301d --- /dev/null +++ b/templates/include_snippet.mjml @@ -0,0 +1 @@ +Hello includes! \ No newline at end of file