modelpipe 0.5.0

Reach an OpenAI-compatible model server from anywhere over p2p — no VPN, no account, no cloud in the path
Documentation
//! Tests for [`super`] — the ALPN, the address bridge, relay validation.
//!
//! Split out via `#[path]` so `transport.rs` stays inside the file-size
//! budget.
//!
//! Almost everything here runs without a network: the bridge between a
//! ticket and an iroh address is a pure translation, and it is where a
//! mistake would be least visible and most expensive. The tests that bind
//! an endpoint say so.
//!
//! Exactly one needs the internet rather than a socket:
//! `relay_only_mints_a_ticket_that_names_the_relay_and_nothing_else` waits
//! for a relay handshake, because an endpoint with no IP transport has
//! nothing at all to advertise until one completes. That dependency is the
//! switch's own subject rather than a shortcut in the test.

use std::collections::BTreeSet;

use super::*;

/// RFC 8032 §7.1 TEST 1 — a real curve point, so `EndpointId::from_bytes`
/// has something valid to accept.
const VALID_KEY: &str = "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a";

fn key(hex: &str) -> [u8; 32] {
    let mut out = [0u8; 32];
    for (i, byte) in out.iter_mut().enumerate() {
        *byte = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16).expect("hex");
    }
    out
}

fn endpoint_addr(addrs: Vec<TransportAddr>) -> EndpointAddr {
    EndpointAddr {
        id: EndpointId::from_bytes(&key(VALID_KEY)).expect("a real curve point"),
        addrs: addrs.into_iter().collect::<BTreeSet<_>>(),
    }
}

// ── The ALPN ─────────────────────────────────────────────────────────────

/// An unversioned ALPN leaves no negotiation lever, and the ticket's own
/// version byte cannot cover for it — the two version spaces are
/// independent, so a ticket that parses perfectly still reaches a peer you
/// cannot speak to.
#[test]
fn the_alpn_carries_a_version() {
    let text = std::str::from_utf8(ALPN).expect("ASCII");
    assert_eq!(text, "modelpipe/0");
    let (name, version) = text.split_once('/').expect("a version component");
    assert_eq!(name, "modelpipe");
    assert!(
        version.parse::<u32>().is_ok(),
        "the version must be a number a later one can follow: {version}"
    );
}

// ── The address bridge ───────────────────────────────────────────────────

#[tokio::test]
async fn a_ticket_round_trips_through_an_iroh_address() {
    let original = endpoint_addr(vec![
        TransportAddr::Relay("https://relay.example.com./".parse().expect("relay url")),
        TransportAddr::Ip("192.168.1.7:4433".parse().unwrap()),
        TransportAddr::Ip("[2001:db8::1]:8080".parse().unwrap()),
    ]);

    let ticket = ticket_from(&original);
    let back = addr_from(&ticket).expect("a valid key must convert back");

    assert_eq!(back.id, original.id, "the identity survives");
    assert_eq!(back.addrs, original.addrs, "and so does every address");
}

/// The identity is what the pairing actually rests on, so it survives even
/// a ticket with nothing else in it.
#[test]
fn an_address_free_ticket_still_names_its_endpoint() {
    let original = endpoint_addr(vec![]);
    let ticket = ticket_from(&original);
    let back = addr_from(&ticket).expect("valid");

    assert_eq!(back.id, original.id);
    assert!(back.addrs.is_empty());
}

/// `TransportAddr` is `#[non_exhaustive]` with a `Custom` variant, which is
/// exactly the situation the drop arm exists for: iroh may learn transports
/// v0 has no tag for. A ticket carrying fewer paths is slower in the worst
/// case and never broken — the address set exists to help a peer *avoid*
/// the relay, not to connect at all.
#[test]
fn an_address_v0_cannot_describe_is_dropped_rather_than_failing_the_mint() {
    let original = endpoint_addr(vec![
        TransportAddr::Relay("https://relay.example.com./".parse().expect("relay url")),
        TransportAddr::Ip("192.168.1.7:4433".parse().unwrap()),
    ]);
    let ticket = ticket_from(&original);

    // Everything v0 *can* describe is still there.
    assert_eq!(ticket.addrs().len(), 2);
    let back = addr_from(&ticket).expect("valid");
    assert_eq!(back.addrs.len(), 2);
}

/// The format spec says a parser treats the endpoint id as 32 opaque bytes
/// and leaves curve validity to the transport. iroh agrees, and defers it
/// further still: `EndpointId::from_bytes` accepts *any* 32 bytes —
/// all-ones, all-zeros, the high bit alone — because decompressing the
/// point is left until a signature is actually verified.
///
/// So the conversion below succeeds for a key nobody holds, and the
/// pairing fails later at dial, as unreachable. That is the right shape
/// (nobody is at that address and nobody ever was) and it is worth pinning:
/// if a future iroh validates eagerly, this test changes and the error path
/// in `addr_from` starts firing.
#[test]
fn an_endpoint_id_is_taken_as_opaque_bytes_and_judged_at_dial_time() {
    for bytes in [[0x00u8; 32], [0xFFu8; 32]] {
        let ticket = Ticket::new(bytes, vec![], BackendHint::OpenAiCompatible);
        let addr = addr_from(&ticket)
            .expect("iroh accepts any 32 bytes; validity is decided when dialling");
        assert_eq!(addr.id.as_bytes(), &bytes, "carried through unaltered");
    }
}

/// A ticket is pasted by a human and may name a relay this build cannot
/// parse. Dropping that one address costs the pairing a path, not the
/// pairing — the same reasoning as the unknown-address-tag rule.
#[test]
fn a_relay_url_iroh_will_not_parse_costs_one_path_and_not_the_pairing() {
    let ticket = Ticket::new(
        key(VALID_KEY),
        vec![
            TicketAddr::Relay("not a url at all".to_owned()),
            TicketAddr::V4("192.168.1.7:4433".parse().unwrap()),
        ],
        BackendHint::OpenAiCompatible,
    );
    let back = addr_from(&ticket).expect("the pairing survives");
    assert_eq!(back.addrs.len(), 1, "only the usable address remains");
}

// ── Relay validation ─────────────────────────────────────────────────────

#[test]
fn a_well_formed_relay_url_is_accepted() {
    for url in [
        "https://relay.example.com/",
        "http://127.0.0.1:3340/",
        "https://relay.example.com.:443/",
    ] {
        assert!(validate_relay(url).is_ok(), "{url}");
    }
}

#[test]
fn a_value_that_is_not_a_url_is_refused_before_the_listener_starts() {
    for url in ["", "not a url", "relay.example.com", "://missing-scheme"] {
        match validate_relay(url) {
            Err(ServeError::InvalidRelay { url: named }) => {
                assert_eq!(named, url, "the error must name what was refused");
            }
            other => panic!("{url:?} should be InvalidRelay, got {other:?}"),
        }
    }
}

/// The reason validation returns `()`. Every URL library normalizes, and a
/// ticket carries relay URLs verbatim — so the parse is used for its
/// verdict and then discarded, and the string the operator gave is the
/// string that travels.
#[test]
fn validation_yields_a_verdict_and_never_a_normalized_url() {
    let awkward = "https://Relay.Example.COM.:443/";
    assert!(validate_relay(awkward).is_ok());

    // The ticket keeps what it was given, whatever a URL parser would have
    // made of it.
    let ticket = Ticket::new(
        key(VALID_KEY),
        vec![TicketAddr::Relay(awkward.to_owned())],
        BackendHint::OpenAiCompatible,
    );
    let reparsed: Ticket = ticket.to_string().parse().expect("round trips");
    assert_eq!(
        reparsed.addrs(),
        [TicketAddr::Relay(awkward.to_owned())],
        "the operator's spelling survives the ticket"
    );
}

// ── Binding ──────────────────────────────────────────────────────────────

/// The one test here that touches the network stack. It binds a real
/// endpoint, which is why it is a single case rather than a table: what is
/// being checked is that the builder is wired up and the ALPN is
/// registered, not anything about connectivity.
#[tokio::test]
async fn an_endpoint_binds_and_reports_its_own_identity() {
    let endpoint = bind(None, None, NetOptions::default())
        .await
        .expect("binding must succeed");
    let addr = endpoint.addr();

    let ticket = ticket_from(&addr);
    assert_eq!(
        ticket.endpoint_id(),
        addr.id.as_bytes(),
        "the ticket names the endpoint that minted it"
    );
    assert_eq!(ticket.fingerprint().len(), 12);
    endpoint.close().await;
}

/// A relay value that is not a URL is refused up front rather than
/// surfacing later as an unexplained transport failure.
#[tokio::test]
async fn binding_with_an_unparseable_relay_fails_before_the_endpoint_exists() {
    let Err(failure) = bind(Some("not a url"), None, NetOptions::default()).await else {
        panic!("an unparseable relay must be refused");
    };
    let err = ServeError::from(failure);
    match &err {
        ServeError::InvalidRelay { url } => assert_eq!(url, "not a url"),
        other => panic!("expected InvalidRelay, got {other:?}"),
    }
    assert!(!err.is_retryable(), "and it is the operator's to fix");
}

/// The bridge the whole of `--identity` rests on: the same key bytes bind
/// to the same endpoint id, so the same ticket keeps naming this listener
/// across restarts.
///
/// `identity.rs` proves the bytes survive a restart and this proves what
/// they are worth — neither half means anything alone. Two endpoints are
/// bound rather than one, because "the same key gives the same id" is only
/// interesting next to a different key giving a different one.
#[tokio::test]
async fn the_same_key_binds_to_the_same_endpoint_and_a_different_one_does_not() {
    let key = [7u8; crate::identity::KEY_BYTES];
    let other = [9u8; crate::identity::KEY_BYTES];

    let first = bind(None, Some(key), NetOptions::default())
        .await
        .expect("binds");
    let again = bind(None, Some(key), NetOptions::default())
        .await
        .expect("binds");
    let elsewhere = bind(None, Some(other), NetOptions::default())
        .await
        .expect("binds");

    assert_eq!(
        ticket_from(&first.addr()).endpoint_id(),
        ticket_from(&again.addr()).endpoint_id(),
        "a stored key is what makes a ticket outlive the process"
    );
    assert_ne!(
        ticket_from(&first.addr()).endpoint_id(),
        ticket_from(&elsewhere.addr()).endpoint_id(),
        "and a different key is a different listener"
    );

    first.close().await;
    again.close().await;
    elsewhere.close().await;
}

/// Each network option removes one contact and nothing else: an endpoint
/// still binds, still has an identity, and still mints a ticket with every
/// combination off. Connectivity is not what this checks — that the
/// builder accepts the configuration is.
#[tokio::test]
async fn an_endpoint_binds_with_every_network_contact_switched_off() {
    for (port_mapping, discovery) in [(false, true), (true, false), (false, false)] {
        let net = NetOptions {
            port_mapping,
            discovery,
            ..NetOptions::default()
        };
        let endpoint = bind(None, None, net)
            .await
            .expect("binding must succeed whatever is switched off");
        assert_eq!(ticket_from(&endpoint.addr()).fingerprint().len(), 12);
        endpoint.close().await;
    }
}

/// Relay-only removes every IP transport, so a ticket minted under it names
/// the relay **and** nothing else.
///
/// That is the switch working rather than a limitation of it: the point is
/// to make relayed the only outcome, and a ticket still carrying the LAN
/// addresses would let a holder on the same network go direct and quietly
/// measure the thing that was being excluded.
///
/// Both halves, because the absence alone is satisfied by a ticket carrying
/// nothing at all — which is undialable, the opposite of what this switch
/// promises, and is exactly what the endpoint advertises for the first
/// couple of seconds of its life. The relay arrives with a handshake and not
/// with the bind, which is what [`wait_online`] is here for and what
/// `ServeOptions::wait_online` exists for on the public surface. This test
/// therefore needs a route to a relay; without one there is no such thing as
/// a working `--relay-only` listener to test.
#[tokio::test]
async fn relay_only_mints_a_ticket_that_names_the_relay_and_nothing_else() {
    let net = NetOptions {
        relay_only: true,
        ..NetOptions::default()
    };
    let endpoint = bind(None, None, net).await.expect("binding must succeed");
    wait_online(&endpoint, Duration::from_secs(20)).await;

    let ticket = ticket_from(&endpoint.addr());

    assert!(
        ticket
            .addrs()
            .iter()
            .any(|addr| matches!(addr, TicketAddr::Relay(_))),
        "the relay is the only path this endpoint has, so the ticket has to \
         carry it or name nowhere at all: {:?}",
        ticket.addrs()
    );
    assert!(
        !ticket
            .addrs()
            .iter()
            .any(|addr| matches!(addr, TicketAddr::V4(_) | TicketAddr::V6(_))),
        "an endpoint with no IP transport has no IP address to advertise: {:?}",
        ticket.addrs()
    );
    endpoint.close().await;
}

/// The connect side's relay is validated by the same rule as the serve
/// side's, and refused as its own permanent variant.
#[test]
fn a_connect_relay_that_is_not_a_url_is_refused_as_the_connect_error() {
    match validate_relay_for_connect("not a url") {
        Err(ConnectError::InvalidRelay { url }) => assert_eq!(url, "not a url"),
        other => panic!("expected InvalidRelay, got {other:?}"),
    }
    assert!(validate_relay_for_connect("https://relay.example.com/").is_ok());
}

/// The control for the default: no key means a fresh identity every time,
/// which is the disposable ticket every version before this one had.
#[tokio::test]
async fn binding_without_a_key_mints_a_new_identity_each_time() {
    let first = bind(None, None, NetOptions::default())
        .await
        .expect("binds");
    let second = bind(None, None, NetOptions::default())
        .await
        .expect("binds");

    assert_ne!(
        ticket_from(&first.addr()).endpoint_id(),
        ticket_from(&second.addr()).endpoint_id(),
        "the default must stay ephemeral"
    );

    first.close().await;
    second.close().await;
}