# Copyright (C) 2026 Danilo M. GPL-2.0-only import unittest from unittest.mock import MagicMock from types import SimpleNamespace import firefly_cli.commands.piggy as piggy from firefly_cli.errors import FireflyError def _pb(accounts): return {"id": "3", "name": "Round-ups", "accounts": accounts} def _ctx(pb, put_accounts=None): """Resolver returns `pb`; the PUT echoes `put_accounts` (what Firefly saved).""" resolver = MagicMock() resolver.piggy.return_value = pb client = MagicMock() client.request.return_value = {"data": {"id": "3", "attributes": { "name": "Round-ups", "accounts": put_accounts or []}}} return SimpleNamespace(client=client, resolver=resolver, human=False) ONE = [{"account_id": "6", "name": "BBVA", "current_amount": "1.50"}] TWO = ONE + [{"account_id": "8", "name": "Fineco", "current_amount": "0.00"}] class TestPiggyAddRemove(unittest.TestCase): def test_add_single_account_puts_new_total(self): ctx = _ctx(_pb(ONE), [{"account_id": "6", "current_amount": "3.59"}]) rc = piggy.cmd_add(SimpleNamespace(ref="Round-ups", amount="2.09", account=None), ctx) self.assertEqual(rc, 0) method, path = ctx.client.request.call_args[0][:2] self.assertEqual((method, path), ("PUT", "/api/v1/piggy-banks/3")) body = ctx.client.request.call_args[1]["body"] self.assertEqual(body, {"accounts": [{"account_id": "6", "current_amount": "3.59"}]}) def test_remove_subtracts(self): ctx = _ctx(_pb(ONE), [{"account_id": "6", "current_amount": "1.00"}]) piggy.cmd_remove(SimpleNamespace(ref="Round-ups", amount="0.50", account=None), ctx) body = ctx.client.request.call_args[1]["body"] self.assertEqual(body["accounts"][0]["current_amount"], "1.00") def test_remove_below_zero_is_error_without_write(self): ctx = _ctx(_pb(ONE)) with self.assertRaises(FireflyError): piggy.cmd_remove(SimpleNamespace(ref="Round-ups", amount="5", account=None), ctx) ctx.client.request.assert_not_called() def test_multi_account_needs_account_flag(self): ctx = _ctx(_pb(TWO)) with self.assertRaises(FireflyError): piggy.cmd_add(SimpleNamespace(ref="Round-ups", amount="1", account=None), ctx) def test_multi_account_resends_others_without_amount(self): # Firefly syncs the account list: omitting an account would unlink it. ctx = _ctx(_pb(TWO), [{"account_id": "8", "current_amount": "1.00"}]) piggy.cmd_add(SimpleNamespace(ref="Round-ups", amount="1", account="fineco"), ctx) body = ctx.client.request.call_args[1]["body"] self.assertEqual(body["accounts"], [{"account_id": "6"}, {"account_id": "8", "current_amount": "1.00"}]) def test_unknown_account_is_error(self): ctx = _ctx(_pb(ONE)) with self.assertRaises(FireflyError): piggy.cmd_add(SimpleNamespace(ref="Round-ups", amount="1", account="Nope"), ctx) def test_silent_refusal_is_detected(self): # Firefly skips an add it can't afford but still returns 200. ctx = _ctx(_pb(ONE), [{"account_id": "6", "current_amount": "1.50"}]) with self.assertRaises(FireflyError): piggy.cmd_add(SimpleNamespace(ref="Round-ups", amount="2", account=None), ctx) def test_bad_amount(self): for bad in ("0", "-1", "abc"): with self.assertRaises(FireflyError): piggy.cmd_add(SimpleNamespace(ref="x", amount=bad, account=None), _ctx(_pb(ONE))) if __name__ == "__main__": unittest.main()