Files
Budget_Tool_Martinhal/backend/tests/test_departments.py
T

128 lines
6.6 KiB
Python
Raw Normal View History

2026-10-29 20:40:02 +00:00
from io import BytesIO
import openpyxl
import pytest
from fastapi.testclient import TestClient
from app.main import app
H = {"X-Requested-With": "budget-app"}
def login(email, password):
c = TestClient(app)
c.headers.update(H)
assert c.post("/api/auth/login", json={"email": email, "password": password}).status_code == 200
return c
@pytest.fixture()
def org(client, imported):
"""IT (with the imported 2027 budget) and F&B, one editor and one approver in each."""
it = client.get("/api/departments").json()[0]
fb = client.post("/api/departments", json={"name": "F&B", "categories_from": "defaults"}).json()
users = {}
for key, role, dept in (("it_ed", "editor", it), ("it_ap", "approver", it),
("fb_ed", "editor", fb), ("fb_ap", "approver", fb)):
r = client.post("/api/users", json={"email": f"{key}@t.local", "name": key, "role": role,
"password": "password-123", "department_id": dept["id"]})
assert r.status_code == 200, r.text
users[key] = login(f"{key}@t.local", "password-123")
return {"it": it, "fb": fb, "year": imported, **users}
def test_categories_are_per_department(client, org):
it_cats = client.get(f"/api/categories?department_id={org['it']['id']}").json()
fb_cats = client.get(f"/api/categories?department_id={org['fb']['id']}").json()
assert len(it_cats) == len(fb_cats) == 50
assert not {c["id"] for c in it_cats} & {c["id"] for c in fb_cats}
# a line in an IT budget cannot use an F&B category
r = client.post(f"/api/years/{org['year']}/lines", json={"category_id": fb_cats[0]["id"], "name": "x"})
assert r.status_code == 400 and r.json()["detail"] == "invalid_category"
# the same year can exist once per department
r = org["fb_ed"].post("/api/years", json={"year": 2027})
assert r.status_code == 200 and r.json()["department"] == "F&B"
def test_other_departments_can_only_view(org):
fb = org["fb_ed"]
y = fb.get(f"/api/years/{org['year']}").json()
assert y["department"] == "IT" and y["actions"]["edit"] is False
line = y["categories"][1]["lines"][0]
assert fb.put(f"/api/lines/{line['id']}", json={"name": "hack"}).status_code == 403
assert fb.post(f"/api/years/{org['year']}/status", json={"action": "submit"}).status_code == 403
assert fb.get(f"/api/years/{org['year']}/reports/cost-report.xlsx").status_code == 200
assert org["it_ed"].put(f"/api/lines/{line['id']}", json={"remarks": "ok"}).status_code == 200
def test_custom_workflow(client, org):
it = org["it"]["id"]
users = {u["email"]: u["id"] for u in client.get("/api/users").json()}
r = client.put(f"/api/departments/{it}/workflow", json={"steps": [
{"name": "Department head", "approver_mode": "role"},
{"name": "Finance", "approver_mode": "users", "approver_ids": [users["fb_ap@t.local"]]},
]})
assert r.status_code == 200 and [s["name"] for s in r.json()["steps"]] == ["Department head", "Finance"]
y = org["year"]
assert org["it_ed"].post(f"/api/years/{y}/status", json={"action": "submit"}).json()["current_step"] == 0
# step 1: only IT approvers; the F&B approver is not on this step
assert org["fb_ap"].post(f"/api/years/{y}/status", json={"action": "approve"}).status_code == 403
assert org["it_ed"].post(f"/api/years/{y}/status", json={"action": "approve"}).status_code == 403
r = org["it_ap"].post(f"/api/years/{y}/status", json={"action": "approve"})
assert r.json()["status"] == "in_review" and r.json()["current_step"] == 1
# step 2: only the named user
assert org["it_ap"].post(f"/api/years/{y}/status", json={"action": "approve"}).status_code == 403
r = org["fb_ap"].post(f"/api/years/{y}/status", json={"action": "approve"})
assert r.json()["status"] == "approved"
# the workflow cannot be changed while a budget is in review
org["it_ap"].post(f"/api/years/{y}/status", json={"action": "reopen", "comment": "fix a line"})
org["it_ed"].post(f"/api/years/{y}/status", json={"action": "submit"})
assert client.put(f"/api/departments/{it}/workflow", json={"steps": []}).status_code == 409
def test_template_roundtrip(client, org):
fb = org["fb"]["id"]
r = org["fb_ed"].get(f"/api/departments/{fb}/template.xlsx?lang=pt&year=2028&rate=3")
assert r.status_code == 200
wb = openpyxl.load_workbook(BytesIO(r.content))
assert wb.sheetnames == ["Instruções", "Linhas", "Listas"]
ws = wb["Linhas"]
rows = [
["Formação", "Curso de barista", "", "Chiado", 1000, "=4*300", "Percentagem", 10, "", "nota"],
["Vinhos", "Carta de vinhos", "", "", 5000, 5000, "Fator", 1.2, "Preços", ""],
["Receitas", "Eventos", "", "Grupo", 0, None, None, None, "", ""] + [100] * 12,
["Nova categoria", "Algo novo", "", "Lua", 10, 10, "", None, "", ""],
]
for i, row in enumerate(rows):
for j, v in enumerate(row):
ws.cell(9 + i, j + 1, v)
buf = BytesIO()
wb.save(buf)
r = org["fb_ed"].post("/api/years/import", files={"file": ("t.xlsx", buf.getvalue())},
data={"department_id": str(fb)})
assert r.status_code == 200, r.text
res = r.json()
assert res["lines"] == 4 and res["categories_created"] == ["Nova categoria"]
assert any("Lua" in w for w in res["warnings"])
y = client.get(f"/api/years/{res['year']['id']}").json()
assert y["year"] == 2028 and y["default_rate"] == pytest.approx(0.03) and y["department"] == "F&B"
lines = {l["name"]: l for c in y["categories"] for l in c["lines"]}
assert lines["Curso de barista"]["adjusted"] == 1320
assert lines["Carta de vinhos"]["adjusted"] == 6000
assert lines["Eventos"]["adjusted"] == 1200 and lines["Eventos"]["monthly_kind"] == "manual"
assert lines["Algo novo"]["adjusted"] == pytest.approx(10.3)
# the IT editor cannot import into F&B
r = org["it_ed"].post("/api/years/import", files={"file": ("t.xlsx", buf.getvalue())},
data={"department_id": str(fb), "replace": "true"})
assert r.status_code == 403
def test_prefilled_template_reimports_same_totals(client, org):
it = org["it"]["id"]
r = client.get(f"/api/departments/{it}/template.xlsx?lang=en&from_year_id={org['year']}&year=2029&rate=5.5")
res = client.post("/api/years/import", files={"file": ("t.xlsx", r.content)}, data={"department_id": str(it)}).json()
assert res["lines"] == 54
a = client.get(f"/api/years/{org['year']}").json()["costs"]
b = client.get(f"/api/years/{res['year']['id']}").json()["costs"]
assert a["adjusted"] == pytest.approx(b["adjusted"], abs=0.01) and a["prior"] == b["prior"]