tovuk 0.1.84

Deploy Rust workers, static frontends, and full-stack services to Tovuk.
use super::super::{
    args::CliOptions,
    errors::{Result, agent_error},
};
use super::generic::{print_authenticated_mutation, print_paged_authenticated};
use reqwest::Method;
use serde_json::{Value, json};

const ACCOUNT_ACTIVITY_PATH: &str = "/v1/account/activity";

pub(crate) fn account_command(cli: &CliOptions) -> Result<()> {
    match cli.args.first().map_or("show", String::as_str) {
        "show" => print_paged_authenticated(cli, "/v1/account"),
        "activity" => print_paged_authenticated(cli, ACCOUNT_ACTIVITY_PATH),
        "update" => account_update(cli),
        _ => Err(agent_error(
            "unknown_account_command",
            "Unknown account command.",
            "Use `tovuk account show --json`, `tovuk account activity --json`, or `tovuk account update --handle <handle> --display-name <name> --json`.",
            cli.output.json,
        )),
    }
}

fn account_update(cli: &CliOptions) -> Result<()> {
    print_authenticated_mutation(
        cli,
        Method::PUT,
        "/v1/account",
        Some(account_update_body(cli)?),
    )
}

fn account_update_body(cli: &CliOptions) -> Result<Value> {
    let handle = account_update_handle(cli)?;
    let display_name = account_update_display_name(cli, &handle);
    Ok(json!({
        "handle": handle,
        "displayName": display_name,
    }))
}

fn account_update_handle(cli: &CliOptions) -> Result<String> {
    if !cli.account.handle.is_empty() {
        return Ok(cli.account.handle.clone());
    }
    cli.args
        .get(1)
        .cloned()
        .filter(|value| !value.is_empty())
        .ok_or_else(|| {
            agent_error(
                "account_handle_required",
                "Account handle is required.",
                "Use `tovuk account update --handle <handle> --display-name <name> --json`.",
                cli.output.json,
            )
        })
}

fn account_update_display_name(cli: &CliOptions, handle: &str) -> String {
    if !cli.account.display_name.is_empty() {
        return cli.account.display_name.clone();
    }
    let positional_name = cli
        .args
        .iter()
        .skip(2)
        .cloned()
        .collect::<Vec<_>>()
        .join(" ");
    if positional_name.trim().is_empty() {
        handle.to_owned()
    } else {
        positional_name.trim().to_owned()
    }
}

#[cfg(test)]
mod tests {
    use super::{ACCOUNT_ACTIVITY_PATH, account_update_body};
    use crate::cli::args::CliOptions;
    use serde_json::json;

    #[test]
    fn account_update_prefers_explicit_flags() {
        let mut cli = CliOptions {
            command: "account".to_owned(),
            args: vec!["update".to_owned(), "ignored".to_owned()],
            ..CliOptions::default()
        };
        cli.account.handle = "tovuk-team".to_owned();
        cli.account.display_name = "Tovuk Team".to_owned();

        assert_eq!(
            account_update_body(&cli).ok(),
            Some(json!({ "handle": "tovuk-team", "displayName": "Tovuk Team" }))
        );
    }

    #[test]
    fn account_update_accepts_positional_handle_and_name() {
        let cli = CliOptions {
            command: "account".to_owned(),
            args: vec![
                "update".to_owned(),
                "tovuk-team".to_owned(),
                "Tovuk".to_owned(),
                "Team".to_owned(),
            ],
            ..CliOptions::default()
        };

        assert_eq!(
            account_update_body(&cli).ok(),
            Some(json!({ "handle": "tovuk-team", "displayName": "Tovuk Team" }))
        );
    }

    #[test]
    fn account_update_requires_handle() {
        let cli = CliOptions {
            command: "account".to_owned(),
            args: vec!["update".to_owned()],
            ..CliOptions::default()
        };

        let message = account_update_body(&cli)
            .err()
            .map(|error| error.to_string());
        assert_eq!(message.as_deref(), Some("Account handle is required."));
    }

    #[test]
    fn account_activity_uses_consolidated_account_route() {
        assert_eq!(ACCOUNT_ACTIVITY_PATH, "/v1/account/activity");
        let retired_path = format!("/v1/{}", "activity");
        assert_ne!(ACCOUNT_ACTIVITY_PATH, retired_path);
    }
}