2026-04-17 11:04:18 -06:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_index_renders_all_sections(client):
|
|
|
|
|
response = client.get("/")
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
for label in [
|
|
|
|
|
"Incomes",
|
|
|
|
|
"Fixed Amount Bills",
|
|
|
|
|
"Debt Minimums",
|
|
|
|
|
"Food and Essentials",
|
|
|
|
|
"Subscriptions",
|
|
|
|
|
"Other",
|
|
|
|
|
"Primary Debt Target",
|
|
|
|
|
]:
|
|
|
|
|
assert label in response.text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_add_entry_updates_total(client):
|
|
|
|
|
response = client.post(
|
|
|
|
|
"/sections/income/entries",
|
|
|
|
|
data={"name": "Paycheck", "amount": "2500.00"},
|
|
|
|
|
)
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert "Paycheck" in response.text
|
|
|
|
|
assert "$2500.00" in response.text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_add_debt_minimum_also_returns_target_card(client):
|
|
|
|
|
response = client.post(
|
|
|
|
|
"/sections/debt_minimum/entries",
|
|
|
|
|
data={"name": "Card A", "amount": "40.00"},
|
|
|
|
|
)
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert "section-debt_minimum" in response.text
|
|
|
|
|
assert "section-debt_target" in response.text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_delete_entry(client):
|
|
|
|
|
create = client.post(
|
|
|
|
|
"/sections/other/entries", data={"name": "Gift", "amount": "25.00"}
|
|
|
|
|
)
|
|
|
|
|
assert create.status_code == 200
|
|
|
|
|
response = client.delete("/entries/1")
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert "Gift" not in response.text
|
|
|
|
|
assert "$0.00" in response.text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_set_and_clear_debt_target(client):
|
|
|
|
|
client.post(
|
|
|
|
|
"/sections/debt_minimum/entries",
|
|
|
|
|
data={"name": "Card A", "amount": "40.00"},
|
|
|
|
|
)
|
|
|
|
|
set_resp = client.post("/debt-target", data={"debt_minimum_id": "1"})
|
|
|
|
|
assert set_resp.status_code == 200
|
|
|
|
|
assert "Card A" in set_resp.text
|
|
|
|
|
|
|
|
|
|
clear_resp = client.post("/debt-target", data={"debt_minimum_id": ""})
|
|
|
|
|
assert clear_resp.status_code == 200
|
|
|
|
|
assert "No target selected" in clear_resp.text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_debt_target_clears_when_referenced_row_deleted(client):
|
|
|
|
|
client.post(
|
|
|
|
|
"/sections/debt_minimum/entries",
|
|
|
|
|
data={"name": "Card A", "amount": "40.00"},
|
|
|
|
|
)
|
|
|
|
|
client.post("/debt-target", data={"debt_minimum_id": "1"})
|
|
|
|
|
del_resp = client.delete("/entries/1")
|
|
|
|
|
assert del_resp.status_code == 200
|
|
|
|
|
assert "No target selected" in del_resp.text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_reject_negative_amount(client):
|
|
|
|
|
response = client.post(
|
|
|
|
|
"/sections/other/entries", data={"name": "Bad", "amount": "-5"}
|
|
|
|
|
)
|
|
|
|
|
assert response.status_code == 400
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_reject_non_debt_minimum_target(client):
|
|
|
|
|
client.post(
|
|
|
|
|
"/sections/income/entries",
|
|
|
|
|
data={"name": "Paycheck", "amount": "10.00"},
|
|
|
|
|
)
|
|
|
|
|
response = client.post("/debt-target", data={"debt_minimum_id": "1"})
|
|
|
|
|
assert response.status_code == 400
|
2026-04-17 19:01:52 -06:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_entry_edit_returns_edit_form(client):
|
|
|
|
|
client.post(
|
|
|
|
|
"/sections/subscription/entries",
|
|
|
|
|
data={"name": "Twitch", "amount": "10.99"},
|
|
|
|
|
)
|
|
|
|
|
response = client.get("/entries/1/edit")
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert 'class="entry-row editing"' in response.text
|
|
|
|
|
assert 'name="name"' in response.text
|
|
|
|
|
assert 'name="amount"' in response.text
|
|
|
|
|
assert 'name="notes"' in response.text
|
|
|
|
|
assert 'value="Twitch"' in response.text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_entry_edit_missing_returns_404(client):
|
|
|
|
|
response = client.get("/entries/9999/edit")
|
|
|
|
|
assert response.status_code == 404
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_entry_edit_other_rows_stay_in_read_mode(client):
|
|
|
|
|
client.post("/sections/subscription/entries", data={"name": "Twitch", "amount": "10.99"})
|
|
|
|
|
client.post("/sections/subscription/entries", data={"name": "Netflix", "amount": "15.49"})
|
|
|
|
|
response = client.get("/entries/1/edit")
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert response.text.count('entry-row editing') == 1
|
|
|
|
|
assert response.text.count('entry-row reading') == 1
|
2026-04-17 19:05:09 -06:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_post_entry_updates_name_and_amount(client):
|
|
|
|
|
client.post(
|
|
|
|
|
"/sections/subscription/entries",
|
|
|
|
|
data={"name": "Twitch", "amount": "10.99"},
|
|
|
|
|
)
|
|
|
|
|
response = client.post(
|
|
|
|
|
"/entries/1",
|
|
|
|
|
data={"name": "Twitch Prime", "amount": "11.99", "notes": ""},
|
|
|
|
|
)
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert "Twitch Prime" in response.text
|
|
|
|
|
assert "$11.99" in response.text
|
|
|
|
|
# returns to read mode
|
|
|
|
|
assert 'class="entry-row reading"' in response.text
|
|
|
|
|
# OOB swaps for zero widget and group total
|
|
|
|
|
assert 'id="zero-widget"' in response.text
|
|
|
|
|
assert 'id="group-total-flexible"' in response.text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_post_entry_updates_notes_as_badge(client):
|
|
|
|
|
client.post(
|
|
|
|
|
"/sections/subscription/entries",
|
|
|
|
|
data={"name": "Spotify", "amount": "17.48"},
|
|
|
|
|
)
|
|
|
|
|
response = client.post(
|
|
|
|
|
"/entries/1",
|
|
|
|
|
data={"name": "Spotify", "amount": "17.48", "notes": "family plan"},
|
|
|
|
|
)
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert "note-badge" in response.text
|
|
|
|
|
assert "family plan" in response.text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_post_entry_debt_minimum_includes_target_oob(client):
|
|
|
|
|
client.post(
|
|
|
|
|
"/sections/debt_minimum/entries",
|
|
|
|
|
data={"name": "Card A", "amount": "50.00"},
|
|
|
|
|
)
|
|
|
|
|
response = client.post(
|
|
|
|
|
"/entries/1",
|
|
|
|
|
data={"name": "Card A", "amount": "60.00", "notes": ""},
|
|
|
|
|
)
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert 'id="section-debt_target"' in response.text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_post_entry_empty_name_returns_400(client):
|
|
|
|
|
client.post(
|
|
|
|
|
"/sections/subscription/entries",
|
|
|
|
|
data={"name": "Twitch", "amount": "10.99"},
|
|
|
|
|
)
|
|
|
|
|
response = client.post(
|
|
|
|
|
"/entries/1",
|
|
|
|
|
data={"name": " ", "amount": "11.99", "notes": ""},
|
|
|
|
|
)
|
|
|
|
|
assert response.status_code == 400
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_post_entry_negative_amount_returns_400(client):
|
|
|
|
|
client.post(
|
|
|
|
|
"/sections/subscription/entries",
|
|
|
|
|
data={"name": "Twitch", "amount": "10.99"},
|
|
|
|
|
)
|
|
|
|
|
response = client.post(
|
|
|
|
|
"/entries/1",
|
|
|
|
|
data={"name": "Twitch", "amount": "-1.00", "notes": ""},
|
|
|
|
|
)
|
|
|
|
|
assert response.status_code == 400
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_post_entry_non_numeric_amount_returns_400(client):
|
|
|
|
|
client.post(
|
|
|
|
|
"/sections/subscription/entries",
|
|
|
|
|
data={"name": "Twitch", "amount": "10.99"},
|
|
|
|
|
)
|
|
|
|
|
response = client.post(
|
|
|
|
|
"/entries/1",
|
|
|
|
|
data={"name": "Twitch", "amount": "eleven", "notes": ""},
|
|
|
|
|
)
|
|
|
|
|
assert response.status_code == 400
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_post_entry_missing_returns_404(client):
|
|
|
|
|
response = client.post(
|
|
|
|
|
"/entries/9999",
|
|
|
|
|
data={"name": "Whatever", "amount": "1.00", "notes": ""},
|
|
|
|
|
)
|
|
|
|
|
assert response.status_code == 404
|
2026-04-17 19:08:38 -06:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_section_returns_read_mode(client):
|
|
|
|
|
client.post(
|
|
|
|
|
"/sections/subscription/entries",
|
|
|
|
|
data={"name": "Twitch", "amount": "10.99"},
|
|
|
|
|
)
|
|
|
|
|
# enter edit mode first
|
|
|
|
|
edit = client.get("/entries/1/edit")
|
|
|
|
|
assert 'entry-row editing' in edit.text
|
|
|
|
|
# now "cancel" via GET /sections/{section}
|
|
|
|
|
response = client.get("/sections/subscription")
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert 'entry-row reading' in response.text
|
|
|
|
|
assert 'entry-row editing' not in response.text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_section_invalid_returns_422(client):
|
|
|
|
|
# FastAPI rejects an unknown Section enum value at routing
|
|
|
|
|
response = client.get("/sections/not_a_real_section")
|
|
|
|
|
assert response.status_code == 422
|