Gunnar

mikronova/draupnir

src/redfish/wire.rs

//! **The Redfish wire vocabulary — ONE writer, both ends.**
//!
//! Draupnir owns both sides of this protocol: the [client](crate::redfish)
//! (`backend-redfish`, which drives a real iDRAC/iLO/OpenBMC) and the
//! [server](crate::redfish_server) (`redfish-server`, draupnir's own BMC fronting
//! KVM). The resource **paths**, the JSON **property names**, the **action** names
//! and the enum **tokens** are therefore written down exactly once, here, and both
//! ends read them from this module.
//!
//! That is not tidiness — it is the only thing that keeps the two from drifting into
//! a private dialect. A server that routed on a hand-typed
//! `"/redfish/v1/Systems/{}/Actions/ComputerSystem.Reset"` would pass every test
//! against a client that hand-typed the same string, and fail against a real BMC the
//! first time either string was edited. Here the client composes `host + path()` and
//! the server matches on `path()`, so "the two agree" is true by construction rather
//! than by coincidence.
//!
//! # What this module is NOT
//!
//! It is not a claim of conformance. The tokens below are transcribed from DMTF's
//! published schema and the shapes are pinned against DMTF's published mockup by
//! `tests/redfish_server_conformance.rs`; *that* test is the anchor, and it reads
//! DMTF's own JSON, not this file. See `tests/fixtures/dmtf/PROVENANCE.md`.
//!
//! Path/vocabulary is pure `std` and always compiles. The JSON body builders and
//! readers need `serde_json`, so they ride whichever end pulled it in.

use crate::BootTarget;

// ---------------------------------------------------------------------------
// Resource paths — the ABSOLUTE path half of every URL. The client prefixes the
// BMC host; the server routes on these verbatim.
// ---------------------------------------------------------------------------

/// The protocol-version probe (`GET /redfish` → `{"v1": "/redfish/v1/"}`) — DSP0266
/// §"Protocol version". The one URI in the whole service whose shape is fixed by the
/// specification rather than by a schema.
pub const PROTOCOL_VERSION_PATH: &str = "/redfish";

/// The service root, WITH its trailing slash. DMTF's own mockup gives the
/// `ServiceRoot` an `@odata.id` of `/redfish/v1/`, so this is the canonical spelling;
/// a server should also answer the slash-less form.
pub const SERVICE_ROOT_PATH: &str = "/redfish/v1/";

/// The service root without the trailing slash — the spelling a client is most
/// likely to type, and the one every BMC also answers.
pub const SERVICE_ROOT_PATH_BARE: &str = "/redfish/v1";

/// The `ComputerSystemCollection` path.
pub const SYSTEMS_PATH: &str = "/redfish/v1/Systems";

/// The `ComputerSystem` resource path: `/redfish/v1/Systems/{system_id}`.
pub fn system_path(system_id: &str) -> String {
    format!("{SYSTEMS_PATH}/{system_id}")
}

/// The `VirtualMediaCollection` path for a system.
pub fn virtual_media_collection_path(system_id: &str) -> String {
    format!("{}/VirtualMedia", system_path(system_id))
}

/// A `VirtualMedia` resource path (the CD/DVD slot), e.g.
/// `/redfish/v1/Systems/{system_id}/VirtualMedia/CD`.
pub fn virtual_media_path(system_id: &str, slot: &str) -> String {
    format!("{}/{slot}", virtual_media_collection_path(system_id))
}

/// A `VirtualMedia` action path, e.g.
/// `/redfish/v1/Systems/{system_id}/VirtualMedia/{slot}/Actions/VirtualMedia.InsertMedia`.
pub fn virtual_media_action_path(system_id: &str, slot: &str, action: &str) -> String {
    format!(
        "{}/Actions/VirtualMedia.{action}",
        virtual_media_path(system_id, slot)
    )
}

/// The `ComputerSystem.Reset` action path.
pub fn reset_path(system_id: &str) -> String {
    format!("{}/Actions/ComputerSystem.Reset", system_path(system_id))
}

/// The `SessionCollection` path the `ServiceRoot`'s **required** `Links.Sessions`
/// points at.
pub const SESSIONS_PATH: &str = "/redfish/v1/SessionService/Sessions";

// ---------------------------------------------------------------------------
// Action names.
// ---------------------------------------------------------------------------

/// The `VirtualMedia` action that mounts an image.
pub const INSERT_MEDIA: &str = "InsertMedia";
/// The `VirtualMedia` action that unmounts it.
pub const EJECT_MEDIA: &str = "EjectMedia";

/// The `Actions` key naming `ComputerSystem.Reset` on a `ComputerSystem`.
pub const ACTION_RESET: &str = "#ComputerSystem.Reset";
/// The `Actions` key naming `VirtualMedia.InsertMedia` on a `VirtualMedia`.
pub const ACTION_INSERT_MEDIA: &str = "#VirtualMedia.InsertMedia";
/// The `Actions` key naming `VirtualMedia.EjectMedia` on a `VirtualMedia`.
pub const ACTION_EJECT_MEDIA: &str = "#VirtualMedia.EjectMedia";

// ---------------------------------------------------------------------------
// Property names — the client WRITES them, the server READS them.
// ---------------------------------------------------------------------------

/// The JSON property names this protocol exchanges. Constants, not literals, so a
/// rename is one edit and both ends move together.
pub mod prop {
    /// `VirtualMedia.Image` — the media URI (the `InsertMedia` parameter, and the
    /// property the mounted image is readable back from).
    pub const IMAGE: &str = "Image";
    /// `VirtualMedia.Inserted`.
    pub const INSERTED: &str = "Inserted";
    /// `VirtualMedia.WriteProtected`.
    pub const WRITE_PROTECTED: &str = "WriteProtected";
    /// `VirtualMedia.ImageName`.
    pub const IMAGE_NAME: &str = "ImageName";
    /// `ComputerSystem.Boot`.
    pub const BOOT: &str = "Boot";
    /// `ComputerSystem.Boot.BootSourceOverrideEnabled`.
    pub const BOOT_SOURCE_OVERRIDE_ENABLED: &str = "BootSourceOverrideEnabled";
    /// `ComputerSystem.Boot.BootSourceOverrideTarget`.
    pub const BOOT_SOURCE_OVERRIDE_TARGET: &str = "BootSourceOverrideTarget";
    /// `ComputerSystem.Boot.BootSourceOverrideMode`.
    pub const BOOT_SOURCE_OVERRIDE_MODE: &str = "BootSourceOverrideMode";
    /// The `ComputerSystem.Reset` action parameter.
    pub const RESET_TYPE: &str = "ResetType";
    /// `ComputerSystem.PowerState`.
    pub const POWER_STATE: &str = "PowerState";
    /// The `@Redfish.AllowableValues` annotation suffix — appended to a property or
    /// parameter name (`ResetType@Redfish.AllowableValues`).
    pub const ALLOWABLE_VALUES_SUFFIX: &str = "@Redfish.AllowableValues";
    /// An action object's `target` (the URI a POST goes to).
    pub const TARGET: &str = "target";
}

/// `{name}@Redfish.AllowableValues` — the annotation a service publishes so a client
/// can discover the legal values of a property or action parameter *before* sending
/// one. Both ends build the key here so a bad `ResetType` refusal can be checked
/// against the very annotation that advertised the good ones.
pub fn allowable_values_key(name: &str) -> String {
    format!("{name}{}", prop::ALLOWABLE_VALUES_SUFFIX)
}

// ---------------------------------------------------------------------------
// Enum tokens.
// ---------------------------------------------------------------------------

/// `BootSourceOverrideEnabled: "Once"` — a ONE-TIME override, consumed by the next
/// boot. The only value draupnir's client ever sets.
pub const OVERRIDE_ONCE: &str = "Once";
/// `BootSourceOverrideEnabled: "Disabled"` — no override; the node's own boot list.
pub const OVERRIDE_DISABLED: &str = "Disabled";
/// `BootSourceOverrideEnabled: "Continuous"`.
pub const OVERRIDE_CONTINUOUS: &str = "Continuous";

/// The `BootSourceOverrideEnabled` values a service accepts (DMTF
/// `ComputerSystem.v1_22_0.json#/definitions/BootSourceOverrideEnabled`).
pub const OVERRIDE_ENABLED_ALLOWABLE: &[&str] =
    &[OVERRIDE_DISABLED, OVERRIDE_ONCE, OVERRIDE_CONTINUOUS];

/// `ResetType: "On"` — power the node up.
pub const RESET_ON: &str = "On";
/// `ResetType: "ForceOn"`.
pub const RESET_FORCE_ON: &str = "ForceOn";
/// `ResetType: "ForceOff"` — cut power.
pub const RESET_FORCE_OFF: &str = "ForceOff";
/// `ResetType: "GracefulShutdown"`.
pub const RESET_GRACEFUL_SHUTDOWN: &str = "GracefulShutdown";
/// `ResetType: "ForceRestart"`.
pub const RESET_FORCE_RESTART: &str = "ForceRestart";
/// `ResetType: "GracefulRestart"`.
pub const RESET_GRACEFUL_RESTART: &str = "GracefulRestart";

/// The `ResetType` values draupnir's BMC accepts, in the order it publishes them as
/// `ResetType@Redfish.AllowableValues`.
///
/// This is a **subset** of DMTF's `Resource.json#/definitions/ResetType` enum, which
/// is exactly what the specification asks of an implementation: advertise what you
/// can actually do. `tests/redfish_server_conformance.rs` proves the subset relation
/// against DMTF's own enum, so a token that is merely *plausible* (`"PowerOn"`,
/// `"Reset"`) cannot survive here.
pub const RESET_TYPE_ALLOWABLE: &[&str] = &[
    RESET_ON,
    RESET_FORCE_ON,
    RESET_FORCE_OFF,
    RESET_GRACEFUL_SHUTDOWN,
    RESET_FORCE_RESTART,
    RESET_GRACEFUL_RESTART,
];

/// Whether `reset_type` is one this service will act on.
pub fn is_allowable_reset_type(reset_type: &str) -> bool {
    RESET_TYPE_ALLOWABLE.contains(&reset_type)
}

/// The `BootSourceOverrideTarget` tokens draupnir models — a subset of DMTF's
/// `ComputerSystem.json#/definitions/BootSource` enum, one per [`BootTarget`].
pub const BOOT_TARGET_ALLOWABLE: &[&str] = &["None", "Pxe", "Cd", "Hdd", "BiosSetup"];

/// `BootSourceOverrideTarget: "None"` — the override target after it is consumed or
/// disabled.
pub const BOOT_TARGET_NONE: &str = "None";

/// Map a [`BootTarget`] onto its Redfish `BootSourceOverrideTarget` token.
pub fn target_str(target: BootTarget) -> &'static str {
    match target {
        BootTarget::Cd => "Cd",
        BootTarget::Pxe => "Pxe",
        BootTarget::Hdd => "Hdd",
        BootTarget::BiosSetup => "BiosSetup",
    }
}

/// The inverse of [`target_str`] — the SERVER's half. `"None"` and any token draupnir
/// does not model map to [`None`], which is the honest answer: an unmodelled target
/// must be refused by name, never silently treated as `Cd`.
pub fn target_from_str(s: &str) -> Option<BootTarget> {
    match s {
        "Cd" => Some(BootTarget::Cd),
        "Pxe" => Some(BootTarget::Pxe),
        "Hdd" => Some(BootTarget::Hdd),
        "BiosSetup" => Some(BootTarget::BiosSetup),
        _ => None,
    }
}

// ---------------------------------------------------------------------------
// HTTP Basic auth — the encoder the client uses and the decoder the server uses,
// side by side, over ONE alphabet.
// ---------------------------------------------------------------------------

/// The standard base64 alphabet.
const B64: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

/// Minimal std-only base64 (standard alphabet, padded) — the client's Basic-auth
/// header encoder. Avoids pulling a crate for four bytes of encoding.
pub fn base64_encode(input: &[u8]) -> String {
    let mut out = String::with_capacity(input.len().div_ceil(3) * 4);
    for chunk in input.chunks(3) {
        let b0 = chunk[0];
        let b1 = *chunk.get(1).unwrap_or(&0);
        let b2 = *chunk.get(2).unwrap_or(&0);
        let n = ((b0 as u32) << 16) | ((b1 as u32) << 8) | (b2 as u32);
        out.push(B64[((n >> 18) & 63) as usize] as char);
        out.push(B64[((n >> 12) & 63) as usize] as char);
        out.push(if chunk.len() > 1 {
            B64[((n >> 6) & 63) as usize] as char
        } else {
            '='
        });
        out.push(if chunk.len() > 2 {
            B64[(n & 63) as usize] as char
        } else {
            '='
        });
    }
    out
}

/// The inverse — the SERVER's half of the Basic-auth header. Strict: any character
/// outside the alphabet (including whitespace) is a decode failure, because a
/// credential check that silently skipped junk would accept a mangled header.
pub fn base64_decode(input: &str) -> Option<Vec<u8>> {
    let bytes = input.as_bytes();
    if bytes.len() % 4 != 0 {
        return None;
    }
    let mut out = Vec::with_capacity(bytes.len() / 4 * 3);
    for chunk in bytes.chunks(4) {
        let mut n: u32 = 0;
        let mut pad = 0usize;
        for (i, &c) in chunk.iter().enumerate() {
            let v = if c == b'=' {
                // Padding is legal only in the last two positions, and never before
                // a non-padding byte.
                if i < 2 {
                    return None;
                }
                pad += 1;
                0
            } else {
                if pad > 0 {
                    return None;
                }
                B64.iter().position(|&t| t == c)? as u32
            };
            n |= v << (18 - 6 * i);
        }
        out.push((n >> 16) as u8);
        if pad < 2 {
            out.push((n >> 8) as u8);
        }
        if pad < 1 {
            out.push(n as u8);
        }
    }
    Some(out)
}

/// The `Authorization: Basic …` header value for `username`/`password`.
pub fn basic_auth_header(username: &str, password: &str) -> String {
    format!("Basic {}", base64_encode(format!("{username}:{password}").as_bytes()))
}

/// Split an `Authorization` header value back into `(username, password)` — the
/// exact inverse of [`basic_auth_header`]. `None` unless it is a well-formed
/// `Basic` credential; the scheme name is matched case-insensitively (RFC 7235).
pub fn parse_basic_auth(header: &str) -> Option<(String, String)> {
    let rest = header.strip_prefix("Basic ").or_else(|| {
        let (scheme, rest) = header.split_once(' ')?;
        scheme.eq_ignore_ascii_case("basic").then_some(rest)
    })?;
    let decoded = base64_decode(rest.trim())?;
    let text = String::from_utf8(decoded).ok()?;
    let (user, pass) = text.split_once(':')?;
    Some((user.to_string(), pass.to_string()))
}

// ---------------------------------------------------------------------------
// Action BODIES — the writer and the reader of each, adjacent, so a field name
// can never be added on one side only.
// ---------------------------------------------------------------------------

/// The `VirtualMedia.InsertMedia` request body — mount `iso` read-only.
#[cfg(any(feature = "backend-redfish", feature = "redfish-server"))]
pub fn insert_media_body(iso: &str) -> serde_json::Value {
    serde_json::json!({
        prop::IMAGE: iso,
        prop::INSERTED: true,
        prop::WRITE_PROTECTED: true,
    })
}

/// What an `InsertMedia` body asked for, as the server reads it back.
#[cfg(any(feature = "backend-redfish", feature = "redfish-server"))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InsertMediaRequest {
    /// The media URI to mount (`Image`) — the only REQUIRED parameter.
    pub image: String,
    /// `Inserted`; DMTF's default when the parameter is omitted is `true`.
    pub inserted: bool,
    /// `WriteProtected`; DMTF's default when omitted is `true`.
    pub write_protected: bool,
}

/// Read an `InsertMedia` body. `Err` names the missing REQUIRED parameter —
/// `Image` — so the server can answer with `Base.ActionParameterMissing` rather
/// than inventing an empty media URI.
#[cfg(any(feature = "backend-redfish", feature = "redfish-server"))]
pub fn read_insert_media_body(v: &serde_json::Value) -> Result<InsertMediaRequest, &'static str> {
    let image = v
        .get(prop::IMAGE)
        .and_then(|i| i.as_str())
        .ok_or(prop::IMAGE)?;
    Ok(InsertMediaRequest {
        image: image.to_string(),
        inserted: v
            .get(prop::INSERTED)
            .and_then(|i| i.as_bool())
            .unwrap_or(true),
        write_protected: v
            .get(prop::WRITE_PROTECTED)
            .and_then(|i| i.as_bool())
            .unwrap_or(true),
    })
}

/// The one-time `Boot` override PATCH body for `target`.
#[cfg(any(feature = "backend-redfish", feature = "redfish-server"))]
pub fn boot_override_body(target: BootTarget) -> serde_json::Value {
    serde_json::json!({
        prop::BOOT: {
            prop::BOOT_SOURCE_OVERRIDE_ENABLED: OVERRIDE_ONCE,
            prop::BOOT_SOURCE_OVERRIDE_TARGET: target_str(target),
        }
    })
}

/// What a `Boot` PATCH asked for, as the server reads it back. Both members are
/// optional because Redfish PATCH is a partial update: a body may set only the
/// target, only the enablement, or both.
#[cfg(any(feature = "backend-redfish", feature = "redfish-server"))]
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct BootOverrideRequest {
    /// The raw `BootSourceOverrideTarget` token, if present.
    pub target: Option<String>,
    /// The raw `BootSourceOverrideEnabled` token, if present.
    pub enabled: Option<String>,
}

/// Read the `Boot` object out of a `ComputerSystem` PATCH body. `None` when the body
/// carries no `Boot` at all — which is a legal (if pointless) PATCH, not an error.
#[cfg(any(feature = "backend-redfish", feature = "redfish-server"))]
pub fn read_boot_override_body(v: &serde_json::Value) -> Option<BootOverrideRequest> {
    let boot = v.get(prop::BOOT)?;
    Some(BootOverrideRequest {
        target: boot
            .get(prop::BOOT_SOURCE_OVERRIDE_TARGET)
            .and_then(|t| t.as_str())
            .map(str::to_string),
        enabled: boot
            .get(prop::BOOT_SOURCE_OVERRIDE_ENABLED)
            .and_then(|t| t.as_str())
            .map(str::to_string),
    })
}

/// The `ComputerSystem.Reset` body for a Redfish `ResetType` (`On` / `ForceOff` / …).
#[cfg(any(feature = "backend-redfish", feature = "redfish-server"))]
pub fn reset_body(reset_type: &str) -> serde_json::Value {
    serde_json::json!({ prop::RESET_TYPE: reset_type })
}

/// Read a `ComputerSystem.Reset` body. `Err` names the missing parameter.
#[cfg(any(feature = "backend-redfish", feature = "redfish-server"))]
pub fn read_reset_body(v: &serde_json::Value) -> Result<String, &'static str> {
    v.get(prop::RESET_TYPE)
        .and_then(|r| r.as_str())
        .map(str::to_string)
        .ok_or(prop::RESET_TYPE)
}

#[cfg(test)]
mod tests {
    use super::*;

    // The paths are the routing table on the server side and the URL suffix on the
    // client side. Pinning the literals here is what makes the two ends' agreement
    // checkable at all — everything else in this module derives from them.
    #[test]
    fn resource_paths_are_the_dmtf_shapes() {
        assert_eq!(system_path("System.Embedded.1"),
                   "/redfish/v1/Systems/System.Embedded.1");
        assert_eq!(virtual_media_collection_path("437XR1138R2"),
                   "/redfish/v1/Systems/437XR1138R2/VirtualMedia");
        assert_eq!(virtual_media_path("437XR1138R2", "CD1"),
                   "/redfish/v1/Systems/437XR1138R2/VirtualMedia/CD1");
        assert_eq!(
            virtual_media_action_path("437XR1138R2", "CD1", INSERT_MEDIA),
            "/redfish/v1/Systems/437XR1138R2/VirtualMedia/CD1/Actions/VirtualMedia.InsertMedia"
        );
        assert_eq!(
            reset_path("437XR1138R2"),
            "/redfish/v1/Systems/437XR1138R2/Actions/ComputerSystem.Reset"
        );
    }

    /// Every path is a child of the collection above it — the containment DMTF's
    /// `@odata.id` graph asserts. RED-when-broken: build one path from a literal
    /// instead of from its parent and the chain breaks here.
    #[test]
    fn the_paths_nest_the_way_the_odata_graph_does() {
        let sys = system_path("s1");
        assert!(sys.starts_with(SYSTEMS_PATH));
        let vmc = virtual_media_collection_path("s1");
        assert!(vmc.starts_with(&sys), "{vmc} under {sys}");
        let vm = virtual_media_path("s1", "CD");
        assert!(vm.starts_with(&vmc));
        assert!(virtual_media_action_path("s1", "CD", INSERT_MEDIA).starts_with(&vm));
        assert!(reset_path("s1").starts_with(&sys));
    }

    #[test]
    fn boot_targets_round_trip_through_their_redfish_tokens() {
        for t in [
            BootTarget::Cd,
            BootTarget::Pxe,
            BootTarget::Hdd,
            BootTarget::BiosSetup,
        ] {
            assert_eq!(target_from_str(target_str(t)), Some(t), "{t:?}");
            assert!(
                BOOT_TARGET_ALLOWABLE.contains(&target_str(t)),
                "{t:?} is advertised as allowable"
            );
        }
        // An unmodelled target is REFUSED, never coerced. RED-when-broken: add a
        // `_ => Some(BootTarget::Cd)` fallback and this fails.
        assert_eq!(target_from_str("Usb"), None);
        assert_eq!(target_from_str(BOOT_TARGET_NONE), None);
        assert_eq!(target_from_str("cd"), None, "the tokens are case-sensitive");
    }

    #[test]
    fn the_reset_types_the_client_sends_are_the_ones_the_server_advertises() {
        // The client's two live calls (`power_on` → On, `power_off` → ForceOff) must
        // both be in the advertised set, or draupnir's own client would be refused by
        // draupnir's own BMC.
        assert!(is_allowable_reset_type(RESET_ON));
        assert!(is_allowable_reset_type(RESET_FORCE_OFF));
        assert!(!is_allowable_reset_type("PowerOn"), "a plausible non-token");
        assert!(!is_allowable_reset_type("on"), "case-sensitive");
        assert!(!is_allowable_reset_type(""));
    }

    #[test]
    fn allowable_values_annotation_key_is_the_odata_spelling() {
        assert_eq!(
            allowable_values_key(prop::RESET_TYPE),
            "ResetType@Redfish.AllowableValues"
        );
    }

    #[test]
    fn base64_matches_known_vectors_and_round_trips() {
        for (raw, enc) in [
            (&b""[..], ""),
            (&b"f"[..], "Zg=="),
            (&b"fo"[..], "Zm8="),
            (&b"foo"[..], "Zm9v"),
            (&b"foob"[..], "Zm9vYg=="),
            (&b"admin:secret"[..], "YWRtaW46c2VjcmV0"),
        ] {
            assert_eq!(base64_encode(raw), enc);
            assert_eq!(base64_decode(enc).as_deref(), Some(raw), "decode {enc}");
        }
        // Every byte value survives the round trip (the alphabet is complete).
        let all: Vec<u8> = (0u8..=255).collect();
        assert_eq!(base64_decode(&base64_encode(&all)).unwrap(), all);
    }

    #[test]
    fn base64_decode_refuses_junk_instead_of_skipping_it() {
        // RED-when-broken: a lenient decoder that ignores unknown characters would
        // accept a mangled credential header.
        assert_eq!(base64_decode("Zm9v!"), None, "bad length");
        assert_eq!(base64_decode("Zm9 v"), None, "space is not in the alphabet");
        assert_eq!(base64_decode("Z==="), None, "padding cannot start at index 1");
        assert_eq!(base64_decode("Zm=v"), None, "padding cannot precede data");
        assert_eq!(base64_decode("Zg=/"), None);
    }

    #[test]
    fn basic_auth_round_trips_between_the_client_and_the_server_halves() {
        let h = basic_auth_header("admin", "hunter2:with:colons");
        assert_eq!(h, "Basic YWRtaW46aHVudGVyMjp3aXRoOmNvbG9ucw==");
        // The password may contain ':' — only the FIRST separates it from the user.
        assert_eq!(
            parse_basic_auth(&h),
            Some(("admin".into(), "hunter2:with:colons".into()))
        );
        // RFC 7235: the scheme token is case-insensitive.
        assert_eq!(
            parse_basic_auth("basic YWRtaW46c2VjcmV0"),
            Some(("admin".into(), "secret".into()))
        );
        assert_eq!(parse_basic_auth("Bearer abc"), None);
        assert_eq!(parse_basic_auth("Basic !!!!"), None);
        // No colon at all is not a credential.
        assert_eq!(parse_basic_auth(&format!("Basic {}", base64_encode(b"admin"))), None);
    }

    #[cfg(any(feature = "backend-redfish", feature = "redfish-server"))]
    mod bodies {
        use super::*;

        /// THE anti-drift proof: what the client writes is exactly what the server
        /// reads. RED-when-broken: rename a field on one side only and this fails
        /// instead of the two ends silently disagreeing at runtime.
        #[test]
        fn every_action_body_the_client_writes_is_read_back_by_the_server_half() {
            let insert = read_insert_media_body(&insert_media_body("https://d/x.iso")).unwrap();
            assert_eq!(insert.image, "https://d/x.iso");
            assert!(insert.inserted);
            assert!(insert.write_protected, "the install medium is read-only");

            let ovr = read_boot_override_body(&boot_override_body(BootTarget::Cd)).unwrap();
            assert_eq!(ovr.target.as_deref(), Some("Cd"));
            assert_eq!(ovr.enabled.as_deref(), Some(OVERRIDE_ONCE));

            assert_eq!(read_reset_body(&reset_body(RESET_ON)).unwrap(), RESET_ON);
        }

        #[test]
        fn a_body_missing_its_required_parameter_names_the_parameter() {
            let e = read_insert_media_body(&serde_json::json!({ "Inserted": true })).unwrap_err();
            assert_eq!(e, prop::IMAGE);
            let e = read_reset_body(&serde_json::json!({})).unwrap_err();
            assert_eq!(e, prop::RESET_TYPE);
            // A non-string Image is missing, not silently stringified.
            assert!(read_insert_media_body(&serde_json::json!({ "Image": 7 })).is_err());
        }

        #[test]
        fn insert_media_defaults_match_the_dmtf_parameter_defaults() {
            // DMTF: omitted `Inserted`/`WriteProtected` default to true.
            let r = read_insert_media_body(&serde_json::json!({ "Image": "u" })).unwrap();
            assert!(r.inserted && r.write_protected);
            let r = read_insert_media_body(
                &serde_json::json!({ "Image": "u", "Inserted": false, "WriteProtected": false }),
            )
            .unwrap();
            assert!(!r.inserted && !r.write_protected);
        }

        #[test]
        fn a_patch_with_no_boot_object_is_read_as_no_override_not_as_an_error() {
            assert_eq!(read_boot_override_body(&serde_json::json!({})), None);
            // A partial PATCH carries only what it names.
            let partial = read_boot_override_body(
                &serde_json::json!({ "Boot": { "BootSourceOverrideTarget": "Hdd" } }),
            )
            .unwrap();
            assert_eq!(partial.target.as_deref(), Some("Hdd"));
            assert_eq!(partial.enabled, None);
        }
    }
}