osproxy-server 1.0.2

The osproxy binary: process lifecycle and wiring. No business logic.
Documentation
//! The `/debug/breakglass` admin endpoint returns the captured forensic tape as
//! a JSON array (oldest first), without touching auth, routing, or the upstream,
//! it is a read of in-process telemetry, the operator-facing counterpart of the
//! `ring_buffer` capture.

#![allow(clippy::unwrap_used)]

use std::sync::Arc;

use osproxy_core::{ClusterId, EndpointKind, IndexName};
use osproxy_engine::Pipeline;
use osproxy_observe::BreakGlassBuffer;
use osproxy_server::auth::ReferenceAuthenticator;
use osproxy_server::handler::AppHandler;
use osproxy_server::tenancy::ReferenceTenancy;
use osproxy_sink::OpenSearchSink;
use osproxy_spi::HttpMethod;
use osproxy_tenancy::TenancyRouter;
use osproxy_transport::{IngressHandler, IngressRequest};
use serde_json::{json, Value};

fn get(path: &str) -> IngressRequest {
    IngressRequest {
        method: HttpMethod::Get,
        protocol: osproxy_spi::Protocol::Http1,
        path: path.to_owned(),
        endpoint: EndpointKind::Unknown,
        logical_index: String::new(),
        doc_id: None,
        headers: vec![],
        body: vec![],
        query: None,
        client_cert_subject: None,
        secure: false,
    }
}

#[tokio::test]
async fn the_breakglass_endpoint_returns_the_captured_tape() {
    let tape = Arc::new(BreakGlassBuffer::new(8));
    // Two captures, as a ring_buffer directive would have produced.
    tape.capture(json!({"request_id": "r1", "outcome": "error"}));
    tape.capture(json!({"request_id": "r2", "outcome": "ok"}));

    let sink = OpenSearchSink::new();
    let tenancy = ReferenceTenancy::new(
        ClusterId::from("c"),
        IndexName::from("shared"),
        "http://unused",
    );
    let pipeline = Pipeline::new(TenancyRouter::new(tenancy), sink).with_break_glass(tape.clone());
    let handler = AppHandler::new(pipeline, ReferenceAuthenticator::dev());

    let resp = handler.handle(get("/debug/breakglass")).await;
    assert_eq!(resp.status, 200);
    let body: Value = serde_json::from_slice(&resp.body).unwrap();
    let entries = body.as_array().expect("the tape is a JSON array");
    assert_eq!(entries.len(), 2, "both captures are returned, oldest first");
    assert_eq!(entries[0]["request_id"], "r1");
    assert_eq!(entries[1]["request_id"], "r2");
}

#[tokio::test]
async fn debug_endpoints_are_refused_when_disabled() {
    // Production posture: with /debug off, both diagnostics surfaces report
    // not_enabled (pre-auth, like when on), while /metrics stays available.
    let tape = Arc::new(BreakGlassBuffer::new(8));
    tape.capture(json!({"request_id": "r1", "outcome": "ok"}));

    let sink = OpenSearchSink::new();
    let tenancy = ReferenceTenancy::new(
        ClusterId::from("c"),
        IndexName::from("shared"),
        "http://unused",
    );
    let pipeline = Pipeline::new(TenancyRouter::new(tenancy), sink).with_break_glass(tape);
    let handler =
        AppHandler::new(pipeline, ReferenceAuthenticator::dev()).with_debug_endpoints(false);

    let bg = handler.handle(get("/debug/breakglass")).await;
    assert_eq!(bg.status, 404, "breakglass refused when disabled");
    assert!(String::from_utf8_lossy(&bg.body).contains("not_enabled"));

    let explain = handler.handle(get("/debug/explain/whatever")).await;
    assert_eq!(explain.status, 404, "explain refused when disabled");
    assert!(String::from_utf8_lossy(&explain.body).contains("not_enabled"));

    // /metrics is the always-on, prod-safe surface, unaffected by the switch.
    let metrics = handler.handle(get("/metrics")).await;
    assert_eq!(metrics.status, 200, "metrics stays on when /debug is off");
}

#[tokio::test]
async fn an_empty_tape_is_an_empty_array() {
    let sink = OpenSearchSink::new();
    let tenancy = ReferenceTenancy::new(
        ClusterId::from("c"),
        IndexName::from("shared"),
        "http://unused",
    );
    let handler = AppHandler::new(
        Pipeline::new(TenancyRouter::new(tenancy), sink),
        ReferenceAuthenticator::dev(),
    );

    let resp = handler.handle(get("/debug/breakglass")).await;
    assert_eq!(resp.status, 200);
    let body: Value = serde_json::from_slice(&resp.body).unwrap();
    assert_eq!(body, json!([]), "no captures → empty array, never an error");
}