Lines
88.57 %
Functions
80 %
Branches
100 %
//! Integration tests for `create-transaction-logical`.
//!
//! Each test receives an isolated Postgres DB from `sqlx::test`, installs it
//! as the server-side test pool, seeds the minimum fixtures, then calls
//! `run_create_transaction_logical` directly and asserts the outcome.
use server::command::account::CreateAccount;
use server::command::commodity::CreateCommodity;
use server::command::{CmdResult, FinanceEntity};
use sqlx::PgPool;
use uuid::Uuid;
use super::run_logical::run_create_transaction_logical;
/// Install `pool` as the server-side test pool so every server command in this
/// thread uses the isolated test database.
fn install_pool(pool: &PgPool) {
server::db::DB_POOL.with(|c| c.set(pool as *const _));
}
/// Insert a minimal user row so server commands that JOIN on users succeed.
async fn seed_user(pool: &PgPool, user_id: Uuid) -> anyhow::Result<()> {
sqlx::query(
"INSERT INTO users \
(id, user_name, email, photo, verified, user_password, user_role, db_name, created_at) \
VALUES ($1, 'Test', 'test@test.com', 'x', false, 'pw', 'user', 'db', NOW())",
)
.bind(user_id)
.execute(pool)
.await?;
Ok(())
/// Create a commodity and return its UUID.
async fn seed_commodity(user_id: Uuid, symbol: &str, name: &str) -> anyhow::Result<Uuid> {
match CreateCommodity::new()
.symbol(symbol.to_string())
.name(name.to_string())
.user_id(user_id)
.run()
.await?
{
Some(CmdResult::String(id)) => Ok(Uuid::parse_str(&id)?),
other => anyhow::bail!("unexpected CreateCommodity result: {other:?}"),
/// Create an account with the given name and return its UUID.
async fn seed_account(user_id: Uuid, name: &str) -> anyhow::Result<Uuid> {
match CreateAccount::new()
Some(CmdResult::Entity(FinanceEntity::Account(a))) => Ok(a.id),
other => anyhow::bail!("unexpected CreateAccount result: {other:?}"),
#[sqlx::test(migrator = "server::db::MIGRATOR")]
async fn single_currency_two_split_succeeds(pool: PgPool) -> anyhow::Result<()> {
install_pool(&pool);
let user_id = Uuid::new_v4();
seed_user(&pool, user_id).await?;
let c_id = seed_commodity(user_id, "USD", "US Dollar").await?;
let a_id = seed_account(user_id, "Assets").await?;
let b_id = seed_account(user_id, "Expenses").await?;
let payload = format!(
"(:splits ((:from \"{a_id}\" :to \"{b_id}\" \
:from-commodity \"{c_id}\" :to-commodity \"{c_id}\" :value 100)))"
);
let tx_id_str = run_create_transaction_logical(user_id, Some(payload))
.await
.map_err(|e| anyhow::anyhow!("{e}"))?;
let tx_id = Uuid::parse_str(&tx_id_str)?;
assert!(!tx_id.is_nil(), "returned tx uuid must not be nil");
let split_count = sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM splits WHERE tx_id = $1")
.bind(tx_id)
.fetch_one(&pool)
assert_eq!(split_count, 2, "single logical split → 2 physical splits");
async fn cross_currency_records_price_row(pool: PgPool) -> anyhow::Result<()> {
let usd_id = seed_commodity(user_id, "USD", "US Dollar").await?;
let eur_id = seed_commodity(user_id, "EUR", "Euro").await?;
// 100 USD → 92 EUR exchange
"(:note \"fx test\" :splits ((:from \"{a_id}\" :to \"{b_id}\" \
:from-commodity \"{usd_id}\" :to-commodity \"{eur_id}\" \
:value 100 :to-amount 92)))"
assert!(!tx_id.is_nil());
let price_count = sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM prices")
assert_eq!(
price_count, 1,
"cross-currency split must record a price row"
async fn ambiguous_account_name_returns_error(pool: PgPool) -> anyhow::Result<()> {
// Create two accounts with identical name "Cash"
seed_account(user_id, "Cash").await?;
"(:splits ((:from \"Cash\" :to \"{b_id}\" \
:from-commodity \"{c_id}\" :to-commodity \"{c_id}\" :value 50)))"
let err = run_create_transaction_logical(user_id, Some(payload))
.unwrap_err();
assert!(
err.to_string().contains("ambiguous account name"),
"expected ambiguity error, got: {err}"
let tx_count = sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM transactions")
assert_eq!(tx_count, 0, "no transaction must be created on error");
async fn unknown_account_returns_error(pool: PgPool) -> anyhow::Result<()> {
"(:splits ((:from \"NoSuchAccount\" :to \"{b_id}\" \
:from-commodity \"{c_id}\" :to-commodity \"{c_id}\" :value 10)))"
err.to_string().contains("unknown account"),
"expected unknown-account error, got: {err}"
async fn unknown_commodity_returns_error(pool: PgPool) -> anyhow::Result<()> {
:from-commodity \"GHOST\" :to-commodity \"GHOST\" :value 1)))"
err.to_string().contains("unknown commodity"),
"expected unknown-commodity error, got: {err}"
async fn nonexistent_account_uuid_returns_error(pool: PgPool) -> anyhow::Result<()> {
// A syntactically valid uuid that belongs to no account.
let ghost = Uuid::new_v4();
"(:splits ((:from \"{ghost}\" :to \"{b_id}\" \
"expected unknown-account error for a nonexistent uuid, got: {err}"
async fn missing_to_amount_on_cross_currency_returns_error(pool: PgPool) -> anyhow::Result<()> {
// No :to-amount on a cross-currency split
:from-commodity \"{usd_id}\" :to-commodity \"{eur_id}\" :value 100)))"
err.to_string().contains("to_amount") || err.to_string().contains("required"),
"expected missing-to-amount error, got: {err}"