from io import BytesIO import openpyxl import pytest # Category totals from the "Capex e Custos 2027 v4" PDF. Lines are rounded to cents before # summing, so a category can differ from the PDF by a cent or two. PDF_TOTALS = { "Contract Services": (600758.54, 615940.02), "Cost of Cell Phones": (96532.00, 111011.80), "Cost of Internet Services": (74160.00, 85284.00), "Cost of Local Calls": (5575.00, 6269.13), "Cost of Long Distance Calls": (11415.00, 12488.47), "Other Cost of Services": (176847.00, 185226.36), "Hardware": (37636.00, 83361.00), "Property Operation and Maintenance": (31283.00, 55015.83), "Other IT Equipment": (72566.00, 70747.24), "Travel-Other": (14400.00, 16880.00), } def test_requires_login(): from fastapi.testclient import TestClient from app.main import app assert TestClient(app).get("/api/years").status_code == 401 def test_writes_need_header(client): r = client.post("/api/years", json={"year": 2030}, headers={"X-Requested-With": ""}) assert r.status_code == 403 def test_import_matches_pdf(client, imported): y = client.get(f"/api/years/{imported}").json() assert y["year"] == 2027 and y["default_rate"] == pytest.approx(0.055) got = {c["category"]["name_en"]: c["totals"] for c in y["categories"] if c["lines"]} for name, (prior, adjusted) in PDF_TOTALS.items(): assert got[name]["prior"] == pytest.approx(prior, abs=0.005) assert got[name]["adjusted"] == pytest.approx(adjusted, abs=0.02) assert y["costs"]["prior"] == pytest.approx(1121172.54, abs=0.005) assert y["costs"]["adjusted"] == pytest.approx(1242223.85, abs=0.05) assert sum(len(c["lines"]) for c in y["categories"]) == 54 hw = next(c for c in y["categories"] if c["category"]["name_en"] == "Hardware") oriente = next(l for l in hw["lines"] if "ORIENTE" in l["name"]) assert oriente["adj_kind"] == "factor" and oriente["adj_value"] == pytest.approx(3.055) assert "Hardware price increase" in oriente["adj_reason"] protel = next(l for c in y["categories"] for l in c["lines"] if l["name"] == "Protel") assert protel["prior_expr"] == "5512*12" def test_line_crud_and_month_spread(client, imported): cats = client.get("/api/categories").json() training = next(c for c in cats if c["name_en"] == "Training") r = client.post(f"/api/years/{imported}/lines", json={ "category_id": training["id"], "name": "Microsoft certification", "prior_expr": "1000", "base_expr": "=4*300", "adj_kind": "percent", "adj_value": 10}) assert r.status_code == 200, r.text line_id = r.json()["id"] y = client.get(f"/api/years/{imported}").json() line = next(l for c in y["categories"] for l in c["lines"] if l["id"] == line_id) assert line["base"] == 1200 and line["adjusted"] == 1320 and sum(line["months"]) == pytest.approx(1320) assert client.put(f"/api/lines/{line_id}", json={"base_expr": "abc"}).status_code == 400 r = client.put(f"/api/lines/{line_id}", json={"monthly_kind": "manual"}) assert r.status_code == 200 y = client.get(f"/api/years/{imported}").json() line = next(l for c in y["categories"] for l in c["lines"] if l["id"] == line_id) assert line["monthly_values"][0] == 110 and line["adjusted"] == pytest.approx(1320) assert client.delete(f"/api/lines/{line_id}").status_code == 200 audit = client.get(f"/api/years/{imported}/audit").json() assert [a["action"] for a in audit[:3]] == ["delete", "update", "create"] def test_status_workflow_and_roles(client, imported): it = client.get("/api/departments").json()[0]["id"] r = client.post("/api/users", json={"email": "ed@test.local", "name": "Ed", "role": "editor", "password": "editor-pass-1", "department_id": it}) assert r.status_code == 200 from fastapi.testclient import TestClient from app.main import app ed = TestClient(app) ed.headers.update({"X-Requested-With": "budget-app"}) assert ed.post("/api/auth/login", json={"email": "ed@test.local", "password": "editor-pass-1"}).status_code == 200 assert ed.get("/api/users").status_code == 403 assert ed.post(f"/api/years/{imported}/status", json={"action": "submit"}).status_code == 200 assert ed.post(f"/api/years/{imported}/status", json={"action": "approve"}).status_code == 403 line = client.get(f"/api/years/{imported}").json()["categories"][1]["lines"][0] assert ed.put(f"/api/lines/{line['id']}", json={"name": "x"}).status_code == 409 assert client.post(f"/api/years/{imported}/status", json={"action": "approve"}).json()["status"] == "approved" assert client.post(f"/api/years/{imported}/status", json={"action": "reopen"}).status_code == 400 # reason needed assert client.post(f"/api/years/{imported}/status", json={"action": "reopen", "comment": "new contract"}).json()["status"] == "draft" def test_rollover(client, imported): r = client.post("/api/years", json={"year": 2028, "copy_from_id": imported}) assert r.status_code == 200 y = client.get(f"/api/years/{r.json()['id']}").json() assert y["costs"]["prior"] == pytest.approx(1242223.85, abs=0.05) @pytest.mark.parametrize("kind", ["cost-report", "budget"]) @pytest.mark.parametrize("lang", ["pt", "en"]) def test_reports(client, imported, kind, lang): html = client.get(f"/api/years/{imported}/reports/{kind}.html?lang={lang}") assert html.status_code == 200 assert ("= 2 pdf = client.get(f"/api/years/{imported}/reports/{kind}.pdf?lang={lang}") assert pdf.status_code == 200 and pdf.content[:4] == b"%PDF"