Skip to content

Commit

Permalink
test: Snapshot test HTML and text generation
Browse files Browse the repository at this point in the history
This commit also decouples the HTML and text functions from Axum
to ease testing and reduce duplication.
  • Loading branch information
JadedBlueEyes committed May 17, 2024
1 parent 7f3e713 commit 2c7f204
Show file tree
Hide file tree
Showing 11 changed files with 177 additions and 73 deletions.
17 changes: 17 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
24 changes: 24 additions & 0 deletions fixtures/basic.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<!doctype html><html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office"><head><title></title><!--[if !mso]><!--><meta http-equiv="X-UA-Compatible" content="IE=edge"><!--<![endif]--><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
#outlook a { padding: 0; }
body { margin: 0; padding: 0; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
table, td { border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
img { border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; }
p { display: block; margin: 13px 0; }
</style>
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix { width:100% !important; }
</style>
<![endif]-->
<!--[if !mso]><!--><link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css"><style type="text/css">@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);</style><!--<![endif]--></head><body style="word-spacing:normal;"><div><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;"><tbody><tr><td align="center" bgcolor="#414141" role="presentation" valign="middle" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#414141;"><p style="display:inline-block;background:#414141;color:#ffffff;font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;">Hello world!</p></td></tr></tbody></table></div></body></html>
3 changes: 3 additions & 0 deletions fixtures/basic.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
────────────
Hello world!
────────────
24 changes: 24 additions & 0 deletions fixtures/include.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<!doctype html><html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office"><head><title></title><!--[if !mso]><!--><meta http-equiv="X-UA-Compatible" content="IE=edge"><!--<![endif]--><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
#outlook a { padding: 0; }
body { margin: 0; padding: 0; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
table, td { border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
img { border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; }
p { display: block; margin: 13px 0; }
</style>
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix { width:100% !important; }
</style>
<![endif]-->
<!--[if !mso]><!--><link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css"><style type="text/css">@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);</style><!--<![endif]--></head><body style="word-spacing:normal;"><div><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;"><tbody><tr><td align="center" bgcolor="#414141" role="presentation" valign="middle" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#414141;"><p style="display:inline-block;background:#414141;color:#ffffff;font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;">Hello includes!</p></td></tr></tbody></table></div></body></html>
3 changes: 3 additions & 0 deletions fixtures/include.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
───────────────
Hello includes!
───────────────
161 changes: 94 additions & 67 deletions src/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, EngineError> {
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,
Expand All @@ -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<String>) -> Result<Response, EngineError> {
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<String>) -> Result<Response, EngineError> {
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<String, EngineError> {
let content = render_html(template_id).await?;

let text = html2text::config::plain().string_from_read(content.as_bytes(), 50)?;
Ok(text)
}

#[utoipa::path(
Expand All @@ -86,54 +95,72 @@ pub async fn render_html(Path(template_id): Path<String>) -> Result<Response, En
("template_id" = String, Path, description = "Template to render"),
)
)]
pub async fn render_text(Path(template_id): Path<String>) -> Result<Response, EngineError> {
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<String>) -> Result<Response, EngineError> {
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<dyn std::error::Error>> {
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::<TemplateFiles>()
.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<dyn std::error::Error>> {
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::<crate::render::TemplateFiles>()
.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)
// }
}
8 changes: 4 additions & 4 deletions src/serve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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() {
Expand All @@ -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(),
Expand Down
6 changes: 5 additions & 1 deletion templates/basic.mjml
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
<mj-button>Hello</mj-button>
<mjml>
<mj-body>
<mj-button>Hello world!</mj-button>
</mj-body>
</mjml>
2 changes: 1 addition & 1 deletion templates/include.mjml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<mjml>
<mj-body>
<mj-include path="basic.mjml" />
<mj-include path="include_snippet.mjml" />
</mj-body>
</mjml>
1 change: 1 addition & 0 deletions templates/include_snippet.mjml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<mj-button>Hello includes!</mj-button>

0 comments on commit 2c7f204

Please sign in to comment.