koban-cli 0.3.0

A Rust CLI for Invoice Ninja, built for humans and AI agents
use super::*;

#[tokio::test]
async fn resource_actions_hit_official_live_routes_when_confirmed() {
    let server = MockServer::start();
    let config = Config::from_values(server.base_url(), "token").expect("config");

    let recurring_start = server.mock(|when, then| {
        when.method(GET)
            .path("/api/v1/recurring_invoices/recurring_1/start");
        then.status(200)
            .json_body(serde_json::json!({"data": {"id": "recurring_1"}}));
    });
    let product_archive = server.mock(|when, then| {
        when.method(POST).path("/api/v1/products/bulk");
        then.status(200)
            .json_body(serde_json::json!({"data": [{"id": "product_1"}]}));
    });

    execute_with_config(
        Cli {
            output: OutputFormat::Json,
            command: Some(Commands::RecurringInvoices(ResourceCommand::Action(
                ResourceActionArgs {
                    id: "recurring_1".to_string(),
                    action: "start".to_string(),
                    payload: empty_resource_payload_args(),
                    safety: WriteSafetyArgs {
                        dry_run: false,
                        yes: true,
                    },
                    include: Vec::new(),
                },
            ))),
        },
        config.clone(),
    )
    .await
    .expect("recurring invoice action");

    execute_with_config(
        Cli {
            output: OutputFormat::Json,
            command: Some(Commands::Products(ResourceCommand::Action(
                ResourceActionArgs {
                    id: "product_1".to_string(),
                    action: "archive".to_string(),
                    payload: {
                        let mut args = empty_resource_payload_args();
                        args.fields.push("reason=duplicate".to_string());
                        args
                    },
                    safety: WriteSafetyArgs {
                        dry_run: false,
                        yes: true,
                    },
                    include: Vec::new(),
                },
            ))),
        },
        config,
    )
    .await
    .expect("product bulk action");

    recurring_start.assert();
    product_archive.assert();
}

#[tokio::test]
async fn get_resource_actions_reject_payloads_before_network() {
    let config = Config::from_values("http://localhost:1234", "token").expect("config");

    let error = execute_with_config(
        Cli {
            output: OutputFormat::Json,
            command: Some(Commands::RecurringInvoices(ResourceCommand::Action(
                ResourceActionArgs {
                    id: "recurring_1".to_string(),
                    action: "start".to_string(),
                    payload: {
                        let mut args = empty_resource_payload_args();
                        args.fields.push("reason=now".to_string());
                        args
                    },
                    safety: WriteSafetyArgs {
                        dry_run: true,
                        yes: false,
                    },
                    include: Vec::new(),
                },
            ))),
        },
        config,
    )
    .await
    .expect_err("GET action payload should fail");
    assert!(matches!(error, KobanError::InvalidPayload { .. }));
    assert!(
        error.to_string().contains("GET resource actions"),
        "got: {error}"
    );
}

#[tokio::test]
async fn bulk_backed_actions_respect_resource_bulk_guards() {
    let config = Config::from_values("http://localhost:1234", "token").expect("config");

    let error = execute_with_config(
        Cli {
            output: OutputFormat::Json,
            command: Some(Commands::Locations(ResourceCommand::Action(
                ResourceActionArgs {
                    id: "location_1".to_string(),
                    action: "archive".to_string(),
                    payload: empty_resource_payload_args(),
                    safety: WriteSafetyArgs {
                        dry_run: true,
                        yes: false,
                    },
                    include: Vec::new(),
                },
            ))),
        },
        config,
    )
    .await
    .expect_err("locations do not support bulk-backed actions");
    assert!(error.to_string().contains("does not support `bulk`"));
}

#[tokio::test]
async fn unsupported_pdf_downloads_fail_before_network() {
    let config = Config::from_values("http://localhost:1234", "token").expect("config");
    let tempdir = tempfile::tempdir().expect("tempdir");

    let error = execute_with_config(
        Cli {
            output: OutputFormat::Json,
            command: Some(Commands::Clients(ResourceCommand::Download(DownloadArgs {
                id: "client_invitation".to_string(),
                output_file: tempdir.path().join("client.pdf"),
                force: false,
                include: Vec::new(),
            }))),
        },
        config,
    )
    .await
    .expect_err("clients do not have PDF downloads");
    assert!(
        error.to_string().contains("does not support PDF downloads"),
        "got: {error}"
    );
}