Flatten budget-app contents into repo root
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user