nils-api-testing-core 1.12.0

Library crate for nils-api-testing-core in the nils-cli workspace.
Documentation
use anyhow::Context;

use crate::Result;
use crate::websocket::runner::WebsocketExecutedRequest;
use crate::websocket::schema::{WebsocketExpect, WebsocketRequest};

pub fn evaluate_text_expect(expect: &WebsocketExpect, text: &str, label: &str) -> Result<()> {
    if let Some(contains) = expect.text_contains.as_deref()
        && !text.contains(contains)
    {
        anyhow::bail!("{label} textContains failed: expected to contain '{contains}'");
    }

    if let Some(expr) = expect.jq.as_deref() {
        let json: serde_json::Value = serde_json::from_str(text)
            .with_context(|| format!("{label} jq requires a JSON response text"))?;
        if !crate::jq::eval_exit_status(&json, expr).unwrap_or(false) {
            anyhow::bail!("{label} jq failed: {expr}");
        }
    }

    Ok(())
}

pub fn evaluate_main_response(
    request: &WebsocketRequest,
    executed: &WebsocketExecutedRequest,
) -> Result<()> {
    let Some(expect) = request.expect.as_ref() else {
        return Ok(());
    };

    let last = executed.last_received.as_deref().ok_or_else(|| {
        anyhow::anyhow!("websocket expect requires at least one received message")
    })?;

    evaluate_text_expect(expect, last, "websocket expect")
}

#[cfg(test)]
mod tests {
    use pretty_assertions::assert_eq;

    use super::*;

    fn request_with_expect(expect: Option<WebsocketExpect>) -> WebsocketRequest {
        WebsocketRequest {
            url: None,
            headers: Vec::new(),
            connect_timeout_seconds: None,
            steps: Vec::new(),
            expect,
            raw: serde_json::json!({}),
        }
    }

    fn executed_with_text(text: Option<&str>) -> WebsocketExecutedRequest {
        WebsocketExecutedRequest {
            target: "ws://127.0.0.1:9001/ws".to_string(),
            transcript: Vec::new(),
            last_received: text.map(ToString::to_string),
        }
    }

    #[test]
    fn websocket_expect_accepts_matching_text_contains_and_jq() {
        let expect = WebsocketExpect {
            jq: Some(".ok == true".to_string()),
            text_contains: Some("ok".to_string()),
        };
        let request = request_with_expect(Some(expect));
        let executed = executed_with_text(Some("{\"ok\":true}"));
        evaluate_main_response(&request, &executed).unwrap();
    }

    #[test]
    fn websocket_expect_rejects_missing_receive_text() {
        let expect = WebsocketExpect {
            jq: None,
            text_contains: Some("ok".to_string()),
        };
        let request = request_with_expect(Some(expect));
        let executed = executed_with_text(None);
        let err = evaluate_main_response(&request, &executed).unwrap_err();
        assert!(format!("{err:#}").contains("at least one received message"));
    }

    #[test]
    fn websocket_expect_rejects_failed_jq() {
        let err = evaluate_text_expect(
            &WebsocketExpect {
                jq: Some(".ok == false".to_string()),
                text_contains: None,
            },
            "{\"ok\":true}",
            "test",
        )
        .unwrap_err();
        assert!(format!("{err:#}").contains("jq failed"));
    }

    #[test]
    fn websocket_expect_rejects_missing_text_contains() {
        let err = evaluate_text_expect(
            &WebsocketExpect {
                jq: None,
                text_contains: Some("needle".to_string()),
            },
            "haystack",
            "test",
        )
        .unwrap_err();
        assert!(format!("{err:#}").contains("textContains failed"));
    }

    #[test]
    fn websocket_expect_accepts_empty_when_no_expect() {
        let request = request_with_expect(None);
        let executed = executed_with_text(None);
        let result = evaluate_main_response(&request, &executed);
        assert_eq!(result.is_ok(), true);
    }
}