#[path = "../support/mod.rs"]
mod support;
use std::fs;
use support::{Sandbox, text};
#[test]
fn list_merges_local_and_deployed_names() {
let sandbox = Sandbox::new();
let root = sandbox.new_app("shop", &[]);
let dev_vars = fs::read_to_string(root.join(".dev.vars")).unwrap();
fs::write(root.join(".dev.vars"), format!("{dev_vars}# a comment\n\nGITHUB_CLIENT_ID=\"abc\"\nnot a pair\n"))
.unwrap();
sandbox.set("has_secret");
let (report, ok) = sandbox.json(&["secrets", "list"], &root);
assert!(ok, "{report}");
assert_eq!(report["command"], "secrets list");
assert_eq!(
report["secrets"],
serde_json::json!([
{"name": "GITHUB_CLIENT_ID", "local": true, "deployed": false},
{"name": "MAIL_ADAPTER", "local": true, "deployed": false},
{"name": "OTHER", "local": false, "deployed": true},
{"name": "SECRET_KEY_BASE", "local": true, "deployed": true},
])
);
assert_eq!(report["next"], serde_json::json!(["ocre secrets push GITHUB_CLIENT_ID --file <production values>"]));
assert_eq!(sandbox.calls(), ["cf workers secrets list --worker shop"]);
let (stdout, _) = text(&sandbox.ocre(&["secrets", "list"], &root));
assert!(stdout.contains(" OTHER deployed\n"), "{stdout}");
fs::remove_file(root.join(".dev.vars")).unwrap();
sandbox.set("secret_list_fails");
let (report, ok) = sandbox.json(&["secrets", "list"], &root);
assert!(ok, "{report}");
assert_eq!(report["secrets"], serde_json::json!([]));
assert!(report.get("next").is_none());
fs::create_dir(root.join(".dev.vars")).unwrap();
let (report, ok) = sandbox.json(&["secrets", "list"], &root);
assert!(!ok);
assert!(report["error"].as_str().unwrap().starts_with("cannot read .dev.vars"), "{report}");
}
#[test]
fn push_uploads_named_values_and_deletes_the_file() {
let sandbox = Sandbox::new();
let root = sandbox.new_app("shop", &[]);
fs::write(root.join(".prod.vars"), "GITHUB_CLIENT_ID='id-1'\nGITHUB_CLIENT_SECRET = s3cret\nUNUSED=x\n").unwrap();
let (report, ok) =
sandbox.json(&["secrets", "push", "GITHUB_CLIENT_ID", "GITHUB_CLIENT_SECRET", "--file", ".prod.vars"], &root);
assert!(ok, "{report}");
assert_eq!(report["ran"], serde_json::json!(["uploaded GITHUB_CLIENT_ID", "uploaded GITHUB_CLIENT_SECRET"]));
let calls = sandbox.calls();
assert_eq!(calls[0], "cf workers secrets bulk --worker shop --file .wrangler/ocre-secrets-push.json");
let body: serde_json::Value = serde_json::from_str(calls[1].strip_prefix("uploaded ").unwrap()).unwrap();
assert_eq!(
body,
serde_json::json!({"secrets": {
"GITHUB_CLIENT_ID": {"name": "GITHUB_CLIENT_ID", "type": "secret_text", "text": "id-1"},
"GITHUB_CLIENT_SECRET": {"name": "GITHUB_CLIENT_SECRET", "type": "secret_text", "text": "s3cret"},
}})
);
assert_eq!(calls.len(), 2);
assert!(!root.join(".wrangler/ocre-secrets-push.json").exists(), "values do not stay on disk");
let (report, _) = sandbox.json(&["secrets", "list"], &root);
let deployed: Vec<&str> = report["secrets"]
.as_array()
.unwrap()
.iter()
.filter(|secret| secret["deployed"] == true)
.map(|secret| secret["name"].as_str().unwrap())
.collect();
assert!(deployed.contains(&"GITHUB_CLIENT_ID") && deployed.contains(&"GITHUB_CLIENT_SECRET"), "{report}");
sandbox.set("secret_bulk_fails");
let (report, ok) = sandbox.json(&["secrets", "push", "UNUSED", "--file", ".prod.vars"], &root);
assert!(!ok);
let error = report["error"].as_str().unwrap();
assert!(
error.starts_with("`cf workers secrets bulk --worker shop --file .wrangler/ocre-secrets-push.json` failed"),
"{report}"
);
assert!(!root.join(".wrangler/ocre-secrets-push.json").exists(), "deleted after a failure too");
}
#[test]
fn push_refuses_missing_names_and_development_values() {
let sandbox = Sandbox::new();
let root = sandbox.new_app("shop", &[]);
let (report, ok) = sandbox.json(&["secrets", "push"], &root);
assert!(!ok);
assert_eq!(report["error"], "name the secrets to upload");
let (report, ok) = sandbox.json(&["secrets", "push", "SECRET_KEY_BASE"], &root);
assert!(!ok);
assert_eq!(report["error"], "SECRET_KEY_BASE in .dev.vars is a development value");
assert!(report["hint"].as_str().unwrap().contains("--file"));
let (report, ok) = sandbox.json(&["secrets", "push", "NOPE"], &root);
assert!(!ok);
assert_eq!(report["error"], "NOPE is not set in .dev.vars");
assert!(sandbox.calls().iter().all(|call| !call.starts_with("uploaded")), "nothing uploaded");
}
#[test]
fn a_redeploy_keeps_the_pushed_secrets() {
let sandbox = Sandbox::new();
let root = sandbox.new_app("shop", &[]);
let deployed = |sandbox: &Sandbox| {
let (report, ok) = sandbox.json(&["secrets", "list"], &root);
assert!(ok, "{report}");
let secrets = report["secrets"].as_array().unwrap();
secrets
.iter()
.filter(|s| s["deployed"] == true)
.map(|s| s["name"].as_str().unwrap().to_owned())
.collect::<Vec<_>>()
};
let (report, ok) = sandbox.json(&["deploy"], &root);
assert!(ok && report["secret_created"] == true, "{report}");
fs::write(root.join(".prod.vars"), format!("{}A=1\nB=2\n", fs::read_to_string(root.join(".prod.vars")).unwrap()))
.unwrap();
let (report, ok) = sandbox.json(&["secrets", "push", "A", "B", "--file", ".prod.vars"], &root);
assert!(ok, "{report}");
assert_eq!(deployed(&sandbox), ["A", "B", "OTHER", "SECRET_KEY_BASE"]);
let (report, ok) = sandbox.json(&["deploy"], &root);
assert!(ok && report.get("secret_created").is_none(), "{report}");
assert_eq!(deployed(&sandbox), ["A", "B", "OTHER", "SECRET_KEY_BASE"]);
assert_eq!(fs::read_to_string(sandbox.work.join("../state/uploaded_secrets")).unwrap(), "{}");
}
#[test]
fn store_secrets_are_bound_once_and_updated_in_place() {
let sandbox = Sandbox::new();
let root = sandbox.new_app("shop", &[]);
fs::write(root.join(".prod.vars"), "RESEND_API_KEY=re_1\nRUNPOD_TOKEN=rp_1\n").unwrap();
let (report, ok) =
sandbox.json(&["secrets", "push", "RESEND_API_KEY", "RUNPOD_TOKEN", "--store", "--file", ".prod.vars"], &root);
assert!(ok, "{report}");
assert_eq!(
report["ran"],
serde_json::json!([
"stored RESEND_API_KEY in the Secrets Store store1",
"stored RUNPOD_TOKEN in the Secrets Store store1"
])
);
assert_eq!(report["updated"], serde_json::json!(["cloudflare.config.ts"]));
assert_eq!(report["next"], serde_json::json!(["ocre deploy (the Worker reads a new binding once deployed)"]));
let config = fs::read_to_string(root.join("cloudflare.config.ts")).unwrap();
assert!(
config.contains(
"RESEND_API_KEY: bindings.secretsStoreSecret({ storeId: \"store1\", secretName: \"RESEND_API_KEY\" }),"
),
"{config}"
);
let calls = sandbox.calls();
assert!(
calls.contains(&"cf secrets-store secrets create store1 --body @.wrangler/ocre-store-secret.json".to_owned()),
"{calls:?}"
);
assert!(
calls.contains(&r#"store body [{"name":"RESEND_API_KEY","scopes":["workers"],"value":"re_1"}]"#.to_owned()),
"{calls:?}"
);
assert!(!root.join(".wrangler/ocre-store-secret.json").exists(), "the value does not stay on disk");
sandbox.clear_calls();
fs::write(root.join(".prod.vars"), "RESEND_API_KEY=re_2\n").unwrap();
let (report, ok) = sandbox.json(&["secrets", "push", "RESEND_API_KEY", "--store", "--file", ".prod.vars"], &root);
assert!(ok, "{report}");
assert!(report.get("updated").is_none() && report.get("next").is_none(), "{report}");
let calls = sandbox.calls();
assert!(calls.contains(&"cf secrets-store secrets edit id-RESEND_API_KEY --store-id store1 --body @.wrangler/ocre-store-secret.json".to_owned()), "{calls:?}");
assert!(calls.contains(&r#"store body {"scopes":["workers"],"value":"re_2"}"#.to_owned()), "{calls:?}");
let (report, ok) = sandbox.json(&["secrets", "list"], &root);
assert!(ok, "{report}");
let resend = report["secrets"].as_array().unwrap().iter().find(|s| s["name"] == "RESEND_API_KEY").unwrap().clone();
assert_eq!(
resend,
serde_json::json!({"name": "RESEND_API_KEY", "local": false, "deployed": true, "store": "store1"})
);
let (stdout, _) = text(&sandbox.ocre(&["secrets", "list"], &root));
assert!(
stdout.lines().any(|line| line.starts_with(" RESEND_API_KEY ") && line.ends_with("in the Secrets Store")),
"{stdout}"
);
}
#[test]
fn store_pushes_explain_what_they_cannot_do() {
let sandbox = Sandbox::new();
let root = sandbox.new_app("shop", &[]);
fs::write(root.join(".prod.vars"), "SECRET_KEY_BASE=x\nMAIL_FROM=a@b.c\nAPI=1\n").unwrap();
let push = |names: &[&str]| {
let mut args = vec!["secrets", "push"];
args.extend(names);
args.extend(["--store", "--file", ".prod.vars"]);
sandbox.json(&args, &root).0
};
assert_eq!(push(&["SECRET_KEY_BASE"])["error"], "SECRET_KEY_BASE cannot live in the Secrets Store");
assert_eq!(push(&["MAIL_FROM"])["error"], "MAIL_FROM is already a `text` binding in cloudflare.config.ts");
assert_eq!(push(&["MISSING"])["error"], "MISSING is not set in .prod.vars");
assert_eq!(push(&[])["error"], "name the secrets to upload");
sandbox.write_state("stores.json", "[]");
let error = push(&["API"]);
assert_eq!(error["error"], "the account has no Secrets Store");
sandbox.clear_calls();
fs::write(root.join(".prod.vars"), "SECRET_KEY_BASE=x\nAPI=1\n").unwrap();
let (report, ok) =
sandbox.json(&["secrets", "push", "OTHER", "--store", "--store-id", "s9", "--file", ".prod.vars"], &root);
assert!(!ok, "OTHER is not in the file: {report}");
fs::write(root.join(".prod.vars"), "OTHER=1\n").unwrap();
let (report, ok) =
sandbox.json(&["secrets", "push", "OTHER", "--store", "--store-id", "s9", "--file", ".prod.vars"], &root);
assert!(ok, "{report}");
assert!(report["next"][0].as_str().unwrap().starts_with("the Worker also has its own OTHER secret"), "{report}");
assert!(!sandbox.calls().iter().any(|call| call.starts_with("cf secrets-store stores")), "{:?}", sandbox.calls());
}
#[test]
fn dev_copies_dev_vars_values_into_the_local_store() {
let sandbox = Sandbox::new();
let root = sandbox.new_app("shop", &[]);
let config = fs::read_to_string(root.join("cloudflare.config.ts")).unwrap().replace(
"// ocre:env",
"// ocre:env\n\t\t\tAPI_KEY: bindings.secretsStoreSecret({ storeId: \"s1\", secretName: \"SHARED_KEY\" }),\n\t\t\t\
UNSET: bindings.secretsStoreSecret({ storeId: \"s1\", secretName: \"UNSET\" }),\n\t\t\t\
COMPUTED: bindings.secretsStoreSecret({ storeId: STORE, secretName: \"COMPUTED\" }),",
);
fs::write(root.join("cloudflare.config.ts"), config).unwrap();
let vars = fs::read_to_string(root.join(".dev.vars")).unwrap();
fs::write(root.join(".dev.vars"), format!("{vars}API_KEY=dev-value\n")).unwrap();
let (report, ok) = sandbox.json(&["dev", "--port", "9124"], &root);
assert!(ok, "{report}");
assert_eq!(report["ran"], serde_json::json!(["API_KEY: .dev.vars value copied into the local Secrets Store"]));
assert!(
sandbox.calls().contains(
&"wrangler secrets-store secret create s1 --name SHARED_KEY --value dev-value --scopes workers --persist-to .wrangler/state"
.to_owned()
),
"{:?}",
sandbox.calls()
);
let (report, _) = sandbox.json(&["secrets", "list"], &root);
let api = report["secrets"].as_array().unwrap().iter().find(|s| s["name"] == "API_KEY").unwrap().clone();
assert_eq!(api, serde_json::json!({"name": "API_KEY", "local": true, "deployed": false, "store": "s1"}));
assert_eq!(report["next"], serde_json::json!(["ocre secrets push API_KEY --store --file <production values>"]));
let (stdout, _) = text(&sandbox.ocre(&["secrets", "list"], &root));
assert!(
stdout.lines().any(|line| line.starts_with(" API_KEY ") && line.ends_with("bound, not in the Secrets Store")),
"{stdout}"
);
sandbox.set("local_store_fails");
let (report, ok) = sandbox.json(&["dev", "--port", "9124"], &root);
assert!(!ok);
let error = report["error"].as_str().unwrap();
assert!(
error.starts_with("cannot copy API_KEY from .dev.vars into the local Secrets Store")
&& !error.contains("dev-value"),
"{error}"
);
}