dslite-b4 0.1.2

DS-Lite B4 tunnel management daemon
Documentation
//! Conversion between daemon discovery configuration and HB46PP models.

use hb46pp::{
    Capability, FirmwareVersionError, ProductError, ProvisioningData, ProvisioningRequest,
    ProvisioningRequestError, VendorIdError,
};
use thiserror::Error;

use crate::config::{AftrAddress, DiscoveryConfig};

#[derive(Debug, Error)]
/// Errors constructing an HB46PP provisioning request.
pub enum RequestError {
    #[error(transparent)]
    /// The configured vendor identifier is invalid.
    VendorId(#[from] VendorIdError),
    #[error(transparent)]
    /// The configured product identifier is invalid.
    Product(#[from] ProductError),
    #[error(transparent)]
    /// The daemon version cannot be represented on the wire.
    FirmwareVersion(#[from] FirmwareVersionError),
    #[error(transparent)]
    /// The complete request violates an HB46PP constraint.
    Request(#[from] ProvisioningRequestError),
}

/// Constructs a DS Lite provisioning request from daemon configuration.
pub fn provisioning_request(config: &DiscoveryConfig) -> Result<ProvisioningRequest, RequestError> {
    let vendor_id = config.vendor_id.parse()?;
    let product = config.product.parse()?;
    let version = env!("CARGO_PKG_VERSION").replace('.', "_").parse()?;

    ProvisioningRequest::new(
        vendor_id,
        product,
        version,
        vec![Capability::DsLite],
        None,
        None,
    )
    .map_err(RequestError::from)
}

#[derive(Debug, Error, PartialEq, Eq)]
/// Errors extracting a DS Lite offer from provisioning data.
pub enum DsliteOfferError {
    #[error("DS Lite provisioning offer is missing the aftr field")]
    /// The selected DS Lite offer has no `aftr` member.
    MissingAftr,
    #[error("DS Lite provisioning offer field aftr must be a string")]
    /// The selected offer's `aftr` member is not a string.
    InvalidAftr,
}

/// Extracts the AFTR endpoint from the selected DS Lite offer, if present.
pub fn dslite_aftr(data: &ProvisioningData) -> Result<Option<AftrAddress>, DsliteOfferError> {
    let Some(offer) = data.select(&[Capability::DsLite]) else {
        return Ok(None);
    };

    let Some(aftr) = offer.parameters().get("aftr") else {
        return Err(DsliteOfferError::MissingAftr);
    };
    let Some(aftr) = aftr.as_str() else {
        return Err(DsliteOfferError::InvalidAftr);
    };

    Ok(Some(aftr.to_string().into()))
}

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

    #[test]
    fn creates_dslite_provisioning_request_from_discovery_config() {
        let request = provisioning_request(&DiscoveryConfig::default()).unwrap();

        assert_eq!(request.vendor_id().as_str(), "000000");
        assert_eq!(request.product().as_str(), "dslite-b4");
        assert_eq!(
            request.version().as_str(),
            env!("CARGO_PKG_VERSION").replace('.', "_")
        );
        assert_eq!(request.capabilities(), &[Capability::DsLite]);
        assert!(request.token().is_none());
        assert!(request.credentials().is_none());
    }

    #[test]
    fn converts_dslite_offer_to_aftr_address() {
        let data = ProvisioningData::parse(
            r#"{
                "enabler_name": "example",
                "order": ["dslite"],
                "dslite": {"aftr": "aftr.example"}
            }"#,
        )
        .unwrap();

        let result = dslite_aftr(&data).unwrap();

        assert!(matches!(
            result,
            Some(AftrAddress::Fqdn(ref name)) if name == "aftr.example"
        ));
    }

    #[test]
    fn returns_none_when_dslite_is_not_offered() {
        let data = ProvisioningData::parse(
            r#"{
                "enabler_name": "example",
                "order": [],
                "dslite": {"aftr": "aftr.example"}
            }"#,
        )
        .unwrap();

        let result = dslite_aftr(&data).unwrap();

        assert!(result.is_none());
    }

    #[test]
    fn rejects_dslite_offer_without_aftr() {
        let data = ProvisioningData::parse(
            r#"{
                "enabler_name": "example",
                "order": ["dslite"],
                "dslite": {}
            }"#,
        )
        .unwrap();

        let result = dslite_aftr(&data);

        assert!(matches!(result, Err(DsliteOfferError::MissingAftr)));
    }

    #[test]
    fn rejects_non_string_aftr() {
        let data = ProvisioningData::parse(
            r#"{
                "enabler_name": "example",
                "order": ["dslite"],
                "dslite": {"aftr": 42}
            }"#,
        )
        .unwrap();

        let result = dslite_aftr(&data);

        assert!(matches!(result, Err(DsliteOfferError::InvalidAftr)));
    }
}