diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-15 11:02:34 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-15 11:02:34 +0200 |
| commit | 07f4134d38e53bf6c9235f3ad18dd5b33e89d9ff (patch) | |
| tree | 65eed6420426560a7e8127baa3ea7a703d1223a0 /tests/unit | |
| parent | 50cb852130986249cacec411e23392a8610e12f1 (diff) | |
| download | firefly-cli-master.tar.gz firefly-cli-master.zip | |
New `piggy` group:
- `piggy list` lists piggy banks with saved/target amounts and linked
accounts.
- `piggy add|remove <name|id> <amount> [--account NAME]` changes the
saved amount on a linked account via PUT /piggy-banks/{id}.
Firefly syncs the whole account list on that PUT, so every linked account
is resent (others without current_amount, which keeps their value) or it
would be unlinked. Removing below zero is rejected before any write, and
the saved amount is read back so a refused add fails loudly instead of
returning 200 unchanged.
add/remove currently hit an upstream bug on Firefly III <= 6.7.9:
PiggyBankFactory::linkToAccountIds passes the pivot current_amount (an int
from the DB) to bcsub() uncast, so any PUT with "accounts" 500s
(firefly-iii/firefly-iii#13063). Documented in SKILL.md/README; the live
integration test skips on that error and runs once upstream is fixed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'tests/unit')
| -rw-r--r-- | tests/unit/test_commands_piggy.py | 80 | ||||
| -rw-r--r-- | tests/unit/test_resolver.py | 11 |
2 files changed, 91 insertions, 0 deletions
diff --git a/tests/unit/test_commands_piggy.py b/tests/unit/test_commands_piggy.py new file mode 100644 index 0000000..24565ac --- /dev/null +++ b/tests/unit/test_commands_piggy.py @@ -0,0 +1,80 @@ +# Copyright (C) 2026 Danilo M. <danix@danix.xyz> 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() diff --git a/tests/unit/test_resolver.py b/tests/unit/test_resolver.py index 8c93a84..d9a1dcd 100644 --- a/tests/unit/test_resolver.py +++ b/tests/unit/test_resolver.py @@ -84,3 +84,14 @@ class TestResolverBudget(unittest.TestCase): self.assertEqual(got["name"], "Fun") # Must hit the show endpoint, not list. client.request.assert_called_with("GET", "/api/v1/budgets/7") + +class TestPiggyResolver(unittest.TestCase): + def test_piggy_by_name(self): + c = client_returning([{"id": "1", "attrs": {"name": "BBVA risparmi"}}]) + self.assertEqual(Resolver(c).piggy("bbva risparmi")["id"], "1") + + def test_piggy_by_id_hits_show_endpoint(self): + c = MagicMock() + c.request.return_value = {"data": {"id": "1", "attributes": {"name": "X"}}} + Resolver(c).piggy("1") + self.assertEqual(c.request.call_args[0][1], "/api/v1/piggy-banks/1") |
