Lines
100 %
Functions
Branches
//! Tests for cli-core form descriptors and validators.
#[cfg(test)]
mod descriptor_tests {
use super::super::{
descriptors::{ACCOUNT_CREATE, COMMODITY_CONVERT, COMMODITY_CREATE, TRANSACTION_CREATE},
to_sexpr,
};
#[test]
fn account_create_sexpr_contains_form_name() {
let s = to_sexpr(&ACCOUNT_CREATE);
assert!(
s.contains(":form \"account-create\""),
"missing form name: {s}"
);
}
fn account_create_sexpr_has_required_name_field() {
assert!(s.contains(":key \"name\""), "missing name field: {s}");
s.contains(":required t"),
"name field must be required: {s}"
fn account_create_sexpr_has_optional_parent_select() {
assert!(s.contains(":key \"parent\""), "missing parent field: {s}");
s.contains("(select :source accounts)"),
"parent must be account select: {s}"
s.contains(":required nil"),
"parent field must be optional: {s}"
fn commodity_create_sexpr_has_required_symbol() {
let s = to_sexpr(&COMMODITY_CREATE);
assert!(s.contains(":key \"symbol\""), "missing symbol: {s}");
assert!(s.contains(":required t"), "symbol must be required: {s}");
fn commodity_create_sexpr_name_is_required() {
!s.contains(":required nil"),
"commodity-create has no optional field; name must be required: {s}"
fn transaction_create_sexpr_has_splits_widget() {
let s = to_sexpr(&TRANSACTION_CREATE);
assert!(s.contains(":widget splits"), "missing splits widget: {s}");
assert!(s.contains(":key \"date\""), "missing date field: {s}");
assert!(s.contains(":key \"note\""), "missing note field: {s}");
fn commodity_convert_sexpr_has_commodity_selects() {
let s = to_sexpr(&COMMODITY_CONVERT);
s.contains("(select :source commodities)"),
"missing commodity select: {s}"
assert!(s.contains(":key \"from\""), "missing from field: {s}");
assert!(s.contains(":key \"to\""), "missing to field: {s}");
fn to_sexpr_is_parseable_by_reader() {
use nomiscript::Reader;
Reader::parse(&s).expect("to_sexpr must produce valid s-expression");
mod validator_tests;
mod account_options_tests {
use super::super::parse_account_options;
use crate::eval::{build_create_account_form, build_create_commodity_form};
fn parse_empty_accounts_list_returns_empty() {
let wire = r#"(:id 1 :value "()")"#;
assert_eq!(parse_account_options(wire), vec![]);
fn parse_garbage_returns_empty() {
assert_eq!(parse_account_options("not a wire frame"), vec![]);
assert_eq!(parse_account_options(""), vec![]);
fn parse_single_account_extracts_id_and_name() {
let uuid = "550e8400-e29b-41d4-a716-446655440001";
let wire = format!(
r#"(:id 2 :value "((:account :id \"{uuid}\" :name \"Expenses\" :parent \"\"))")"#
let result = parse_account_options(&wire);
assert_eq!(result.len(), 1);
assert_eq!(result[0].0, uuid);
assert_eq!(result[0].1, "Expenses");
fn build_create_commodity_form_produces_correct_form() {
let f = build_create_commodity_form("USD", "US Dollar");
assert_eq!(f, r#"(create-commodity "USD" "US Dollar")"#);
fn build_create_commodity_form_escapes_special_chars() {
let f = build_create_commodity_form("US\"D", "Dollar");
assert!(f.contains("\\\""), "should escape double quote: {f}");
fn build_create_account_form_no_parent_uses_empty_string() {
// create-account has a fixed arity of 2; a root account passes "" parent.
let f = build_create_account_form("Expenses", None);
assert_eq!(f, r#"(create-account "Expenses" "")"#);
fn build_create_account_form_empty_parent_uses_empty_string() {
let f = build_create_account_form("Expenses", Some(""));
fn build_create_account_form_with_parent() {
let f = build_create_account_form("Sub", Some(uuid));
assert_eq!(f, format!(r#"(create-account "Sub" "{uuid}")"#));
mod update_payload_tests {
use super::super::payload::build_transaction_update_payload;
use super::super::validators::LogicalSplitInput;
const TX_ID: &str = "tx00-0000-0000-0000-000000000001";
const FROM: &str = "aaaa0000-0000-0000-0000-000000000001";
const TO: &str = "aaaa0000-0000-0000-0000-000000000002";
const COMM: &str = "cccc0000-0000-0000-0000-000000000001";
const COMM2: &str = "cccc0000-0000-0000-0000-000000000002";
fn row_same(value: &str) -> LogicalSplitInput {
LogicalSplitInput {
from: FROM.to_string(),
to: TO.to_string(),
from_commodity: COMM.to_string(),
to_commodity: COMM.to_string(),
amount: value.to_string(),
to_amount: None,
fn row_cross(value: &str, to_amount: &str) -> LogicalSplitInput {
to_commodity: COMM2.to_string(),
to_amount: Some(to_amount.to_string()),
fn update_single_currency_produces_update_transaction_logical() {
let result = build_transaction_update_payload(TX_ID, &[row_same("50")], "", "").unwrap();
result.starts_with("(update-transaction-logical "),
"form prefix: {result}"
assert!(result.contains(TX_ID), "tx id in payload: {result}");
result.contains(":transaction-id"),
":transaction-id key: {result}"
assert!(result.contains(":value 50"), "integer ratio: {result}");
!result.contains(":to-amount"),
"no to-amount same commodity: {result}"
fn update_cross_currency_includes_to_amount() {
let result =
build_transaction_update_payload(TX_ID, &[row_cross("153.81", "15000")], "", "")
.unwrap();
result.contains(":value 15381/100"),
"decimal to ratio: {result}"
result.contains(":to-amount 15000"),
"to-amount present: {result}"
fn update_note_and_date_included() {
build_transaction_update_payload(TX_ID, &[row_same("10")], "Groceries", "2024-01-15")
assert!(result.contains(":note"), "note present: {result}");
assert!(result.contains(":date"), "date present: {result}");
assert!(result.contains("Groceries"), "note text: {result}");
fn update_empty_note_and_date_omitted() {
let result = build_transaction_update_payload(TX_ID, &[row_same("5")], "", "").unwrap();
assert!(!result.contains(":note"), "no note when empty: {result}");
assert!(!result.contains(":date"), "no date when empty: {result}");
fn update_cross_commodity_missing_to_amount_is_error() {
let row = LogicalSplitInput {
amount: "10".to_string(),
build_transaction_update_payload(TX_ID, &[row], "", "").is_err(),
"cross-commodity without to_amount must fail"
mod commodity_options_tests {
use super::super::parse_commodity_options;
fn parse_empty_commodities_list_returns_empty() {
assert_eq!(parse_commodity_options(wire), vec![]);
assert_eq!(parse_commodity_options("not a wire frame"), vec![]);
assert_eq!(parse_commodity_options(""), vec![]);
fn parse_single_commodity_with_name_builds_symbol_dash_name_label() {
let uuid = "550e8400-e29b-41d4-a716-446655440002";
let wire =
format!(r#"(:id 2 :value "((:id \"{uuid}\" :symbol \"USD\" :name \"US Dollar\"))")"#);
let result = parse_commodity_options(&wire);
assert_eq!(result[0].1, "USD \u{2013} US Dollar");
fn parse_commodity_without_name_uses_symbol_only() {
let uuid = "550e8400-e29b-41d4-a716-446655440003";
let wire = format!(r#"(:id 3 :value "((:id \"{uuid}\" :symbol \"EUR\" :name \"\"))")"#);
assert_eq!(result[0].1, "EUR");
fn parse_multiple_commodities() {
let u1 = "550e8400-e29b-41d4-a716-446655440004";
let u2 = "550e8400-e29b-41d4-a716-446655440005";
r#"(:id 4 :value "((:id \"{u1}\" :symbol \"USD\" :name \"Dollar\") (:id \"{u2}\" :symbol \"BTC\" :name \"Bitcoin\"))")"#
assert_eq!(result.len(), 2);
assert_eq!(result[0].0, u1);
assert_eq!(result[1].0, u2);
assert_eq!(result[1].1, "BTC \u{2013} Bitcoin");