liminal-server 0.24.9

Standalone server for the liminal messaging bus
Documentation
use super::*;

#[test]
fn health_endpoint_returns_json_200_regardless_of_readiness()
-> Result<(), Box<dyn std::error::Error>> {
    let readiness = SharedReadinessState::new(ReadinessState::default());
    let server = start_health_server(loopback_ephemeral()?, readiness)?;

    let response = get(server.local_addr(), "/health")?;
    server.shutdown()?;

    assert_status(&response, 200);
    assert!(response.contains("Content-Type: application/json\r\n"));
    let body = json_body(&response)?;
    assert_eq!(body["status"], "healthy");

    Ok(())
}

#[test]
fn ready_endpoint_returns_503_before_main_listener_binds() -> Result<(), Box<dyn std::error::Error>>
{
    let readiness = SharedReadinessState::new(ReadinessState::new(
        true,
        false,
        ClusterReadiness::NotConfigured,
    ));
    let server = start_health_server(loopback_ephemeral()?, readiness)?;

    let response = get(server.local_addr(), "/ready")?;
    server.shutdown()?;

    assert_status(&response, 503);
    assert!(response.contains("Content-Type: application/json\r\n"));
    let body = json_body(&response)?;
    assert_eq!(body["ready"], false);
    assert_eq!(body["unmet_conditions"][0], "listener_bound");

    Ok(())
}

#[test]
fn ready_endpoint_returns_200_after_all_startup_gates() -> Result<(), Box<dyn std::error::Error>> {
    let readiness = SharedReadinessState::new(ReadinessState::ready_without_cluster());
    let server = start_health_server(loopback_ephemeral()?, readiness)?;

    let response = get(server.local_addr(), "/ready")?;
    server.shutdown()?;

    assert_status(&response, 200);
    let body = json_body(&response)?;
    assert_eq!(body["ready"], true);
    let Some(unmet_conditions) = body["unmet_conditions"].as_array() else {
        return Err("unmet_conditions should be an array".into());
    };
    assert!(unmet_conditions.is_empty());

    Ok(())
}

#[test]
fn ready_endpoint_updates_from_shared_readiness_state() -> Result<(), Box<dyn std::error::Error>> {
    let readiness = SharedReadinessState::new(ReadinessState::default());
    let server = start_health_server(loopback_ephemeral()?, readiness.clone())?;

    let response = get(server.local_addr(), "/ready")?;
    assert_status(&response, 503);

    readiness.set_config_loaded(true);
    readiness.set_listener_bound(true);
    let response = get(server.local_addr(), "/ready")?;
    server.shutdown()?;

    assert_status(&response, 200);

    Ok(())
}

#[test]
fn clustered_ready_transitions_503_to_200_when_membership_established()
-> Result<(), Box<dyn std::error::Error>> {
    // A clustered server starts with the cluster gate unmet: config loaded and
    // listener bound, but membership not yet established (G2). /ready is 503.
    let readiness = SharedReadinessState::new(ReadinessState::new(
        true,
        true,
        ClusterReadiness::Configured {
            membership_established: false,
        },
    ));
    let server = start_health_server(loopback_ephemeral()?, readiness.clone())?;

    let response = get(server.local_addr(), "/ready")?;
    assert_status(&response, 503);
    let body = json_body(&response)?;
    assert_eq!(body["ready"], false);
    assert_eq!(
        body["unmet_conditions"][0],
        serde_json::to_value(ReadinessCondition::ClusterMembershipEstablished)?
    );

    // The cluster start's on_established hook flips exactly this flag; once set,
    // /ready must transition to 200 with no unmet conditions.
    readiness.set_cluster_membership_established(true);
    let response = get(server.local_addr(), "/ready")?;
    server.shutdown()?;

    assert_status(&response, 200);
    let body = json_body(&response)?;
    assert_eq!(body["ready"], true);
    let Some(unmet_conditions) = body["unmet_conditions"].as_array() else {
        return Err("unmet_conditions should be an array".into());
    };
    assert!(unmet_conditions.is_empty());

    Ok(())
}

#[test]
fn cluster_readiness_is_listed_when_configured_but_not_joined()
-> Result<(), Box<dyn std::error::Error>> {
    let readiness = SharedReadinessState::new(ReadinessState::new(
        true,
        true,
        ClusterReadiness::Configured {
            membership_established: false,
        },
    ));
    let response = response_for_request(b"GET /ready HTTP/1.1\r\n\r\n", &served(readiness))?;
    let response = String::from_utf8(response)?;

    assert_status(&response, 503);
    let body = json_body(&response)?;
    assert_eq!(
        body["unmet_conditions"][0],
        serde_json::to_value(ReadinessCondition::ClusterMembershipEstablished)?
    );

    Ok(())
}

/// THE UNLOADABLE-CONVERSATIONS READ SURFACE, RED UNTIL THE ROUTE EXISTS.
///
/// The node already records every conversation it refused to load
/// (`server/participant/production/handler.rs`, `record_unloadable`) and
/// then offers an operator no way to read it back: the accessor has no
/// caller. Containment without a read surface is a node that serves nothing
/// on one conversation forever and answers no question about it.
///
/// This pin fixes the shape of the answer and nothing else — that the route
/// EXISTS, answers JSON 200, and carries the three fields an operator reads:
/// how many conversations are refused, which ones, and whether a participant
/// record is even attached (so a `count` of zero is never confused with a
/// node that has no participant installed at all).
///
/// The unknown-path arm in the same test is the discriminator: without it a
/// 200 here would also be satisfied by a server that answers 200 to
/// everything.
#[test]
fn unloadable_conversations_route_answers_the_operator_a_json_shape()
-> Result<(), Box<dyn std::error::Error>> {
    let readiness = SharedReadinessState::new(ReadinessState::default());
    let server = start_health_server(loopback_ephemeral()?, readiness)?;

    let response = get(server.local_addr(), "/unloadable-conversations")?;
    let unknown = get(server.local_addr(), "/unloadable-conversations-typo")?;
    server.shutdown()?;

    // The discriminator first: a neighbouring path is still not served, so
    // the 200 below is this route's own answer.
    assert_status(&unknown, 404);

    assert_status(&response, 200);
    assert!(
        response.contains("Content-Type: application/json\r\n"),
        "the unloadable-conversations route must answer JSON: {response}"
    );
    let body = json_body(&response)?;
    assert_eq!(
        body["count"], 0,
        "a server with no participant record attached refuses nothing: {body}"
    );
    assert_eq!(
        body["participant_installed"], false,
        "no participant record is attached to this server, and the surface must say so \
         rather than let a zero count read as a clean node: {body}"
    );
    let Some(conversations) = body["conversations"].as_array() else {
        return Err("conversations should be an array".into());
    };
    assert!(
        conversations.is_empty(),
        "no conversation was refused: {conversations:?}"
    );

    Ok(())
}

#[test]
fn unsupported_paths_are_not_served() -> Result<(), Box<dyn std::error::Error>> {
    let readiness = SharedReadinessState::default();
    let response = response_for_request(b"GET /unknown HTTP/1.1\r\n\r\n", &served(readiness))?;
    let response = String::from_utf8(response)?;

    assert_status(&response, 404);

    Ok(())
}

#[test]
fn unsupported_methods_on_health_paths_are_rejected() -> Result<(), Box<dyn std::error::Error>> {
    let readiness = SharedReadinessState::default();
    let response = response_for_request(b"POST /health HTTP/1.1\r\n\r\n", &served(readiness))?;
    let response = String::from_utf8(response)?;

    assert_status(&response, 405);

    Ok(())
}

// -----------------------------------------------------------------------
// R18 amendment A7 (§0.18) — the operator credential re-issue route
// -----------------------------------------------------------------------

#[test]
fn health_reports_every_bound_wire_address_without_opening_a_socket()
-> Result<(), Box<dyn std::error::Error>> {
    let state = served(SharedReadinessState::default());
    let addresses = vec!["127.0.0.1:7600".parse()?, "100.64.0.1:7600".parse()?];
    state
        .listen_addresses
        .set(addresses)
        .map_err(|_| "addresses were already installed")?;
    let response = String::from_utf8(response_for_request(
        b"GET /health HTTP/1.1\r\n\r\n",
        &state,
    )?)?;
    let body = json_body(&response)?;
    assert_eq!(
        body["listen_addresses"],
        serde_json::json!(["127.0.0.1:7600", "100.64.0.1:7600"])
    );
    Ok(())
}