Gunnar

mikronova/draupnir

tests/redfish_server_conformance.rs

//! **Conformance — the anchor.** draupnir's Redfish server, checked against DMTF's
//! own published schema, message registry and mockup.
//!
//! # Why this file is written the way it is
//!
//! draupnir owns BOTH ends of Redfish: the client (`src/redfish.rs`) and the server
//! (`src/redfish_server.rs`). Pointing one at the other is a **mirror** — both halves
//! can be wrong in the same way and every assertion still passes. That is the
//! identity-value failure: *a test sitting on 1.0 cannot tell a working transform
//! from a dead one.* A loopback green would prove self-consistency, not Redfish.
//!
//! So nothing here is asserted against a literal typed by whoever wrote the server.
//! Every shape is read out of `tests/fixtures/dmtf/` — DMTF's `ServiceRoot`,
//! `ComputerSystem`, `VirtualMedia`, collection and error JSON Schema, the
//! `Base.1.19.0` message registry, and the `public-rackmount1` mockup DMTF ships with
//! its own Redfish Mockup Server. Provenance, URLs and digests are in
//! `tests/fixtures/dmtf/PROVENANCE.md`, and **every fixture is re-hashed here before
//! it is read**: a fixture edited to make an assertion pass is itself a red.
//!
//! The one thing that IS draupnir's own is the HTTP transport, and that is
//! deliberately the third-party `ureq` — an independent implementation, so malformed
//! HTTP from the server is caught by something that did not write it.
//!
//! # What this file does NOT prove
//!
//! That a real iDRAC or iLO interoperates. Conformance to DMTF's published shapes is
//! what makes interop *plausible*; only a burn against real hardware makes it
//! measured, and there is no BMC on this box. See `tests/redfish_kvm_loop_proof.rs`
//! for the live half, which is recorded as `iso-redfish-sim` — never `iso-metal`.
//!
//! Runs with no KVM, no `/dev/kvm` and no VM: the server is fronted by a **recording**
//! backend that captures the [`BootSpec`] it is handed and launches nothing.
//!
//! ```text
//! cargo test --features redfish-server --test redfish_server_conformance
//! ```
#![cfg(feature = "redfish-server")]

use std::collections::BTreeSet;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};

use serde_json::Value;

use draupnir::redfish::wire::{self, prop};
use draupnir::redfish_server::{
    NodeConfig, RedfishKvmServer, ALL_MESSAGES, BASE_REGISTRY,
};
use draupnir::{Boot, BootOrder, BootSpec, Lifecycle, Machine, PowerState, Result};

// ===========================================================================
// The fixtures, and the proof they are DMTF's and not ours
// ===========================================================================

/// Every vendored DMTF fixture with the digest recorded in `PROVENANCE.md`.
///
/// The table is the tamper seal. Editing a fixture to make an assertion below pass
/// changes its hash, and [`fixture`] refuses to hand it over — so "the anchor" cannot
/// quietly become "whatever we needed it to be".
const FIXTURES: &[(&str, &str)] = &[
    (
        "schemas/ServiceRoot.v1_16_1.json",
        "d8dbb8748ec06e43c03a972e0f7d398286fe95ea908f9b184e8d5a8da391b519",
    ),
    (
        "schemas/ComputerSystem.v1_22_0.json",
        "3ba322e18e5d9445c51157fe4b2b3e835bf3992b4a4e60733bf6202342ee615e",
    ),
    (
        "schemas/ComputerSystem.json",
        "8df03de13e06e4e5b16fd3bfaa806567b364a2f26f63492130023078c83e0c23",
    ),
    (
        "schemas/ComputerSystemCollection.json",
        "c17c1b287bc2320dfbd3bb3d7ec644ab2726d31f33cbfeac96d6109c3324bc4e",
    ),
    (
        "schemas/VirtualMedia.v1_6_3.json",
        "d4c4aa15fd15989243c33c188665f6705f5e2bd1e8424b739178591214190e21",
    ),
    (
        "schemas/VirtualMediaCollection.json",
        "71ec62b65aa41a33ae66ae3ef01e194e61956df2227ce1d4a620ff1f487ba799",
    ),
    (
        "schemas/SessionCollection.json",
        "96dbe3f737e6b6708d40b84187e6bf6e442186567b84cf2615cf24c2bb75a143",
    ),
    (
        "schemas/Resource.json",
        "a600172f7b9090c95efad59e3329cbed5d7140d758ab22645785d16ef243bd1c",
    ),
    (
        "schemas/redfish-error.v1_0_2.json",
        "6ba0f876b30d7c118ee0645c72f3cb3df865a139a5eea757dd66cb4e16aebd24",
    ),
    (
        "schemas/Message.v1_1_2.json",
        "d543ea0eb8f4aa9fea14f3b3419011f5262466e8e5cc3308966eef57c57445dd",
    ),
    (
        "registries/Base.1.19.0.json",
        "b44a0bdc30e2834eb7f1cf0aaadd5d8ce1ee27008632b87e210964a018675c9e",
    ),
    (
        "mockup/public-rackmount1/index.json",
        "3cdbdf2c7bc87d35b4fe7124be221da3e846f1815ebb963178f039114a55383e",
    ),
    (
        "mockup/public-rackmount1/Systems/index.json",
        "4dd474ce66cd7e11426d781c706dc8c31aff50f8bf57f64dfea8e7eee3f0b765",
    ),
    (
        "mockup/public-rackmount1/Systems/437XR1138R2/index.json",
        "af03202a1bcd4f16ee8dab92364fc7b1f78cb3088f74d3b79294e9c22ee2957b",
    ),
    (
        "mockup/public-rackmount1/Systems/437XR1138R2/VirtualMedia/index.json",
        "7402719f415af8b1a340f297051a844949d8d6ad6e3be138e31118a69f29e268",
    ),
    (
        "mockup/public-rackmount1/Systems/437XR1138R2/VirtualMedia/CD1/index.json",
        "d94e82a2a901965b36d1b710a8d07a2b67d06948c4a548e3165cf4a12a65ff67",
    ),
];

/// The system id DMTF's `public-rackmount1` mockup uses, and the CD slot it names.
/// Both are read straight out of the mockup below rather than assumed.
const MOCK_SYSTEM: &str = "437XR1138R2";
const MOCK_SLOT: &str = "CD1";

fn fixture_root() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/dmtf")
}

/// Load a vendored DMTF fixture, refusing it unless its digest still matches
/// `PROVENANCE.md`.
fn fixture(rel: &str) -> Value {
    use sha2::{Digest, Sha256};
    let expected = FIXTURES
        .iter()
        .find(|(p, _)| *p == rel)
        .unwrap_or_else(|| panic!("{rel} is not in the sealed fixture table"))
        .1;
    let path = fixture_root().join(rel);
    let bytes = std::fs::read(&path).unwrap_or_else(|e| {
        panic!(
            "the DMTF anchor {} is missing — this suite cannot run without it, and a \
             skip here would mean asserting draupnir against draupnir: {e}",
            path.display()
        )
    });
    // sha2 0.11's digest is a `hybrid_array::Array`, which has no `LowerHex`
    // (0.10's `GenericArray` did) — spell the lowercase hex out.
    let got: String = Sha256::digest(&bytes)
        .iter()
        .map(|b| format!("{b:02x}"))
        .collect();
    assert_eq!(
        got,
        expected,
        "{rel} does not match the digest in PROVENANCE.md. A DMTF fixture edited to \
         make a conformance assertion pass is not an anchor."
    );
    serde_json::from_slice(&bytes).unwrap_or_else(|e| panic!("{rel} is not JSON: {e}"))
}

/// The suite is worthless if the fixtures are absent or altered, so prove the seal
/// FIRST and loudly — never skip.
#[test]
fn every_dmtf_fixture_is_present_and_unaltered() {
    assert!(!FIXTURES.is_empty());
    for (rel, _) in FIXTURES {
        let v = fixture(rel);
        assert!(v.is_object(), "{rel} is not a JSON object");
    }
    // Non-vacuity: these really are DMTF's documents, not ours. Every schema and the
    // registry carry DMTF's own copyright/owning-entity marker.
    let base = fixture("registries/Base.1.19.0.json");
    assert_eq!(base["Id"], BASE_REGISTRY, "the registry we cite is the one we vendored");
    // The registry spells it `OwningEntity` (a Redfish resource property); the schema
    // documents spell it `owningEntity` (a JSON-Schema annotation). Both are DMTF's.
    assert_eq!(base["OwningEntity"], "DMTF");
    assert!(base["@Redfish.Copyright"]
        .as_str()
        .unwrap_or_default()
        .contains("DMTF"));
    for rel in FIXTURES.iter().map(|(r, _)| *r).filter(|r| r.starts_with("schemas/")) {
        assert_eq!(fixture(rel)["owningEntity"], "DMTF", "{rel}");
    }
    for rel in FIXTURES.iter().map(|(r, _)| *r).filter(|r| r.starts_with("mockup/")) {
        let v = fixture(rel);
        assert!(
            v["@Redfish.Copyright"]
                .as_str()
                .unwrap_or_default()
                .contains("DMTF"),
            "{rel} is not a DMTF mockup"
        );
    }
    println!(
        "ANCHOR: {} DMTF fixtures verified against PROVENANCE.md digests",
        FIXTURES.len()
    );
}

// ===========================================================================
// A very small JSON-Schema reader — enough for DMTF's Redfish dialect
// ===========================================================================

/// The `properties` map of a definition, following the collection dialect's
/// `anyOf: [ idRef, <the real object> ]`.
fn schema_properties(def: &Value) -> &serde_json::Map<String, Value> {
    if let Some(p) = def.get("properties").and_then(Value::as_object) {
        return p;
    }
    def.get("anyOf")
        .and_then(Value::as_array)
        .and_then(|branches| {
            branches
                .iter()
                .find_map(|b| b.get("properties").and_then(Value::as_object))
        })
        .unwrap_or_else(|| panic!("definition has no properties: {def}"))
}

/// The `required` array of a definition, through the same `anyOf` dialect.
fn schema_required(def: &Value) -> Vec<String> {
    let arr = def.get("required").or_else(|| {
        def.get("anyOf")
            .and_then(Value::as_array)
            .and_then(|bs| bs.iter().find_map(|b| b.get("required")))
    });
    arr.and_then(Value::as_array)
        .map(|a| {
            a.iter()
                .filter_map(Value::as_str)
                .map(str::to_string)
                .collect()
        })
        .unwrap_or_default()
}

/// DMTF's `patternProperties` escape hatch, hand-matched (no regex crate):
/// `^([a-zA-Z_][a-zA-Z0-9_]*)?@(odata|Redfish|Message)\.[a-zA-Z_][a-zA-Z0-9_]*$`.
///
/// This is what makes `@odata.id`, `Members@odata.count` and
/// `ResetType@Redfish.AllowableValues` legal keys that the `properties` map does not
/// list. Matching it by hand rather than waving all `@` keys through is the point: a
/// typo'd `ResetType@Redfish.AllowedValues` still has to be a *well-formed*
/// annotation, and `Boot@Bogus.Thing` is not one.
fn is_redfish_annotation(key: &str) -> bool {
    let Some((lhs, rhs)) = key.split_once('@') else {
        return false;
    };
    let ident = |s: &str| {
        let mut c = s.chars();
        matches!(c.next(), Some(f) if f.is_ascii_alphabetic() || f == '_')
            && c.all(|ch| ch.is_ascii_alphanumeric() || ch == '_')
    };
    if !lhs.is_empty() && !ident(lhs) {
        return false;
    }
    let Some((ns, term)) = rhs.split_once('.') else {
        return false;
    };
    matches!(ns, "odata" | "Redfish" | "Message") && ident(term)
}

/// Assert that `resource` declares no property DMTF's `definition` does not know, and
/// omits none that it requires.
#[track_caller]
fn check_against_schema(what: &str, resource: &Value, schema: &Value, definition: &str) {
    let def = schema
        .get("definitions")
        .and_then(|d| d.get(definition))
        .unwrap_or_else(|| panic!("{definition} is not in the vendored schema for {what}"));
    let known: BTreeSet<&str> = schema_properties(def).keys().map(String::as_str).collect();
    assert!(!known.is_empty(), "{what}: the schema listed no properties");

    let obj = resource
        .as_object()
        .unwrap_or_else(|| panic!("{what} is not an object"));
    assert!(!obj.is_empty(), "{what} is empty — a vacuous pass");
    for key in obj.keys() {
        assert!(
            known.contains(key.as_str()) || is_redfish_annotation(key),
            "{what} declares `{key}`, which DMTF's {definition} does not define and \
             which is not a well-formed Redfish annotation"
        );
    }
    for req in schema_required(def) {
        assert!(
            obj.contains_key(&req),
            "{what} is missing `{req}`, which DMTF's {definition} marks required"
        );
    }
}

/// Assert `#Namespace.vX_Y_Z.Type` (or `#Namespace.Type` for an unversioned
/// collection) names a type DMTF's schema file for that exact version declares.
#[track_caller]
fn check_odata_type(what: &str, resource: &Value, schema_file: &str) -> String {
    let ty = resource["@odata.type"]
        .as_str()
        .unwrap_or_else(|| panic!("{what} has no @odata.type"));
    let body = ty
        .strip_prefix('#')
        .unwrap_or_else(|| panic!("{what}: @odata.type must start with '#', got {ty}"));
    let parts: Vec<&str> = body.split('.').collect();
    let (namespace, version, type_name) = match parts.as_slice() {
        [ns, ver, tn] => (*ns, Some(*ver), *tn),
        [ns, tn] => (*ns, None, *tn),
        _ => panic!("{what}: malformed @odata.type {ty}"),
    };
    let schema = fixture(schema_file);
    let id = schema["$id"].as_str().unwrap_or_default();
    let expect_file = match version {
        Some(v) => format!("{namespace}.{v}.json"),
        None => format!("{namespace}.json"),
    };
    assert!(
        id.ends_with(&expect_file),
        "{what}: @odata.type {ty} names {expect_file}, but the vendored schema is {id}"
    );
    assert!(
        schema["definitions"].get(type_name).is_some(),
        "{what}: {expect_file} declares no type `{type_name}`"
    );
    type_name.to_string()
}

/// The enum a DMTF definition declares.
fn schema_enum(schema: &Value, definition: &str) -> BTreeSet<String> {
    schema["definitions"][definition]["enum"]
        .as_array()
        .unwrap_or_else(|| panic!("{definition} declares no enum"))
        .iter()
        .filter_map(Value::as_str)
        .map(str::to_string)
        .collect()
}

// ===========================================================================
// The server under test, and its transport
// ===========================================================================

/// A backend that records the [`BootSpec`] it is handed and launches nothing — so the
/// wire can be proven on a box with no `/dev/kvm`, and so what the BMC *decided to
/// boot* is inspectable without waiting for a machine.
#[derive(Default)]
struct Recorder {
    specs: Mutex<Vec<BootSpec>>,
    powered: Mutex<bool>,
}

impl Recorder {
    fn last(&self) -> Option<BootSpec> {
        self.specs.lock().unwrap().last().cloned()
    }
}

impl Boot for Recorder {
    fn boot(&self, spec: &BootSpec) -> Result<Machine> {
        self.specs.lock().unwrap().push(spec.clone());
        *self.powered.lock().unwrap() = true;
        Ok(Machine::started(format!("rec-{}", spec.name), spec))
    }
}

impl Lifecycle for Recorder {
    fn power_on(&self, _m: &Machine) -> Result<()> {
        *self.powered.lock().unwrap() = true;
        Ok(())
    }
    fn power_off(&self, _m: &Machine) -> Result<()> {
        *self.powered.lock().unwrap() = false;
        Ok(())
    }
    fn status(&self, _m: &Machine) -> Result<PowerState> {
        Ok(if *self.powered.lock().unwrap() {
            PowerState::On
        } else {
            PowerState::Off
        })
    }
}

/// A file that really exists, to be the inserted medium. Only its EXISTENCE matters
/// here — the real ISO is booted by `tests/redfish_kvm_loop_proof.rs`.
fn probe_medium(tag: &str) -> PathBuf {
    let p = std::env::temp_dir().join(format!(
        "draupnir-rfconf-{tag}-{}.iso",
        std::process::id()
    ));
    std::fs::write(&p, b"not really an iso").unwrap();
    p
}

const USER: &str = "admin";
const PASS: &str = "conformance-secret";

/// A server matching DMTF's mockup identity (same system id, same CD slot), so the
/// mockup's own `@odata.id` strings can be compared to ours verbatim.
fn start_server(disk: Option<&str>) -> (RedfishKvmServer, Arc<Recorder>) {
    let rec = Arc::new(Recorder::default());
    let mut cfg = NodeConfig::new(MOCK_SYSTEM)
        .media_slot(MOCK_SLOT)
        .credentials(USER, PASS);
    if let Some(d) = disk {
        cfg = cfg.local_disk(d);
    }
    let server = RedfishKvmServer::start_with(cfg, rec.clone()).expect("the BMC starts");
    (server, rec)
}

/// A `ureq` agent that PINS the server's certificate — full TLS verification stays
/// on, with that one cert as the sole trusted root. Exactly the posture draupnir's
/// own client uses against a real BMC.
fn agent(server: &RedfishKvmServer) -> ureq::Agent {
    use ureq::tls::{Certificate, RootCerts, TlsConfig};
    let cert = Certificate::from_pem(server.cert_pem().as_bytes())
        .expect("the server hands out a well-formed PEM");
    let tls = TlsConfig::builder()
        .root_certs(RootCerts::from([cert]))
        .build();
    ureq::config::Config::builder()
        .tls_config(tls)
        // Inspect 4xx/5xx bodies rather than losing them to an Err.
        .http_status_as_error(false)
        .build()
        .into()
}

/// One exchange: status, headers, body.
struct Exchange {
    status: u16,
    allow: Option<String>,
    odata_version: Option<String>,
    www_authenticate: Option<String>,
    connection: Option<String>,
    body: Value,
}

fn exchange(resp: ureq::http::Response<ureq::Body>) -> Exchange {
    let status = resp.status().as_u16();
    let hdr = |n: &str| {
        resp.headers()
            .get(n)
            .and_then(|v| v.to_str().ok())
            .map(str::to_string)
    };
    let allow = hdr("allow");
    let odata_version = hdr("odata-version");
    let www_authenticate = hdr("www-authenticate");
    let connection = hdr("connection");
    let text = resp.into_body().read_to_string().unwrap_or_default();
    let body = serde_json::from_str(&text).unwrap_or(Value::Null);
    Exchange {
        status,
        allow,
        odata_version,
        www_authenticate,
        connection,
        body,
    }
}

fn get(a: &ureq::Agent, server: &RedfishKvmServer, path: &str) -> Exchange {
    exchange(
        a.get(format!("{}{path}", server.base_url()))
            .header("Authorization", &wire::basic_auth_header(USER, PASS))
            .call()
            .expect("the BMC answers"),
    )
}

fn post(a: &ureq::Agent, server: &RedfishKvmServer, path: &str, body: &Value) -> Exchange {
    exchange(
        a.post(format!("{}{path}", server.base_url()))
            .header("Authorization", &wire::basic_auth_header(USER, PASS))
            .send_json(body)
            .expect("the BMC answers"),
    )
}

fn patch(a: &ureq::Agent, server: &RedfishKvmServer, path: &str, body: &Value) -> Exchange {
    exchange(
        a.patch(format!("{}{path}", server.base_url()))
            .header("Authorization", &wire::basic_auth_header(USER, PASS))
            .send_json(body)
            .expect("the BMC answers"),
    )
}

// ===========================================================================
// 1 — the URL shapes are DMTF's, read out of DMTF's mockup
// ===========================================================================

/// **The strongest anchor in this file.** DMTF's mockup carries the real
/// `@odata.id` of every resource and the real `target` of the Reset action. Those
/// strings — DMTF's, not ours — must equal what [`draupnir::redfish::wire`] builds,
/// which is the same module the CLIENT builds its URLs from.
///
/// RED-when-broken: change any path function and this fails against DMTF's own JSON.
#[test]
fn the_wire_paths_are_the_uris_dmtfs_mockup_publishes() {
    let root = fixture("mockup/public-rackmount1/index.json");
    let systems = fixture("mockup/public-rackmount1/Systems/index.json");
    let system = fixture("mockup/public-rackmount1/Systems/437XR1138R2/index.json");
    let vmc = fixture("mockup/public-rackmount1/Systems/437XR1138R2/VirtualMedia/index.json");
    let vm = fixture("mockup/public-rackmount1/Systems/437XR1138R2/VirtualMedia/CD1/index.json");

    assert_eq!(root["@odata.id"], wire::SERVICE_ROOT_PATH);
    assert_eq!(root["Systems"]["@odata.id"], wire::SYSTEMS_PATH);
    assert_eq!(
        root["Links"]["Sessions"]["@odata.id"], wire::SESSIONS_PATH,
        "even the session collection URI is DMTF's"
    );
    assert_eq!(systems["@odata.id"], wire::SYSTEMS_PATH);

    // The mockup's single member IS the system URI our path function builds.
    assert_eq!(
        systems["Members"][0]["@odata.id"],
        wire::system_path(MOCK_SYSTEM)
    );
    assert_eq!(system["@odata.id"], wire::system_path(MOCK_SYSTEM));
    assert_eq!(
        system["VirtualMedia"]["@odata.id"],
        wire::virtual_media_collection_path(MOCK_SYSTEM)
    );
    assert_eq!(
        vmc["@odata.id"],
        wire::virtual_media_collection_path(MOCK_SYSTEM)
    );
    assert_eq!(
        vm["@odata.id"],
        wire::virtual_media_path(MOCK_SYSTEM, MOCK_SLOT)
    );
    // The Reset action's POST target, straight from DMTF's Actions block.
    assert_eq!(
        system["Actions"][wire::ACTION_RESET][prop::TARGET],
        wire::reset_path(MOCK_SYSTEM)
    );
    // And the collection really does list the slot our client targets by default.
    let members: Vec<&str> = vmc["Members"]
        .as_array()
        .unwrap()
        .iter()
        .filter_map(|m| m["@odata.id"].as_str())
        .collect();
    assert!(
        members.contains(&wire::virtual_media_path(MOCK_SYSTEM, MOCK_SLOT).as_str()),
        "DMTF's VirtualMedia collection lists {members:?}"
    );
}

/// The client composes `host + path`; the server routes on `path`. Prove the two are
/// the *same string* rather than two strings that happen to agree — by driving the
/// client's own URL builders and comparing to the server's routes.
#[cfg(feature = "backend-redfish")]
#[test]
fn the_client_asks_for_exactly_the_paths_the_server_routes() {
    let bmc = draupnir::BmcEndpoint {
        host: "https://bmc.example".into(),
        username: USER.into(),
        system_id: MOCK_SYSTEM.into(),
    };
    // Everything after the host must be the wire path — no second spelling exists.
    for (url, path) in [
        (
            format!("https://bmc.example{}", wire::system_path(MOCK_SYSTEM)),
            wire::system_path(MOCK_SYSTEM),
        ),
        (
            format!("https://bmc.example{}", wire::reset_path(MOCK_SYSTEM)),
            wire::reset_path(MOCK_SYSTEM),
        ),
    ] {
        assert_eq!(url.strip_prefix(&bmc.host).unwrap(), path);
    }
}

// ===========================================================================
// 2 — the six endpoints, each against DMTF's schema and mockup
// ===========================================================================

#[test]
fn the_service_root_conforms_and_matches_dmtfs_mockup_shape() {
    let (server, _rec) = start_server(None);
    let a = agent(&server);

    // DSP0266's protocol-version probe, reachable unauthenticated.
    let probe = exchange(
        a.get(format!("{}{}", server.base_url(), wire::PROTOCOL_VERSION_PATH))
            .call()
            .unwrap(),
    );
    assert_eq!(probe.status, 200);
    assert_eq!(probe.body["v1"], wire::SERVICE_ROOT_PATH);

    let r = get(&a, &server, wire::SERVICE_ROOT_PATH);
    assert_eq!(r.status, 200);
    assert_eq!(
        r.odata_version.as_deref(),
        Some("4.0"),
        "every Redfish response carries OData-Version"
    );
    check_odata_type("ServiceRoot", &r.body, "schemas/ServiceRoot.v1_16_1.json");
    check_against_schema(
        "ServiceRoot",
        &r.body,
        &fixture("schemas/ServiceRoot.v1_16_1.json"),
        "ServiceRoot",
    );

    // The mockup's own structure: a Systems pointer and a resolvable Links.Sessions.
    let mock = fixture("mockup/public-rackmount1/index.json");
    assert_eq!(r.body["@odata.id"], mock["@odata.id"]);
    assert_eq!(r.body["Systems"]["@odata.id"], mock["Systems"]["@odata.id"]);
    assert_eq!(
        r.body["Links"]["Sessions"]["@odata.id"],
        mock["Links"]["Sessions"]["@odata.id"]
    );
    // ...and it RESOLVES. A required pointer to a 404 would be conformance on paper.
    let sessions = get(&a, &server, wire::SESSIONS_PATH);
    assert_eq!(sessions.status, 200, "Links.Sessions must not dangle");
    check_odata_type("SessionCollection", &sessions.body, "schemas/SessionCollection.json");
    check_against_schema(
        "SessionCollection",
        &sessions.body,
        &fixture("schemas/SessionCollection.json"),
        "SessionCollection",
    );

    // The slash-less spelling answers too, as every BMC's does.
    assert_eq!(get(&a, &server, wire::SERVICE_ROOT_PATH_BARE).status, 200);
}

#[test]
fn the_system_collection_conforms_and_lists_the_node() {
    let (server, _rec) = start_server(None);
    let a = agent(&server);
    let r = get(&a, &server, wire::SYSTEMS_PATH);
    assert_eq!(r.status, 200);
    check_odata_type(
        "ComputerSystemCollection",
        &r.body,
        "schemas/ComputerSystemCollection.json",
    );
    check_against_schema(
        "ComputerSystemCollection",
        &r.body,
        &fixture("schemas/ComputerSystemCollection.json"),
        "ComputerSystemCollection",
    );
    // The count and the array agree — DMTF's mockup pairs them and so must we.
    let mock = fixture("mockup/public-rackmount1/Systems/index.json");
    for key in mock.as_object().unwrap().keys().filter(|k| *k != "@Redfish.Copyright") {
        assert!(
            r.body.get(key).is_some(),
            "our collection omits `{key}`, which DMTF's mockup carries"
        );
    }
    assert_eq!(r.body["Members@odata.count"], 1);
    assert_eq!(
        r.body["Members"][0]["@odata.id"],
        wire::system_path(MOCK_SYSTEM)
    );
}

#[test]
fn the_computer_system_conforms_and_its_actions_block_matches_dmtfs() {
    let (server, _rec) = start_server(Some("/var/lib/node.qcow2"));
    let a = agent(&server);
    let r = get(&a, &server, &wire::system_path(MOCK_SYSTEM));
    assert_eq!(r.status, 200);

    let schema = fixture("schemas/ComputerSystem.v1_22_0.json");
    check_odata_type("ComputerSystem", &r.body, "schemas/ComputerSystem.v1_22_0.json");
    check_against_schema("ComputerSystem", &r.body, &schema, "ComputerSystem");
    // The nested objects are schema'd too — an invented `Boot` key would otherwise
    // hide inside a property the top level does allow.
    check_against_schema("ComputerSystem.Boot", &r.body["Boot"], &schema, "Boot");
    check_against_schema("ComputerSystem.Actions", &r.body["Actions"], &schema, "Actions");
    check_against_schema(
        "ComputerSystem.Actions.#ComputerSystem.Reset",
        &r.body["Actions"][wire::ACTION_RESET],
        &schema,
        "Reset",
    );

    // DMTF's mockup carries a Reset action with a target and an allowable-values
    // annotation; ours must carry the same KEYS (the values are instance-specific).
    let mock = fixture("mockup/public-rackmount1/Systems/437XR1138R2/index.json");
    let mock_reset = &mock["Actions"][wire::ACTION_RESET];
    for key in mock_reset.as_object().unwrap().keys() {
        assert!(
            r.body["Actions"][wire::ACTION_RESET].get(key).is_some(),
            "our Reset action omits `{key}`, which DMTF's mockup publishes"
        );
    }
    assert_eq!(
        r.body["Actions"][wire::ACTION_RESET][prop::TARGET],
        wire::reset_path(MOCK_SYSTEM)
    );
    // The Boot object publishes the same three properties DMTF's mockup does, plus
    // its allowable-values annotation.
    for key in [
        prop::BOOT_SOURCE_OVERRIDE_ENABLED,
        prop::BOOT_SOURCE_OVERRIDE_TARGET,
        prop::BOOT_SOURCE_OVERRIDE_MODE,
    ] {
        assert!(mock["Boot"].get(key).is_some(), "the mockup carries {key}");
        assert!(r.body["Boot"].get(key).is_some(), "we omit {key}");
    }
    assert!(r.body["Boot"]
        .get(wire::allowable_values_key(prop::BOOT_SOURCE_OVERRIDE_TARGET))
        .is_some());
}

#[test]
fn the_virtual_media_resource_conforms_and_publishes_both_actions() {
    let (server, _rec) = start_server(None);
    let a = agent(&server);

    let coll = get(&a, &server, &wire::virtual_media_collection_path(MOCK_SYSTEM));
    assert_eq!(coll.status, 200);
    check_odata_type(
        "VirtualMediaCollection",
        &coll.body,
        "schemas/VirtualMediaCollection.json",
    );
    check_against_schema(
        "VirtualMediaCollection",
        &coll.body,
        &fixture("schemas/VirtualMediaCollection.json"),
        "VirtualMediaCollection",
    );

    let r = get(&a, &server, &wire::virtual_media_path(MOCK_SYSTEM, MOCK_SLOT));
    assert_eq!(r.status, 200);
    let schema = fixture("schemas/VirtualMedia.v1_6_3.json");
    check_odata_type("VirtualMedia", &r.body, "schemas/VirtualMedia.v1_6_3.json");
    check_against_schema("VirtualMedia", &r.body, &schema, "VirtualMedia");
    check_against_schema("VirtualMedia.Actions", &r.body["Actions"], &schema, "Actions");
    check_against_schema(
        "VirtualMedia.Actions.#VirtualMedia.InsertMedia",
        &r.body["Actions"][wire::ACTION_INSERT_MEDIA],
        &schema,
        "InsertMedia",
    );
    check_against_schema(
        "VirtualMedia.Actions.#VirtualMedia.EjectMedia",
        &r.body["Actions"][wire::ACTION_EJECT_MEDIA],
        &schema,
        "EjectMedia",
    );
    assert_eq!(
        r.body["Actions"][wire::ACTION_INSERT_MEDIA][prop::TARGET],
        wire::virtual_media_action_path(MOCK_SYSTEM, MOCK_SLOT, wire::INSERT_MEDIA)
    );

    // The properties DMTF's mockup of this very slot publishes, we publish too.
    let mock = fixture("mockup/public-rackmount1/Systems/437XR1138R2/VirtualMedia/CD1/index.json");
    for key in [
        "Id",
        "Name",
        "MediaTypes",
        "ConnectedVia",
        prop::IMAGE,
        prop::IMAGE_NAME,
        prop::INSERTED,
        prop::WRITE_PROTECTED,
    ] {
        assert!(mock.get(key).is_some(), "the mockup carries {key}");
        assert!(r.body.get(key).is_some(), "we omit {key}");
    }
    assert_eq!(r.body["Id"], MOCK_SLOT);
}

// ===========================================================================
// 3 — the advertised vocabularies are DMTF's enums
// ===========================================================================

/// `ResetType@Redfish.AllowableValues` must be a SUBSET of DMTF's `ResetType` enum,
/// and `BootSourceOverrideTarget@Redfish.AllowableValues` of DMTF's `BootSource`.
///
/// Subset, not equality: advertising only what an implementation can actually do is
/// exactly what the specification asks. But a token that is merely plausible —
/// `PowerOn`, `Reboot`, `CDROM` — is not in DMTF's enum, and this is where it dies.
#[test]
fn every_advertised_value_is_a_token_dmtfs_enum_declares() {
    let (server, _rec) = start_server(None);
    let a = agent(&server);
    let sys = get(&a, &server, &wire::system_path(MOCK_SYSTEM)).body;

    let reset_enum = schema_enum(&fixture("schemas/Resource.json"), "ResetType");
    let advertised: Vec<&str> = sys["Actions"][wire::ACTION_RESET]
        [wire::allowable_values_key(prop::RESET_TYPE)]
    .as_array()
    .expect("ResetType allowable values are published")
    .iter()
    .filter_map(Value::as_str)
    .collect();
    assert!(!advertised.is_empty());
    for v in &advertised {
        assert!(
            reset_enum.contains(*v),
            "we advertise ResetType `{v}`, which DMTF's Resource.json ResetType enum \
             does not declare (DMTF has: {reset_enum:?})"
        );
    }
    // The two the CLIENT actually sends must be advertised, or draupnir's own client
    // would be refused by draupnir's own BMC.
    assert!(advertised.contains(&wire::RESET_ON));
    assert!(advertised.contains(&wire::RESET_FORCE_OFF));

    let boot_enum = schema_enum(&fixture("schemas/ComputerSystem.json"), "BootSource");
    let targets: Vec<&str> = sys["Boot"][wire::allowable_values_key(prop::BOOT_SOURCE_OVERRIDE_TARGET)]
        .as_array()
        .expect("BootSourceOverrideTarget allowable values are published")
        .iter()
        .filter_map(Value::as_str)
        .collect();
    assert!(!targets.is_empty());
    for v in &targets {
        assert!(
            boot_enum.contains(*v),
            "we advertise BootSourceOverrideTarget `{v}`, not in DMTF's BootSource enum"
        );
    }
    // The token the client sets for BootOrder::Medium must be advertised.
    assert!(targets.contains(&wire::target_str(draupnir::BootTarget::Cd)));
    assert!(targets.contains(&wire::target_str(draupnir::BootTarget::Hdd)));

    // BootSourceOverrideEnabled's live value is in DMTF's enum too.
    let enabled_enum = schema_enum(
        &fixture("schemas/ComputerSystem.v1_22_0.json"),
        "BootSourceOverrideEnabled",
    );
    for v in wire::OVERRIDE_ENABLED_ALLOWABLE {
        assert!(enabled_enum.contains(*v), "{v} is not a DMTF token");
    }
    assert!(enabled_enum
        .contains(sys["Boot"][prop::BOOT_SOURCE_OVERRIDE_ENABLED].as_str().unwrap()));

    // And PowerState.
    let power_enum = schema_enum(&fixture("schemas/Resource.json"), "PowerState");
    assert!(power_enum.contains(sys[prop::POWER_STATE].as_str().unwrap()));
}

// ===========================================================================
// 4 — the error vocabulary is DMTF's registry, entry for entry
// ===========================================================================

/// Every message `src/redfish_server.rs` can emit is compared field-for-field with
/// DMTF's Base registry. A hand-typed error string that merely *sounds* like Redfish
/// dies here.
#[test]
fn every_error_message_matches_dmtfs_base_registry_entry() {
    let reg = fixture("registries/Base.1.19.0.json");
    assert_eq!(reg["Id"], BASE_REGISTRY);
    assert!(!ALL_MESSAGES.is_empty());
    for m in ALL_MESSAGES {
        let entry = reg["Messages"].get(m.id).unwrap_or_else(|| {
            panic!("{} is not an entry in DMTF's {BASE_REGISTRY} registry", m.id)
        });
        assert_eq!(entry["Message"], m.template, "{}: Message template", m.id);
        assert_eq!(
            entry["MessageSeverity"], m.severity,
            "{}: MessageSeverity",
            m.id
        );
        assert_eq!(
            entry["NumberOfArgs"].as_u64().unwrap() as usize,
            m.nargs,
            "{}: NumberOfArgs",
            m.id
        );
        assert_eq!(entry["Resolution"], m.resolution, "{}: Resolution", m.id);
        assert_eq!(
            m.message_id(),
            format!("{BASE_REGISTRY}.{}", m.id),
            "MessageId is registry-qualified"
        );
    }
    println!(
        "ANCHOR: {} error messages verified against DMTF {BASE_REGISTRY}",
        ALL_MESSAGES.len()
    );
}

/// A live error payload matches DMTF's `redfish-error.v1_0_2` and its embedded
/// `Message.v1_1_2`.
#[test]
fn a_live_error_payload_conforms_to_the_dmtf_error_schema() {
    let (server, _rec) = start_server(None);
    let a = agent(&server);
    let r = post(
        &a,
        &server,
        &wire::reset_path(MOCK_SYSTEM),
        &wire::reset_body("Reboot"),
    );
    assert_eq!(r.status, 400);

    let err_schema = fixture("schemas/redfish-error.v1_0_2.json");
    check_against_schema("error payload", &r.body, &err_schema, "RedfishError");
    check_against_schema(
        "error contents",
        &r.body["error"],
        &err_schema,
        "RedfishErrorContents",
    );
    let info = &r.body["error"]["@Message.ExtendedInfo"][0];
    check_against_schema(
        "extended info",
        info,
        &fixture("schemas/Message.v1_1_2.json"),
        "Message",
    );
    // The rendered Message really is the registry template with the args filled in.
    let reg = fixture("registries/Base.1.19.0.json");
    let template = reg["Messages"]["ActionParameterValueNotInList"]["Message"]
        .as_str()
        .unwrap();
    let args: Vec<&str> = info["MessageArgs"]
        .as_array()
        .unwrap()
        .iter()
        .filter_map(Value::as_str)
        .collect();
    let mut expect = template.to_string();
    for (i, arg) in args.iter().enumerate().rev() {
        expect = expect.replace(&format!("%{}", i + 1), arg);
    }
    assert_eq!(
        info["Message"].as_str().unwrap(),
        expect,
        "the rendered Message must be DMTF's template with MessageArgs substituted"
    );
    assert_eq!(r.body["error"]["message"], info["Message"]);
}

// ===========================================================================
// 5 — status codes, Allow, and the RED controls
// ===========================================================================

/// **RED 1 — a bad `ResetType` is refused BY NAME.**
#[test]
fn a_reset_type_outside_the_advertised_list_is_refused_by_name_and_nothing_boots() {
    let (server, rec) = start_server(None);
    let a = agent(&server);
    for bad in ["Reboot", "PowerOn", "on", "", "ForceOff "] {
        let r = post(
            &a,
            &server,
            &wire::reset_path(MOCK_SYSTEM),
            &wire::reset_body(bad),
        );
        assert_eq!(r.status, 400, "ResetType {bad:?} must be a 400");
        assert_eq!(
            r.body["error"]["code"],
            format!("{BASE_REGISTRY}.ActionParameterValueNotInList"),
            "ResetType {bad:?}"
        );
        assert!(
            r.body["error"]["message"]
                .as_str()
                .unwrap()
                .contains(&format!("'{bad}'")),
            "the refusal QUOTES the value it refused: {}",
            r.body["error"]["message"]
        );
        assert!(r.body["error"]["message"]
            .as_str()
            .unwrap()
            .contains(prop::RESET_TYPE));
    }
    assert!(
        rec.last().is_none(),
        "a refused ResetType must never have booted anything"
    );
    // ...and a MISSING ResetType names the missing parameter rather than defaulting.
    let r = post(&a, &server, &wire::reset_path(MOCK_SYSTEM), &serde_json::json!({}));
    assert_eq!(r.status, 400);
    assert_eq!(
        r.body["error"]["code"],
        format!("{BASE_REGISTRY}.ActionParameterMissing")
    );
    assert!(rec.last().is_none());
}

/// **RED 2 — `InsertMedia` pointing at an image that is not there is refused BY
/// NAME,** before anything is mounted.
///
/// An emulator that accepted it would hand a later boot an empty tray, and the
/// failure would read as "the appliance did not come up" instead of "you named an ISO
/// that was never built" — different bugs with different owners.
#[test]
fn insert_media_naming_an_image_that_does_not_exist_is_refused_by_name() {
    let (server, _rec) = start_server(None);
    let a = agent(&server);
    let insert_path = wire::virtual_media_action_path(MOCK_SYSTEM, MOCK_SLOT, wire::INSERT_MEDIA);

    for ghost in [
        "/nonexistent/never-built-by-anyone.iso",
        "file:///nonexistent/never-built-by-anyone.iso",
    ] {
        let r = post(&a, &server, &insert_path, &wire::insert_media_body(ghost));
        assert_eq!(r.status, 400, "{ghost}");
        assert_eq!(
            r.body["error"]["code"],
            format!("{BASE_REGISTRY}.ResourceMissingAtURI")
        );
        assert!(
            r.body["error"]["message"].as_str().unwrap().contains(ghost),
            "the refusal NAMES the URI: {}",
            r.body["error"]["message"]
        );
    }
    // A directory is not a medium either.
    let r = post(&a, &server, &insert_path, &wire::insert_media_body("/tmp"));
    assert_eq!(r.status, 400);

    // Nothing was mounted by any of that.
    let vm = get(&a, &server, &wire::virtual_media_path(MOCK_SYSTEM, MOCK_SLOT));
    assert_eq!(vm.body[prop::INSERTED], false);
    assert!(vm.body[prop::IMAGE].is_null());
    assert!(server.inserted_image().is_none());

    // A missing Image PARAMETER names the parameter, not the file.
    let r = post(&a, &server, &insert_path, &serde_json::json!({ "Inserted": true }));
    assert_eq!(r.status, 400);
    assert_eq!(
        r.body["error"]["code"],
        format!("{BASE_REGISTRY}.ActionParameterMissing")
    );
    assert!(r.body["error"]["message"].as_str().unwrap().contains(prop::IMAGE));
}

/// **RED 3 — the boot override is APPLIED OUTPUT, not a `204` on the wire.**
///
/// This is the control that matters most. A server can answer `204 No Content` to a
/// `Boot` PATCH and then boot exactly what it would have booted anyway; every
/// status-code assertion in this file would still pass. So the check is on the
/// [`BootSpec`] the BMC actually handed its backend:
///
/// * `Cd` with media inserted → [`BootOrder::Medium`] and the medium NAMED.
/// * `Hdd`, **with the same media still in the tray** → [`BootOrder::Disk`] and
///   `medium_path() == None`. The ISO is not attached to the machine at all, which is
///   what makes the override applied rather than merely acknowledged.
///
/// `tests/redfish_kvm_loop_proof.rs` carries the same control one level down, on a
/// real guest's serial console.
#[test]
fn a_boot_override_changes_what_the_node_actually_boots_not_just_the_status_code() {
    let iso = probe_medium("override");
    let disk = probe_medium("disk"); // stands in for a local disk; only existence matters
    let (server, rec) = start_server(Some(&disk.to_string_lossy()));
    let a = agent(&server);
    let insert_path = wire::virtual_media_action_path(MOCK_SYSTEM, MOCK_SLOT, wire::INSERT_MEDIA);
    let system_path = wire::system_path(MOCK_SYSTEM);
    let reset_path = wire::reset_path(MOCK_SYSTEM);

    // Insert once. The tray stays loaded for BOTH legs, exactly as on real hardware.
    let r = post(
        &a,
        &server,
        &insert_path,
        &wire::insert_media_body(&iso.to_string_lossy()),
    );
    assert_eq!(r.status, 204, "a successful action is 204 No Content");

    // ── leg A: override to Cd ────────────────────────────────────────────────
    let r = patch(
        &a,
        &server,
        &system_path,
        &wire::boot_override_body(draupnir::BootTarget::Cd),
    );
    assert_eq!(r.status, 204);
    // Readable back before it is consumed.
    let sys = get(&a, &server, &system_path).body;
    assert_eq!(sys["Boot"][prop::BOOT_SOURCE_OVERRIDE_TARGET], "Cd");
    assert_eq!(
        sys["Boot"][prop::BOOT_SOURCE_OVERRIDE_ENABLED],
        wire::OVERRIDE_ONCE
    );

    assert_eq!(
        post(&a, &server, &reset_path, &wire::reset_body(wire::RESET_ON)).status,
        204
    );
    let booted = rec.last().expect("the BMC booted something");
    assert_eq!(booted.boot_order, BootOrder::Medium);
    assert_eq!(
        booted.medium_path(),
        Some(iso.to_string_lossy().as_ref()),
        "the Cd override boots OFF the medium"
    );
    assert_eq!(server.last_boot_spec().unwrap(), booted);

    // ── leg B: same tray, override to Hdd ────────────────────────────────────
    assert_eq!(
        post(&a, &server, &reset_path, &wire::reset_body(wire::RESET_FORCE_OFF)).status,
        204
    );
    let r = patch(
        &a,
        &server,
        &system_path,
        &wire::boot_override_body(draupnir::BootTarget::Hdd),
    );
    assert_eq!(r.status, 204);
    assert_eq!(
        post(&a, &server, &reset_path, &wire::reset_body(wire::RESET_ON)).status,
        204
    );
    let booted = rec.last().expect("the BMC booted something");
    assert_eq!(booted.boot_order, BootOrder::Disk);
    assert_eq!(
        booted.medium_path(),
        None,
        "an Hdd override must DETACH the medium — a 204 that left the ISO attached \
         would be an override accepted and never applied"
    );
    // The medium is still IN THE TRAY; it is the boot that stopped using it.
    let vm = get(&a, &server, &wire::virtual_media_path(MOCK_SYSTEM, MOCK_SLOT)).body;
    assert_eq!(vm[prop::INSERTED], true);
    assert_eq!(vm[prop::IMAGE], iso.to_string_lossy().as_ref());

    let _ = std::fs::remove_file(&iso);
    let _ = std::fs::remove_file(&disk);
}

/// A one-time override is CONSUMED by the boot it steered — a stale `Once` must not
/// silently steer the next one.
#[test]
fn a_once_override_is_consumed_by_the_boot_it_applied_to() {
    let iso = probe_medium("once");
    let (server, _rec) = start_server(None);
    let a = agent(&server);
    let insert_path = wire::virtual_media_action_path(MOCK_SYSTEM, MOCK_SLOT, wire::INSERT_MEDIA);
    post(
        &a,
        &server,
        &insert_path,
        &wire::insert_media_body(&iso.to_string_lossy()),
    );
    patch(
        &a,
        &server,
        &wire::system_path(MOCK_SYSTEM),
        &wire::boot_override_body(draupnir::BootTarget::Cd),
    );
    post(
        &a,
        &server,
        &wire::reset_path(MOCK_SYSTEM),
        &wire::reset_body(wire::RESET_ON),
    );
    let sys = get(&a, &server, &wire::system_path(MOCK_SYSTEM)).body;
    assert_eq!(
        sys["Boot"][prop::BOOT_SOURCE_OVERRIDE_ENABLED],
        wire::OVERRIDE_DISABLED
    );
    assert_eq!(sys["Boot"][prop::BOOT_SOURCE_OVERRIDE_TARGET], "None");
    let _ = std::fs::remove_file(&iso);
}

/// A `BootSourceOverrideTarget` this node cannot honour is refused by name — never
/// coerced into one it can.
#[test]
fn an_unhonourable_boot_target_is_refused_rather_than_silently_substituted() {
    let (server, rec) = start_server(None);
    let a = agent(&server);
    for target in ["Pxe", "BiosSetup", "Usb", "NotAToken"] {
        let r = patch(
            &a,
            &server,
            &wire::system_path(MOCK_SYSTEM),
            &serde_json::json!({ prop::BOOT: { prop::BOOT_SOURCE_OVERRIDE_TARGET: target } }),
        );
        assert_eq!(r.status, 400, "{target}");
        assert_eq!(
            r.body["error"]["code"],
            format!("{BASE_REGISTRY}.PropertyValueNotInList")
        );
        assert!(r.body["error"]["message"].as_str().unwrap().contains(target));
    }
    // Nothing was applied and nothing booted.
    let sys = get(&a, &server, &wire::system_path(MOCK_SYSTEM)).body;
    assert_eq!(sys["Boot"][prop::BOOT_SOURCE_OVERRIDE_TARGET], "None");
    assert!(rec.last().is_none());
}

#[test]
fn a_wrong_method_answers_405_with_an_allow_header() {
    let (server, _rec) = start_server(None);
    let a = agent(&server);
    let cases = [
        (
            "GET",
            wire::reset_path(MOCK_SYSTEM),
            "POST",
        ),
        (
            "POST",
            wire::system_path(MOCK_SYSTEM),
            "GET, PATCH",
        ),
        (
            "PATCH",
            wire::virtual_media_action_path(MOCK_SYSTEM, MOCK_SLOT, wire::INSERT_MEDIA),
            "POST",
        ),
        ("PATCH", wire::SYSTEMS_PATH.to_string(), "GET"),
        ("POST", wire::SESSIONS_PATH.to_string(), "GET"),
    ];
    for (method, path, allow) in cases {
        let url = format!("{}{path}", server.base_url());
        let req = ureq::http::Request::builder()
            .method(method)
            .uri(&url)
            .header("Authorization", wire::basic_auth_header(USER, PASS))
            .body(())
            .expect("a well-formed request");
        let r = exchange(a.run(req).expect("the BMC answers"));
        assert_eq!(r.status, 405, "{method} {path}");
        assert_eq!(r.allow.as_deref(), Some(allow), "{method} {path}");
        assert_eq!(r.odata_version.as_deref(), Some("4.0"));
    }
}

#[test]
fn everything_below_the_service_root_requires_a_credential() {
    let (server, _rec) = start_server(None);
    let a = agent(&server);
    let bare = |path: &str| {
        exchange(
            a.get(format!("{}{path}", server.base_url()))
                .call()
                .unwrap(),
        )
    };
    // The two DSP0266 unauthenticated URIs.
    assert_eq!(bare(wire::PROTOCOL_VERSION_PATH).status, 200);
    assert_eq!(bare(wire::SERVICE_ROOT_PATH).status, 200);
    // Everything else.
    for p in [
        wire::SYSTEMS_PATH.to_string(),
        wire::system_path(MOCK_SYSTEM),
        wire::virtual_media_path(MOCK_SYSTEM, MOCK_SLOT),
    ] {
        let r = bare(&p);
        assert_eq!(r.status, 401, "{p}");
        assert!(r.www_authenticate.unwrap_or_default().starts_with("Basic "), "{p}");
        assert_eq!(
            r.body["error"]["code"],
            format!("{BASE_REGISTRY}.NoValidSession"),
            "{p}"
        );
    }
    // A wrong password is not a credential.
    let r = exchange(
        a.get(format!("{}{}", server.base_url(), wire::system_path(MOCK_SYSTEM)))
            .header("Authorization", &wire::basic_auth_header(USER, "wrong"))
            .call()
            .unwrap(),
    );
    assert_eq!(r.status, 401);
}

/// **Regression guard for a measured flake.**
///
/// With HTTP keep-alive this suite failed with `ECONNRESET` in **3 of 28** full runs
/// (measured on this box, 2026-08-15); after the fix, **0 of 40**.
/// Instrumenting the accept loop showed the classic idle-close race — the client's
/// pool retired a connection the server was already closing. The service now answers
/// one request per connection and says so, which removes the race by construction
/// rather than by hoping the client retries.
///
/// So: every response must carry `Connection: close`, and a long run of sequential
/// requests through ONE agent must all succeed. RED-when-broken: put keep-alive back
/// and the header assertion fails immediately, and the loop below starts flaking
/// again the way the suite used to.
#[test]
fn every_response_closes_its_connection_instead_of_racing_the_clients_pool() {
    let (server, _rec) = start_server(None);
    let a = agent(&server);
    for path in [
        wire::SERVICE_ROOT_PATH.to_string(),
        wire::SYSTEMS_PATH.to_string(),
        wire::system_path(MOCK_SYSTEM),
        wire::virtual_media_path(MOCK_SYSTEM, MOCK_SLOT),
    ] {
        let r = get(&a, &server, &path);
        assert_eq!(r.status, 200, "{path}");
        assert_eq!(
            r.connection.as_deref().map(str::to_ascii_lowercase).as_deref(),
            Some("close"),
            "{path} must announce that it closes the connection"
        );
    }
    // 60 sequential requests through one agent, all of which must land. Under the
    // old keep-alive path this is exactly where the reset appeared.
    for i in 0..60 {
        let r = get(&a, &server, &wire::system_path(MOCK_SYSTEM));
        assert_eq!(r.status, 200, "request {i} of a long sequential run");
    }
}

#[test]
fn an_unknown_uri_is_a_dmtf_shaped_404() {
    let (server, _rec) = start_server(None);
    let a = agent(&server);
    let r = get(&a, &server, "/redfish/v1/Systems/some-other-node");
    assert_eq!(r.status, 404);
    check_against_schema(
        "404 payload",
        &r.body,
        &fixture("schemas/redfish-error.v1_0_2.json"),
        "RedfishError",
    );
    assert_eq!(
        r.body["error"]["code"],
        format!("{BASE_REGISTRY}.ResourceNotFound")
    );
    assert!(r.body["error"]["message"]
        .as_str()
        .unwrap()
        .contains("some-other-node"));
}

/// The TLS identity is real and PINNABLE — the whole reason the server needs a cert
/// at all is that draupnir's client pins one.
#[test]
fn the_servers_certificate_is_a_real_pinnable_identity() {
    let (server, _rec) = start_server(None);
    let pem = server.cert_pem();
    assert!(pem.starts_with("-----BEGIN CERTIFICATE-----"), "PEM shape");
    assert!(pem.contains("-----END CERTIFICATE-----"));
    // A pinned agent reaches it (verification ON, this cert the sole root).
    assert_eq!(get(&agent(&server), &server, wire::SERVICE_ROOT_PATH).status, 200);

    // ...and an agent that pins the WRONG cert must FAIL. Without this, "pinning
    // works" would be indistinguishable from "verification is off".
    let (other, _r2) = start_server(None);
    use ureq::tls::{Certificate, RootCerts, TlsConfig};
    let wrong = Certificate::from_pem(other.cert_pem().as_bytes()).unwrap();
    let a: ureq::Agent = ureq::config::Config::builder()
        .tls_config(TlsConfig::builder().root_certs(RootCerts::from([wrong])).build())
        .http_status_as_error(false)
        .build()
        .into();
    let err = a
        .get(format!("{}{}", server.base_url(), wire::SERVICE_ROOT_PATH))
        .call();
    assert!(
        err.is_err(),
        "a client pinning a DIFFERENT cert must be refused by TLS, not served"
    );
    // No key material was ever written to disk.
    assert!(!pem.contains("PRIVATE KEY"), "the PEM handed out is the cert only");
}

// ===========================================================================
// 6 — draupnir's own CLIENT drives the six endpoints end to end
// ===========================================================================

/// The mirror leg, kept honest by everything above: with the shapes already anchored
/// to DMTF, driving the real client through the real server proves the two ends
/// actually connect — `InsertMedia` → boot override → `Reset` → power readback, over
/// TLS with the cert pinned.
#[cfg(feature = "backend-redfish")]
#[test]
fn draupnirs_own_redfish_client_drives_the_whole_burn_against_this_server() {
    use draupnir::redfish::RedfishBoot;
    use draupnir::{Boot, Lifecycle, VirtualMedia};

    let iso = probe_medium("client");
    let (server, rec) = start_server(None);
    let bmc = server.bmc_endpoint();
    let client = RedfishBoot::new()
        .with_password(PASS)
        .media_id(MOCK_SLOT)
        .pin_cert_pem(server.cert_pem().as_bytes().to_vec());

    // The SAME spec `iso_boot` builds, routed to metal — honesty rule 2.
    let spec = BootSpec::iso_boot("conformance-burn", iso.to_string_lossy()).on_metal(bmc.clone());
    let machine = client.boot(&spec).expect("the client drives the burn");
    assert_eq!(machine.id, bmc.system_id);

    // The BMC really did insert, override and power on — read back off the wire.
    assert_eq!(
        server.inserted_image().as_deref(),
        Some(iso.to_string_lossy().as_ref())
    );
    let booted = rec.last().expect("the node booted");
    assert_eq!(booted.boot_order, BootOrder::Medium);
    assert_eq!(booted.medium_path(), Some(iso.to_string_lossy().as_ref()));

    // Power readback through the client's Lifecycle.
    let bound = RedfishBoot::for_node(bmc, PASS)
        .media_id(MOCK_SLOT)
        .pin_cert_pem(server.cert_pem().as_bytes().to_vec());
    assert_eq!(bound.status(&machine).unwrap(), PowerState::On);
    bound.power_off(&machine).expect("ForceOff");
    assert_eq!(bound.status(&machine).unwrap(), PowerState::Off);

    // Eject, and the tray really empties.
    bound.eject_media(&bound_endpoint(&server)).expect("EjectMedia");
    assert!(server.inserted_image().is_none());

    let _ = std::fs::remove_file(&iso);
}

#[cfg(feature = "backend-redfish")]
fn bound_endpoint(server: &RedfishKvmServer) -> draupnir::BmcEndpoint {
    server.bmc_endpoint()
}