crates/upcloud-api/src/mock_door.rs
//! **Behaviour 63's guard: did the mock actually HEAR this run?**
//!
//! terraform's UpCloud provider reaches a mock only through its undocumented
//! `UPCLOUD_DEBUG_API_BASE_URL`. A provider release that drops that knob sends
//! a "mock" `apply` or `destroy` to THE ACCOUNT — with whatever token the shell
//! carried. The guard (lane T13's ledger, behaviour 63; lane T14 wires it):
//!
//! 1. a mock run presents a token minted for THIS run ([`mint_token`]) — never
//! a token of the account, so a provider that ignored the knob is refused by
//! the account with a 401 instead of acting;
//! 2. before `apply`/`destroy`, a read-only step (a `plan`) runs with it;
//! 3. [`require_heard`] asks the mock's `/mock/heard` door whether that token's
//! requests — and a `GET /1.3/account` among them — arrived. Zero is a
//! refusal by name: the provider spoke to somebody, and it was not the mock.
use crate::Endpoint;
/// A bearer for ONE mock run: `ucat_mock_<pid>_<nanos>`. It is not a secret;
/// it is a name the mock can hear.
pub fn mint_token() -> String {
let nanos = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_nanos()).unwrap_or(0);
format!("ucat_mock_{}_{nanos}", std::process::id())
}
/// What the mock heard from one token.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Heard {
pub requests: u64,
pub account_calls: u64,
}
/// Ask the mock at `endpoint` what it heard from `token`. The account has no
/// such door: asking it is a programming error, refused by name.
pub fn heard(endpoint: &Endpoint, token: &str) -> Result<Heard, String> {
let Endpoint::Mock(base) = endpoint else {
return Err("REFUSED [heard-asked-of-the-account] /mock/heard is a door of the mock; the account has none".into());
};
let root = base.as_str().trim_end_matches("/1.3");
let digest = nornir_hash::sha256_hex(token.as_bytes());
let url = format!("{root}/mock/heard?token_sha256={digest}");
let cfg = ureq::Agent::config_builder().http_status_as_error(false).timeout_global(Some(std::time::Duration::from_secs(10))).build();
let mut res = ureq::Agent::new_with_config(cfg).get(&url).call().map_err(|e| format!("GET /mock/heard: {e}"))?;
let status = res.status().as_u16();
let text = res.body_mut().read_to_string().map_err(|e| format!("GET /mock/heard: {e}"))?;
if status != 200 {
return Err(format!("GET /mock/heard answered {status} — {} (a mock without behaviour 63 cannot vouch for a run)", text.trim()));
}
let v: serde_json::Value = serde_json::from_str(&text).map_err(|e| format!("GET /mock/heard: not JSON ({e}): {text}"))?;
Ok(Heard {
requests: v["requests"].as_u64().unwrap_or(0),
account_calls: v["account_calls"].as_u64().unwrap_or(0),
})
}
/// **Refuse `what` unless the mock heard `token`**, including a
/// `GET /1.3/account`. `Ok` carries what was heard, for the transcript.
pub fn require_heard(endpoint: &Endpoint, token: &str, what: &str) -> Result<Heard, String> {
let h = heard(endpoint, token)?;
if h.account_calls == 0 {
return Err(format!(
"REFUSED [mock-never-heard-this-run] before {what}: the mock at {} heard {} request(s) and NO `GET \
/1.3/account` from this run's token. The client that was supposed to be aimed at it spoke to somebody \
else — the likeliest is a terraform provider that no longer honours UPCLOUD_DEBUG_API_BASE_URL, which \
would send this {what} to THE ACCOUNT. Nothing was applied.",
endpoint.base_for_display(),
h.requests
));
}
Ok(h)
}