Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/monetize

crates/mock-upcloud/src/http.rs

//! **The socket: HTTP/1.1, hand-rolled, because one of the behaviours is not a
//! status code.**
//!
//! Behaviour 2 is that `GET /1.3/price` fails at the **transport** level —
//! connection reset, nothing written — and monetize's start-time credential
//! probe reported "the credential in UPCLOUD_TOKEN could not be verified" for
//! what was a dropped socket. A framework that hands back a `Response` cannot
//! express "write nothing and close", and a mock that answered `503` there would
//! never have provoked the misreport. So the server is a `TcpListener`, a request
//! parser and a response writer: about two hundred lines, and every one of the
//! twenty-two behaviours is reachable from it.
//!
//! Keep-alive is supported (the real plugin's `reqwest` client pools
//! connections, and a mock that closed every socket would hide a pooling bug).
//! Bodies are read by `Content-Length`; `Transfer-Encoding: chunked` requests
//! are refused by name, because the real API does not send them and a silent
//! mis-parse is worse than a refusal.
//!
//! # The surface
//!
//! | method | path | notes |
//! |---|---|---|
//! | GET | `/1.3/price` | behaviour 2 lives here |
//! | GET | `/1.3/account` | what a credential probe reads |
//! | GET | `/1.3/server` | `?label=k=v`, repeatable; **thin rows** (behaviour 4) |
//! | GET | `/1.3/server/{uuid}` | the only place attachments and IPs exist; `404 SERVER_NOT_FOUND` for a uuid the LIST carries (36, by name) |
//! | POST | `/1.3/server` | 98–105 s in `maintenance` (behaviour 6); `412 out_of_stock` in a sold-out zone (37); `simple_backup` over a disk with its own `backup_rule` is `409 BACKUP_RULE_CONFLICT` (73) |
//! | PUT | `/1.3/server/{uuid}` | plan · boot_order · labels · remote_access (behaviours 9, 14, 17) · `simple_backup` (`409 BACKUP_RULE_CONFLICT` over a disk with its own rule, 73) |
//! | DELETE | `/1.3/server/{uuid}` | `?storages=1&backups=keep`; needs `stopped` (11); slow (7) |
//! | POST | `/1.3/server/{uuid}/start` | `412 out_of_stock` (behaviour 3) |
//! | POST | `/1.3/server/{uuid}/stop` | `soft`/`hard`; `timeout` is a STRING |
//! | POST | `/1.3/server/{uuid}/restart` | |
//! | POST | `/1.3/server/{uuid}/storage/attach` | a disk with a `backup_rule` onto a `simple_backup` server is `409 BACKUP_RULE_CONFLICT` (73) |
//! | POST | `/1.3/server/{uuid}/storage/detach` | refused on a started server (16); can answer `200` and leave the device attached (38, by name) |
//! | POST | `/1.3/server/{uuid}/cdrom/eject` | works on a started server (16) |
//! | POST | `/1.3/server/{uuid}/cdrom/load` | a medium into an EMPTY tray; `409 CDROM_DEVICE_IN_USE` otherwise (L110, documented, not measured) |
//! | GET | `/1.3/server/{uuid}/firewall_rule` | **`403 ERROR_AUTHENTICATION_FAILED` for a deleted server** (1); the SAME 403 for a live one under a credential without the firewall permission (35, by name) |
//! | GET | `/1.3/storage` | includes public templates |
//! | GET | `/1.3/storage/private` | the account's own |
//! | GET | `/1.3/storage/backup` | the account's backups, paged; each names its `origin` (S8) |
//! | GET | `/1.3/storage/{uuid}` | |
//! | POST | `/1.3/storage` | `backup_rule` kept and rendered back (`retention` a string) |
//! | PUT | `/1.3/storage/{uuid}` | a shrink is refused by name (22); `backup_rule` set or cleared (`{}`); set on a disk a `simple_backup` server holds is `409 BACKUP_RULE_CONFLICT` (73) |
//! | POST | `/1.3/storage/{uuid}/resize` | takes a `Resize Backup` FIRST and hands it back as `resize_backup` (34) |
//! | DELETE | `/1.3/storage/{uuid}` | `?backups=delete` takes the backups whose `origin` it is; kept otherwise (the account's default) |
//! | POST | `/1.3/storage/{uuid}/import` | direct upload |
//! | POST | `/1.3/storage/{uuid}/backup` | a `type: backup` storage, `origin` the volume (upgrade-backup) |
//! | POST | `/1.3/storage/{backup}/restore` | `204`; only onto a stopped server (upgrade-backup); carries what its box wrote (S8) |
//! | POST | `/1.3/storage/{uuid}/clone` | any online storage, a backup too (S8: a lost front's disk cloned from its daily backup) |
//!
//! A server created with `simple_backup "HHMM,dailies"` is backed up by the mock itself at
//! that hour (UTC, virtual clock; seven kept per disk) — S8, `Estate::run_simple_backups`.
//!
//! # The terraform door
//!
//! `private-holger-ops` reaches the same account through `UpCloudLtd/upcloud`
//! 5.44 rather than through the plugin, and asks for four things the plugin
//! never asks for. They are listed apart because they are INFERRED from the
//! provider's own calls rather than measured against the account — see
//! [`crate::tf`].
//!
//! | method | path | notes |
//! |---|---|---|
//! | GET | `/1.3/plan` | read before EVERY server create; an unknown plan is refused there |
//! | GET | `/1.3/storage/public` · `/template` · `/favorite` | how an OS template is resolved BY TITLE |
//! | POST | `/1.3/storage/{uuid}/templatize` | a storage made into a custom template (templates, 2026-10-12) |
//! | PUT · POST | `/1.3/server/{uuid}/firewall_rule` | the rule SET, written whole (PUT) or appended to (POST) |
//! | DELETE | `/1.3/server/{uuid}/firewall_rule/{position}` | one rule by position |
//!
//! `POST /1.3/server` takes the same door's richer body: the machine's network
//! interfaces, its boot order, its firewall flag, its timezone and every device
//! — the template to clone AND the volumes to attach.
//!
//! Everything else answers `404 MOCK_UPCLOUD_NOT_IMPLEMENTED` naming the path.
//!
//! # The mock's own door
//!
//! `/mock/…` is not an UpCloud path and cannot collide with one: `/mock/estate`
//! (everything, for a test's assertions), `/mock/fault/{name}/arm|disarm`,
//! `/mock/relay` (behaviour 12: reshuffle the addresses), `/mock/seed`.

use crate::estate::{BootOrder, Estate, Label, Refusal, StorageKind};
use crate::faults::{Fault, Faults};
use crate::render;
use serde_json::{json, Value};
use std::sync::{Arc, Mutex};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};

/// The whole mock: one estate behind one lock. A real UpCloud account is one
/// serialized thing too — two `POST /1.3/storage` calls do not interleave — so
/// the lock is not a simplification, it is the provider's own concurrency.
pub struct Mock {
    pub estate: Mutex<Estate>,
    /// **UpCloud Object Storage, as the shop's release media need it** (2026-10-09):
    /// `bucket/key` → the object's bytes. `PUT /objsto/<bucket>/<key>` (a SigV4
    /// Authorization header present) writes one; a presigned `GET` reads it.
    pub objects: Mutex<std::collections::BTreeMap<String, Vec<u8>>>,
    /// **Behaviour 63: who the mock has heard from.** Per credential DIGEST
    /// (sha256 of the bearer value, never the value): how many requests, how
    /// many of them `GET /1.3/account`. The terraform provider reaches this
    /// mock only through the undocumented `UPCLOUD_DEBUG_API_BASE_URL`; a
    /// release that drops it would send a "mock" run to the account. A verb
    /// that mints a per-run token and asks `/mock/heard` before `apply` or
    /// `destroy` proves the provider spoke to THIS mock, or refuses.
    pub heard: Mutex<std::collections::BTreeMap<String, Heard>>,
    /// Every call, `(status, method, path, error_code)`, when built with
    /// [`Mock::recording`]. The terraform contract test reads it; a storm never
    /// turns it on.
    pub calls: Option<Mutex<Vec<RecordedCall>>>,
    /// ★ **Log every call, in order, on stderr.** Off by default and never on
    /// in a storm: a hundred thousand purchases is ten million lines.
    ///
    /// It exists because the ORDER a client calls in is a fact about the client
    /// that nothing else in this crate can show, and on 2026-09-21 that order
    /// was the whole question — `UpCloudLtd/upcloud` 5.44.1 met
    /// `SERVER_STATE_ILLEGAL` on a filesystem resize and there was no way to
    /// tell whether it had resized before stopping, after starting, or whether
    /// the mock had simply never finished the stop. A mock that reproduces
    /// provider defects and cannot say what was called when is asking every
    /// user to guess.
    pub log: bool,
    /// **Test-only injections** — see [`Injection`]. Compiled only with the
    /// `test-inject` feature, so a mock built for anything else can never answer
    /// with an invented error.
    #[cfg(feature = "test-inject")]
    pub injections: Mutex<Vec<Injection>>,
}

/// One recorded call: `(status, method, path, error_code)`.
pub type RecordedCall = (u16, String, String, String);

impl Mock {
    fn build(estate: Estate, calls: Option<Mutex<Vec<RecordedCall>>>, log: bool) -> Arc<Mock> {
        Arc::new(Mock {
            estate: Mutex::new(estate),
            objects: Mutex::default(),
            heard: Mutex::default(),
            calls,
            log,
            #[cfg(feature = "test-inject")]
            injections: Mutex::default(),
        })
    }

    pub fn new(estate: Estate) -> Arc<Mock> {
        Mock::build(estate, None, false)
    }

    /// The same mock, keeping every call for a test to read ([`Mock::calls`]).
    pub fn recording(estate: Estate) -> Arc<Mock> {
        Mock::build(estate, Some(Mutex::default()), false)
    }

    /// The same mock, narrating. See [`Mock::log`].
    pub fn logging(estate: Estate) -> Arc<Mock> {
        Mock::build(estate, None, true)
    }
}

/// One credential's traffic, as the mock heard it (behaviour 63).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Heard {
    pub requests: u64,
    pub account_calls: u64,
}

impl Mock {
    fn heard(&self, credential: &str, method: &str, path: &str) {
        let key = crate::digest::sha256_hex(credential.as_bytes());
        let mut h = self.heard.lock().unwrap();
        let e = h.entry(key).or_default();
        e.requests += 1;
        if method == "GET" && path.trim_end_matches('/') == "/1.3/account" {
            e.account_calls += 1;
        }
    }

    /// What the mock heard from the credential whose sha256 is `digest`.
    pub fn heard_from(&self, digest: &str) -> Heard {
        self.heard
            .lock()
            .unwrap()
            .get(digest)
            .copied()
            .unwrap_or_default()
    }
}

/// Bind and serve until the process ends. Returns the bound port, which is what
/// a test needs when it asked for port 0.
pub async fn serve(
    mock: Arc<Mock>,
    addr: &str,
) -> std::io::Result<(u16, tokio::task::JoinHandle<()>)> {
    let listener = TcpListener::bind(addr).await?;
    let port = listener.local_addr()?.port();
    // The import reply hands back a `direct_upload_url`. Pointed at the mock's
    // own socket, a caller that FOLLOWS that URL (rather than building one) is
    // exercised end to end without being told where to go.
    mock.estate.lock().unwrap().upload_base = format!("http://127.0.0.1:{port}");
    let h = tokio::spawn(async move {
        loop {
            let Ok((sock, _)) = listener.accept().await else {
                continue;
            };
            // **TCP_NODELAY, and it is not a micro-optimisation.** The reply
            // was written as a head and then a body, two segments, and the
            // client's delayed ACK met Nagle's algorithm: MEASURED 143
            // requests per second on LOOPBACK, about 7 ms each, where the work
            // is microseconds. A hundred thousand purchases is a hundred
            // requests each, so the stall alone was two days of the storm.
            // The head and the body are now one write as well — that is the
            // actual fix; this is the belt.
            let _ = sock.set_nodelay(true);
            let m = mock.clone();
            tokio::spawn(async move {
                let _ = connection(m, sock).await;
            });
        }
    });
    Ok((port, h))
}

// ── the parser ───────────────────────────────────────────────────────────────

struct Req {
    method: String,
    path: String,
    query: Vec<(String, String)>,
    authorized: bool,
    body: Value,
    /// The body as it arrived. The upload session needs the BYTES, because the
    /// digests it answers with are the real digests of them.
    raw: Vec<u8>,
}

pub(crate) enum Outcome {
    Reply {
        status: u16,
        body: Value,
    },
    /// A body that is not JSON: an object's bytes (`/objsto/`).
    Bytes {
        status: u16,
        bytes: Vec<u8>,
    },
    /// **Behaviour 2.** Write nothing, close the socket. The client sees a
    /// transport failure, not a status — which is the distinction that was
    /// misreported as a bad credential.
    Reset,
}

fn reply(status: u16, body: Value) -> Outcome {
    Outcome::Reply { status, body }
}

fn refuse(r: Refusal) -> Outcome {
    reply(r.status, render::error(r.code, &r.message))
}

async fn connection(mock: Arc<Mock>, mut sock: TcpStream) -> std::io::Result<()> {
    let mut buf: Vec<u8> = Vec::with_capacity(8 * 1024);
    loop {
        // Read until the headers are complete.
        let head_end = loop {
            if let Some(i) = find_crlfcrlf(&buf) {
                break i;
            }
            let mut chunk = [0u8; 4096];
            let n = sock.read(&mut chunk).await?;
            if n == 0 {
                return Ok(());
            }
            buf.extend_from_slice(&chunk[..n]);
        };
        let head = String::from_utf8_lossy(&buf[..head_end]).to_string();
        let mut lines = head.split("\r\n");
        let Some(start) = lines.next() else {
            return Ok(());
        };
        let mut parts = start.split_whitespace();
        let method = parts.next().unwrap_or("").to_string();
        let target = parts.next().unwrap_or("/").to_string();

        let mut content_length = 0usize;
        let mut authorized = false;
        let mut credential = String::new();
        let mut chunked = false;
        let mut keep_alive = true;
        for l in lines {
            let Some((k, v)) = l.split_once(':') else {
                continue;
            };
            let (k, v) = (k.trim().to_ascii_lowercase(), v.trim());
            match k.as_str() {
                "content-length" => content_length = v.parse().unwrap_or(0),
                "authorization" => {
                    authorized = !v.is_empty();
                    credential = v.strip_prefix("Bearer ").unwrap_or(v).trim().to_string();
                }
                "transfer-encoding" => chunked = v.eq_ignore_ascii_case("chunked"),
                "connection" => keep_alive = !v.eq_ignore_ascii_case("close"),
                _ => {}
            }
        }

        let body_start = head_end + 4;
        while buf.len() < body_start + content_length {
            let mut chunk = [0u8; 4096];
            let n = sock.read(&mut chunk).await?;
            if n == 0 {
                return Ok(());
            }
            buf.extend_from_slice(&chunk[..n]);
        }
        let raw: Vec<u8> = buf[body_start..body_start + content_length].to_vec();
        buf.drain(..body_start + content_length);

        let outcome = if chunked && target.starts_with("/uploader/") {
            // **Behaviour 42.** The direct-upload host wants a length: a chunked
            // PUT answers 411 (gunnar `http.rs`: "a chunked PUT to the
            // direct-upload URL answers 411, so Content-Length must be set").
            reply(
                411,
                render::error("LENGTH_REQUIRED", "Content-Length is required"),
            )
        } else if chunked {
            reply(
                400,
                render::error(
                    "MOCK_UPCLOUD_CHUNKED_REQUEST",
                    "the real API is never sent a chunked request by this estate, and the mock refuses to guess",
                ),
            )
        } else {
            answer(
                &mock,
                &method,
                &target,
                authorized.then_some(credential.as_str()),
                raw,
            )
        };

        match outcome {
            Outcome::Reset => return Ok(()),
            Outcome::Bytes { status, bytes } => {
                let head = format!(
                    "HTTP/1.1 {status} {}\r\nContent-Type: application/octet-stream\r\nContent-Length: {}\r\nConnection: {}\r\n\r\n",
                    reason(status),
                    bytes.len(),
                    if keep_alive { "keep-alive" } else { "close" }
                );
                sock.write_all(head.as_bytes()).await?;
                sock.write_all(&bytes).await?;
                sock.flush().await?;
                if !keep_alive {
                    return Ok(());
                }
            }
            Outcome::Reply { status, body } => {
                let text = if body.is_null() {
                    String::new()
                } else {
                    body.to_string()
                };
                let head = format!(
                    "HTTP/1.1 {status} {}\r\nContent-Type: {}\r\nContent-Length: {}\r\nConnection: {}\r\n\r\n",
                    reason(status),
                    if status == 403 && text.contains("correlation_id") {
                        // Behaviour 1 answers problem+json, as the real one does.
                        "application/problem+json"
                    } else {
                        "application/json"
                    },
                    text.len(),
                    if keep_alive { "keep-alive" } else { "close" }
                );
                // ONE write: a head segment followed by a body segment is what
                // Nagle holds on to. See the note at `accept`.
                let mut out = Vec::with_capacity(head.len() + text.len());
                out.extend_from_slice(head.as_bytes());
                out.extend_from_slice(text.as_bytes());
                sock.write_all(&out).await?;
                sock.flush().await?;
                if !keep_alive {
                    return Ok(());
                }
            }
        }
    }
}

fn reason(s: u16) -> &'static str {
    match s {
        200 => "OK",
        201 => "Created",
        202 => "Accepted",
        204 => "No Content",
        400 => "Bad Request",
        401 => "Unauthorized",
        402 => "Payment Required",
        403 => "Forbidden",
        404 => "Not Found",
        409 => "Conflict",
        411 => "Length Required",
        412 => "Precondition Failed",
        429 => "Too Many Requests",
        502 => "Bad Gateway",
        503 => "Service Unavailable",
        511 => "Network Authentication Required",
        _ => "Error",
    }
}

fn find_crlfcrlf(b: &[u8]) -> Option<usize> {
    b.windows(4).position(|w| w == b"\r\n\r\n")
}

fn split_target(t: &str) -> (String, Vec<(String, String)>) {
    match t.split_once('?') {
        None => (t.to_string(), vec![]),
        Some((p, q)) => {
            let params = q
                .split('&')
                .filter(|s| !s.is_empty())
                .map(|kv| {
                    let (k, v) = kv.split_once('=').unwrap_or((kv, ""));
                    (percent_decode(k), percent_decode(v))
                })
                .collect();
            (p.to_string(), params)
        }
    }
}

/// `%3D` → `=`, `+` → space. The whole need: a label filter is
/// `?label=monetize_ref%3Dabc`, and nothing else in this API is encoded.
fn percent_decode(s: &str) -> String {
    let b = s.as_bytes();
    let mut out = Vec::with_capacity(b.len());
    let mut i = 0;
    while i < b.len() {
        match b[i] {
            b'%' if i + 2 < b.len() => {
                let h = (hex(b[i + 1]), hex(b[i + 2]));
                if let (Some(a), Some(c)) = h {
                    out.push(a * 16 + c);
                    i += 3;
                    continue;
                }
                out.push(b[i]);
                i += 1;
            }
            b'+' => {
                out.push(b' ');
                i += 1;
            }
            c => {
                out.push(c);
                i += 1;
            }
        }
    }
    String::from_utf8_lossy(&out).to_string()
}

fn hex(c: u8) -> Option<u8> {
    match c {
        b'0'..=b'9' => Some(c - b'0'),
        b'a'..=b'f' => Some(c - b'a' + 10),
        b'A'..=b'F' => Some(c - b'A' + 10),
        _ => None,
    }
}

// ── the router ───────────────────────────────────────────────────────────────

fn route(mock: &Arc<Mock>, r: Req) -> Outcome {
    if r.path.starts_with("/mock/") {
        return mock_door(mock, &r);
    }
    // The upload session lives OUTSIDE /1.3, at the host the import reply named
    // — `https://<zone>.img.upcloud.com/uploader/session/<uuid>` at the real
    // provider, the mock's own socket here. It takes no Authorization header:
    // the session id in the path IS the credential, which is worth reproducing
    // because it means the URL is a bearer token in a log line.
    if let Some(uuid) = r.path.strip_prefix("/uploader/session/") {
        let uuid = uuid.to_string();
        let mut e = mock.estate.lock().unwrap();
        e.tick();
        return match e.upload(&uuid, &r.raw) {
            Err(x) => refuse(x),
            // **The PUT answers the import object BARE — `{"written_bytes": …,
            // "sha256sum": …}` — not wrapped in `storage_import`** (behaviour 67).
            // MEASURED by the live re-image on 2026-09-21 08:42 (gunnar-upcloud
            // reads `written_bytes` at the top level and its receipt says
            // `done: true`); the wrapped shape here failed that same procedure
            // with "the upload reply carries no `written_bytes`" (lane T14).
            // UN-ENVELOPED: the uploader answers the import's fields at the
            // top level — `{"written_bytes": …, "md5sum": …, "sha256sum": …}`
            // (gunnar deploy/upcloud/src/upload.rs:5-8), which is what the
            // live tool parsed on every real re-image. The import object's
            // own `GET /storage/{uuid}/import` keeps its `storage_import`
            // envelope; this reply never had one.
            Ok(im) => reply(200, render::import(&im)),
        };
    }
    if let Some(rest) = r.path.strip_prefix("/objsto/") {
        return object_face(mock, &r, rest);
    }
    if !r.authorized {
        // No credential at all is the one case that IS a clean 401. A revoked
        // one is not (behaviour 5) — it answers 0 rows and 403s, which is the
        // ambiguity the whole estate has tripped over.
        return reply(
            401,
            render::error("AUTHENTICATION_FAILED", "no Authorization header was sent"),
        );
    }
    let Some(rest) = r.path.strip_prefix("/1.3") else {
        return reply(404, render::not_implemented(&r.method, &r.path));
    };
    // **Behaviour 39.** An empty segment (`/1.3//storage`, a base URL with a
    // trailing slash) is a 404 at UpCloud, with a message that names no path
    // (gunnar `api.rs`, MEASURED). The mock used to collapse it and answer.
    if rest.contains("//") {
        return reply(404, render::error("NOT_FOUND", "Not found."));
    }
    {
        let e = mock.estate.lock().unwrap();
        // **Behaviour 43.** A dead token is a clean 401, on every call
        // (MEASURED 2026-09-14, private-gunnar-ops ROTATION §2.3).
        if e.faults.fires(Fault::DeadToken) {
            return reply(
                401,
                render::error(
                    "AUTHENTICATION_FAILED",
                    "Authentication failed using the given username and password.",
                ),
            );
        }
        // **Behaviour 40.** A read answers 502 now and then (gunnar `wait.rs`).
        if r.method == "GET" && e.faults.fires(Fault::ReadBadGateway) {
            return reply(502, render::error("BAD_GATEWAY", "Bad Gateway"));
        }
    }
    let seg: Vec<&str> = rest
        .trim_matches('/')
        .split('/')
        .filter(|s| !s.is_empty())
        .collect();
    let mut e = mock.estate.lock().unwrap();
    e.tick();
    let m = r.method.as_str();

    match (m, seg.as_slice()) {
        // ── price ───────────────────────────────────────────────────────────
        ("GET", ["price"]) => {
            if e.faults.fires(Fault::PriceTransportReset) {
                return Outcome::Reset;
            }
            let zone = e.zone.clone();
            reply(200, render::price(&zone))
        }

        // What a credential probe reads. A REVOKED credential answers 403 here
        // while still answering 200-with-no-rows on the lists: the two together
        // are behaviour 5, and neither on its own reproduces it.
        ("GET", ["account"]) => {
            if e.faults.fires(Fault::RevokedCredential) {
                let cid = e.next_correlation_id();
                return reply(403, render::auth_failed(&cid));
            }
            reply(
                200,
                json!({"account": {"username": "mock", "credits": 100_000.0}}),
            )
        }

        ("GET", ["zone"]) => reply(
            200,
            json!({"zones": {"zone": [{"id": e.zone, "description": "Stockholm #1", "public": "yes"}]}}),
        ),

        // ── servers ─────────────────────────────────────────────────────────
        ("GET", ["server"]) => {
            let labels = label_filters(&r.query);
            let created = !e.faults.fires(Fault::WithholdCreatedField);
            let rows: Vec<Value> = paged(e.servers_matching(&labels), &r.query)
                .into_iter()
                .map(|s| render::server(s, false, created))
                .collect();
            reply(200, json!({"servers": {"server": rows}}))
        }

        ("GET", ["server", uuid]) => {
            if e.faults.fires(Fault::RevokedCredential) {
                let cid = e.next_correlation_id();
                return reply(403, render::auth_failed(&cid));
            }
            let created = !e.faults.fires(Fault::WithholdCreatedField);
            match e.server(uuid) {
                // **Behaviour 36.** The list carries this uuid and the detail
                // says it does not exist. Same envelope as a genuine 404, on
                // purpose: the caller cannot tell them apart HERE, only by
                // holding the list beside it.
                Some(_) if e.faults.fires(Fault::DetailNotFoundForListedServer) => reply(
                    404,
                    render::error("SERVER_NOT_FOUND", &format!("server {uuid} not found")),
                ),
                Some(s) => reply(200, json!({"server": render::server(s, true, created)})),
                None => reply(
                    404,
                    render::error("SERVER_NOT_FOUND", &format!("server {uuid} not found")),
                ),
            }
        }

        // **Behaviour 1.** The firewall endpoint, for a server that is not
        // there, answers 403 ERROR_AUTHENTICATION_FAILED with a correlation id.
        // Whether it was deleted a minute ago or never existed, the answer is
        // the same, and it is indistinguishable from a revoked token.
        //
        // **Behaviour 35** is the same 403, byte for byte, for a server that IS
        // there and reads 200 on its own detail — a credential without the
        // firewall permission. Nothing in this reply tells the two apart; the
        // server's own detail does.
        ("GET", ["server", _uuid, "firewall_rule"])
        | ("GET", ["server", _uuid, "firewall_rule", _]) => {
            let uuid = seg[1];
            if e.server(uuid).is_some() && !e.faults.fires(Fault::FirewallForbidden) {
                let rules = e.rules(uuid).unwrap_or(&[]).to_vec();
                // One rule asked for by position, or the whole set.
                if let Some(pos) = seg.get(3) {
                    return match rules.iter().find(|r| r.position == *pos) {
                        Some(r) => {
                            reply(200, json!({ "firewall_rule": crate::tf::render_rule(r) }))
                        }
                        None => reply(
                            404,
                            render::error(
                                "FIREWALL_RULE_NOT_FOUND",
                                &format!("no rule at position {pos}"),
                            ),
                        ),
                    };
                }
                return reply(200, crate::tf::render_rules(&rules));
            }
            let cid = e.next_correlation_id();
            reply(403, render::auth_failed(&cid))
        }

        // ── the firewall, WRITTEN ────────────────────────────────────────────
        // The set is replaced whole. The provider writes it in one PUT and
        // terraform's `upcloud_firewall_rules` is one resource per machine for
        // the same reason: UpCloud's firewall is a SET, and a caller that
        // thinks it is patching one rule is replacing all of them.
        //
        // The 403 of behaviours 1 and 35 is a READ answer and is not repeated
        // here: nothing has measured what a write to a deleted server's
        // firewall does, and inventing it would be the mock claiming a
        // behaviour. A write to a uuid that is not there is the ordinary 404
        // `set_rules` gives.
        ("PUT", ["server", _uuid, "firewall_rule"])
        | ("POST", ["server", _uuid, "firewall_rule"]) => {
            let uuid = seg[1].to_string();
            // Behaviour 74: every rule's fields as the API checks them, on the RAW body (the
            // parser below fills a missing direction/action the API never assumes).
            let existing = if m == "POST" {
                e.rules(&uuid).map_or(0, <[_]>::len)
            } else {
                0
            };
            if let Some(x) = crate::documented::firewall_refusal(&r.body, existing) {
                return refuse(x);
            }
            let mut rules = crate::tf::rules_from_body(&r.body);
            // A single POST APPENDS to the set; a PUT replaces it. Both end in
            // `set_rules`, which renumbers, so there is one writer.
            if m == "POST" {
                let mut existing = e.rules(&uuid).unwrap_or(&[]).to_vec();
                existing.append(&mut rules);
                rules = existing;
            }
            match e.set_rules(&uuid, rules) {
                Err(x) => refuse(x),
                Ok(()) => {
                    let rules = e.rules(&uuid).unwrap_or(&[]).to_vec();
                    reply(
                        if m == "POST" { 201 } else { 200 },
                        crate::tf::render_rules(&rules),
                    )
                }
            }
        }

        ("DELETE", ["server", _uuid, "firewall_rule", _pos]) => {
            let uuid = seg[1].to_string();
            let pos = seg[3].to_string();
            let kept: Vec<_> = e
                .rules(&uuid)
                .unwrap_or(&[])
                .iter()
                .filter(|r| r.position != pos)
                .cloned()
                .collect();
            match e.set_rules(&uuid, kept) {
                Err(x) => refuse(x),
                Ok(()) => reply(204, Value::Null),
            }
        }

        // ── the plan table ──────────────────────────────────────────────────
        // The provider reads this before EVERY server create and refuses a plan
        // that is not in it. See `tf::plans`.
        ("GET", ["plan"]) => reply(200, crate::tf::plans()),

        ("POST", ["server"]) => {
            let b = &r.body["server"];
            // Behaviour 74: the create's documented refusals, before anything is minted.
            let labels_in = read_server_labels(&b["labels"]);
            if let Some(x) = crate::documented::zone_refusal(b["zone"].as_str())
                .or_else(|| crate::documented::server_title_refusal(b["title"].as_str()))
                .or_else(|| crate::documented::hostname_refusal(b["hostname"].as_str()))
                .or_else(|| crate::documented::simple_backup_refusal(b["simple_backup"].as_str()))
                .or_else(|| crate::documented::label_refusal(&labels_in))
            {
                return refuse(x);
            }
            let plan = b["plan"].as_str().unwrap_or("1xCPU-1GB").to_string();
            if !render::plan_known(&plan) {
                return refuse(Refusal::new(
                    400,
                    "INVALID_PLAN",
                    format!("no such plan: {plan}"),
                ));
            }
            let title = b["title"].as_str().unwrap_or("").to_string();
            let hostname = b["hostname"].as_str().unwrap_or(&title).to_string();
            let zone = b["zone"].as_str().unwrap_or(&e.zone).to_string();
            let labels = read_server_labels(&b["labels"]);
            let mut devs: Vec<Value> = b["storage_devices"]["storage_device"]
                .as_array()
                .cloned()
                .unwrap_or_default();
            // **The boot disk is the one at `virtio:0`**, not the first in the
            // body: terraform's `storage_devices` is a SET, so a server laid on a
            // BLANK system disk beside data volumes sends them in hash order
            // (MEASURED 2026-09-26: main came up on a data member, which SeaBIOS
            // tried to boot and hung). A body that names no position keeps its
            // first device as the boot disk, as before.
            if let Some(i) = devs.iter().position(boot_position) {
                let d = devs.remove(i);
                devs.insert(0, d);
            }
            // Behaviour 74: a storage ATTACHED at create lives in the server's zone
            // (`409 ZONE_MISMATCH`); a clone's source may be anywhere (it is copied).
            for d in devs
                .iter()
                .filter(|d| d["action"].as_str() == Some("attach"))
            {
                if let Some(st) = d["storage"].as_str().and_then(|u| e.storage(u)) {
                    if !st.zone.is_empty() && st.zone != zone {
                        return refuse(Refusal::new(
                            409,
                            "ZONE_MISMATCH",
                            format!(
                                "storage {} is in {}, the server in {zone}",
                                st.uuid, st.zone
                            ),
                        ));
                    }
                }
            }
            let dev = devs.first().cloned().unwrap_or(Value::Null);
            let disk_title = dev["title"].as_str().unwrap_or("boot").to_string();
            let disk_gib = dev["size"].as_u64().unwrap_or(20);
            // Further entries that name a `storage` put an EXISTING one on the
            // new server (terraform sends `"action": "attach"`; the plugin may not) — an installer medium as `"type": "cdrom"` is
            // the case this estate uses. Checked BEFORE anything is minted, so a
            // refused create leaves no half a server behind.
            let mut attach: Vec<(String, String, Option<String>)> = Vec::new();
            for d in devs.iter().skip(1) {
                let Some(st) = d["storage"].as_str().map(str::to_string) else {
                    continue;
                };
                let kind = if d["type"].as_str() == Some("cdrom") {
                    "cdrom"
                } else {
                    "disk"
                }
                .to_string();
                match e.storage(&st) {
                    None => {
                        return refuse(Refusal::new(
                            404,
                            "STORAGE_NOT_FOUND",
                            format!("storage {st} not found"),
                        ))
                    }
                    Some(x) if x.state != "online" => {
                        return refuse(Refusal::new(
                            409,
                            "STORAGE_STATE_ILLEGAL",
                            format!("storage {st} is {} — wait for online", x.state),
                        ))
                    }
                    Some(_) => attach.push((st, kind, d["address"].as_str().map(str::to_string))),
                }
            }
            // **Behaviour 73**: `simple_backup` over a disk that carries its own `backup_rule`
            // is `409 BACKUP_RULE_CONFLICT`, refused before anything is minted.
            // A clone's `storage` is its SOURCE, not a disk of this server.
            let disks = devs
                .iter()
                .filter(|d| {
                    d["type"].as_str() != Some("cdrom") && d["action"].as_str() != Some("clone")
                })
                .filter_map(|d| d["storage"].as_str());
            if let Err(x) =
                e.backup_rule_conflict(b["simple_backup"].as_str().unwrap_or("no"), disks)
            {
                return refuse(x);
            }
            match e.create_server(
                &title,
                &hostname,
                &plan,
                &zone,
                labels,
                &disk_title,
                disk_gib,
            ) {
                Err(x) => refuse(x),
                Ok(uuid) => {
                    // ── the terraform half of the create body ───────────────
                    // The plugin sends a title, a plan and one disk. terraform
                    // sends the machine's interfaces, its boot order, its
                    // firewall flag, its timezone and EVERY device — the
                    // template to clone AND the data volume to attach. All of
                    // it is said here, in the create, because that is where the
                    // provider says it; a machine that had to be PUT afterwards
                    // to get its second interface would go through
                    // `modify_server`'s stop/start and model a provider nobody
                    // has.
                    let ifaces = crate::tf::interfaces_from_body(b);
                    let boot = b["boot_order"].as_str().and_then(BootOrder::parse);
                    let firewall_on = b["firewall"].as_str().unwrap_or("off") == "on";
                    let metadata = b["metadata"].as_str().unwrap_or("yes") != "no";
                    let tz = b["timezone"].as_str().unwrap_or("UTC").to_string();
                    if let Err(x) =
                        e.configure_server(&uuid, ifaces, boot, firewall_on, metadata, &tz)
                    {
                        return refuse(x);
                    }
                    // Device 0 `"action": "clone"` of a template, and the keys
                    // `login_user` lets in: what a template boot needs.
                    let template = (dev["action"].as_str() == Some("clone"))
                        .then(|| dev["storage"].as_str())
                        .flatten();
                    let keys: Vec<String> = b["login_user"]["ssh_keys"]["ssh_key"]
                        .as_array()
                        .map(|a| {
                            a.iter()
                                .filter_map(|k| k.as_str().map(str::to_string))
                                .collect()
                        })
                        .unwrap_or_default();
                    if let Err(x) = e.set_create_origin(&uuid, template, keys) {
                        return refuse(x);
                    }
                    // `user_data`: what the metadata service answers this server (a release
                    // image's per-order pairing seed).
                    if let Err(x) = e.set_user_data(&uuid, b["user_data"].as_str().unwrap_or("")) {
                        return refuse(x);
                    }
                    if let Err(x) =
                        e.set_simple_backup(&uuid, b["simple_backup"].as_str().unwrap_or("no"))
                    {
                        return refuse(x);
                    }
                    // Device 0 `"action": "attach"` of a storage terraform laid
                    // itself: a server on a BLANK disk (no template).
                    if dev["action"].as_str() == Some("attach") {
                        if let Some(st) = dev["storage"].as_str() {
                            if let Err(x) = e.adopt_boot_disk(&uuid, st) {
                                return refuse(x);
                            }
                        }
                    }
                    // Device 0 is the boot disk `create_server` already minted
                    // from the template. Every device AFTER it that names an
                    // existing storage is an attach (validated above, before
                    // anything was minted). A cdrom is legal HERE — the server
                    // has never run — which `attach_at_create` knows and a
                    // later `attach` does not.
                    for (st, kind, want) in &attach {
                        if let Err(x) = e.attach_at_create(&uuid, st, kind, want.as_deref()) {
                            return refuse(x);
                        }
                    }
                    // The object is committed. Whether the CALLER hears about it
                    // is a separate question (behaviour: CommitThenDropReply) —
                    // and a plugin that retries without a label search buys twice.
                    if e.faults.fires(Fault::CommitThenDropReply) {
                        return Outcome::Reset;
                    }
                    let created = !e.faults.fires(Fault::WithholdCreatedField);
                    let s = e.server(&uuid).expect("just created");
                    reply(201, json!({"server": render::server(s, true, created)}))
                }
            }
        }

        ("PUT", ["server", uuid]) => {
            let b = &r.body["server"];
            let plan = b["plan"].as_str().map(str::to_string);
            let bo = b["boot_order"].as_str().and_then(parse_boot_order);
            let labels = if b["labels"].is_null() {
                None
            } else {
                Some(read_server_labels(&b["labels"]))
            };
            if let Some(x) = labels
                .as_deref()
                .and_then(crate::documented::label_refusal)
                .or_else(|| crate::documented::simple_backup_refusal(b["simple_backup"].as_str()))
            {
                return refuse(x);
            }
            // `"yes"`/`"no"` — strings, like everything else boolean in this API.
            let ra = b["remote_access_enabled"].as_str().map(|s| s == "yes");
            let rap = b["remote_access_password"].as_str().map(str::to_string);
            // hostname and title are changed in place (terraform plans them so,
            // MEASURED 5.44.1). The mock dropped both, and the provider then
            // refused its own apply: ".hostname: was gunnar-front2, but now
            // gunnar-front" (lane T13, 2026-09-21).
            let rename = (
                b["hostname"].as_str().map(str::to_string),
                b["title"].as_str().map(str::to_string),
            );
            // `metadata: "yes" | "no"` — the metadata service on or off (a sold estate's
            // seed is no longer served once the shop turns it off after Done). Accepted in
            // any state: whether UpCloud refuses it on a started server is NOT MEASURED.
            let metadata = b["metadata"].as_str().map(|m| m != "no");
            // `simple_backup` changed in place (terraform's `simple_backup` block); behaviour 73
            // is asked BEFORE anything of the PUT is applied.
            let simple_backup = b["simple_backup"].as_str().map(str::to_string);
            if let Some(p) = &plan {
                if !render::plan_known(p) {
                    return refuse(Refusal::new(
                        400,
                        "INVALID_PLAN",
                        format!("no such plan: {p}"),
                    ));
                }
            }
            if let (Some(sb), Some(s)) = (&simple_backup, e.server(uuid)) {
                let disks: Vec<String> = s
                    .devices
                    .iter()
                    .filter(|d| d.kind == "disk")
                    .map(|d| d.storage.clone())
                    .collect();
                if let Err(x) = e.backup_rule_conflict(sb, disks.iter().map(String::as_str)) {
                    return refuse(x);
                }
            }
            match e.modify_server(uuid, plan.as_deref(), bo, labels, ra, rap.as_deref()) {
                Err(x) => refuse(x),
                Ok(()) => {
                    if let Some(sb) = &simple_backup {
                        if let Err(x) = e.set_simple_backup(uuid, sb) {
                            return refuse(x);
                        }
                    }
                    e.rename_server(uuid, rename.0.as_deref(), rename.1.as_deref());
                    if let Some(on) = metadata {
                        e.set_metadata(uuid, on);
                    }
                    let created = !e.faults.fires(Fault::WithholdCreatedField);
                    let s = e.server(uuid).expect("modified");
                    // **Behaviour 41.** 202, MEASURED (RESUME-2026-09-19: boot_order PUT → 202).
                    reply(202, json!({"server": render::server(s, true, created)}))
                }
            }
        }

        ("DELETE", ["server", uuid]) => {
            let with_storages = r
                .query
                .iter()
                .any(|(k, v)| k == "storages" && (v == "1" || v == "true"));
            // `storages=1&backups=delete` takes the volumes' backups too.
            let volumes: Vec<String> = e
                .server(uuid)
                .map(|s| s.devices.iter().map(|d| d.storage.clone()).collect())
                .unwrap_or_default();
            let and_backups =
                with_storages && r.query.iter().any(|(k, v)| k == "backups" && v == "delete");
            match e.delete_server(uuid, with_storages) {
                Err(x) => refuse(x),
                // 204, and the server is still there, in `maintenance`, for the
                // next five minutes (behaviour 7). A caller that reads 204 as
                // "gone" is wrong and its next list proves it.
                Ok(()) => {
                    if and_backups {
                        for v in &volumes {
                            e.delete_backups_of(v);
                        }
                    }
                    reply(204, Value::Null)
                }
            }
        }

        ("POST", ["server", uuid, "start"]) => match e.start_server(uuid) {
            Err(x) => refuse(x),
            // Behaviour 76: started, the reply lost — once.
            Ok(()) if e.faults.fires(Fault::StartThenDropReplyOnce) => {
                e.faults.disarm(Fault::StartThenDropReplyOnce);
                Outcome::Reset
            }
            Ok(()) => {
                let created = !e.faults.fires(Fault::WithholdCreatedField);
                let s = e.server(uuid).expect("started");
                reply(200, json!({"server": render::server(s, true, created)}))
            }
        },

        ("POST", ["server", uuid, "stop"]) => {
            // **Behaviour 47.** `timeout_action` belongs to RESTART; a stop that
            // carries it is a 400 (MEASURED 2026-09-14, memory:
            // upcloud-reimage-install-loop). The error code is not recorded.
            if !r.body["stop_server"]["timeout_action"].is_null() {
                return refuse(Refusal::new(
                    400,
                    "INVALID_STOP_SERVER",
                    "stop_server has no attribute timeout_action (it belongs to restart_server)",
                ));
            }
            let hard = r.body["stop_server"]["stop_type"].as_str() == Some("hard");
            // `timeout` is a STRING on the wire (behaviour 68); a number is read too.
            let timeout_s = r.body["stop_server"]["timeout"]
                .as_str()
                .and_then(|t| t.trim().parse::<u64>().ok())
                .or_else(|| r.body["stop_server"]["timeout"].as_u64());
            match e.stop_server_within(uuid, hard, timeout_s) {
                Err(x) => refuse(x),
                Ok(()) => {
                    let created = !e.faults.fires(Fault::WithholdCreatedField);
                    let s = e.server(uuid).expect("stopping");
                    reply(200, json!({"server": render::server(s, true, created)}))
                }
            }
        }

        // A restart is a stop and a start, and it goes through BOTH, so an
        // `out_of_stock` on the way back up (behaviour 3) leaves the box
        // stopped — which is the shape of the outage that was measured, and
        // not something a single `restart` state would ever show.
        ("POST", ["server", uuid, "restart"]) => {
            let uuid = uuid.to_string();
            if let Err(x) = e.stop_server(&uuid, false) {
                return refuse(x);
            }
            e.run_to_quiet();
            match e.start_server(&uuid) {
                Err(x) => refuse(x),
                Ok(()) => reply(200, json!({"server": {"uuid": uuid}})),
            }
        }

        ("POST", ["server", uuid, "storage", "attach"]) => {
            let d = &r.body["storage_device"];
            let storage = d["storage"].as_str().unwrap_or("").to_string();
            let kind = d["type"].as_str().unwrap_or("disk").to_string();
            let want = d["address"].as_str().map(str::to_string);
            // Behaviour 77: the release clone was swept under the caller — once.
            if kind == "cdrom" && e.faults.fires(Fault::CdromStorageGoneOnce) {
                e.faults.disarm(Fault::CdromStorageGoneOnce);
                let _ = e.delete_storage(&storage);
                return refuse(Refusal::new(
                    404,
                    "STORAGE_NOT_FOUND",
                    "the storage does not exist",
                ));
            }
            match e.attach_at(uuid, &storage, &kind, want.as_deref()) {
                Err(x) => refuse(x),
                Ok(_) => {
                    let created = !e.faults.fires(Fault::WithholdCreatedField);
                    let s = e.server(uuid).expect("attached");
                    reply(200, json!({"server": render::server(s, true, created)}))
                }
            }
        }

        ("POST", ["server", uuid, "storage", "detach"]) => {
            let address = r.body["storage_device"]["address"]
                .as_str()
                .unwrap_or("")
                .to_string();
            match e.detach(uuid, &address) {
                Err(x) => refuse(x),
                Ok(()) => {
                    let created = !e.faults.fires(Fault::WithholdCreatedField);
                    let s = e.server(uuid).expect("detached");
                    reply(200, json!({"server": render::server(s, true, created)}))
                }
            }
        }

        ("POST", ["server", uuid, "cdrom", "eject"]) => match e.eject(uuid) {
            Err(x) => refuse(x),
            Ok(()) => {
                let created = !e.faults.fires(Fault::WithholdCreatedField);
                let s = e.server(uuid).expect("ejected");
                reply(200, json!({"server": render::server(s, true, created)}))
            }
        },

        ("POST", ["server", uuid, "cdrom", "load"]) => {
            let storage = r.body["storage_device"]["storage"]
                .as_str()
                .unwrap_or("")
                .to_string();
            match e.load_cdrom(uuid, &storage) {
                Err(x) => refuse(x),
                Ok(()) => {
                    let created = !e.faults.fires(Fault::WithholdCreatedField);
                    let s = e.server(uuid).expect("loaded");
                    reply(200, json!({"server": render::server(s, true, created)}))
                }
            }
        }

        // ── storages ────────────────────────────────────────────────────────
        // A bare `GET /1.3/storage` lists the PUBLIC templates too — thousands
        // of rows at the real provider, four here, and none of them the
        // account's. `/storage/private` is the one a cleanup must use.
        ("GET", ["storage"]) | ("GET", ["storage", "private"]) => {
            let private_only = seg.len() == 2;
            let labels = label_filters(&r.query);
            let created = !e.faults.fires(Fault::WithholdCreatedField);
            let rows: Vec<Value> = paged(e.storages_matching(&labels, private_only), &r.query)
                .into_iter()
                .map(|s| render::storage(&e, s, false, created))
                .collect();
            reply(200, json!({"storages": {"storage": rows}}))
        }

        // **`backup` is a TYPE filter** (S8): the account's backups, paged — each names its
        // `origin`, which a deleted disk's backups keep naming.
        ("GET", ["storage", "backup"]) => {
            let created = !e.faults.fires(Fault::WithholdCreatedField);
            let rows: Vec<Value> = paged(
                e.storages_matching(&[], true)
                    .into_iter()
                    .filter(|s| s.kind == StorageKind::Backup)
                    .collect(),
                &r.query,
            )
            .into_iter()
            .map(|s| render::storage(&e, s, false, created))
            .collect();
            reply(200, json!({"storages": {"storage": rows}}))
        }

        // **`public`, `template` and `favorite` are ACCESS FILTERS, not uuids.**
        // The provider resolves an OS template by TITLE through one of these
        // before it creates a server — holger's estate names
        // `Ubuntu Server 24.04 LTS (Noble Numbat)` and never a uuid — and
        // without them the router read `template` as a storage uuid and
        // answered `STORAGE_NOT_FOUND`, which reads as a missing image rather
        // than a missing endpoint.
        ("GET", ["storage", filter @ ("public" | "template" | "favorite")]) => {
            let labels = label_filters(&r.query);
            let created = !e.faults.fires(Fault::WithholdCreatedField);
            let rows: Vec<Value> = e
                .storages_matching(&labels, false)
                .into_iter()
                .filter(|s| match *filter {
                    // `favorite` is an account's own shortlist and this account
                    // has none — an empty list, not an error.
                    "favorite" => false,
                    _ => s.kind == StorageKind::Template,
                })
                .map(|s| render::storage(&e, s, false, created))
                .collect();
            reply(200, json!({"storages": {"storage": rows}}))
        }

        ("GET", ["storage", uuid]) => {
            if e.faults.fires(Fault::RevokedCredential) {
                let cid = e.next_correlation_id();
                return reply(403, render::auth_failed(&cid));
            }
            let created = !e.faults.fires(Fault::WithholdCreatedField);
            match e.storage(uuid) {
                Some(s) => reply(
                    200,
                    json!({"storage": render::storage(&e, s, true, created)}),
                ),
                None => reply(
                    404,
                    render::error("STORAGE_NOT_FOUND", &format!("storage {uuid} not found")),
                ),
            }
        }

        ("POST", ["storage"]) => {
            let b = &r.body["storage"];
            let title = b["title"].as_str().unwrap_or("").to_string();
            let size = b["size"]
                .as_u64()
                .or_else(|| b["size"].as_str().and_then(|s| s.parse().ok()))
                .unwrap_or(0);
            // Behaviour 74: the documented refusals of a create.
            let labels = read_flat_labels(&b["labels"]);
            if let Some(x) = crate::documented::storage_title_refusal(b["title"].as_str())
                .or_else(|| crate::documented::size_refusal(size))
                .or_else(|| crate::documented::tier_refusal(b["tier"].as_str()))
                .or_else(|| crate::documented::label_refusal(&labels))
            {
                return refuse(x);
            }
            let tier = b["tier"].as_str().unwrap_or("maxiops").to_string();
            let zone = b["zone"].as_str().unwrap_or(&e.zone).to_string();
            match e
                .create_storage(&title, size, &tier, &zone, labels)
                .and_then(|uuid| e.set_backup_rule(&uuid, &b["backup_rule"]).map(|_| uuid))
            {
                Err(x) => refuse(x),
                Ok(uuid) => {
                    if e.faults.fires(Fault::CommitThenDropReply) {
                        return Outcome::Reset;
                    }
                    let created = !e.faults.fires(Fault::WithholdCreatedField);
                    let s = e.storage(&uuid).expect("just created");
                    reply(
                        201,
                        json!({"storage": render::storage(&e, s, true, created)}),
                    )
                }
            }
        }

        ("PUT", ["storage", uuid]) => {
            let b = &r.body["storage"];
            let size = b["size"]
                .as_u64()
                .or_else(|| b["size"].as_str().and_then(|s| s.parse().ok()));
            let title = b["title"].as_str().map(str::to_string);
            // `labels`: the storage's whole label set REPLACED (a release clone is labelled
            // right after its clone call, which takes none).
            let labels = (!b["labels"].is_null()).then(|| read_flat_labels(&b["labels"]));
            // Behaviour 74: a label set or a title the API refuses changes nothing.
            if let Some(x) = labels
                .as_deref()
                .and_then(crate::documented::label_refusal)
                .or_else(|| {
                    title
                        .as_deref()
                        .and_then(|t| crate::documented::storage_title_refusal(Some(t)))
                })
            {
                return refuse(x);
            }
            // Behaviour 73: a rule on a disk a simple-backup server holds is refused before the
            // modify changes anything.
            match e
                .storage_backup_rule_conflict(uuid, &b["backup_rule"])
                .and_then(|_| e.modify_storage(uuid, size, title.as_deref()))
                .and_then(|_| e.set_backup_rule(uuid, &b["backup_rule"]))
            {
                Err(x) => refuse(x),
                Ok(()) => {
                    if let Some(l) = labels {
                        e.set_storage_labels(uuid, l);
                    }
                    let created = !e.faults.fires(Fault::WithholdCreatedField);
                    let s = e.storage(uuid).expect("modified");
                    reply(
                        200,
                        json!({"storage": render::storage(&e, s, true, created)}),
                    )
                }
            }
        }

        // `?backups=keep` (the account's default) leaves the storage's backups
        // standing; `?backups=delete` takes them too (the front's daily backup,
        // 2026-10-09: an estate's teardown must not leave them billing).
        ("DELETE", ["storage", uuid]) => match e.delete_storage(uuid) {
            Err(x) => refuse(x),
            Ok(()) => {
                if r.query.iter().any(|(k, v)| k == "backups" && v == "delete") {
                    e.delete_backups_of(uuid);
                }
                reply(204, Value::Null)
            }
        },

        // **Behaviour 34.** The filesystem resize takes a backup FIRST and hands
        // it back whole as `resize_backup` — and that object then sits on the
        // account, provider-titled, until somebody deletes it. See
        // `Estate::resize_filesystem`.
        ("POST", ["storage", uuid, "resize"]) => match e.resize_filesystem(uuid) {
            Err(x) => refuse(x),
            Ok(backup) => {
                let created = !e.faults.fires(Fault::WithholdCreatedField);
                let b = e.storage(&backup).expect("just minted");
                reply(
                    200,
                    json!({"resize_backup": render::storage(&e, b, true, created)}),
                )
            }
        },

        // ── the direct-upload import ────────────────────────────────────────
        // Two clocks. This opens the session; the PUT fills it; and the STORAGE
        // then sits in `syncing` for a hundred and something seconds AFTER the
        // import object already says `completed`.
        ("POST", ["storage", uuid, "import"]) => {
            let source = r.body["storage_import"]["source"]
                .as_str()
                .unwrap_or("direct_upload")
                .to_string();
            // **The http import** (2026-10-09: the release media in Object Storage): UpCloud
            // FETCHES `source_location` itself. The mock fetches only from this machine (its own
            // `/objsto/`, or a test's server on loopback), on a thread of its own — the reply
            // does not wait, as UpCloud's does not — and the bytes then land exactly as a PUT's.
            if source == "http_import" {
                let url = r.body["storage_import"]["source_location"]
                    .as_str()
                    .unwrap_or("")
                    .to_string();
                if !(url.starts_with("http://127.0.0.1:") || url.starts_with("http://localhost:")) {
                    return refuse(Refusal::new(
                        400,
                        "SOURCE_LOCATION_INVALID",
                        "the mock imports only from this machine (http://127.0.0.1:<port>/…)",
                    ));
                }
                return match e.start_import(uuid, &source) {
                    Err(x) => refuse(x),
                    Ok(_) => {
                        let im = e.http_import_started(uuid, &url).expect("just started");
                        let (m, id) = (Arc::clone(mock), uuid.to_string());
                        std::thread::spawn(move || {
                            let got = fetch_loopback(&url);
                            let mut e = m.estate.lock().unwrap();
                            match got {
                                Ok(bytes) => {
                                    if let Err(x) = e.upload(&id, &bytes) {
                                        e.http_import_failed(
                                            &id,
                                            "STORAGE_IMPORT_FAILED",
                                            &x.message,
                                        );
                                    }
                                }
                                Err(why) => e.http_import_failed(&id, "SOURCE_UNAVAILABLE", &why),
                            }
                        });
                        reply(201, json!({"storage_import": render::import(&im)}))
                    }
                };
            }
            match e.start_import(uuid, &source) {
                Err(x) => refuse(x),
                Ok(im) => reply(201, json!({"storage_import": render::import(&im)})),
            }
        }

        ("GET", ["storage", uuid, "import"]) => {
            match e.storage(uuid).and_then(|s| s.import.as_ref()) {
                None => reply(
                    404,
                    render::error(
                        "STORAGE_IMPORT_NOT_FOUND",
                        &format!("no import session on {uuid}"),
                    ),
                ),
                Some(im) => reply(200, json!({"storage_import": render::import(im)})),
            }
        }

        // **The candidate fix.** See `Estate::clone_storage`: by default a clone
        // waits exactly as long as an import, because whether it skips the sync
        // is NOT MEASURED and the mock will not invent a saving.
        // The pre-upgrade backup and its restore (upgrade-backup, 2026-10-01).
        ("POST", ["storage", uuid, "backup"]) => {
            let title = r.body["storage"]["title"]
                .as_str()
                .unwrap_or("backup")
                .to_string();
            match e.backup_storage(uuid, &title) {
                Err(x) => refuse(x),
                Ok(new) => {
                    let created = !e.faults.fires(Fault::WithholdCreatedField);
                    let s = e.storage(&new).expect("just taken");
                    reply(
                        201,
                        json!({"storage": render::storage(&e, s, true, created)}),
                    )
                }
            }
        }
        ("POST", ["storage", uuid, "restore"]) => match e.restore_backup(uuid) {
            Err(x) => refuse(x),
            Ok(()) => reply(204, Value::Null),
        },

        // Templates (2026-10-12): a storage made into a NEW storage of type `template`.
        ("POST", ["storage", uuid, "templatize"]) => {
            let Some(title) = r.body["storage"]["title"]
                .as_str()
                .filter(|t| !t.is_empty() && t.chars().count() <= 255)
                .map(str::to_string)
            else {
                return refuse(Refusal::new(
                    400,
                    "TITLE_MISSING",
                    "The required attribute title is missing or invalid (1-255 characters).",
                ));
            };
            match e.templatize_storage(uuid, &title) {
                Err(x) => refuse(x),
                Ok(new) => {
                    let s = e.storage(&new).expect("just templatized");
                    reply(201, json!({"storage": render::storage(&e, s, true, true)}))
                }
            }
        }

        ("POST", ["storage", uuid, "clone"]) => {
            // Behaviour 74: a clone names its title (1-255) and its zone; a tier, when named,
            // is one the API knows.
            let b = &r.body["storage"];
            if let Some(x) = crate::documented::storage_title_refusal(b["title"].as_str())
                .or_else(|| crate::documented::zone_refusal(b["zone"].as_str()))
                .or_else(|| crate::documented::tier_refusal(b["tier"].as_str()))
            {
                return refuse(x);
            }
            let title = b["title"].as_str().unwrap_or("clone").to_string();
            match e.clone_storage_tier(uuid, &title, b["tier"].as_str()) {
                Err(x) => refuse(x),
                Ok(new) => {
                    // Committed; whether the caller hears of it is a separate question.
                    if e.faults.fires(Fault::CloneThenDropReply) {
                        return Outcome::Reset;
                    }
                    let created = !e.faults.fires(Fault::WithholdCreatedField);
                    let s = e.storage(&new).expect("just cloned");
                    reply(
                        201,
                        json!({"storage": render::storage(&e, s, true, created)}),
                    )
                }
            }
        }

        _ => reply(404, render::not_implemented(m, &r.path)),
    }
}

/// **One request against a mock, IN PROCESS, with no socket at all.**
///
/// The router is the only place several behaviours live — the transport reset,
/// the dropped reply, the firewall's 403 — and [`crate::self_check`] has to
/// reach them without binding a port, spawning a runtime or shelling out to
/// anything. So the request struct is built here and handed straight to
/// [`route`].
///
/// **Status `0` means [`Outcome::Reset`]**: the socket was closed with nothing
/// written, which is not a status and must never be confused with one. A caller
/// that treats 0 as success is making the exact mistake behaviour 2 was written
/// for.
///
/// `authorized` is always true: the unauthenticated 401 is a property of the
/// header parser above [`route`], not of the estate, and a calibration that
/// drove it would be measuring the parser.
pub fn probe(mock: &Arc<Mock>, method: &str, target: &str, body: Value) -> (u16, Value) {
    let (path, query) = split_target(target);
    let raw = if body.is_null() {
        vec![]
    } else {
        body.to_string().into_bytes()
    };
    let r = Req {
        method: method.to_string(),
        path,
        query,
        authorized: true,
        body,
        raw,
    };
    match route(mock, r) {
        Outcome::Reset => (0, Value::Null),
        Outcome::Reply { status, body } => (status, body),
        Outcome::Bytes { status, bytes } => (status, json!({"bytes": bytes.len()})),
    }
}

/// `"cdrom"`, `"disk"`, or the provider's list spelling `"cdrom,disk"` /
/// `"disk,cdrom"` — whichever comes FIRST is the order that matters.
fn parse_boot_order(s: &str) -> Option<BootOrder> {
    // The exact spelling first, so `cdrom,disk` reads back as it was written
    // (a terraform permadiff otherwise); any other list by its first entry.
    if let Some(b) = BootOrder::parse(s) {
        return Some(b);
    }
    match s.split(',').next().map(str::trim) {
        Some("cdrom") => Some(BootOrder::Cdrom),
        Some("disk") => Some(BootOrder::Disk),
        _ => None,
    }
}

/// `?label=key=value`, repeatable, all must match.
/// **A list is PAGED** (`limit`, `offset`), as UpCloud's docs say its lists are: 25 rows
/// when no `limit` is asked, never more than 100 (NOT MEASURED against the account). Rows
/// keep the estate's own order, so the pages of one read do not overlap.
pub const DEFAULT_PAGE: usize = 25;
pub const MAX_PAGE: usize = 100;

fn paged<T>(rows: Vec<T>, q: &[(String, String)]) -> Vec<T> {
    let num = |k: &str| {
        q.iter()
            .find(|(key, _)| key == k)
            .and_then(|(_, v)| v.parse::<usize>().ok())
    };
    let limit = num("limit").unwrap_or(DEFAULT_PAGE).clamp(1, MAX_PAGE);
    let offset = num("offset").unwrap_or(0);
    rows.into_iter().skip(offset).take(limit).collect()
}

fn label_filters(q: &[(String, String)]) -> Vec<(String, String)> {
    q.iter()
        .filter(|(k, _)| k == "label")
        .filter_map(|(_, v)| {
            v.split_once('=')
                .map(|(a, b)| (a.to_string(), b.to_string()))
        })
        .collect()
}

fn read_flat_labels(v: &Value) -> Vec<Label> {
    v.as_array()
        .map(|a| {
            a.iter()
                .filter_map(|l| {
                    Some(Label {
                        key: l["key"].as_str()?.to_string(),
                        value: l["value"].as_str().unwrap_or("").to_string(),
                    })
                })
                .collect()
        })
        .unwrap_or_default()
}

/// A server's labels ride in an envelope (`{"label": [...]}`) while a storage's
/// do not. Both shapes are accepted on the way in, because both have been sent.
fn read_server_labels(v: &Value) -> Vec<Label> {
    if v["label"].is_array() {
        read_flat_labels(&v["label"])
    } else {
        read_flat_labels(v)
    }
}

// ── Object Storage: the release media ─────────────────────────────────────────

/// **`/objsto/<bucket>/<key>`** — the S3 shape the shop's release media need: `PUT` with an
/// Authorization header (SigV4 at the real one; the mock checks it is there, not its
/// signature — it holds no secret), and a PRESIGNED `GET`: `X-Amz-Signature`,
/// `X-Amz-Date` and `X-Amz-Expires` in the query and not expired, else 403.
fn object_face(mock: &Arc<Mock>, r: &Req, key: &str) -> Outcome {
    if key.is_empty() || !key.contains('/') {
        return reply(
            400,
            render::error("INVALID_OBJECT_KEY", "an object is /objsto/<bucket>/<key>"),
        );
    }
    match r.method.as_str() {
        "PUT" if !r.authorized => reply(
            403,
            render::error(
                "ACCESS_DENIED",
                "a PUT carries a signed Authorization header",
            ),
        ),
        "PUT" => {
            mock.objects
                .lock()
                .unwrap()
                .insert(key.to_string(), r.raw.clone());
            reply(200, Value::Null)
        }
        "GET" | "HEAD" => {
            let q = |k: &str| r.query.iter().find(|(n, _)| n == k).map(|(_, v)| v.clone());
            let now = std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .map(|d| d.as_secs())
                .unwrap_or(0);
            if let Err(why) = presign_holds(
                q("X-Amz-Signature"),
                q("X-Amz-Date"),
                q("X-Amz-Expires"),
                now,
            ) {
                return reply(403, render::error("ACCESS_DENIED", &why));
            }
            match mock.objects.lock().unwrap().get(key) {
                Some(b) => Outcome::Bytes {
                    status: 200,
                    bytes: if r.method == "HEAD" {
                        Vec::new()
                    } else {
                        b.clone()
                    },
                },
                None => reply(
                    404,
                    render::error("NO_SUCH_KEY", &format!("no object {key}")),
                ),
            }
        }
        _ => reply(405, render::error("METHOD_NOT_ALLOWED", "PUT, GET or HEAD")),
    }
}

/// Pure: **a presigned GET is still good** at `now` (Unix seconds): signed, dated
/// `YYYYMMDDTHHMMSSZ`, and `now` within `X-Amz-Expires` seconds of that date.
pub fn presign_holds(
    signature: Option<String>,
    date: Option<String>,
    expires: Option<String>,
    now: u64,
) -> Result<(), String> {
    if signature.as_deref().is_none_or(str::is_empty) {
        return Err("the GET is not presigned (no X-Amz-Signature)".into());
    }
    let date = date.ok_or("no X-Amz-Date")?;
    let expires: u64 = expires
        .and_then(|e| e.parse().ok())
        .ok_or("no X-Amz-Expires")?;
    let at = amz_date_secs(&date)
        .ok_or_else(|| format!("X-Amz-Date {date:?} is not YYYYMMDDTHHMMSSZ"))?;
    if now > at.saturating_add(expires) {
        return Err(format!(
            "the presigned URL expired {} s ago",
            now - at - expires
        ));
    }
    Ok(())
}

/// `YYYYMMDDTHHMMSSZ` → Unix seconds (UTC), pure.
fn amz_date_secs(d: &str) -> Option<u64> {
    let b = d.as_bytes();
    if b.len() != 16 || b[8] != b'T' || b[15] != b'Z' {
        return None;
    }
    let n = |r: std::ops::Range<usize>| d.get(r)?.parse::<i64>().ok();
    let (y, mo, da, h, mi, s) = (
        n(0..4)?,
        n(4..6)?,
        n(6..8)?,
        n(9..11)?,
        n(11..13)?,
        n(13..15)?,
    );
    // Days from civil (Howard Hinnant), UTC.
    let y2 = if mo <= 2 { y - 1 } else { y };
    let era = y2.div_euclid(400);
    let yoe = y2 - era * 400;
    let doy = (153 * (if mo > 2 { mo - 3 } else { mo + 9 }) + 2) / 5 + da - 1;
    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
    let days = era * 146_097 + doe - 719_468;
    u64::try_from(days * 86_400 + h * 3_600 + mi * 60 + s).ok()
}

/// **One GET of a loopback URL** (the http import's fetch), HTTP/1.0: the body of a 200.
fn fetch_loopback(url: &str) -> Result<Vec<u8>, String> {
    use std::io::{Read as _, Write as _};
    let rest = url.strip_prefix("http://").ok_or("not http://")?;
    let (hostport, path) = rest
        .split_once('/')
        .map(|(h, p)| (h, format!("/{p}")))
        .unwrap_or((rest, "/".to_string()));
    let mut c = std::net::TcpStream::connect(hostport).map_err(|e| format!("{hostport}: {e}"))?;
    c.set_read_timeout(Some(std::time::Duration::from_secs(600)))
        .ok();
    write!(
        c,
        "GET {path} HTTP/1.0\r\nHost: {hostport}\r\nConnection: close\r\n\r\n"
    )
    .map_err(|e| e.to_string())?;
    let mut all = Vec::new();
    c.read_to_end(&mut all).map_err(|e| e.to_string())?;
    let at = all
        .windows(4)
        .position(|w| w == b"\r\n\r\n")
        .ok_or("no HTTP head")?;
    let head = String::from_utf8_lossy(&all[..at]).to_string();
    let status = head.split_whitespace().nth(1).unwrap_or("");
    if status != "200" {
        return Err(format!("the source answered HTTP {status}"));
    }
    Ok(all[at + 4..].to_vec())
}

// ── the mock's own door ──────────────────────────────────────────────────────

fn mock_door(mock: &Arc<Mock>, r: &Req) -> Outcome {
    let seg: Vec<&str> = r.path.trim_matches('/').split('/').skip(1).collect();
    let mut e = mock.estate.lock().unwrap();
    match (r.method.as_str(), seg.as_slice()) {
        ("GET", ["seed"]) => reply(
            200,
            json!({"seed": e.seed(), "lays": e.lays(), "now_ms": e.clock.now_ms()}),
        ),
        // **Behaviour 63.** What the mock heard from ONE credential, named by
        // its sha256 (the mock keeps no credential). A verb asks this before
        // `apply`/`destroy`: zero account calls from this run's token means the
        // provider did not talk to this mock, and the run must stop.
        ("GET", ["heard"]) => {
            let d = q(&r.query, "token_sha256");
            let h = mock.heard_from(&d);
            reply(
                200,
                json!({"token_sha256": d, "requests": h.requests, "account_calls": h.account_calls}),
            )
        }
        // **Behaviour 62.** Inbound through the provider's firewall.
        ("GET", ["inbound", uuid]) => {
            e.settle();
            let port: u16 = q(&r.query, "port").parse().unwrap_or(22);
            let proto = {
                let p = q(&r.query, "proto");
                if p.is_empty() {
                    "tcp".to_string()
                } else {
                    p
                }
            };
            match e.inbound(&q(&r.query, "from"), uuid, &proto, port) {
                Err(x) => reply(x.status, json!({"error": x.message})),
                Ok(reach) => reply(200, json!({"ok": reach.is_ok(), "why": reach.why()})),
            }
        }
        ("GET", ["udp-reply", uuid]) => {
            let port: u16 = q(&r.query, "port").parse().unwrap_or(53);
            match e.udp_reply_arrives(uuid, &q(&r.query, "from"), port) {
                Err(x) => reply(x.status, json!({"error": x.message})),
                Ok(yes) => reply(200, json!({"arrives": yes})),
            }
        }
        // **Behaviour 62, the installer link.** Does `layer` (the box that lays)
        // pair with `laid` over udp/51821 — the dial through `laid`'s firewall,
        // the answer through `layer`'s (UpCloud tracks no UDP flow)?
        ("GET", ["wireguard", layer, laid]) => {
            e.settle();
            match e.wireguard_pairs(layer, laid) {
                Err(x) => reply(x.status, json!({"error": x.message})),
                Ok(reach) => reply(200, json!({"pairs": reach.is_ok(), "why": reach.why()})),
            }
        }
        // **Behaviour 58.** The names the guest gives the disks.
        ("GET", ["disks", uuid]) => reply(
            200,
            json!({"disks": e.guest_disk_names(uuid).into_iter().map(|(a, n)| json!({"address": a, "name": n})).collect::<Vec<_>>()}),
        ),
        ("GET", ["estate"]) => {
            // `?as_is`: the estate NOW, without first running it to quiet. A
            // rebooting medium left in the tray (behaviour 69) never goes
            // quiet — each pass schedules the next — so the quiet view would
            // fast-forward a thousand passes to answer one question.
            if !r.query.iter().any(|(k, _)| k == "as_is") {
                e.run_to_quiet();
            }
            let servers: Vec<Value> = e
                .all_servers()
                .map(|s| json!({"uuid": s.uuid, "title": s.title, "state": s.state, "plan": s.plan,
                                "guest": format!("{:?}", s.guest), "public_ip": s.public_ip,
                                "utility_ip": s.utility_ip, "vnc_port": s.vnc_port,
                                "reported_vnc_port": s.reported_vnc_port, "boot_order": s.boot_order.as_str()}))
                .collect();
            let storages: Vec<Value> = e
                .all_storages()
                .filter(|s| s.kind != StorageKind::Template)
                .map(|s| json!({"uuid": s.uuid, "title": s.title, "state": s.state, "size": s.size_gib,
                                "type": s.kind.as_str(), "origin": s.origin,
                                "labels": s.labels.iter().map(|l| format!("{}={}", l.key, l.value)).collect::<Vec<_>>()}))
                .collect();
            reply(200, json!({"servers": servers, "storages": storages}))
        }
        // Move the virtual clock by hand: a test that must wait out a measured
        // window (the 2 s console settle) says so instead of polling for it.
        ("POST", ["advance", ms]) => {
            let ms: u64 = ms.parse().unwrap_or(0);
            e.clock.advance_ms(ms);
            e.settle();
            reply(200, json!({"now_ms": e.clock.now_ms()}))
        }
        ("POST", ["fault", name, verb]) => match Fault::parse(name) {
            None => reply(404, json!({"error": format!("no such fault: {name}")})),
            Some(f) => {
                match *verb {
                    "arm" => e.faults.arm(f),
                    "disarm" => e.faults.disarm(f),
                    _ => return reply(400, json!({"error": "arm or disarm"})),
                }
                reply(
                    200,
                    json!({"fault": f.name(), "armed": e.faults.is_armed(f)}),
                )
            }
        },
        // **The guest's clock, and everything that follows from it.** Not an
        // UpCloud path: the provider has no endpoint that would tell you your
        // guest's wall clock is wrong, which is a large part of why it took an
        // afternoon. The mock has one so a fix can be asserted.
        ("GET", ["clock", uuid]) => {
            e.settle();
            let Some(s) = e.server(uuid) else {
                return reply(404, json!({"error": format!("no such server: {uuid}")}));
            };
            // The day it was measured, so the offset is the measured one.
            let t = crate::guest_clock::days_from_civil(2026, 9, 20) * 86_400;
            let skew = s.clock_skew_ms(t);
            let reds = crate::guest_clock::cascade(skew, crate::guest_clock::SkewWindow::default());
            reply(
                200,
                json!({
                    "uuid": s.uuid,
                    "zone": s.zone,
                    "hypervisor_rtc": "UTC",
                    "guest_reads_rtc_as": format!("{:?}", s.rtc),
                    "guest_clock_skew_ms": skew,
                    "udp_reply_arrives": crate::guest_clock::udp_reply_arrives(&e.faults),
                    "red": reds.iter().map(|r| json!({"row": r.row, "why": r.why})).collect::<Vec<_>>(),
                }),
            )
        }
        // **Reachability always names the asker.** There is no "is that address
        // up" here, only "is it up from there", because both of the asymmetries
        // this models make the answer depend on who is asking.
        ("GET", ["reach", from]) => {
            e.settle();
            let dest = r
                .query
                .iter()
                .find(|(k, _)| k == "dest")
                .map(|(_, v)| v.clone())
                .unwrap_or_default();
            let port: u16 = r
                .query
                .iter()
                .find(|(k, _)| k == "port")
                .and_then(|(_, v)| v.parse().ok())
                .unwrap_or(22);
            match e.reach(from, &dest, port) {
                Err(x) => reply(x.status, json!({"error": x.message})),
                Ok(reach) => reply(
                    200,
                    json!({
                        "from": from, "dest": dest, "port": port,
                        "ok": reach.is_ok(),
                        "why": reach.why(),
                        "kind": match &reach {
                            crate::net::Reach::Ok => "ok",
                            crate::net::Reach::NoRouteOutbound { .. } => "no-route-outbound",
                            crate::net::Reach::NoHairpin { .. } => "no-hairpin",
                            crate::net::Reach::Refused { .. } => "refused",
                            crate::net::Reach::Dropped { .. } => "dropped",
                        },
                        // The other direction, always, side by side — because a
                        // caller that asked only one of these is the caller who
                        // spent an afternoon on a clock.
                        "inbound_ok": crate::net::inbound_reaches(true),
                    }),
                ),
            }
        }

        // The offer the utility NIC received. Always complete; what varies is
        // whether the guest took it.
        ("GET", ["dhcp", uuid]) => match e.server(uuid) {
            None => reply(404, json!({"error": format!("no such server: {uuid}")})),
            Some(s) => {
                let o = s.dhcp_offer();
                reply(
                    200,
                    json!({
                        "address": o.address,
                        "prefix": o.prefix,
                        "router": o.router,
                        "option_121": o.classless_static_routes.iter().map(|x| x.to_string()).collect::<Vec<_>>(),
                        "guest_dhcp_client": format!("{:?}", s.dhcp_client),
                    }),
                )
            }
        },

        ("POST", ["dnat", on_server, port, to_address]) => {
            let port: u16 = port.parse().unwrap_or(0);
            e.dnat.push(crate::net::Dnat {
                on_server: on_server.to_string(),
                port,
                to_address: to_address.to_string(),
                to_port: port,
            });
            reply(200, json!({"rules": e.dnat.len()}))
        }

        // **Three paths, one key.** A re-image mints a new host key every time,
        // so a changed key is expected and cannot be alarming on its own; the
        // only thing that separates a new machine from a stolen name is these
        // three agreeing.
        ("GET", ["hostkey", uuid]) => {
            e.settle();
            let mut keys = serde_json::Map::new();
            for p in crate::estate::HostKeyPath::ALL {
                match e.host_key_via(uuid, p) {
                    Err(x) => return reply(x.status, json!({"error": x.message})),
                    Ok(k) => {
                        keys.insert(p.name().to_string(), json!(k));
                    }
                }
            }
            let distinct: std::collections::BTreeSet<&str> =
                keys.values().filter_map(|v| v.as_str()).collect();
            reply(
                200,
                json!({
                    "paths": keys,
                    "agree": distinct.len() == 1,
                    "verdict": if distinct.len() == 1 {
                        "one key on three paths: this is the machine that was re-imaged"
                    } else {
                        "the paths disagree: a name is answering for a machine that is not behind the DNAT"
                    },
                }),
            )
        }

        // **The same re-image, as the two observers saw it.**
        ("GET", ["reimage", uuid]) => {
            let (uart, wall) = e.timings.reimage_observers(e.seed(), uuid);
            reply(
                200,
                json!({
                    "guest_uart_ms": uart,
                    "ladder_wall_ms": wall,
                    "ratio": wall / uart.max(1),
                    "note": "the installer is not slow; the provider is. create, media sync, firmware, boot order, DHCP.",
                    "observers": {
                        "guest_uart_ms": "the guest's own PID 1, from inside",
                        "ladder_wall_ms": "the ladder's `install-time`, wall, from outside"
                    }
                }),
            )
        }

        // What the machine behind a server left: argv, frame hashes, stdout
        // bytes, disk sizes. 404 from an engine that runs no machines.
        ("GET", ["guest", uuid]) => {
            e.tick();
            match e.engine.evidence(uuid) {
                Some(v) => reply(200, json!({"engine": e.engine.name(), "guest": v})),
                None => reply(
                    404,
                    json!({"error": format!("engine {} has no machine for {uuid}", e.engine.name())}),
                ),
            }
        }

        ("POST", ["relay"]) => {
            e.relay();
            reply(200, json!({"lays": e.lays()}))
        }
        ("POST", ["seed", s]) => {
            let seed: u64 = s.parse().unwrap_or(0);
            let speed = e.clock.speed_milli();
            // The engine outlives the estate it served, and every machine and
            // disk it holds goes now: nothing in the new estate can name them.
            let engine = e.engine.clone();
            engine.forget_all();
            *e = Estate::new(crate::Clock::new(speed), Faults::seeded(seed), seed)
                .with_engine(engine);
            reply(200, json!({"seed": seed}))
        }
        _ => reply(
            404,
            json!({"error": format!("no such mock door: {}", r.path)}),
        ),
    }
}

/// One query parameter, or empty.
fn q(query: &[(String, String)], key: &str) -> String {
    query
        .iter()
        .find(|(k, _)| k == key)
        .map(|(_, v)| v.clone())
        .unwrap_or_default()
}

// ── the ONE door every face comes through ────────────────────────────────────

/// **One request, answered exactly as the HTTP face answers it** — the
/// bookkeeping (behaviour 63's `heard`, [`Mock::calls`], [`Mock::log`]) and the
/// router, in one place. The socket parser above calls it; so does
/// [`crate::face::FakeUpCloud`], the in-process trait face. That is what makes
/// them one world with two faces rather than two worlds: a Rust caller and
/// terraform reach the same `route`, the same `Estate` methods, the same
/// guest engine, and are heard and recorded the same way.
///
/// `credential` is the Authorization header's value, `None` when none was sent
/// (the one case that is a clean 401).
pub(crate) fn answer(
    mock: &Arc<Mock>,
    method: &str,
    target: &str,
    credential: Option<&str>,
    raw: Vec<u8>,
) -> Outcome {
    let (path, query) = split_target(target);
    let body: Value = if raw.is_empty() {
        Value::Null
    } else {
        serde_json::from_slice(&raw).unwrap_or(Value::Null)
    };
    let said = format!("{method} {path}");
    let (m_rec, p_rec) = (method.to_string(), path.clone());
    let authorized = credential.is_some();
    if let Some(c) = credential {
        mock.heard(c, method, &path);
    }
    #[cfg(feature = "test-inject")]
    let injected = mock.injected(method, &path);
    #[cfg(not(feature = "test-inject"))]
    let injected: Option<Outcome> = None;
    let out = match injected {
        Some(o) => o,
        None => route(
            mock,
            Req {
                method: method.to_string(),
                path,
                query,
                authorized,
                body,
                raw,
            },
        ),
    };
    if let Some(calls) = &mock.calls {
        let (st, code) = match &out {
            Outcome::Reset => (0, String::new()),
            Outcome::Reply { status, body } => (
                *status,
                body["error"]["error_code"]
                    .as_str()
                    .unwrap_or("")
                    .to_string(),
            ),
            Outcome::Bytes { status, .. } => (*status, String::new()),
        };
        calls.lock().unwrap().push((st, m_rec, p_rec, code));
    }
    if mock.log {
        // The server's own state is printed beside the call, because the
        // question a log is opened for is almost always "what state was it in
        // when that happened".
        let states = {
            let e = mock.estate.lock().unwrap();
            e.all_servers()
                .map(|s| format!("{}={}", &s.uuid[..4], s.state))
                .collect::<Vec<_>>()
                .join(" ")
        };
        let code = match &out {
            Outcome::Reset => 0,
            Outcome::Reply { status, .. } | Outcome::Bytes { status, .. } => *status,
        };
        eprintln!("  {code:>3}  {said:<52}  [{states}]");
    }
    out
}

// ── test-only injections ─────────────────────────────────────────────────────

/// **An INJECTION: "answer the next N matching requests with this error".**
///
/// Not a behaviour, and it claims NOTHING about UpCloud. It exists for a
/// client's test that must prove how it handles an answer the provider CAN
/// give (a `409` on an eject, a refused delete) at a moment no behaviour of
/// this mock produces it on demand. Every such test says what it injected.
///
/// Compiled only with the `test-inject` feature: the published mock, and
/// every storm or rehearsal built without that feature, cannot answer with an
/// invented error — the field and the check do not exist in it.
#[cfg(feature = "test-inject")]
pub struct Injection {
    /// `GET`, `POST`, `PUT`, `DELETE`.
    pub method: String,
    /// Does this request match? Handed the path (`/1.3/…`) and the estate, so
    /// a test can name "the storage titled X" without knowing its uuid.
    pub matches: Box<dyn Fn(&str, &Estate) -> bool + Send + Sync>,
    pub status: u16,
    pub code: String,
    pub message: String,
    /// How many matching requests are answered this way; then it is spent.
    pub times: u32,
}

#[cfg(feature = "test-inject")]
impl Mock {
    /// Arm one [`Injection`].
    pub fn inject(&self, i: Injection) {
        self.injections.lock().unwrap().push(i);
    }

    fn injected(&self, method: &str, path: &str) -> Option<Outcome> {
        let e = self.estate.lock().unwrap();
        let mut all = self.injections.lock().unwrap();
        let hit = all
            .iter_mut()
            .find(|i| i.times > 0 && i.method == method && (i.matches)(path, &e))?;
        hit.times -= 1;
        Some(reply(hit.status, render::error(&hit.code, &hit.message)))
    }
}

/// Pure: does this create-body device sit at the first virtio position
/// (`"address": "virtio:0"`, or `"address": "virtio"` with `"address_position": "0"`)?
fn boot_position(d: &Value) -> bool {
    let a = d["address"].as_str().unwrap_or("");
    a == "virtio:0" || (a == "virtio" && d["address_position"].as_str() == Some("0"))
}

#[cfg(test)]
mod boot_position_tests {
    /// A presigned GET holds while signed and unexpired; the date is UTC, leap days included.
    #[test]
    fn a_presigned_get_holds_only_signed_and_unexpired() {
        assert_eq!(amz_date_secs("19700101T000000Z"), Some(0));
        assert_eq!(amz_date_secs("20261009T120000Z"), Some(1_791_547_200));
        assert_eq!(amz_date_secs("20000229T235959Z"), Some(951_868_799));
        assert_eq!(amz_date_secs("2026-10-09"), None);
        let s = |v: &str| Some(v.to_string());
        let at = 1_791_547_200;
        assert!(presign_holds(s("sig"), s("20261009T120000Z"), s("3600"), at + 3_600).is_ok());
        assert!(
            presign_holds(s("sig"), s("20261009T120000Z"), s("3600"), at + 3_601)
                .unwrap_err()
                .contains("expired")
        );
        assert!(presign_holds(None, s("20261009T120000Z"), s("3600"), at)
            .unwrap_err()
            .contains("not presigned"));
        assert!(presign_holds(s("sig"), None, s("3600"), at).is_err());
    }

    use super::*;

    #[test]
    fn the_boot_disk_is_the_one_at_virtio_zero_whatever_the_body_order() {
        assert!(boot_position(
            &json!({"address": "virtio:0", "storage": "s"})
        ));
        assert!(boot_position(
            &json!({"address": "virtio", "address_position": "0"})
        ));
        assert!(!boot_position(&json!({"address": "virtio:1"})));
        assert!(!boot_position(&json!({"storage": "s"})));
    }

    #[test]
    fn a_list_pages_by_limit_and_offset_and_defaults_to_twenty_five() {
        let rows: Vec<u32> = (0..130).collect();
        let q = |p: &[(&str, &str)]| {
            p.iter()
                .map(|(k, v)| (k.to_string(), v.to_string()))
                .collect::<Vec<_>>()
        };
        assert_eq!(paged(rows.clone(), &[]), (0..25).collect::<Vec<_>>());
        assert_eq!(
            paged(rows.clone(), &q(&[("limit", "1000")])).len(),
            MAX_PAGE
        );
        assert_eq!(
            paged(rows.clone(), &q(&[("limit", "100"), ("offset", "100")])),
            (100..130).collect::<Vec<_>>()
        );
        assert!(paged(rows, &q(&[("offset", "200")])).is_empty());
    }
}