ocre-cli 0.2.0

Command-line tool for Ocre: create, generate, migrate, run and deploy apps.
//! `ocre g mailer` and `ocre g mailbox`, run through the real binary.

#[path = "../support/mod.rs"]
mod support;

use std::fs;

use support::{Sandbox, text};

#[test]
fn mailer_generates_templates_and_registers_the_module() {
    let sandbox = Sandbox::new();
    let root = sandbox.new_app("shop", &[]);
    let (report, ok) = sandbox.json(&["g", "mailer", "UserMailer", "welcome", "PasswordReset"], &root);
    assert!(ok, "{report}");
    assert_eq!(
        report["created"],
        serde_json::json!([
            "src/mailers/user.rs",
            "templates/mailers/user/welcome.txt",
            "templates/mailers/user/welcome.html",
            "templates/mailers/user/password_reset.txt",
            "templates/mailers/user/password_reset.html",
            "templates/mailers/layout.html",
            "templates/mailers/layout.txt",
            "src/mailers/mod.rs",
        ])
    );
    assert_eq!(report["updated"], serde_json::json!(["src/lib.rs"]));
    assert_eq!(
        report["next"][0],
        "send it from a handler: ocre::mail::send(&ctx, mailers::user::welcome(&address)?).await?"
    );
    let mailer = fs::read_to_string(root.join("src/mailers/user.rs")).unwrap();
    assert!(mailer.contains("Generated by `ocre g mailer UserMailer welcome password_reset`"), "{mailer}");
    assert!(mailer.contains("#[template(path = \"mailers/user/password_reset.html\")]\nstruct PasswordResetHtml<'a>"));
    assert!(mailer.contains(
        "pub fn password_reset(to: &str) -> Result<Email> {\n    let text = PasswordResetText { to }.render()?;\n    \
         let html = PasswordResetHtml { to }.render()?;\n    Ok(super::defaults(Email::new(to, \"Password reset\", \
         text).html(html)))\n}"
    ));
    let welcome = fs::read_to_string(root.join("templates/mailers/user/welcome.txt")).unwrap();
    assert!(
        welcome.starts_with(
            "{% extends \"mailers/layout.txt\" %}\n{% block content -%}\nHello {{ to }},\n\nThis is the welcome email."
        ),
        "{welcome}"
    );
    let html = fs::read_to_string(root.join("templates/mailers/user/password_reset.html")).unwrap();
    assert!(html.starts_with("{% extends \"mailers/layout.html\" %}\n{% block content %}\n"), "{html}");
    assert!(html.contains("<p>Hello {{ to }},</p>\n<p>This is the password reset email."), "{html}");
    let layout = fs::read_to_string(root.join("templates/mailers/layout.html")).unwrap();
    assert!(layout.contains("{% block content %}{% endblock %}"), "{layout}");
    let lib = fs::read_to_string(root.join("src/lib.rs")).unwrap();
    assert!(lib.contains("// ocre:modules\nmod mailers;\n"), "{lib}");
    assert!(lib.contains("// ocre:routes\n        .merge(ocre::mail::dev_routes(mailers::PREVIEWS))\n"), "{lib}");

    // A second mailer only extends the registry; the lib already has `mod mailers;`.
    let output = sandbox.ocre(&["g", "mailer", "billing", "receipt"], &root);
    assert!(output.status.success());
    assert!(text(&output).0.starts_with("  create  src/mailers/billing.rs\n"), "{}", text(&output).0);
    assert!(!text(&output).0.contains("layout"), "layouts are created once");
    let registry = fs::read_to_string(root.join("src/mailers/mod.rs")).unwrap();
    assert!(registry.contains("\n// ocre:mailers\npub mod billing;\npub mod user;\n"), "{registry}");
    assert!(registry.contains("pub fn defaults(email: Email) -> Email {\n    email\n}"), "{registry}");
    assert!(
        registry.contains(
            "    // ocre:mailer-previews\n    Preview::new(\"billing/receipt\", || billing::receipt(\"ada@example.com\")),\n    \
             Preview::new(\"user/welcome\", || user::welcome(\"ada@example.com\")),\n    \
             Preview::new(\"user/password_reset\", || user::password_reset(\"ada@example.com\")),\n];"
        ),
        "{registry}"
    );
    let lib = fs::read_to_string(root.join("src/lib.rs")).unwrap();
    assert_eq!((lib.matches("mod mailers;").count(), lib.matches("dev_routes").count()), (1, 1));
}

#[test]
fn mailer_registries_from_older_versions_keep_compiling() {
    let sandbox = Sandbox::new();
    let root = sandbox.new_app("shop", &["--api"]);
    fs::create_dir_all(root.join("src/mailers")).unwrap();
    fs::write(root.join("src/mailers/mod.rs"), "// ocre:mailers\npub mod user;\n").unwrap();
    let (report, ok) = sandbox.json(&["g", "mailer", "Billing", "receipt"], &root);
    assert!(ok, "{report}");
    assert_eq!(report["updated"], serde_json::json!(["src/mailers/mod.rs"]));
    let mailer = fs::read_to_string(root.join("src/mailers/billing.rs")).unwrap();
    assert!(mailer.contains("    Ok(Email::new(to, \"Receipt\", text))\n"), "no `defaults` to call: {mailer}");
    assert!(!fs::read_to_string(root.join("src/lib.rs")).unwrap().contains("dev_routes"), "no previews to serve");
}

#[test]
fn a_mailer_after_the_mailbox_serves_its_previews() {
    let sandbox = Sandbox::new();
    let root = sandbox.new_app("shop", &[]);
    sandbox.json(&["g", "mailbox"], &root);
    assert!(fs::read_to_string(root.join("src/lib.rs")).unwrap().contains(".merge(ocre::mail::dev_routes(&[]))"));
    let (report, ok) = sandbox.json(&["g", "mailer", "User", "welcome"], &root);
    assert!(ok, "{report}");
    let lib = fs::read_to_string(root.join("src/lib.rs")).unwrap();
    assert!(lib.contains(".merge(ocre::mail::dev_routes(mailers::PREVIEWS))") && !lib.contains("(&[])"), "{lib}");

    // Without the routes marker nothing is written.
    fs::write(
        root.join("src/lib.rs"),
        lib.replace("        .merge(ocre::mail::dev_routes(mailers::PREVIEWS))\n", "").replace("// ocre:routes", ""),
    )
    .unwrap();
    fs::remove_file(root.join("src/mailers/user.rs")).unwrap();
    let (report, ok) = sandbox.json(&["g", "mailer", "Team", "invite"], &root);
    assert!(!ok);
    assert_eq!(report["error"], "src/lib.rs is missing the `// ocre:routes` marker");
    assert!(!root.join("src/mailers/team.rs").exists());
}

#[test]
fn mailer_in_an_api_only_app_builds_text_with_format() {
    let sandbox = Sandbox::new();
    let root = sandbox.new_app("api", &["--api"]);
    let (report, ok) = sandbox.json(&["g", "mailer", "Account", "welcome"], &root);
    assert!(ok, "{report}");
    assert_eq!(report["created"], serde_json::json!(["src/mailers/account.rs", "src/mailers/mod.rs"]));
    let mailer = fs::read_to_string(root.join("src/mailers/account.rs")).unwrap();
    assert!(!mailer.contains("askama") && !root.join("templates").exists(), "{mailer}");
    assert!(mailer.contains(
        "pub fn welcome(to: &str) -> Result<Email> {\n    let text = format!(\"Hello {to},\\n\\nThis is the welcome \
         email. Edit it in src/mailers/account.rs.\\n\");\n    Ok(super::defaults(Email::new(to, \"Welcome\", text)))\n}"
    ), "{mailer}");
}

#[test]
fn mailer_rejects_bad_names_and_writes_nothing() {
    let sandbox = Sandbox::new();
    let root = sandbox.new_app("shop", &[]);
    sandbox.json(&["g", "mailer", "User", "welcome"], &root);
    let cases: &[(&[&str], &str)] = &[
        (&["g", "mailer", "User", "reset"], "src/mailers/user.rs already exists"),
        (&["g", "mailer", "2Fast", "welcome"], "invalid mailer name `2Fast`"),
        (&["g", "mailer", "Type", "welcome"], "invalid mailer name `Type`"),
        (&["g", "mailer", "Team", "9lives"], "invalid action name `9lives`"),
        (&["g", "mailer", "Team", "type"], "invalid action name `type`"),
        (&["g", "mailer", "Team", "invite", "Invite"], "action `invite` is listed twice"),
    ];
    for (args, error) in cases {
        let (report, ok) = sandbox.json(args, &root);
        assert!(!ok);
        assert_eq!(report["error"], *error, "{args:?}");
        assert!(report["hint"].is_string(), "{report}");
    }
    assert!(!root.join("src/mailers/team.rs").exists() && !root.join("templates/mailers/team").exists());

    // Without the modules marker nothing is written either.
    fs::write(root.join("src/lib.rs"), "fn main() {}\n").unwrap();
    fs::remove_dir_all(root.join("src/mailers")).unwrap();
    let (report, ok) = sandbox.json(&["g", "mailer", "Team", "invite"], &root);
    assert!(!ok && report["hint"].as_str().unwrap().contains("// ocre:modules"), "{report}");
    assert!(!root.join("src/mailers").exists() && !root.join("templates/mailers/team").exists());

    // A registry without its marker.
    fs::create_dir_all(root.join("src/mailers")).unwrap();
    fs::write(root.join("src/mailers/mod.rs"), "pub mod user;\n").unwrap();
    let (report, _) = sandbox.json(&["g", "mailer", "Team", "invite"], &root);
    assert_eq!(report["error"], "src/mailers/mod.rs is missing the `// ocre:mailers` marker");
}

#[test]
fn mailbox_wires_the_email_event_once() {
    let sandbox = Sandbox::new();
    let root = sandbox.new_app("shop", &[]);
    let (report, ok) = sandbox.json(&["g", "mailbox"], &root);
    assert!(ok, "{report}");
    assert_eq!(report["created"], serde_json::json!(["src/mailbox.rs"]));
    assert_eq!(report["updated"], serde_json::json!(["src/lib.rs"]));
    assert_eq!(report["next"][0], "ocre dev");
    let mailbox = fs::read_to_string(root.join("src/mailbox.rs")).unwrap();
    assert!(mailbox.contains("pub async fn receive(_ctx: Ctx, _email: InboundEmail) -> Result<()> {"), "{mailbox}");
    let lib = fs::read_to_string(root.join("src/lib.rs")).unwrap();
    assert!(lib.contains("// ocre:modules\nmod mailbox;\n"), "{lib}");
    assert!(
        lib.ends_with(
            "#[worker::event(email)]\nasync fn email(message: worker::ForwardableEmailMessage, env: worker::Env, _ctx: \
             worker::Context) -> worker::Result<()> {\n    ocre::mail::receive(message, env, mailbox::receive).await\n}\n"
        ),
        "{lib}"
    );

    let (report, ok) = sandbox.json(&["g", "mailbox"], &root);
    assert!(!ok);
    assert_eq!(report["error"], "src/mailbox.rs already exists");

    // An email handler written by hand is not duplicated.
    fs::remove_file(root.join("src/mailbox.rs")).unwrap();
    let (report, _) = sandbox.json(&["g", "mailbox"], &root);
    assert_eq!(report["error"], "src/lib.rs already handles the `email` event");
    assert!(!root.join("src/mailbox.rs").exists(), "nothing written on failure");

    fs::write(root.join("src/lib.rs"), "fn main() {}\n").unwrap();
    let (report, _) = sandbox.json(&["g", "mailbox"], &root);
    assert_eq!(report["error"], "src/lib.rs is missing the `// ocre:modules` marker");
    assert!(!root.join("src/mailbox.rs").exists());
}

#[test]
fn mailbox_after_a_mailer_keeps_its_previews() {
    let sandbox = Sandbox::new();
    let root = sandbox.new_app("shop", &[]);
    let (report, ok) = sandbox.json(&["g", "mailer", "User", "welcome"], &root);
    assert!(ok, "{report}");
    let (report, ok) = sandbox.json(&["g", "mailbox"], &root);
    assert!(ok, "{report}");
    let lib = fs::read_to_string(root.join("src/lib.rs")).unwrap();
    assert_eq!(lib.matches("dev_routes").count(), 1, "{lib}");
    assert!(lib.contains(".merge(ocre::mail::dev_routes(mailers::PREVIEWS))"), "{lib}");
}

#[test]
fn mailbox_needs_the_routes_marker_for_its_dev_pages() {
    let sandbox = Sandbox::new();
    let root = sandbox.new_app("shop", &[]);
    let lib = fs::read_to_string(root.join("src/lib.rs")).unwrap().replace("// ocre:routes", "");
    fs::write(root.join("src/lib.rs"), &lib).unwrap();
    let (report, ok) = sandbox.json(&["g", "mailbox"], &root);
    assert!(!ok);
    assert_eq!(report["error"], "src/lib.rs is missing the `// ocre:routes` marker");
    assert!(!root.join("src/mailbox.rs").exists(), "nothing written on failure");
    assert_eq!(fs::read_to_string(root.join("src/lib.rs")).unwrap(), lib);
}