Flatten budget-app contents into repo root

This commit is contained in:
jpmvaz
2026-10-29 20:41:01 +00:00
parent c9fd26f1c7
commit 93f2696764
95 changed files with 184 additions and 187 deletions
+127
View File
@@ -0,0 +1,127 @@
from fastapi.testclient import TestClient
from app.main import app
from tests.test_mail import fake_mail # noqa: F401 (fixture)
H = {"X-Requested-With": "budget-app"}
def user(client, email, role, dept=1):
client.post("/api/users", json={"email": email, "name": email.split("@")[0], "role": role,
"password": "password-123", "department_id": dept})
c = TestClient(app)
c.headers.update(H)
assert c.post("/api/auth/login", json={"email": email, "password": "password-123"}).status_code == 200
return c
def approve(client, year_id):
assert client.post(f"/api/years/{year_id}/status", json={"action": "submit"}).status_code == 200
while client.get(f"/api/years/{year_id}").json()["status"] == "in_review":
assert client.post(f"/api/years/{year_id}/status", json={"action": "approve"}).status_code == 200
def test_reopen_request_needs_reason_and_approver(client, imported):
viewer = user(client, "view@t.local", "viewer", dept=None)
approver = user(client, "ap@t.local", "approver")
approve(client, imported)
y = viewer.get(f"/api/years/{imported}").json()
assert y["actions"]["request_reopen"] and not y["actions"]["reopen"]
assert viewer.post(f"/api/years/{imported}/reopen-requests", json={"reason": ""}).status_code == 400
r = viewer.post(f"/api/years/{imported}/reopen-requests", json={"reason": "Contrato novo de telecomunicações"})
assert r.status_code == 200 and r.json()["reopened"] is False
assert viewer.post(f"/api/years/{imported}/reopen-requests", json={"reason": "again please"}).status_code == 409
y = approver.get(f"/api/years/{imported}").json()
assert y["status"] == "approved" and y["reopen_request"]["requested_by"] == "view"
assert y["actions"]["decide_reopen"] and not viewer.get(f"/api/years/{imported}").json()["actions"]["decide_reopen"]
rid = y["reopen_request"]["id"]
assert viewer.post(f"/api/reopen-requests/{rid}/decide", json={"accept": True}).status_code == 403
assert approver.post(f"/api/reopen-requests/{rid}/decide", json={"accept": False}).status_code == 400 # say why
d = approver.post(f"/api/reopen-requests/{rid}/decide", json={"accept": False, "note": "Já aprovado pela direção"})
assert d.json()["request"]["status"] == "declined" and d.json()["year"]["status"] == "approved"
# a second request, accepted this time
rid = viewer.post(f"/api/years/{imported}/reopen-requests", json={"reason": "Falta a linha de licenças"}).json()["request"]["id"]
d = approver.post(f"/api/reopen-requests/{rid}/decide", json={"accept": True}).json()
assert d["request"]["status"] == "accepted" and d["year"]["status"] == "draft"
hist = client.get(f"/api/years/{imported}").json()["reopen_requests"]
assert [h["status"] for h in hist] == ["accepted", "declined"]
acts = [r["action"] for r in client.get("/api/activity?entity=year").json()["rows"]]
assert {"reopen_requested", "reopen_declined", "reopen"} <= set(acts)
def test_approver_reopens_directly_with_reason(client, imported):
approver = user(client, "ap@t.local", "approver")
approve(client, imported)
r = approver.post(f"/api/years/{imported}/reopen-requests", json={"reason": "Correção de valores"}).json()
assert r["reopened"] is True and r["year"]["status"] == "draft"
def test_download_and_reupload_archives_old_version(client, imported):
# downloads keep the name of the file the budget was loaded from
assert 'filename="b.xlsx"' in client.get(f"/api/years/{imported}/workbook.xlsx").headers["content-disposition"]
fresh = client.post("/api/years", json={"year": 2031}).json()["id"]
assert 'filename="Orcamento_IT_2031.xlsx"' in client.get(f"/api/years/{fresh}/workbook.xlsx?lang=pt").headers["content-disposition"]
client.delete(f"/api/years/{fresh}")
r = client.get(f"/api/years/{imported}/workbook.xlsx?lang=pt")
files = {"file": ("Orcamento_IT_2027.xlsx", r.content)}
check = client.post("/api/years/import/check", files=files, data={"department_id": "1"}).json()
assert check["year"] == 2027 and check["existing"]["id"] == imported and check["existing"]["can_replace"]
assert check["existing"]["archived_title"].startswith("Budget 2027 (")
# without confirming, nothing is replaced
assert client.post("/api/years/import", files=files, data={"department_id": "1"}).status_code == 409
res = client.post("/api/years/import", files=files, data={"department_id": "1", "replace": "true"}).json()
new_id = res["year"]["id"]
assert new_id != imported and res["year"]["title"] == "Budget 2027" and res["year"]["source_file"] == "Orcamento_IT_2027.xlsx"
old = client.get(f"/api/years/{imported}").json()
assert old["archived"] and old["title"].startswith("Budget 2027 (") and old["title"] != "Budget 2027"
assert not any(v for k, v in old["actions"].items() if k != "delete")
line = old["categories"][0]["lines"][0]
assert client.put(f"/api/lines/{line['id']}", json={"name": "x"}).status_code in (403, 409)
assert client.post(f"/api/years/{imported}/status", json={"action": "submit"}).status_code in (403, 409)
assert [y["id"] for y in client.get("/api/years").json()] == [new_id]
assert [y["id"] for y in client.get("/api/years?archived=true").json()] == [imported]
assert client.get(f"/api/years/{imported}/reports/budget.pdf").status_code == 200 # still viewable
assert abs(old["costs"]["adjusted"] - client.get(f"/api/years/{new_id}").json()["costs"]["adjusted"]) < 0.01
# the same file again: the downloaded name is kept, and the previous upload gets the date in its file name
again = client.get(f"/api/years/{new_id}/workbook.xlsx").content
check = client.post("/api/years/import/check", files={"file": ("Orcamento_IT_2027.xlsx", again)},
data={"department_id": "1"}).json()
assert check["existing"]["same_file"] and check["existing"]["archived_file"].startswith("Orcamento_IT_2027_")
client.post("/api/years/import", files={"file": ("Orcamento_IT_2027.xlsx", again)},
data={"department_id": "1", "replace": "true"})
names = [y["source_file"] for y in client.get("/api/years?archived=true").json()]
assert any(n.startswith("Orcamento_IT_2027_2") and n.endswith(".xlsx") for n in names)
def test_approved_budget_cannot_be_replaced(client, imported):
data = client.get(f"/api/years/{imported}/workbook.xlsx").content
approve(client, imported)
check = client.post("/api/years/import/check", files={"file": ("b.xlsx", data)}, data={"department_id": "1"}).json()
assert check["existing"]["can_replace"] is False
r = client.post("/api/years/import", files={"file": ("b.xlsx", data)}, data={"department_id": "1", "replace": "true"})
assert r.status_code == 409 and r.json()["detail"] == "year_not_draft"
def test_approver_reopening_answers_pending_request(client, imported):
viewer = user(client, "view@t.local", "viewer", dept=None)
approve(client, imported)
viewer.post(f"/api/years/{imported}/reopen-requests", json={"reason": "Preciso de mudar a linha X"})
r = client.post(f"/api/years/{imported}/reopen-requests", json={"reason": "Concordo, reabrir"}).json()
assert r["reopened"] and r["request"]["status"] == "accepted" and r["request"]["requested_by"] == "view"
assert len(client.get(f"/api/years/{imported}").json()["reopen_requests"]) == 1
def test_reopen_request_emails_the_approvers(client, imported, fake_mail):
from tests.test_mail import FakeSMTP, configure
configure(client)
user(client, "ap@t.local", "approver")
viewer = user(client, "view@t.local", "viewer", dept=None)
approve(client, imported)
viewer.post(f"/api/years/{imported}/reopen-requests", json={"reason": "Licenças em falta"})
_, rcpts, msg = FakeSMTP.sent[-1]
assert set(rcpts) == {"admin@test.local", "ap@t.local"} and "Licenças em falta" in msg.get_content()
rid = client.get(f"/api/years/{imported}").json()["reopen_request"]["id"]
client.post(f"/api/reopen-requests/{rid}/decide", json={"accept": False, "note": "Fica para o próximo ano"})
_, rcpts, msg = FakeSMTP.sent[-1]
assert rcpts == ["view@t.local"] and "Fica para o próximo ano" in msg.get_content()