Lines
100 %
Functions
Branches
use super::super::validators::{
LogicalSplitInput, amount_token, now_template, parse_amount, row_complete, step_date,
validate_date,
};
use chrono::Utc;
use num_rational::Rational64;
#[test]
fn amount_token_integer_and_fraction() {
assert_eq!(amount_token(&Rational64::new(90, 1)), "90");
assert_eq!(amount_token(&Rational64::new(9, 2)), "9/2");
assert_eq!(amount_token(&Rational64::new(-9, 2)), "-9/2");
// Reduction is reflected: 6/4 → 3/2.
assert_eq!(amount_token(&Rational64::new(6, 4)), "3/2");
}
fn parse_amount_integer() {
assert_eq!(parse_amount("100").unwrap(), Rational64::new(100, 1));
fn parse_amount_negative() {
assert_eq!(parse_amount("-7").unwrap(), Rational64::new(-7, 1));
fn parse_amount_fraction() {
assert_eq!(parse_amount("1/3").unwrap(), Rational64::new(1, 3));
assert_eq!(parse_amount("3/4").unwrap(), Rational64::new(3, 4));
fn parse_amount_decimal() {
assert_eq!(parse_amount("153.81").unwrap(), Rational64::new(15381, 100));
fn parse_amount_negative_decimal() {
assert_eq!(parse_amount("-2.5").unwrap(), Rational64::new(-5, 2));
fn parse_amount_zero_denominator_is_error() {
assert!(parse_amount("1/0").is_err());
fn parse_amount_i64_min_operand_is_error_not_panic() {
// `Rational64::new` would overflow normalizing these (negating i64::MIN).
assert!(parse_amount("1/-9223372036854775808").is_err());
assert!(parse_amount("-9223372036854775808/1").is_err());
assert!(parse_amount("-9223372036854775808/2").is_err());
fn parse_amount_garbage_is_error() {
assert!(parse_amount("abc").is_err());
fn parse_amount_empty_is_error() {
assert!(parse_amount("").is_err());
assert!(parse_amount(" ").is_err());
fn parse_amount_multiple_dots_is_error() {
assert!(parse_amount("1.2.3").is_err());
fn validate_date_rfc3339_passthrough() {
let ts = "2026-06-01T00:00:00+00:00";
let dt = validate_date(ts).unwrap();
assert_eq!(dt.to_rfc3339(), ts);
fn validate_date_yyyy_mm_dd_midnight_utc() {
let dt = validate_date("2026-03-15").unwrap();
assert_eq!(dt.date_naive().to_string(), "2026-03-15");
assert_eq!(dt.time().to_string(), "00:00:00");
// Must be UTC
assert_eq!(dt.timezone(), Utc);
fn validate_date_t_format_no_seconds() {
let dt = validate_date("2025-11-30T14:05").unwrap();
assert_eq!(dt.date_naive().to_string(), "2025-11-30");
assert_eq!(dt.time().to_string(), "14:05:00");
fn validate_date_invalid_is_error() {
assert!(validate_date("not-a-date").is_err());
assert!(validate_date("2026-13-01").is_err());
fn now_template_is_accepted_by_validate_date() {
// Verify that the template format round-trips through validate_date.
// rpc/src/natives/transaction/parse.rs::parse_flexible_date also explicitly
// lists "%Y-%m-%dT%H:%M" in its format loop, so this format is server-accepted.
let tmpl = now_template();
assert_eq!(tmpl.len(), 16, "YYYY-MM-DDTHH:MM is 16 chars, got: {tmpl}");
assert!(tmpl.contains('T'), "must have T separator: {tmpl}");
assert!(validate_date(&tmpl).is_ok(), "must parse: {tmpl}");
fn step_date_forward_across_month_boundary_preserves_time() {
let result = step_date("2024-01-31T10:30", 1).unwrap();
assert_eq!(result, "2024-02-01T10:30");
fn step_date_back_across_month_boundary_preserves_time() {
let result = step_date("2024-03-01T08:15", -1).unwrap();
assert_eq!(result, "2024-02-29T08:15");
fn step_date_invalid_input_returns_none() {
// Stepping an unparseable buffer must NOT replace it — return None so the
// caller leaves the user's in-progress text untouched.
assert!(step_date("not-a-date", 1).is_none());
assert!(step_date("", -1).is_none());
fn row_complete_same_commodity_no_to_amount_ok() {
let c = "a1b2c3d4-e5f6-7890-abcd-ef1234567890".to_string();
let row = LogicalSplitInput {
from: "00000000-0000-0000-0000-000000000001".to_string(),
to: "00000000-0000-0000-0000-000000000002".to_string(),
from_commodity: c.clone(),
to_commodity: c.clone(),
amount: "100".to_string(),
to_amount: None,
assert!(row_complete(&row).is_ok());
fn row_complete_cross_commodity_requires_to_amount() {
from_commodity: "aaaa".to_string(),
to_commodity: "bbbb".to_string(),
let err = row_complete(&row).unwrap_err();
assert!(err.contains("to_amount"), "got: {err}");
fn row_complete_cross_commodity_with_to_amount_ok() {
to_amount: Some("15000".to_string()),
fn row_complete_zero_value_is_error() {
let c = "c".to_string();
to_commodity: c,
amount: "0".to_string(),
assert!(err.contains("positive"), "got: {err}");
fn row_complete_negative_value_is_error() {
amount: "-5".to_string(),
fn row_complete_cross_commodity_nonpositive_to_amount_is_error() {
to_amount: Some("0".to_string()),
assert!(err.contains("to_amount must be positive"), "got: {err}");
fn row_complete_same_commodity_with_to_amount_is_error() {
to_amount: Some("100".to_string()),
assert!(err.contains("only valid for cross-currency"), "got: {err}");
fn row_complete_empty_from_is_error() {
from: String::new(),
from_commodity: "c".to_string(),
to_commodity: "c".to_string(),
amount: "1".to_string(),
assert!(err.contains("from"), "got: {err}");
fn row_complete_empty_to_is_error() {
to: String::new(),
assert!(err.contains("to"), "got: {err}");
fn row_complete_bad_amount_is_error() {
amount: "abc".to_string(),
assert!(row_complete(&row).is_err());
fn row_complete_empty_commodities_is_error() {
// Two empty commodity fields must NOT be treated as same-commodity.
from_commodity: String::new(),
to_commodity: String::new(),
assert!(err.contains("commodity"), "got: {err}");