Lines
100 %
Functions
Branches
//! Unit tests for the palette command form builder.
use super::{build_form, build_report_request};
use crate::tabs::reports::ReportKind;
use cli_core::{CommandError, CommandNode, command_tree, find_leaf};
fn args(pairs: &[(&str, &str)]) -> Vec<(String, String)> {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}
fn path(segments: &[&str]) -> Vec<String> {
segments.iter().map(|s| s.to_string()).collect()
fn leaf_node(path_segments: &[&str]) -> CommandNode {
let tree = command_tree();
let node = find_leaf(&tree, path_segments).expect("path must resolve in the canonical tree");
CommandNode {
name: node.name.clone(),
comment: node.comment.clone(),
is_leaf: true,
subcommands: vec![],
arguments: vec![],
// ── version ──────────────────────────────────────────────────────────────────
#[test]
fn version_produces_version_form() {
let node = leaf_node(&["version"]);
let form = build_form(&node, &path(&["version"]), &[])
.unwrap()
.unwrap();
assert_eq!(form, "(get-version)");
// ── account ──────────────────────────────────────────────────────────────────
fn account_list_produces_list_accounts() {
let node = leaf_node(&["account", "list"]);
let form = build_form(&node, &path(&["account", "list"]), &[])
assert_eq!(form, "(list-accounts)");
fn account_balance_produces_get_balances() {
let uuid = "550e8400-e29b-41d4-a716-446655440000";
let node = leaf_node(&["account", "balance"]);
let form = build_form(
&node,
&path(&["account", "balance"]),
&args(&[("account", uuid)]),
)
assert_eq!(form, format!(r#"(get-balances "{uuid}")"#));
fn account_balance_missing_uuid_is_error() {
assert!(build_form(&node, &path(&["account", "balance"]), &[]).is_err());
fn account_balance_bad_uuid_is_error() {
assert!(
build_form(
&args(&[("account", "not-a-uuid")])
.is_err()
);
fn account_create_opens_modal_path() {
let node = leaf_node(&["account", "create"]);
let result = build_form(&node, &path(&["account", "create"]), &[]);
assert!(result.is_ok(), "account create must not error");
result.unwrap().is_none(),
"account create must return Ok(None) for modal path"
fn account_create_with_args_still_modal_path() {
let parent = "550e8400-e29b-41d4-a716-446655440001";
let result = build_form(
&path(&["account", "create"]),
&args(&[("name", "Sub"), ("parent", parent)]),
"account create always returns Ok(None)"
// ── commodity ────────────────────────────────────────────────────────────────
fn commodity_list_produces_list_commodities() {
let node = leaf_node(&["commodity", "list"]);
let form = build_form(&node, &path(&["commodity", "list"]), &[])
assert_eq!(form, "(list-commodities)");
fn commodity_create_opens_modal_path() {
let node = leaf_node(&["commodity", "create"]);
let result = build_form(&node, &path(&["commodity", "create"]), &[]);
assert!(result.is_ok(), "commodity create must not error");
"commodity create must return Ok(None) for modal path"
fn commodity_convert_opens_modal_path() {
let node = leaf_node(&["commodity", "convert"]);
let result = build_form(&node, &path(&["commodity", "convert"]), &[]);
assert!(result.is_ok(), "commodity convert must not error");
"commodity convert must return Ok(None) for modal path"
// ── transaction ───────────────────────────────────────────────────────────────
fn transaction_list_no_account_produces_empty_string_arg() {
let node = leaf_node(&["transaction", "list"]);
let form = build_form(&node, &path(&["transaction", "list"]), &[])
assert_eq!(form, r#"(list-transactions "")"#);
fn transaction_list_with_account() {
let uuid = "550e8400-e29b-41d4-a716-446655440002";
&path(&["transaction", "list"]),
assert_eq!(form, format!(r#"(list-transactions "{uuid}")"#));
fn transaction_list_malformed_account_errors() {
let res = build_form(
&args(&[("account", "not-a-uuid")]),
matches!(res, Err(CommandError::Argument(_))),
"malformed optional account uuid must error, got: {res:?}"
fn account_create_malformed_parent_is_modal_path() {
// Validation is deferred to the form modal, so any args return Ok(None).
&args(&[("name", "Cash"), ("parent", "bogus")]),
assert!(res.is_ok(), "account create must not error");
assert!(res.unwrap().is_none(), "account create is a modal path");
fn transaction_create_opens_modal_path() {
// transaction create always opens the form modal regardless of args.
let node = leaf_node(&["transaction", "create"]);
let result = build_form(&node, &path(&["transaction", "create"]), &[]);
assert!(result.is_ok(), "transaction create must not error");
"transaction create must return Ok(None) for modal path"
fn transaction_create_with_args_still_modal_path() {
let from = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa";
let to = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb";
let cur = "cccccccc-cccc-cccc-cccc-cccccccccccc";
&path(&["transaction", "create"]),
&args(&[
("from", from),
("to", to),
("from_currency", cur),
("to_currency", cur),
("value", "100"),
]),
"transaction create always returns Ok(None)"
// ── config ────────────────────────────────────────────────────────────────────
fn config_get_produces_get_config() {
let node = leaf_node(&["config", "get"]);
&path(&["config", "get"]),
&args(&[("name", "theme")]),
assert_eq!(form, r#"(get-config "theme")"#);
fn config_get_missing_name_is_error() {
assert!(build_form(&node, &path(&["config", "get"]), &[]).is_err());
fn config_set_returns_none_for_modal_path() {
let node = leaf_node(&["config", "set"]);
build_form(&node, &path(&["config", "set"]), &[])
.is_none()
// ── build_report_request ─────────────────────────────────────────────────────
fn reports_balance_with_no_dates_returns_request() {
let req = build_report_request(&path(&["reports", "balance"]), &[])
assert_eq!(req.kind, ReportKind::Balance);
assert!(req.from.is_empty());
assert!(req.to.is_empty());
assert_eq!(req.chart, "bar");
fn reports_balance_with_chart_arg() {
let req = build_report_request(&path(&["reports", "balance"]), &args(&[("chart", "line")]))
assert_eq!(req.chart, "line");
fn reports_activity_with_dates_coerces_rfc3339() {
let req = build_report_request(
&path(&["reports", "activity"]),
&args(&[("from", "2026-01-01"), ("to", "2026-03-31")]),
assert_eq!(req.kind, ReportKind::Activity);
assert_eq!(req.from, "2026-01-01T00:00:00+00:00");
assert_eq!(req.to, "2026-03-31T23:59:59+00:00");
fn reports_activity_without_dates_returns_none_for_modal() {
let result = build_report_request(&path(&["reports", "activity"]), &[]).unwrap();
result.is_none(),
"missing dates must yield None (open modal)"
fn reports_breakdown_with_dates_coerces_rfc3339() {
&path(&["reports", "breakdown"]),
&args(&[("from", "2026-01-01"), ("to", "2026-06-30")]),
assert_eq!(req.kind, ReportKind::Breakdown);
assert_eq!(req.to, "2026-06-30T23:59:59+00:00");
fn reports_breakdown_bad_date_is_error() {
let result = build_report_request(
&args(&[("from", "not-a-date"), ("to", "2026-06-30")]),
assert!(result.is_err(), "bad date must be an error");
fn reports_activity_already_rfc3339_passes_through() {
let from = "2026-01-01T00:00:00+00:00";
let to = "2026-03-31T23:59:59+00:00";
&args(&[("from", from), ("to", to)]),
assert_eq!(req.from, from);
assert_eq!(req.to, to);