ito-core 0.1.33

Core functionality and business logic for Ito
Documentation
use super::*;
use std::io::{Read, Write};
use std::net::TcpListener;
use std::path::PathBuf;
use std::thread;

fn runtime_for(base_url: String) -> BackendRuntime {
    BackendRuntime {
        base_url,
        token: "secret".to_string(),
        timeout: Duration::from_secs(1),
        max_retries: 0,
        backup_dir: PathBuf::from("/tmp/ito-backups"),
        org: "withakay".to_string(),
        repo: "ito".to_string(),
    }
}

fn serve_responses(responses: Vec<&'static str>) -> String {
    let listener = TcpListener::bind("127.0.0.1:0").expect("bind test server");
    let addr = listener.local_addr().expect("local addr");
    thread::spawn(move || {
        for response in responses {
            let (mut stream, _) = listener.accept().expect("accept request");
            let mut request = [0_u8; 2048];
            let _ = stream.read(&mut request).expect("read request");
            stream
                .write_all(response.as_bytes())
                .expect("write response");
        }
    });
    format!("http://{addr}")
}

fn json_response(status: &str, body: &str) -> String {
    format!(
        "HTTP/1.1 {status}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
        body.len()
    )
}

#[test]
fn backend_health_status_default_is_all_false() {
    let status = BackendHealthStatus {
        server_reachable: false,
        server_healthy: false,
        server_ready: false,
        server_version: None,
        ready_reason: None,
        auth_verified: false,
        token_scope: None,
        error: None,
    };

    assert!(!status.server_reachable);
    assert!(!status.server_healthy);
    assert!(!status.server_ready);
    assert!(!status.auth_verified);
    assert!(status.server_version.is_none());
    assert!(status.ready_reason.is_none());
    assert!(status.token_scope.is_none());
    assert!(status.error.is_none());
}

#[test]
fn backend_health_status_serializes_to_json() {
    let status = BackendHealthStatus {
        server_reachable: true,
        server_healthy: true,
        server_ready: true,
        server_version: Some("0.1.0".to_string()),
        ready_reason: None,
        auth_verified: true,
        token_scope: Some("project".to_string()),
        error: None,
    };

    let json = serde_json::to_string(&status).expect("should serialize");
    assert!(json.contains("\"server_reachable\":true"));
    assert!(json.contains("\"server_healthy\":true"));
    assert!(json.contains("\"server_ready\":true"));
    assert!(json.contains("\"server_version\":\"0.1.0\""));
    assert!(json.contains("\"auth_verified\":true"));
    assert!(json.contains("\"token_scope\":\"project\""));
}

#[test]
fn backend_health_status_serializes_error_state() {
    let status = BackendHealthStatus {
        server_reachable: false,
        server_healthy: false,
        server_ready: false,
        server_version: None,
        ready_reason: None,
        auth_verified: false,
        token_scope: None,
        error: Some("Connection refused".to_string()),
    };

    let json = serde_json::to_string(&status).expect("should serialize");
    assert!(json.contains("\"server_reachable\":false"));
    assert!(json.contains("\"error\":\"Connection refused\""));
}

#[test]
fn check_backend_health_success_records_version_ready_and_scope() {
    let base_url = serve_responses(vec![
        Box::leak(json_response("200 OK", r#"{"status":"ok","version":"1.2.3"}"#).into_boxed_str()),
        Box::leak(json_response("200 OK", r#"{"status":"ready","reason":null}"#).into_boxed_str()),
        Box::leak(json_response("200 OK", r#"{"valid":true,"scope":"project"}"#).into_boxed_str()),
    ]);

    let status = check_backend_health(&runtime_for(base_url));

    assert!(status.server_reachable);
    assert!(status.server_healthy);
    assert!(status.server_ready);
    assert!(status.auth_verified);
    assert_eq!(status.server_version.as_deref(), Some("1.2.3"));
    assert_eq!(status.token_scope.as_deref(), Some("project"));
    assert!(status.error.is_none());
}

#[test]
fn check_backend_health_reports_health_status_and_parse_errors() {
    let http_error = check_backend_health(&runtime_for(serve_responses(vec![Box::leak(
        json_response("500 Internal Server Error", "{}").into_boxed_str(),
    )])));
    assert!(http_error.server_reachable);
    assert_eq!(
        http_error.error.as_deref(),
        Some("Health endpoint returned HTTP 500")
    );

    let parse_error = check_backend_health(&runtime_for(serve_responses(vec![Box::leak(
        json_response("200 OK", "not json").into_boxed_str(),
    )])));
    assert!(
        parse_error
            .error
            .as_deref()
            .expect("parse error")
            .starts_with("Failed to parse health response:")
    );
}

#[test]
fn check_backend_health_handles_ready_not_ready_and_unparseable_503() {
    let not_ready = check_backend_health(&runtime_for(serve_responses(vec![
        Box::leak(json_response("200 OK", r#"{"status":"ok","version":"1.2.3"}"#).into_boxed_str()),
        Box::leak(
            json_response(
                "503 Service Unavailable",
                r#"{"status":"starting","reason":"warming"}"#,
            )
            .into_boxed_str(),
        ),
        Box::leak(json_response("200 OK", r#"{"valid":true,"scope":"admin"}"#).into_boxed_str()),
    ])));
    assert!(!not_ready.server_ready);
    assert_eq!(not_ready.ready_reason.as_deref(), Some("warming"));
    assert!(not_ready.auth_verified);

    let unparseable = check_backend_health(&runtime_for(serve_responses(vec![
        Box::leak(json_response("200 OK", r#"{"status":"ok","version":"1.2.3"}"#).into_boxed_str()),
        Box::leak(json_response("503 Service Unavailable", "busy").into_boxed_str()),
        Box::leak(json_response("200 OK", r#"{"valid":true,"scope":"admin"}"#).into_boxed_str()),
    ])));
    assert_eq!(
        unparseable.ready_reason.as_deref(),
        Some("Server returned 503")
    );
}

#[test]
fn check_backend_health_reports_ready_parse_and_status_errors() {
    let parse_error = check_backend_health(&runtime_for(serve_responses(vec![
        Box::leak(json_response("200 OK", r#"{"status":"ok","version":"1.2.3"}"#).into_boxed_str()),
        Box::leak(json_response("200 OK", "not json").into_boxed_str()),
    ])));
    assert!(
        parse_error
            .error
            .as_deref()
            .expect("ready parse error")
            .starts_with("Failed to parse ready response:")
    );

    let status_error = check_backend_health(&runtime_for(serve_responses(vec![
        Box::leak(json_response("200 OK", r#"{"status":"ok","version":"1.2.3"}"#).into_boxed_str()),
        Box::leak(json_response("418 I'm a teapot", "{}").into_boxed_str()),
    ])));
    assert_eq!(
        status_error.error.as_deref(),
        Some("Ready endpoint returned HTTP 418")
    );
}

#[test]
fn check_backend_health_reports_auth_failures_and_tolerates_scope_parse_error() {
    let unauthorized = check_backend_health(&runtime_for(serve_responses(vec![
        Box::leak(json_response("200 OK", r#"{"status":"ok","version":"1.2.3"}"#).into_boxed_str()),
        Box::leak(json_response("200 OK", r#"{"status":"ready","reason":null}"#).into_boxed_str()),
        Box::leak(json_response("401 Unauthorized", "{}").into_boxed_str()),
    ])));
    assert!(!unauthorized.auth_verified);
    assert!(
        unauthorized
            .error
            .as_deref()
            .expect("auth error")
            .starts_with("Authentication failed.")
    );

    let forbidden = check_backend_health(&runtime_for(serve_responses(vec![
        Box::leak(json_response("200 OK", r#"{"status":"ok","version":"1.2.3"}"#).into_boxed_str()),
        Box::leak(json_response("200 OK", r#"{"status":"ready","reason":null}"#).into_boxed_str()),
        Box::leak(json_response("403 Forbidden", "{}").into_boxed_str()),
    ])));
    assert_eq!(
        forbidden.error.as_deref(),
        Some("Organization/repository 'withakay/ito' is not in the server allowlist.")
    );

    let bad_scope = check_backend_health(&runtime_for(serve_responses(vec![
        Box::leak(json_response("200 OK", r#"{"status":"ok","version":"1.2.3"}"#).into_boxed_str()),
        Box::leak(json_response("200 OK", r#"{"status":"ready","reason":null}"#).into_boxed_str()),
        Box::leak(json_response("200 OK", "not json").into_boxed_str()),
    ])));
    assert!(bad_scope.auth_verified);
    assert!(bad_scope.token_scope.is_none());
}