#[path = "../support/mod.rs"]
mod support;
use std::fs;
use serde_json::json;
use support::{Sandbox, text};
fn read(root: &std::path::Path, path: &str) -> String {
fs::read_to_string(root.join(path)).unwrap()
}
const CONFIG: &str = "cloudflare.config.ts";
fn hand_write(root: &std::path::Path, marker: &str, entry: &str) -> String {
let text = read(root, CONFIG).replacen(marker, &format!("{entry}\n\t\t\t{marker}"), 1);
fs::write(root.join(CONFIG), &text).unwrap();
text
}
fn rewrite(root: &std::path::Path, from: &str, to: &str) -> String {
let text = read(root, CONFIG);
assert!(text.contains(from), "{from} in {text}");
let text = text.replacen(from, to, 1);
fs::write(root.join(CONFIG), &text).unwrap();
text
}
#[test]
fn first_job_wires_the_queue_and_later_jobs_extend_the_registry() {
let sandbox = Sandbox::new();
let root = sandbox.new_app("shop", &[]);
let (report, ok) = sandbox.json(&["g", "job", "SendWelcomeJob", "user_id:integer", "note:text?"], &root);
assert!(ok, "{report}");
assert_eq!(report["created"], json!(["src/jobs/send_welcome.rs", "src/jobs/mod.rs"]));
assert_eq!(report["updated"], json!(["src/lib.rs", "cloudflare.config.ts"]));
assert_eq!(
report["next"],
json!([
"enqueue it from a handler: jobs::SendWelcome { user_id, note }.perform_later(&ctx).await?",
"ocre dev (jobs run locally; look for `[ocre jobs]` lines in the output)",
"ocre deploy creates the queue shop-jobs and its dead-letter queue",
])
);
let job = read(&root, "src/jobs/send_welcome.rs");
assert!(
job.starts_with("//! Send welcome job. Generated by `ocre g job SendWelcomeJob user_id:integer note:text?`.")
);
assert!(
job.contains(
"pub struct SendWelcome {\n pub user_id: i64,\n pub note: Option<String>,\n}\n\nimpl SendWelcome {"
),
"{job}"
);
assert!(job.contains("pub async fn perform(self, _ctx: &Ctx) -> Result<()> {\n Ok(())\n }"), "{job}");
let registry = read(&root, "src/jobs/mod.rs");
assert!(
registry.contains("// ocre:jobs\npub mod send_welcome;\npub use send_welcome::SendWelcome;\n"),
"{registry}"
);
assert!(registry.contains(" // ocre:job-variants\n SendWelcome(SendWelcome),\n}"), "{registry}");
assert!(
registry.contains(" // ocre:job-dispatch\n Job::SendWelcome(job) => job.perform(&ctx).await,\n"),
"{registry}"
);
let lib = read(&root, "src/lib.rs");
assert!(lib.contains("// ocre:modules\nmod jobs;\n"), "{lib}");
assert!(
lib.ends_with(
"#[worker::event(queue)]\nasync fn queue(batch: worker::MessageBatch<String>, env: worker::Env, _ctx: \
worker::Context) -> worker::Result<()> {\n ocre::jobs::consume(batch, env, jobs::perform).await\n}\n"
),
"{lib}"
);
let config = read(&root, CONFIG);
assert!(config.contains("\t\t\tJOBS: bindings.queue({ name: \"shop-jobs\" }),\n"), "{config}");
assert!(
config.contains(
"\t\t\ttriggers.queue({ name: \"shop-jobs\", deadLetterQueue: \"shop-jobs-failed\", maxBatchSize: 10, \
maxBatchTimeout: 5, maxRetries: 5 }),\n"
),
"{config}"
);
let output = sandbox.ocre(&["g", "job", "cleanup"], &root);
assert!(output.status.success());
let (stdout, _) = text(&output);
assert!(stdout.starts_with(" create src/jobs/cleanup.rs\n update src/jobs/mod.rs\n"), "{stdout}");
assert!(stdout.contains("jobs::Cleanup {}.perform_later(&ctx)"), "{stdout}");
assert!(!stdout.contains("ocre deploy creates"), "{stdout}");
assert!(read(&root, "src/jobs/cleanup.rs").contains("pub struct Cleanup {}\n\nimpl Cleanup {"));
let registry = read(&root, "src/jobs/mod.rs");
assert!(registry.contains(" Cleanup(Cleanup),\n SendWelcome(SendWelcome),\n"), "{registry}");
assert_eq!(read(&root, "src/lib.rs").matches("event(queue)").count(), 1);
assert_eq!(read(&root, CONFIG).matches("bindings.queue(").count(), 1);
}
#[test]
fn a_named_queue_gets_its_own_cloudflare_queue_once() {
let sandbox = Sandbox::new();
let root = sandbox.new_app("shop", &[]);
let (report, ok) = sandbox.json(&["g", "job", "SendCode", "user_id:integer", "--queue", "urgent"], &root);
assert!(ok, "{report}");
assert_eq!(
report["next"],
json!([
"enqueue it from a handler: jobs::SendCode { user_id }.perform_later(&ctx).await?",
"ocre dev (jobs run locally; look for `[ocre jobs]` lines in the output)",
"ocre deploy creates the queue shop-jobs and its dead-letter queue",
"ocre deploy creates the queue shop-jobs-urgent and its dead-letter queue",
])
);
let job = read(&root, "src/jobs/send_code.rs");
assert!(job.contains("Generated by `ocre g job SendCode user_id:integer --queue urgent`"), "{job}");
assert!(
job.contains(
"pub fn perform_later(self, ctx: &Ctx) -> impl Future<Output = Result<()>> + Send + use<> {\n \
ocre::jobs::queue(ctx, \"urgent\").enqueue(&super::Job::SendCode(self))\n }"
),
"{job}"
);
let config = read(&root, CONFIG);
assert!(config.contains("JOBS_URGENT: bindings.queue({ name: \"shop-jobs-urgent\" }),\n"), "{config}");
assert!(
config.contains(
"triggers.queue({ name: \"shop-jobs-urgent\", deadLetterQueue: \"shop-jobs-urgent-failed\", \
maxBatchSize: 10, maxBatchTimeout: 1, maxRetries: 5 }),\n"
),
"{config}"
);
let (report, ok) = sandbox.json(&["g", "job", "SendSms", "--queue", "urgent"], &root);
assert!(ok, "{report}");
assert_eq!(report["updated"], json!(["src/jobs/mod.rs"]));
let (report, ok) = sandbox.json(&["g", "job", "Cleanup", "--queue", "default"], &root);
assert!(ok, "{report}");
assert!(read(&root, "src/jobs/cleanup.rs").contains("ocre::jobs::enqueue(ctx, &super::Job::Cleanup(self))"));
assert_eq!(read(&root, CONFIG).matches("bindings.queue(").count(), 2);
assert_eq!(read(&root, CONFIG).matches("triggers.queue(").count(), 2);
for bad in ["Urgent", "-x", "x-", "a_b", ""] {
let (report, ok) = sandbox.json(&["g", "job", "Other", &format!("--queue={bad}")], &root);
assert!(!ok);
assert_eq!(report["error"], format!("invalid queue name `{bad}`"));
}
rewrite(&root, "name: \"shop\",", "name: app,");
let (report, _) = sandbox.json(&["g", "job", "Other", "--queue", "low"], &root);
assert_eq!(report["error"], "cloudflare.config.ts has no `worker.name` Ocre can read");
assert!(!root.join("src/jobs/other.rs").exists());
rewrite(&root, "name: app,", "name: \"shop\",");
rewrite(&root, "// ocre:triggers", "");
let before = read(&root, CONFIG);
let (report, _) = sandbox.json(&["g", "job", "Other", "--queue", "low"], &root);
assert_eq!(report["error"], "cloudflare.config.ts is missing the `// ocre:triggers` marker");
assert_eq!(read(&root, CONFIG), before);
assert!(!root.join("src/jobs/other.rs").exists());
}
#[test]
fn schedules_are_listed_and_fired_on_ocre_dev() {
let sandbox = Sandbox::new();
let root = sandbox.new_app("shop", &[]);
let output = sandbox.ocre(&["schedules"], &root);
assert!(text(&output).0.starts_with("No schedules."), "{}", text(&output).0);
let (report, _) = sandbox.json(&["schedules", "run", "nightly"], &root);
assert_eq!(report["error"], "no scheduled task `nightly`");
assert!(report["hint"].as_str().unwrap().contains("ocre g schedule"), "{report}");
sandbox.json(&["g", "schedule", "nightly", "every day at 3am"], &root);
let nightly = "triggers.scheduled({ schedule: \"0 3 * * *\" }),";
rewrite(&root, nightly, &format!("{nightly}\n\t\t\ttriggers.scheduled({{ schedule: \"*/5 * * * *\" }}),"));
let (report, ok) = sandbox.json(&["schedules"], &root);
assert!(ok, "{report}");
assert_eq!(
report["schedules"],
json!([{"cron": "0 3 * * *", "task": "nightly"}, {"cron": "*/5 * * * *", "task": null}])
);
assert!(report["next"][0].as_str().unwrap().starts_with("a cron without a task fails"), "{report}");
let output = sandbox.ocre(&["schedules"], &root);
assert_eq!(
text(&output).0.lines().take(3).collect::<Vec<_>>(),
["CRON (UTC) TASK", "0 3 * * * src/schedules/nightly.rs", "*/5 * * * * (no task: fails when it fires)"]
);
let (report, _) = sandbox.json(&["schedules", "run", "weekly"], &root);
assert_eq!(report["hint"], "tasks: nightly");
let (port, request) = stand_in(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok");
let (report, ok) = sandbox.json(&["schedules", "run", "nightly", "--port", &port.to_string()], &root);
assert!(ok, "{report}");
assert!(request.join().unwrap().starts_with("GET /cdn-cgi/local/scheduled?cron=0+3+*+*+* HTTP/1.1\r\n"));
assert_eq!(report["ran"][0], "fired nightly (0 3 * * *); its `[ocre cron]` line is in the `ocre dev` output");
let (port, request) = stand_in(b"HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\n\r\n");
let (report, ok) = sandbox.json(&["schedules", "run", "nightly", "--port", &port.to_string()], &root);
request.join().unwrap();
assert!(!ok);
assert_eq!(report["error"], "the scheduled task answered HTTP 500");
let (port, request) = stand_in(b"hello");
let (report, _) = sandbox.json(&["schedules", "run", "nightly", "--port", &port.to_string()], &root);
request.join().unwrap();
assert!(report["error"].as_str().unwrap().ends_with("not an HTTP answer"), "{report}");
let port = std::net::TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port();
let (report, _) = sandbox.json(&["schedules", "run", "nightly", "--port", &port.to_string()], &root);
assert!(report["error"].as_str().unwrap().starts_with(&format!("cannot reach `ocre dev` on port {port}:")));
rewrite(&root, "{ schedule: \"*/5 * * * *\" }", "{ schedule: every5 }");
let (report, ok) = sandbox.json(&["schedules"], &root);
assert!(!ok);
assert_eq!(report["error"], "cloudflare.config.ts has a scheduled trigger Ocre cannot read");
assert_eq!(report["hint"], "write it as `triggers.scheduled({ schedule: \"0 3 * * *\" }),`");
}
fn stand_in(response: &'static [u8]) -> (u16, std::thread::JoinHandle<String>) {
use std::io::{Read, Write};
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
let handle = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
let mut request = Vec::new();
let mut chunk = [0u8; 256];
while !request.ends_with(b"\r\n\r\n") {
let read = stream.read(&mut chunk).unwrap();
assert!(read > 0, "the request ends with an empty line");
request.extend_from_slice(&chunk[..read]);
}
stream.write_all(response).unwrap();
String::from_utf8(request).unwrap()
});
(port, handle)
}
#[test]
fn a_hand_written_jobs_producer_is_reused() {
let sandbox = Sandbox::new();
let root = sandbox.new_app("shop", &[]);
let config = hand_write(&root, "// ocre:env", "JOBS: bindings.queue({ name: \"custom\" }),");
let (report, ok) = sandbox.json(&["g", "job", "Ping"], &root);
assert!(ok, "{report}");
assert_eq!(report["updated"], json!(["src/lib.rs"]));
assert_eq!(report["next"][2], "ocre deploy creates the queue custom and its dead-letter queue");
assert_eq!(read(&root, CONFIG), config);
}
#[test]
fn job_errors_name_the_fix_and_write_nothing() {
let sandbox = Sandbox::new();
let root = sandbox.new_app("shop", &[]);
let pristine = |root: &std::path::Path| !root.join("src/jobs").exists();
let cases: &[(&[&str], &str)] = &[
(&["g", "job", "2Fast"], "invalid job name `2Fast`"),
(&["g", "job", "Type"], "invalid job name `Type`"),
(&["g", "job", "Job"], "invalid job name `Job`"),
(&["g", "job", "Import", "path"], "field `path` has no type"),
(&["g", "job", "Import", "path:string^"], "job field `path` cannot be unique"),
(&["g", "job", "Import", "csv:attachment"], "job field `csv` cannot be an attachment"),
];
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!(pristine(&root));
let lib = read(&root, "src/lib.rs");
fs::write(root.join("src/lib.rs"), format!("{lib}\n// #[worker::event(queue)]\n")).unwrap();
let (report, _) = sandbox.json(&["g", "job", "Ping"], &root);
assert_eq!(report["error"], "src/lib.rs already handles the `queue` event");
fs::write(root.join("src/lib.rs"), "fn main() {}\n").unwrap();
let (report, _) = sandbox.json(&["g", "job", "Ping"], &root);
assert_eq!(report["error"], "src/lib.rs is missing the `// ocre:modules` marker");
fs::write(root.join("src/lib.rs"), lib.replace("// ocre:routes", "")).unwrap();
let (report, _) = sandbox.json(&["g", "job", "Ping"], &root);
assert_eq!(report["error"], "src/lib.rs is missing the `// ocre:routes` marker");
fs::write(root.join("src/lib.rs"), &lib).unwrap();
let config = read(&root, CONFIG);
rewrite(&root, "name: \"shop\",", "name: `${prefix}shop`,");
let (report, _) = sandbox.json(&["g", "job", "Ping"], &root);
assert_eq!(report["error"], "cloudflare.config.ts has no `worker.name` Ocre can read");
fs::write(root.join(CONFIG), &config).unwrap();
hand_write(&root, "// ocre:env", "JOBS: bindings.queue({ name: queueName }),");
let (report, _) = sandbox.json(&["g", "job", "Ping"], &root);
assert_eq!(report["error"], "cloudflare.config.ts defines `JOBS` in a form Ocre cannot read");
assert_eq!(report["hint"], "write it as a literal: `JOBS: bindings.queue({ name: \"<app-name>-jobs\" }),`");
for marker in ["// ocre:env", "// ocre:triggers"] {
let without = config.replace(marker, "");
fs::write(root.join(CONFIG), &without).unwrap();
let (report, _) = sandbox.json(&["g", "job", "Ping"], &root);
assert_eq!(report["error"], format!("cloudflare.config.ts is missing the `{marker}` marker"));
assert!(report["hint"].as_str().unwrap().starts_with(&format!("put `{marker}` on its own line")));
assert_eq!(read(&root, CONFIG), without);
}
fs::write(root.join(CONFIG), &config).unwrap();
assert!(pristine(&root));
assert_eq!(read(&root, "src/lib.rs"), lib);
let (report, ok) = sandbox.json(&["g", "job", "Ping"], &root);
assert!(ok, "{report}");
let registry = read(&root, "src/jobs/mod.rs");
for marker in ["// ocre:jobs", "// ocre:job-variants", "// ocre:job-dispatch"] {
fs::write(root.join("src/jobs/mod.rs"), registry.replace(marker, "")).unwrap();
let (report, _) = sandbox.json(&["g", "job", "Pong"], &root);
assert_eq!(report["error"], format!("src/jobs/mod.rs is missing the `{marker}` marker"));
assert!(!root.join("src/jobs/pong.rs").exists());
}
fs::write(root.join("src/jobs/mod.rs"), ®istry).unwrap();
let (report, _) = sandbox.json(&["g", "job", "Ping"], &root);
assert_eq!(report["error"], "src/jobs/ping.rs already exists");
}
#[test]
fn schedules_add_crons_and_dispatch_by_expression() {
let sandbox = Sandbox::new();
let root = sandbox.new_app("shop", &[]);
let (report, ok) = sandbox.json(&["g", "schedule", "NightlyCleanup", " 0 3 * * * "], &root);
assert!(ok, "{report}");
assert_eq!(report["created"], json!(["src/schedules/nightly_cleanup.rs", "src/schedules/mod.rs"]));
assert_eq!(report["updated"], json!(["cloudflare.config.ts", "src/lib.rs"]));
assert_eq!(report["next"][0], "ocre dev, then: ocre schedules run nightly_cleanup");
let task = read(&root, "src/schedules/nightly_cleanup.rs");
assert!(task.starts_with(
"//! Nightly cleanup: scheduled task. Generated by `ocre g schedule NightlyCleanup \"0 3 * * *\"`.\n//!\n\
//! Runs at `0 3 * * *`, UTC,"
));
assert!(task.contains("`curl 'http://localhost:8787/cdn-cgi/local/scheduled?cron=0+3+*+*+*'`"), "{task}");
assert!(task.contains("pub async fn run(_ctx: &Ctx) -> Result<()> {\n Ok(())\n}"), "{task}");
let registry = read(&root, "src/schedules/mod.rs");
assert!(registry.contains("// ocre:schedules\npub mod nightly_cleanup;\n"), "{registry}");
assert!(
registry.contains("// ocre:schedule-dispatch\n \"0 3 * * *\" => nightly_cleanup::run(&ctx).await,\n")
);
let lib = read(&root, "src/lib.rs");
assert!(lib.contains("// ocre:modules\nmod schedules;\n"), "{lib}");
assert!(
lib.ends_with(
"#[worker::event(scheduled)]\nasync fn scheduled(event: worker::ScheduledEvent, env: worker::Env, _ctx: \
worker::ScheduleContext) {\n ocre::jobs::cron(event, env, schedules::run).await\n}\n"
),
"{lib}"
);
assert!(read(&root, CONFIG).contains("\t\t\ttriggers.scheduled({ schedule: \"0 3 * * *\" }),\n"));
let (report, ok) = sandbox.json(&["g", "schedule", "monthly_report", "0 9 * * MON#1"], &root);
assert!(ok, "{report}");
assert_eq!(report["updated"], json!(["cloudflare.config.ts", "src/schedules/mod.rs"]));
assert!(read(&root, "src/schedules/monthly_report.rs").contains("?cron=0+9+*+*+MON%231'"));
let (report, ok) = sandbox.json(&["g", "schedule", "digest", "every monday at 9:30am"], &root);
assert!(ok, "{report}");
assert_eq!(
report["next"][1],
"ocre deploy (Cron Triggers only fire on the deployed Worker; this one runs at `30 9 * * MON`, UTC)"
);
assert!(read(&root, "src/schedules/digest.rs").contains("Runs at `30 9 * * MON` (every monday at 9:30am), UTC"));
assert!(read(&root, "src/schedules/mod.rs").contains("\"30 9 * * MON\" => digest::run(&ctx).await,"));
assert_eq!(crons(&sandbox, &root), ["30 9 * * MON", "0 9 * * MON#1", "0 3 * * *"], "newest first, at the marker");
assert_eq!(read(&root, "src/lib.rs").matches("event(scheduled)").count(), 1);
let cases: &[(&[&str], &str)] = &[
(&["g", "schedule", "again", "0 3 * * *"], "cron `0 3 * * *` is already scheduled in cloudflare.config.ts"),
(&["g", "schedule", "hourly", "0 * * *"], "invalid schedule `0 * * *`"),
(&["g", "schedule", "hourly", "0 * * * ?"], "invalid schedule `0 * * * ?`"),
(&["g", "schedule", "often", "every 30 seconds"], "invalid schedule `every 30 seconds`"),
(&["g", "schedule", "9lives", "0 * * * *"], "invalid schedule name `9lives`"),
(&["g", "schedule", "match", "0 * * * *"], "invalid schedule name `match`"),
(&["g", "schedule", "nightly_cleanup", "5 * * * *"], "src/schedules/nightly_cleanup.rs already exists"),
];
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/schedules/again.rs").exists() && !root.join("src/schedules/hourly.rs").exists());
}
fn crons(sandbox: &Sandbox, root: &std::path::Path) -> Vec<String> {
let (report, ok) = sandbox.json(&["schedules"], root);
assert!(ok, "{report}");
report["schedules"].as_array().unwrap().iter().map(|s| s["cron"].as_str().unwrap().to_owned()).collect()
}
#[test]
fn crons_are_added_at_the_triggers_marker() {
let sandbox = Sandbox::new();
let root = sandbox.new_app("shop", &[]);
let pristine = read(&root, CONFIG);
hand_write(
&root,
"// ocre:triggers",
"triggers.scheduled({ schedule: \"0 1 * * *\" }),\n\t\t\ttriggers.scheduled({ schedule: \"0 2 * * *\" }),\n\t\t\t\
triggers.queue({ name: \"shop-jobs\" }),\n\t\t\ttriggers.scheduled({ schedule: \"0 3 * * *\" }),",
);
for (name, cron) in [("a", "0 4 * * *"), ("b", "0 5 * * *")] {
let (report, ok) = sandbox.json(&["g", "schedule", name, cron], &root);
assert!(ok, "{report}");
assert_eq!(report["next"].as_array().unwrap().len(), 2, "{report}");
}
let (report, ok) = sandbox.json(&["g", "schedule", "c", "0 6 * * *"], &root);
assert!(ok, "{report}");
assert_eq!(
report["next"][2],
"this app now has 6 crons; the free plan allows 5 per account: run several tasks from one cron"
);
assert_eq!(crons(&sandbox, &root), ["0 1 * * *", "0 2 * * *", "0 3 * * *", "0 6 * * *", "0 5 * * *", "0 4 * * *"]);
let (report, _) = sandbox.json(&["g", "schedule", "d", "0 2 * * *"], &root);
assert_eq!(report["error"], "cron `0 2 * * *` is already scheduled in cloudflare.config.ts", "hand-written too");
fs::remove_dir_all(root.join("src/schedules")).unwrap();
let lib = read(&root, "src/lib.rs").replace("event(scheduled)", "");
fs::write(root.join("src/lib.rs"), &lib).unwrap();
let without = pristine.replace("// ocre:triggers", "");
fs::write(root.join(CONFIG), &without).unwrap();
let (report, _) = sandbox.json(&["g", "schedule", "a", "0 3 * * *"], &root);
assert_eq!(report["error"], "cloudflare.config.ts is missing the `// ocre:triggers` marker");
assert!(report["hint"].as_str().unwrap().contains("inside `worker.triggers: [ ... ]`"), "{report}");
assert_eq!(read(&root, CONFIG), without);
fs::write(root.join(CONFIG), &pristine).unwrap();
let unreadable = hand_write(&root, "// ocre:triggers", "triggers.scheduled({ schedule: nightly }),");
let (report, _) = sandbox.json(&["g", "schedule", "a", "0 3 * * *"], &root);
assert_eq!(report["error"], "cloudflare.config.ts has a scheduled trigger Ocre cannot read");
assert_eq!(read(&root, CONFIG), unreadable);
assert!(!root.join("src/schedules").exists());
assert_eq!(read(&root, "src/lib.rs"), lib);
fs::write(root.join(CONFIG), &pristine).unwrap();
fs::write(root.join("src/lib.rs"), format!("{lib}\n// #[worker::event(scheduled)]\n")).unwrap();
let (report, _) = sandbox.json(&["g", "schedule", "a", "0 3 * * *"], &root);
assert_eq!(report["error"], "src/lib.rs already handles the `scheduled` event");
fs::write(root.join("src/lib.rs"), "fn main() {}\n").unwrap();
let (report, _) = sandbox.json(&["g", "schedule", "a", "0 3 * * *"], &root);
assert_eq!(report["error"], "src/lib.rs is missing the `// ocre:modules` marker");
fs::write(root.join("src/lib.rs"), &lib).unwrap();
assert_eq!(read(&root, CONFIG), pristine);
let (report, ok) = sandbox.json(&["g", "schedule", "a", "0 3 * * *"], &root);
assert!(ok, "{report}");
let registry = read(&root, "src/schedules/mod.rs");
for marker in ["// ocre:schedules", "// ocre:schedule-dispatch"] {
fs::write(root.join("src/schedules/mod.rs"), registry.replace(marker, "")).unwrap();
let (report, _) = sandbox.json(&["g", "schedule", "b", "0 4 * * *"], &root);
assert_eq!(report["error"], format!("src/schedules/mod.rs is missing the `{marker}` marker"));
}
assert_eq!(crons(&sandbox, &root), ["0 3 * * *"]);
}
#[test]
fn deploy_creates_missing_queues_once() {
let sandbox = Sandbox::new();
let root = sandbox.new_app("shop", &[]);
sandbox.set("has_secret");
sandbox.remote_database("shop");
let (report, ok) = sandbox.json(&["g", "job", "Ping"], &root);
assert!(ok, "{report}");
sandbox.set("queue_shop-jobs");
sandbox.clear_calls();
let output = sandbox.ocre(&["deploy"], &root);
let (stdout, _) = text(&output);
assert!(output.status.success(), "{stdout}");
assert!(stdout.contains("Created queue shop-jobs-failed on Cloudflare\n"), "{stdout}");
assert_eq!(
sandbox.calls(),
[
"cf d1 list --name shop",
"cf queues list",
"cf queues create --queue-name shop-jobs-failed",
"cf workers secrets list --worker shop",
"cf d1 migrations apply uuid-shop",
"cf deploy --secrets-file .wrangler/ocre-secrets.json",
"secrets file ok",
"build --release",
]
);
sandbox.clear_calls();
let (report, ok) = sandbox.json(&["deploy"], &root);
assert!(ok, "{report}");
assert!(report.get("provisioned").is_none(), "{report}");
assert!(!sandbox.calls().iter().any(|call| call.starts_with("cf queues create")), "{:?}", sandbox.calls());
fs::remove_file(sandbox.work.join("../state/queue_shop-jobs")).unwrap();
let (report, ok) = sandbox.json(&["g", "job", "SendCode", "--queue", "urgent"], &root);
assert!(ok, "{report}");
sandbox.clear_calls();
let (report, ok) = sandbox.json(&["deploy"], &root);
assert!(ok, "{report}");
assert_eq!(
report["provisioned"],
json!(["queue shop-jobs-urgent", "queue shop-jobs", "queue shop-jobs-urgent-failed"])
);
assert_eq!(sandbox.calls().iter().filter(|call| *call == "cf queues list").count(), 1);
let unset = |marker: &str| fs::remove_file(sandbox.work.join("../state").join(marker)).unwrap();
sandbox.set("queues_list_fails");
let (report, ok) = sandbox.json(&["deploy"], &root);
assert!(!ok);
assert_eq!(report["error"], "`cf queues list` failed: ┌ Error\n│ queues_list_fails\n└");
assert!(report["hint"].as_str().unwrap().contains("ocre login"), "{report}");
unset("queues_list_fails");
unset("queue_shop-jobs");
sandbox.set("queues_create_fails");
let (report, ok) = sandbox.json(&["deploy"], &root);
assert!(!ok);
let error = report["error"].as_str().unwrap();
assert!(error.starts_with("`cf queues create --queue-name shop-jobs` failed: ┌ APIError\n│ [10013] "), "{error}");
assert_eq!(
sandbox.calls().last().unwrap(),
"cf queues create --queue-name shop-jobs",
"never deployed without its queues"
);
unset("queues_create_fails");
let config = read(&root, CONFIG);
rewrite(&root, "{ name: \"shop-jobs-urgent\" }", "{ name: urgent }");
sandbox.clear_calls();
let (report, _) = sandbox.json(&["deploy"], &root);
assert_eq!(report["error"], "cloudflare.config.ts defines `JOBS_URGENT` in a form Ocre cannot read");
assert_eq!(report["hint"], "write it as a literal: `JOBS_URGENT: bindings.queue({ name: \"<queue>\" }),`");
fs::write(root.join(CONFIG), &config).unwrap();
rewrite(&root, "triggers.queue({ name: \"shop-jobs\",", "triggers.queue({ name: jobs,");
let (report, _) = sandbox.json(&["deploy"], &root);
assert_eq!(report["error"], "cloudflare.config.ts has a queue trigger Ocre cannot read");
assert!(!sandbox.calls().iter().any(|call| call.starts_with("cf queues") || call.starts_with("cf deploy")));
}