use lettermint::api::email::*;
use lettermint::reqwest::{LettermintClient, LettermintClientError};
use lettermint::testing::emails::{self, Scenario};
use lettermint::{Query, QueryError};
type Result = std::result::Result<(), Box<dyn std::error::Error>>;
fn client() -> LettermintClient {
let token = std::env::var("LETTERMINT_API_TOKEN").expect("LETTERMINT_API_TOKEN must be set");
LettermintClient::new(token)
}
fn sender() -> String {
std::env::var("LETTERMINT_SENDER").expect("LETTERMINT_SENDER must be set")
}
fn format_api_error(err: &QueryError<LettermintClientError>) -> String {
match err {
QueryError::Validation {
message, errors, ..
} => {
let mut msg = "Validation error".to_string();
if let Some(m) = message {
msg.push_str(&format!(": {m}"));
}
if let Some(errs) = errors {
for (field, msgs) in errs {
for m in msgs {
msg.push_str(&format!("\n {field}: {m}"));
}
}
}
msg
}
QueryError::Authentication { message, .. } => {
format!("Authentication error: {message:?}")
}
QueryError::RateLimit { message, .. } => {
format!("Rate limit: {message:?}")
}
QueryError::Api {
status, message, ..
} => {
let mut msg = format!("API {status}");
if let Some(m) = message {
msg.push_str(&format!(": {m}"));
}
msg
}
other => format!("{other}"),
}
}
#[tokio::test]
#[ignore]
async fn send_from_unverified_domain_returns_validation_error() -> Result {
let err = SendEmailRequest::builder()
.from("test@unverified-domain-that-does-not-exist.example")
.to(vec![Scenario::Ok.email()])
.subject("Integration test: unverified domain")
.text("This should fail with a validation error.")
.build()
.execute(&client())
.await
.expect_err("should fail with unverified domain");
match &err {
QueryError::Validation { errors, .. } => {
assert!(errors.is_some(), "expected per-field validation errors");
let errs = errors.as_ref().unwrap();
assert!(errs.contains_key("from"), "expected error on 'from' field");
}
_ => return Err(format!("expected Validation error, got: {err:?}").into()),
}
Ok(())
}
#[tokio::test]
#[ignore]
async fn send_text_email_ok() -> Result {
let resp = SendEmailRequest::builder()
.from(sender())
.to(vec![Scenario::Ok.email()])
.subject("Integration test: text")
.text("This is a plain text integration test.")
.build()
.execute(&client())
.await
.map_err(|e| format_api_error(&e))?;
assert!(!resp.message_id.is_empty());
Ok(())
}
#[tokio::test]
#[ignore]
async fn send_html_email_ok() -> Result {
let resp = SendEmailRequest::builder()
.from(sender())
.to(vec![Scenario::Ok.email()])
.subject("Integration test: html")
.html("<h1>Hello</h1><p>HTML integration test.</p>")
.build()
.execute(&client())
.await
.map_err(|e| format_api_error(&e))?;
assert!(!resp.message_id.is_empty());
Ok(())
}
#[tokio::test]
#[ignore]
async fn send_html_and_text_email_ok() -> Result {
let resp = SendEmailRequest::builder()
.from(sender())
.to(vec![Scenario::Ok.email()])
.subject("Integration test: html+text")
.html("<h1>Hello</h1>")
.text("Hello")
.build()
.execute(&client())
.await
.map_err(|e| format_api_error(&e))?;
assert!(!resp.message_id.is_empty());
Ok(())
}
#[tokio::test]
#[ignore]
async fn send_with_all_options() -> Result {
let from = sender();
let resp = SendEmailRequest::builder()
.from(from.clone())
.to(vec![Scenario::Ok.email()])
.subject("Integration test: full options")
.html("<h1>Full test</h1>")
.text("Full test")
.cc(vec![emails::custom("ok+cc")])
.reply_to(vec![from])
.tag("integration-test")
.metadata(std::collections::HashMap::from([(
"test".to_string(),
"true".to_string(),
)]))
.idempotency_key(format!(
"integration-test-{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis()
))
.build()
.execute(&client())
.await
.map_err(|e| format_api_error(&e))?;
assert!(!resp.message_id.is_empty());
Ok(())
}
#[tokio::test]
#[ignore]
async fn send_with_attachment() -> Result {
use base64::Engine;
let content = base64::engine::general_purpose::STANDARD.encode(b"Hello from integration test");
let resp = SendEmailRequest::builder()
.from(sender())
.to(vec![Scenario::Ok.email()])
.subject("Integration test: attachment")
.text("See attached file.")
.attachments(vec![Attachment::new("test.txt", content)])
.build()
.execute(&client())
.await
.map_err(|e| format_api_error(&e))?;
assert!(!resp.message_id.is_empty());
Ok(())
}
#[tokio::test]
#[ignore]
async fn send_to_random_soft_bounce() -> Result {
let resp = SendEmailRequest::builder()
.from(sender())
.to(vec![Scenario::SoftBounce.random()])
.subject("Integration test: random soft bounce")
.text("This should soft bounce with a unique address.")
.build()
.execute(&client())
.await
.map_err(|e| format_api_error(&e))?;
assert!(!resp.message_id.is_empty());
Ok(())
}
#[tokio::test]
#[ignore]
async fn send_to_soft_bounce() -> Result {
let resp = SendEmailRequest::builder()
.from(sender())
.to(vec![Scenario::SoftBounce.email()])
.subject("Integration test: soft bounce")
.text("This should soft bounce.")
.build()
.execute(&client())
.await
.map_err(|e| format_api_error(&e))?;
assert!(!resp.message_id.is_empty());
Ok(())
}
#[tokio::test]
#[ignore]
async fn send_to_hard_bounce() -> Result {
let resp = SendEmailRequest::builder()
.from(sender())
.to(vec![Scenario::HardBounce.email()])
.subject("Integration test: hard bounce")
.text("This should hard bounce.")
.build()
.execute(&client())
.await
.map_err(|e| format_api_error(&e))?;
assert!(!resp.message_id.is_empty());
Ok(())
}
#[tokio::test]
#[ignore]
async fn batch_send_ok() -> Result {
let from = sender();
let batch = BatchSendRequest::new(vec![
SendEmailRequest::builder()
.from(from.clone())
.to(vec![Scenario::Ok.email()])
.subject("Integration test: batch 1/2")
.text("First email in batch.")
.build(),
SendEmailRequest::builder()
.from(from)
.to(vec![Scenario::Ok.email()])
.subject("Integration test: batch 2/2")
.text("Second email in batch.")
.build(),
])
.expect("batch should be valid");
let responses = batch
.execute(&client())
.await
.map_err(|e| format_api_error(&e))?;
assert_eq!(responses.len(), 2);
for resp in &responses {
assert!(!resp.message_id.is_empty());
}
Ok(())
}
#[tokio::test]
#[ignore]
async fn batch_send_with_idempotency_key() -> Result {
let from = sender();
let key = format!(
"batch-integration-{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis()
);
let batch = BatchSendRequest::new(vec![
SendEmailRequest::builder()
.from(from)
.to(vec![Scenario::Ok.email()])
.subject("Integration test: batch idempotency")
.text("Batch with idempotency key.")
.build(),
])
.expect("batch should be valid")
.with_idempotency_key(key);
let responses = batch
.execute(&client())
.await
.map_err(|e| format_api_error(&e))?;
assert_eq!(responses.len(), 1);
assert!(!responses[0].message_id.is_empty());
Ok(())
}