1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
|
# Copyright (C) 2026 Danilo M. <danix@danix.xyz> GPL-2.0-only
import os, unittest
from firefly_cli.client import Client
from firefly_cli.resolver import Resolver
from firefly_cli.output import unwrap
URL = os.environ.get("FIREFLY_TEST_URL")
TOKEN = os.environ.get("FIREFLY_TEST_TOKEN")
@unittest.skipUnless(URL and TOKEN,
"set FIREFLY_TEST_URL and FIREFLY_TEST_TOKEN to run live tests")
class TestLive(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.client = Client(URL, TOKEN)
cls.resolver = Resolver(cls.client)
def test_about_shape(self):
resp = self.client.request("GET", "/api/v1/about")
self.assertIn("version", resp["data"])
def test_accounts_envelope_and_balance_field(self):
resp = self.client.request("GET", "/api/v1/accounts",
params={"type": "asset", "limit": 5})
self.assertIn("data", resp)
accs = unwrap(resp)
if accs:
# Confirm the balance field name assumed by the design.
self.assertIn("current_balance", accs[0],
"balance field name differs; update account balance cmd + unit test")
def test_create_then_delete_transaction(self):
# Needs an asset account and an expense account on the TEST account.
accs = unwrap(self.client.request("GET", "/api/v1/accounts",
params={"type": "asset", "limit": 2}))
if len(accs) < 1:
self.skipTest("test account has no asset account")
exp = unwrap(self.client.request("GET", "/api/v1/accounts",
params={"type": "expense", "limit": 1}))
if not exp:
self.skipTest("test account has no expense account")
body = {"transactions": [{
"type": "withdrawal",
"date": "2026-06-30",
"amount": "0.01",
"description": "firefly-cli integration test",
"source_id": accs[0]["id"],
"destination_id": exp[0]["id"],
}]}
created = self.client.request("POST", "/api/v1/transactions", body=body)
tx_id = created["data"]["id"]
try:
got = self.client.request("GET", f"/api/v1/transactions/{tx_id}")
self.assertEqual(got["data"]["id"], tx_id)
finally:
self.client.request("DELETE", f"/api/v1/transactions/{tx_id}")
def test_piggy_add_remove_roundtrip(self):
# Create a piggy bank on the TEST account, drive it through the CLI
# handlers, then delete it.
import io, contextlib, json
from types import SimpleNamespace
from firefly_cli.commands import piggy
from firefly_cli.errors import FireflyError
accs = unwrap(self.client.request("GET", "/api/v1/accounts",
params={"type": "asset", "limit": 50}))
acc = next((a for a in accs if float(a.get("current_balance") or 0) >= 10), None)
if acc is None:
self.skipTest("test account has no asset account with >= 10 balance")
created = self.client.request("POST", "/api/v1/piggy-banks", body={
"name": "firefly-cli integration piggy", "target_amount": "100",
"transaction_currency_code": acc.get("currency_code") or "EUR",
"start_date": "2026-01-01",
"accounts": [{"account_id": acc["id"], "current_amount": "0"}]})
pb_id = created["data"]["id"]
ctx = SimpleNamespace(client=self.client, resolver=self.resolver, human=False)
def run(fn, amount):
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
fn(SimpleNamespace(ref=pb_id, amount=amount, account=None), ctx)
return json.loads(buf.getvalue())["accounts"][0]["current_amount"]
try:
from firefly_cli.errors import ApiError
try:
first = run(piggy.cmd_add, "2.09")
except ApiError as e:
if "bcsub()" in str(e):
self.skipTest("firefly-iii/firefly-iii#13063: piggy PUT with accounts 500s")
raise
self.assertEqual(float(first), 2.09)
self.assertEqual(float(run(piggy.cmd_add, "0.91")), 3.00)
self.assertEqual(float(run(piggy.cmd_remove, "1")), 2.00)
with self.assertRaises(FireflyError): # more than the account holds
run(piggy.cmd_add, "99999999")
linked = unwrap(self.client.request("GET", f"/api/v1/piggy-banks/{pb_id}"))["accounts"]
self.assertEqual(len(linked), 1) # PUT did not unlink the account
finally:
self.client.request("DELETE", f"/api/v1/piggy-banks/{pb_id}")
|