//! `autumn generate inbound_mail` — scaffold an inbound mail handler, test
//! fixture, and integration test.
//!
//! For a name like `Replies`, the generator produces:
//! - `src/inbound_mailers/replies.rs` — `RepliesMailHandler` using `#[inbound_mail]`.
//! - `src/inbound_mailers/mod.rs` — created/updated with `pub mod replies;`.
//! - `tests/replies_inbound_mail.rs` — integration test posting a captured
//! Mailgun fixture payload and asserting the handler is invoked.
//! - `src/main.rs` — `mod inbound_mailers;` declaration and
//! `.inbound_mail_router(…)` wired into the app builder chain.
//! - `Cargo.toml` — `"inbound-mailgun"` feature added to the `autumn-web` dependency.
use std::path::Path;
use super::emit::Plan;
use super::model::validate_resource_name;
use super::naming::{pascal, snake};
use super::schema_edit::{add_mod_declaration, ensure_autumn_web_feature, update_main_rs};
use super::{GenerateError, ensure_project_root, read_or_empty};
/// Compute the file actions for `autumn generate inbound_mail <name>`.
///
/// # Errors
///
/// Returns [`GenerateError`] when the project layout or name is invalid.
pub fn plan_inbound_mail(project_root: &Path, name: &str) -> Result<Plan, GenerateError> {
ensure_project_root(project_root)?;
validate_resource_name(name)?;
let snake_name = snake(name);
let pascal_name = pascal(name);
let struct_name = format!("{pascal_name}MailHandler");
let handler_fn = snake_name.clone();
let info_fn = format!("{snake_name}_handler_info");
let module_path = format!("inbound_mailers::{snake_name}::{info_fn}");
let mut plan = Plan::new(project_root);
// ── src/inbound_mailers/<snake>.rs ─────────────────────────────────────
plan.create(
project_root
.join("src")
.join("inbound_mailers")
.join(format!("{snake_name}.rs")),
render_handler_file(&struct_name, &handler_fn, &snake_name),
);
// ── src/inbound_mailers/mod.rs ─────────────────────────────────────────
let mod_path = project_root
.join("src")
.join("inbound_mailers")
.join("mod.rs");
plan.modify(
mod_path.clone(),
add_mod_declaration(&read_or_empty(&mod_path), &snake_name),
);
plan.push_revert(crate::generate::emit::Revert::ModDecl {
path: mod_path,
name: snake_name.clone(),
});
// ── tests/<snake>_inbound_mail.rs ──────────────────────────────────────
plan.create(
project_root
.join("tests")
.join(format!("{snake_name}_inbound_mail.rs")),
render_integration_test(&struct_name, &snake_name, &handler_fn),
);
// ── src/main.rs — add mod inbound_mailers and .inbound_mail_router(…) ─
let main_path = project_root.join("src").join("main.rs");
let main_existing = std::fs::read_to_string(&main_path).map_err(|_| {
GenerateError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("missing {}", main_path.display()),
))
})?;
let with_mods = update_main_rs(&main_existing, &["inbound_mailers"], &[]);
let updated_main = add_inbound_mail_router_to_app(&with_mods, &module_path);
plan.modify(main_path.clone(), updated_main);
// `mod inbound_mailers;` is shared infrastructure (see
// `emit::SHARED_MAIN_MODULE_NAMES`) — only this handler's own router
// registration is reverted here.
plan.push_revert(crate::generate::emit::Revert::InboundMailHandler {
path: main_path,
handler_module_path: module_path,
});
// ── Cargo.toml — ensure autumn-web has the "inbound-mailgun" feature ───
let cargo_path = project_root.join("Cargo.toml");
let cargo_existing = read_or_empty(&cargo_path);
let updated_cargo = ensure_autumn_web_feature(&cargo_existing, "inbound-mailgun");
if updated_cargo != cargo_existing {
plan.modify(cargo_path.clone(), updated_cargo);
}
// Pushed unconditionally — see `plan_cargo_deps`'s matching comment in
// model.rs: destroy recomputes this plan against the already-generated
// Cargo.toml, where the feature is by definition already present.
plan.push_revert(crate::generate::emit::Revert::CargoAutumnWebFeature {
path: cargo_path,
feature: "inbound-mailgun".to_owned(),
owner_dir: Some(project_root.join("src").join("inbound_mailers")),
});
Ok(plan)
}
fn render_handler_file(struct_name: &str, handler_fn: &str, _snake_name: &str) -> String {
format!(
r#"//! Generated by `autumn generate inbound_mail`.
//!
//! Edit freely — once generated this is ordinary user code.
//!
//! The `#[inbound_mail]` macro generates a `{handler_fn}_handler_info()` function
//! that returns an `InboundMailHandlerInfo` ready for registration.
use autumn_web::prelude::*;
use autumn_web::inbound_mail;
use autumn_web::inbound_mail::{{InboundEmail, InboundMailHandlerInfo}};
pub struct {struct_name};
/// Inbound mail handler.
///
/// The `to` attribute specifies the recipient pattern:
/// - Exact: `to = "support@company.com"`
/// - Plus-address: `to = "replies+{{token}}@company.com"` (token in `email.plus_token()`)
/// - Prefix: `to = "ticket+*"` (any ticket+… address)
#[inbound_mail(to = "*", processing = "background")]
async fn {handler_fn}(email: InboundEmail) -> AutumnResult<()> {{
tracing::info!(
from = %email.from,
subject = %email.subject,
attachments = email.attachments.len(),
"{struct_name}: inbound email received"
);
// TODO: implement handler logic
Ok(())
}}
#[cfg(test)]
mod tests {{
use super::*;
#[test]
fn handler_info_has_expected_name() {{
let info = {handler_fn}_handler_info();
assert_eq!(info.name, "{handler_fn}");
}}
}}
"#
)
}
fn render_integration_test(struct_name: &str, snake_name: &str, handler_fn: &str) -> String {
format!(
r#"//! Integration test for `{struct_name}` inbound mail handler.
//!
//! Generated by `autumn generate inbound_mail`. Exercises the full
//! request-to-handler pipeline using a Mailgun webhook fixture.
use std::sync::atomic::{{AtomicUsize, Ordering}};
use autumn_web::inbound_mail::{{
InboundEmail, InboundMailEndpointConfig, InboundMailHandlerInfo, InboundMailRouter,
ProcessingMode, RecipientPattern, compute_mailgun_signature,
}};
use autumn_web::prelude::*;
use autumn_web::test::TestApp;
static HANDLER_CALLS: AtomicUsize = AtomicUsize::new(0);
fn test_handler(
email: InboundEmail,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = autumn_web::AutumnResult<()>> + Send + 'static>,
> {{
HANDLER_CALLS.fetch_add(1, Ordering::SeqCst);
let _ = email;
Box::pin(async {{ Ok(()) }})
}}
#[get("/_ping")]
async fn ping() -> &'static str {{ "pong" }}
fn build_client(signing_key: &str) -> autumn_web::test::TestClient {{
let router = InboundMailRouter::new()
.endpoint(InboundMailEndpointConfig::mailgun("/inbound/mailgun", signing_key))
.handler(InboundMailHandlerInfo {{
name: "{handler_fn}",
pattern: RecipientPattern::Any,
processing: ProcessingMode::Sync,
handler: test_handler,
}});
TestApp::new()
.inbound_mail_router(router)
.routes(routes![ping])
.build()
}}
/// Captured Mailgun fixture — replace with a real payload from your provider
/// dashboard before shipping.
const FIXTURE_PAYLOAD: &[(&str, &str)] = &[
("from", "sender@example.com"),
("to", "inbox@company.com"),
("subject", "Test-email"),
("body-plain", "Hello-from-integration-test"),
];
#[tokio::test]
async fn {snake_name}_handler_processes_valid_mailgun_webhook() {{
HANDLER_CALLS.store(0, Ordering::SeqCst);
let key = "test-mailgun-signing-key";
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
.to_string();
let token = "integration-test-token";
let sig = compute_mailgun_signature(&ts, token, key);
let mut body_parts: Vec<String> = vec![
format!("timestamp={{ts}}"),
format!("token={{token}}"),
format!("signature={{sig}}"),
];
for (k, v) in FIXTURE_PAYLOAD {{
body_parts.push(format!("{{k}}={{v}}"));
}}
let body = body_parts.join("&");
build_client(key)
.post("/inbound/mailgun")
.form(&body)
.send()
.await
.assert_status(200);
assert_eq!(
HANDLER_CALLS.load(Ordering::SeqCst),
1,
"handler must have been invoked exactly once"
);
}}
"#
)
}
/// Insert an `.inbound_mail_router(...)` call with a Mailgun endpoint into
/// the app builder chain in `src/main.rs`. Idempotent.
fn add_inbound_mail_router_to_app(src: &str, handler_module_path: &str) -> String {
let snippet = format!(
".inbound_mail_router(\
::autumn_web::inbound_mail::InboundMailRouter::new()\
.endpoint(::autumn_web::inbound_mail::InboundMailEndpointConfig::mailgun(\
\"/inbound/mailgun\",\
std::env::var(\"MAILGUN_SIGNING_KEY\").unwrap_or_default(),\
))\
.handler({handler_module_path}())\
)"
);
// If a router call already exists, chain the new handler onto it instead of inserting
// a second router. Skip if this specific handler is already registered.
if let Some(router_pos) = src.find(".inbound_mail_router(") {
if src.contains(handler_module_path) {
return src.to_owned();
}
// Find the closing paren of the .inbound_mail_router(...) argument.
let start = router_pos + ".inbound_mail_router(".len();
let bytes = src.as_bytes();
let mut depth = 1usize;
let mut i = start;
while i < bytes.len() {
match bytes[i] {
b'(' => depth += 1,
b')' => {
depth -= 1;
if depth == 0 {
// Insert before the closing `)`, but skip over any
// trailing comma + whitespace that rustfmt may have
// added after the router expression. Without this,
// `handler(old),)` becomes `handler(old),.handler(new))`
// which is invalid Rust.
let mut insert_pos = i;
while insert_pos > start && bytes[insert_pos - 1].is_ascii_whitespace() {
insert_pos -= 1;
}
if insert_pos > start && bytes[insert_pos - 1] == b',' {
insert_pos -= 1;
}
let mut out = src.to_owned();
out.insert_str(insert_pos, &format!(".handler({handler_module_path}())"));
return out;
}
}
_ => {}
}
i += 1;
}
return src.to_owned();
}
// Insert after `.routes(` call if present, otherwise before `.run()`.
let target = if src.contains(".routes(") {
// Find the closing paren of the first .routes() call and insert after
find_after_routes_call(src).map(|pos| {
let mut out = src.to_owned();
out.insert_str(pos, &format!("\n {snippet}"));
out
})
} else {
None
};
target.unwrap_or_else(|| {
// Fallback: insert before `.run()`.
src.find(".run()").map_or_else(
|| format!("{src}\n// TODO: add to app builder: {snippet}"),
|run_pos| {
let mut out = src.to_owned();
out.insert_str(run_pos, &format!("{snippet}\n "));
out
},
)
})
}
fn find_after_routes_call(src: &str) -> Option<usize> {
let mut pos = src.find(".routes(")?;
pos += ".routes(".len();
let bytes = src.as_bytes();
let mut depth = 1usize;
while pos < bytes.len() {
match bytes[pos] {
b'(' => depth += 1,
b')' => {
depth -= 1;
if depth == 0 {
return Some(pos + 1);
}
}
_ => {}
}
pos += 1;
}
None
}
/// Inverse of the chaining performed by [`add_inbound_mail_router_to_app`]
/// (`autumn destroy`, issue #1048).
///
/// `generate` always inserts the `.inbound_mail_router(...)` call (and any
/// chained `.handler(...)` calls) on one unbroken text line, but a project
/// that runs `rustfmt` afterward commonly wraps this particular call across
/// several lines — it's long, carrying an `.endpoint(...)` config with a
/// path and an env-var lookup. Line-based removal would then strip only the
/// line holding `.handler(...)`, leaving the rest of the (now handler-less)
/// `.inbound_mail_router(...)` call behind (issue #1048 PR review). So this
/// locates the WHOLE call by balanced-paren scan — regardless of how many
/// lines it spans — and removes exactly `.handler(<handler_module_path>())`
/// from within it; if other `.handler(...)` calls remain chained on, the
/// shortened call is spliced back in place, otherwise this was the only
/// handler and every full line the call spans is removed.
///
/// A no-op if `handler_module_path` isn't currently registered.
pub(super) fn remove_inbound_mail_handler(existing: &str, handler_module_path: &str) -> String {
const ROUTER_CALL: &str = ".inbound_mail_router(";
let handler_snippet = format!(".handler({handler_module_path}())");
if !existing.contains(&handler_snippet) {
return existing.to_owned();
}
let Some(router_pos) = existing.find(ROUTER_CALL) else {
return existing.to_owned();
};
let Some(call_end) = find_balanced_close_paren(existing, router_pos + ROUTER_CALL.len()) else {
return existing.to_owned();
};
let call_text = &existing[router_pos..call_end];
if !call_text.contains(&handler_snippet) {
return existing.to_owned();
}
let without_handler = call_text.replacen(handler_snippet.as_str(), "", 1);
if without_handler.contains(".handler(") {
let mut out = String::with_capacity(existing.len());
out.push_str(&existing[..router_pos]);
out.push_str(&without_handler);
out.push_str(&existing[call_end..]);
return out;
}
// No handlers left — remove every full line the call spans, whether
// `generate` left it on one line or `rustfmt` later wrapped it.
let start_line = existing[..router_pos].rfind('\n').map_or(0, |i| i + 1);
let end_line = existing[call_end..]
.find('\n')
.map_or(existing.len(), |i| call_end + i + 1);
let mut out = String::with_capacity(existing.len());
out.push_str(&existing[..start_line]);
out.push_str(&existing[end_line..]);
out
}
/// Scan forward from `start` (the byte position just after an already-open
/// `(`, i.e. depth 1) for the matching closing paren, returning the index
/// just past it. `None` if the parens never balance (malformed/truncated
/// input — destroy never guesses).
const fn find_balanced_close_paren(src: &str, start: usize) -> Option<usize> {
let bytes = src.as_bytes();
let mut depth = 1usize;
let mut i = start;
while i < bytes.len() {
match bytes[i] {
b'(' => depth += 1,
b')' => {
depth -= 1;
if depth == 0 {
return Some(i + 1);
}
}
_ => {}
}
i += 1;
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn project_with_main(main_content: &str) -> TempDir {
let tmp = TempDir::new().unwrap();
fs::write(
tmp.path().join("Cargo.toml"),
"[package]\nname=\"x\"\n\n[dependencies]\nautumn-web = \"0.6\"\n",
)
.unwrap();
fs::create_dir_all(tmp.path().join("src")).unwrap();
fs::write(tmp.path().join("src/main.rs"), main_content).unwrap();
tmp
}
fn default_main() -> &'static str {
r#"use autumn_web::prelude::*;
#[get("/")]
async fn index() -> &'static str { "ok" }
#[autumn_web::main]
async fn main() {
autumn_web::app()
.routes(routes![index])
.run()
.await;
}
"#
}
#[test]
fn generate_then_destroy_round_trips_to_original_project_state() {
use crate::generate::Flags;
let tmp = project_with_main(default_main());
let cargo_path = tmp.path().join("Cargo.toml");
let main_path = tmp.path().join("src/main.rs");
let original_cargo = fs::read_to_string(&cargo_path).unwrap();
let original_main = fs::read_to_string(&main_path).unwrap();
let plan = plan_inbound_mail(tmp.path(), "Replies").unwrap();
plan.execute(Flags::default()).unwrap();
assert!(tmp.path().join("src/inbound_mailers/replies.rs").exists());
assert!(
fs::read_to_string(&main_path)
.unwrap()
.contains(".inbound_mail_router(")
);
assert!(
fs::read_to_string(&cargo_path)
.unwrap()
.contains("inbound-mailgun")
);
let destroy_plan = plan_inbound_mail(tmp.path(), "Replies").unwrap();
destroy_plan.revert(Flags::default()).unwrap();
assert!(!tmp.path().join("src/inbound_mailers").exists());
assert_eq!(fs::read_to_string(&main_path).unwrap(), original_main);
assert_eq!(fs::read_to_string(&cargo_path).unwrap(), original_cargo);
}
#[test]
fn destroying_one_of_two_inbound_mail_handlers_keeps_the_other_registered() {
use crate::generate::Flags;
let tmp = project_with_main(default_main());
let first = plan_inbound_mail(tmp.path(), "Replies").unwrap();
first.execute(Flags::default()).unwrap();
let second = plan_inbound_mail(tmp.path(), "Bounces").unwrap();
second.execute(Flags::default()).unwrap();
let main_path = tmp.path().join("src/main.rs");
let main_with_both = fs::read_to_string(&main_path).unwrap();
assert!(main_with_both.contains("inbound_mailers::replies::replies_handler_info"));
assert!(main_with_both.contains("inbound_mailers::bounces::bounces_handler_info"));
let destroy_replies = plan_inbound_mail(tmp.path(), "Replies").unwrap();
destroy_replies.revert(Flags::default()).unwrap();
assert!(!tmp.path().join("src/inbound_mailers/replies.rs").exists());
assert!(tmp.path().join("src/inbound_mailers/bounces.rs").exists());
let main_after = fs::read_to_string(&main_path).unwrap();
assert!(
!main_after.contains("replies_handler_info"),
"the destroyed handler's registration must be gone"
);
assert!(
main_after.contains("inbound_mailers::bounces::bounces_handler_info"),
"the surviving handler must remain registered — must not remove the whole router"
);
assert!(main_after.contains(".inbound_mail_router("));
}
// ── RED: file plan assertions ─────────────────────────────────────────
#[test]
fn plan_creates_handler_file() {
let tmp = project_with_main(default_main());
let plan = plan_inbound_mail(tmp.path(), "Replies").unwrap();
assert!(
plan.actions
.iter()
.any(|a| a.path().ends_with("src/inbound_mailers/replies.rs")),
"plan must include src/inbound_mailers/replies.rs"
);
}
#[test]
fn plan_creates_integration_test() {
let tmp = project_with_main(default_main());
let plan = plan_inbound_mail(tmp.path(), "Replies").unwrap();
assert!(
plan.actions
.iter()
.any(|a| a.path().ends_with("tests/replies_inbound_mail.rs")),
"plan must include tests/replies_inbound_mail.rs"
);
}
#[test]
fn plan_updates_mod_rs() {
let tmp = project_with_main(default_main());
let plan = plan_inbound_mail(tmp.path(), "Replies").unwrap();
assert!(
plan.actions
.iter()
.any(|a| a.path().ends_with("inbound_mailers/mod.rs")),
"plan must include inbound_mailers/mod.rs"
);
}
#[test]
fn plan_updates_main_rs() {
let tmp = project_with_main(default_main());
let plan = plan_inbound_mail(tmp.path(), "Replies").unwrap();
assert!(
plan.actions.iter().any(|a| a.path().ends_with("main.rs")),
"plan must update main.rs"
);
}
#[test]
fn plan_updates_cargo_toml_feature() {
let tmp = project_with_main(default_main());
let plan = plan_inbound_mail(tmp.path(), "Replies").unwrap();
assert!(
plan.actions
.iter()
.any(|a| a.path().ends_with("Cargo.toml")),
"plan must update Cargo.toml"
);
}
#[test]
fn plan_fails_for_invalid_name() {
let tmp = project_with_main(default_main());
let result = plan_inbound_mail(tmp.path(), "123Invalid");
assert!(result.is_err());
}
#[test]
fn plan_fails_outside_project() {
let tmp = TempDir::new().unwrap();
let result = plan_inbound_mail(tmp.path(), "Replies");
assert!(matches!(result.unwrap_err(), GenerateError::NotInProject));
}
#[test]
fn handler_file_contains_inbound_mail_macro() {
let tmp = project_with_main(default_main());
let plan = plan_inbound_mail(tmp.path(), "Support").unwrap();
let handler_action = plan
.actions
.iter()
.find(|a| a.path().ends_with("support.rs"))
.expect("handler file must be in plan");
let contents = match handler_action {
super::super::emit::Action::Create { contents, .. }
| super::super::emit::Action::Modify { contents, .. }
| super::super::emit::Action::CreateIfAbsent { contents, .. } => contents,
super::super::emit::Action::CreateBytes { .. } => {
unreachable!("generator emits no binary files")
}
};
assert!(
contents.contains("#[inbound_mail"),
"handler file must contain #[inbound_mail], got:\n{contents}"
);
}
#[test]
fn integration_test_contains_compute_signature_call() {
let tmp = project_with_main(default_main());
let plan = plan_inbound_mail(tmp.path(), "Support").unwrap();
let test_action = plan
.actions
.iter()
.find(|a| a.path().ends_with("support_inbound_mail.rs"))
.expect("integration test must be in plan");
let contents = match test_action {
super::super::emit::Action::Create { contents, .. }
| super::super::emit::Action::Modify { contents, .. }
| super::super::emit::Action::CreateIfAbsent { contents, .. } => contents,
super::super::emit::Action::CreateBytes { .. } => {
unreachable!("generator emits no binary files")
}
};
assert!(
contents.contains("compute_mailgun_signature"),
"integration test must use compute_mailgun_signature, got:\n{contents}"
);
}
#[test]
fn add_inbound_mail_router_inserts_after_routes() {
let src = r"autumn_web::app()
.routes(routes![index])
.run()
.await;";
let updated =
add_inbound_mail_router_to_app(src, "inbound_mailers::replies::replies_handler_info");
assert!(
updated.contains(".inbound_mail_router("),
"must insert .inbound_mail_router(…), got:\n{updated}"
);
}
#[test]
fn add_inbound_mail_router_is_idempotent_when_handler_already_present() {
// Handler already fully registered — no change expected.
let src = r"autumn_web::app()
.routes(routes![index])
.inbound_mail_router(InboundMailRouter::new().handler(inbound_mailers::replies::replies_handler_info()))
.run()
.await;";
let updated =
add_inbound_mail_router_to_app(src, "inbound_mailers::replies::replies_handler_info");
assert_eq!(
updated, src,
"must not modify src when handler already present"
);
}
#[test]
fn add_inbound_mail_router_chains_new_handler_onto_existing_router() {
let src = r"autumn_web::app()
.routes(routes![index])
.inbound_mail_router(InboundMailRouter::new().handler(replies_handler_info()))
.run()
.await;";
let updated =
add_inbound_mail_router_to_app(src, "inbound_mailers::support::support_handler_info");
let count = updated.matches(".inbound_mail_router(").count();
assert_eq!(
count, 1,
"must not add a second router call, got:\n{updated}"
);
assert!(
updated.contains("support_handler_info()"),
"must chain new handler, got:\n{updated}"
);
}
#[test]
fn remove_inbound_mail_handler_removes_a_rustfmt_wrapped_router_call() {
// rustfmt commonly wraps this call across several lines (it's long:
// an .endpoint(...) config with a path and an env-var lookup).
// Line-based removal would strip only the `.handler(...)` line and
// leave the rest of the (now handler-less) call behind — issue
// #1048 PR review.
let src = "fn main() {\n App::new()\n .routes(routes![index])\n .inbound_mail_router(\n ::autumn_web::inbound_mail::InboundMailRouter::new()\n .endpoint(::autumn_web::inbound_mail::InboundMailEndpointConfig::mailgun(\n \"/inbound/mailgun\",\n std::env::var(\"MAILGUN_SIGNING_KEY\").unwrap_or_default(),\n ))\n .handler(inbound_mailers::replies::replies_handler_info()),\n )\n .run()\n}\n";
let updated =
remove_inbound_mail_handler(src, "inbound_mailers::replies::replies_handler_info");
assert!(
!updated.contains(".inbound_mail_router("),
"the whole call must be removed once its only handler is gone, got:\n{updated}"
);
assert!(
!updated.contains(".handler("),
"no dangling .handler( fragment may remain, got:\n{updated}"
);
assert_eq!(
updated,
"fn main() {\n App::new()\n .routes(routes![index])\n .run()\n}\n"
);
}
#[test]
fn remove_inbound_mail_handler_keeps_other_handler_in_a_rustfmt_wrapped_call() {
let src = "fn main() {\n App::new()\n .inbound_mail_router(\n ::autumn_web::inbound_mail::InboundMailRouter::new()\n .endpoint(::autumn_web::inbound_mail::InboundMailEndpointConfig::mailgun(\n \"/inbound/mailgun\",\n std::env::var(\"MAILGUN_SIGNING_KEY\").unwrap_or_default(),\n ))\n .handler(inbound_mailers::replies::replies_handler_info())\n .handler(inbound_mailers::bounces::bounces_handler_info()),\n )\n .run()\n}\n";
let updated =
remove_inbound_mail_handler(src, "inbound_mailers::replies::replies_handler_info");
assert!(
updated.contains(".inbound_mail_router("),
"the router call must survive while another handler remains, got:\n{updated}"
);
assert!(
updated.contains("bounces_handler_info"),
"the other handler must survive, got:\n{updated}"
);
assert!(
!updated.contains("replies_handler_info"),
"the destroyed handler must be gone, got:\n{updated}"
);
}
#[test]
fn pascal_handler_name_derived_from_pascal_input() {
let tmp = project_with_main(default_main());
let plan = plan_inbound_mail(tmp.path(), "ReplyThread").unwrap();
let handler_action = plan
.actions
.iter()
.find(|a| a.path().ends_with("reply_thread.rs"))
.expect("handler file must be in plan");
let contents = match handler_action {
super::super::emit::Action::Create { contents, .. }
| super::super::emit::Action::Modify { contents, .. }
| super::super::emit::Action::CreateIfAbsent { contents, .. } => contents,
super::super::emit::Action::CreateBytes { .. } => {
unreachable!("generator emits no binary files")
}
};
assert!(
contents.contains("ReplyThreadMailHandler"),
"handler file must contain ReplyThreadMailHandler, got:\n{contents}"
);
}
}