distant 0.20.0

Operate on a remote computer through file and process manipulation
Documentation
use rstest::*;
use serde_json::json;
use test_log::test;

use crate::cli::scripts::*;
use crate::common::fixtures::*;

fn make_cmd(args: Vec<&str>) -> String {
    format!(
        r#"{} {} {}"#,
        *SCRIPT_RUNNER,
        *SCRIPT_RUNNER_ARG,
        args.join(" ")
    )
}

fn trim(arr: &Vec<serde_json::Value>) -> &[serde_json::Value] {
    let arr = arr.as_slice();

    if arr.is_empty() {
        return arr;
    }

    let mut start = 0;
    let mut end = arr.len() - 1;
    let mut i = start;

    fn is_whitespace(value: &serde_json::Value) -> bool {
        value == b' ' || value == b'\t' || value == b'\r' || value == b'\n'
    }

    // Trim from front
    while start < end {
        if is_whitespace(&arr[i]) {
            start = i + 1;
            i += 1;
        } else {
            break;
        }
    }

    i = end;

    // Trim from back
    while end > start {
        if is_whitespace(&arr[i]) {
            end = i - 1;
            i -= 1;
        } else {
            break;
        }
    }

    &arr[start..=end]
}

// Trim and compare value to string
fn check_value_as_str(value: &serde_json::Value, other: &str) {
    let arr = trim(value.as_array().expect("value should be a byte array"));

    if arr != other.as_bytes() {
        let s = arr
            .iter()
            .map(|value| {
                (value
                    .as_u64()
                    .expect("Invalid array value, expected number") as u8) as char
            })
            .collect::<String>();
        panic!("Expected '{other}', but got '{s}'");
    }
}

#[rstest]
#[test(tokio::test)]
async fn should_support_json_to_execute_program_and_return_exit_status(
    mut api_process: CtxCommand<ApiProcess>,
) {
    validate_authentication(&mut api_process).await;

    let cmd = make_cmd(vec![ECHO_ARGS_TO_STDOUT.to_str().unwrap()]);

    let id = rand::random::<u64>().to_string();
    let req = json!({
        "id": id,
        "payload": {
            "type": "proc_spawn",
            "cmd": cmd,
            "pty": null,
        },
    });

    let res = api_process.write_and_read_json(req).await.unwrap().unwrap();

    assert_eq!(res["origin_id"], id, "JSON: {res}");
    assert_eq!(res["payload"]["type"], "proc_spawned", "JSON: {res}");
}

#[rstest]
#[test(tokio::test)]
async fn should_support_json_to_capture_and_print_stdout(mut api_process: CtxCommand<ApiProcess>) {
    validate_authentication(&mut api_process).await;

    let cmd = make_cmd(vec![ECHO_ARGS_TO_STDOUT.to_str().unwrap(), "some output"]);

    // Spawn the process
    let origin_id = rand::random::<u64>().to_string();
    let req = json!({
        "id": origin_id,
        "payload": {
            "type": "proc_spawn",
            "cmd": cmd,
            "pty": null,
        },
    });

    let res = api_process.write_and_read_json(req).await.unwrap().unwrap();

    assert_eq!(res["origin_id"], origin_id, "JSON: {res}");
    assert_eq!(res["payload"]["type"], "proc_spawned", "JSON: {res}");

    // Wait for output to show up (for stderr)
    let res = api_process.read_json_from_stdout().await.unwrap().unwrap();

    assert_eq!(res["origin_id"], origin_id, "JSON: {res}");
    assert_eq!(res["payload"]["type"], "proc_stdout", "JSON: {res}");
    check_value_as_str(&res["payload"]["data"], "some output");

    // Now we wait for the process to complete
    let res = api_process.read_json_from_stdout().await.unwrap().unwrap();

    assert_eq!(res["origin_id"], origin_id, "JSON: {res}");
    assert_eq!(res["payload"]["type"], "proc_done", "JSON: {res}");
    assert_eq!(res["payload"]["success"], true, "JSON: {res}");
}

#[rstest]
#[test(tokio::test)]
async fn should_support_json_to_capture_and_print_stderr(mut api_process: CtxCommand<ApiProcess>) {
    validate_authentication(&mut api_process).await;

    let cmd = make_cmd(vec![ECHO_ARGS_TO_STDERR.to_str().unwrap(), "some output"]);

    // Spawn the process
    let origin_id = rand::random::<u64>().to_string();
    let req = json!({
        "id": origin_id,
        "payload": {
            "type": "proc_spawn",
            "cmd": cmd,
            "pty": null,
        },
    });

    let res = api_process.write_and_read_json(req).await.unwrap().unwrap();

    assert_eq!(res["origin_id"], origin_id, "JSON: {res}");
    assert_eq!(res["payload"]["type"], "proc_spawned", "JSON: {res}");

    // Wait for output to show up (for stderr)
    let res = api_process.read_json_from_stdout().await.unwrap().unwrap();

    assert_eq!(res["origin_id"], origin_id, "JSON: {res}");
    assert_eq!(res["payload"]["type"], "proc_stderr", "JSON: {res}");
    check_value_as_str(&res["payload"]["data"], "some output");

    // Now we wait for the process to complete
    let res = api_process.read_json_from_stdout().await.unwrap().unwrap();

    assert_eq!(res["origin_id"], origin_id, "JSON: {res}");
    assert_eq!(res["payload"]["type"], "proc_done", "JSON: {res}");
    assert_eq!(res["payload"]["success"], true, "JSON: {res}");
}

#[rstest]
#[test(tokio::test)]
async fn should_support_json_to_forward_stdin_to_remote_process(
    mut api_process: CtxCommand<ApiProcess>,
) {
    validate_authentication(&mut api_process).await;

    let cmd = make_cmd(vec![ECHO_STDIN_TO_STDOUT.to_str().unwrap()]);

    // Spawn the process
    let origin_id = rand::random::<u64>().to_string();
    let req = json!({
        "id": origin_id,
        "payload": {
            "type": "proc_spawn",
            "cmd": cmd,
            "pty": null,
        },
    });

    let res = api_process.write_and_read_json(req).await.unwrap().unwrap();

    assert_eq!(res["origin_id"], origin_id, "JSON: {res}");
    assert_eq!(res["payload"]["type"], "proc_spawned", "JSON: {res}");

    // Write output to stdin of process to trigger getting it back as stdout
    let proc_id = res["payload"]["id"]
        .as_u64()
        .expect("Invalid proc id value");
    let id = rand::random::<u64>().to_string();
    let req = json!({
        "id": id,
        "payload": {
            "type": "proc_stdin",
            "id": proc_id,
            "data": b"some output\n",
        },
    });

    let res = api_process.write_and_read_json(req).await.unwrap().unwrap();

    assert_eq!(res["origin_id"], id, "JSON: {res}");
    assert_eq!(res["payload"]["type"], "ok", "JSON: {res}");

    let res = api_process.read_json_from_stdout().await.unwrap().unwrap();

    assert_eq!(res["origin_id"], origin_id, "JSON: {res}");
    assert_eq!(res["payload"]["type"], "proc_stdout", "JSON: {res}");
    check_value_as_str(&res["payload"]["data"], "some output");

    // Now kill the process and wait for it to complete
    let id = rand::random::<u64>().to_string();
    let res = api_process
        .write_and_read_json(json!({
            "id": id,
            "payload": {
                "type": "proc_kill",
                "id": proc_id,
            },
        }))
        .await
        .unwrap()
        .unwrap();

    // If the first response we get is proc_done, then we don't care bout the kill result
    // as it can be an error if the process ended before the kill happened
    //
    // NOTE: The above is a situation in Windows, but I've not seen it happen with Mac/Linux.
    if res["payload"]["type"] == "ok" {
        let res = api_process.read_json_from_stdout().await.unwrap().unwrap();
        assert_eq!(
            res["payload"]["type"], "proc_done",
            "Did not receive proc_done from killed process: {res}"
        );
    } else {
        assert_eq!(res["payload"]["type"], "proc_done", "JSON: {res}");
    }
}

#[rstest]
#[test(tokio::test)]
async fn should_support_json_output_for_error(mut api_process: CtxCommand<ApiProcess>) {
    validate_authentication(&mut api_process).await;

    let id = rand::random::<u64>().to_string();
    let req = json!({
        "id": id,
        "payload": {
            "type": "proc_spawn",
            "cmd": DOES_NOT_EXIST_BIN.to_str().unwrap().to_string(),
            "pty": null,
        },
    });

    let res = api_process.write_and_read_json(req).await.unwrap().unwrap();

    assert_eq!(res["origin_id"], id, "JSON: {res}");
    assert_eq!(res["payload"]["type"], "error", "JSON: {res}");
    assert_eq!(res["payload"]["kind"], "not_found", "JSON: {res}");
}