128 lines
6.6 KiB
Python
128 lines
6.6 KiB
Python
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"]
|