DFiP_Budget_planing/api/tests/integration/test_projects_api_smoke.py

429 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from datetime import datetime
import re
import uuid
import pytest
def test_projects_list_smoke(client, admin_tokens, auth_headers):
response = client.get("/api/v1/projects", headers=auth_headers(admin_tokens))
assert response.status_code == 200
payload = response.json()
assert "result" in payload
assert "count" in payload
assert isinstance(payload["result"], list)
def _create_project(client, admin_tokens, auth_headers, year=2026, branch_id=1) -> tuple[int, str]:
name = f"PT_{uuid.uuid4().hex[:10]}"
response = client.post(
"/api/v1/projects",
json={"name": name, "year": year, "branch_id": branch_id},
headers=auth_headers(admin_tokens),
)
assert response.status_code == 200
payload = response.json()
return payload["project_id"], name
def _add_line(client, admin_tokens, auth_headers, project_id: int, year: int = 2026) -> int:
for expense_item_id in range(1, 80):
response = client.post(
f"/api/v1/projects/{project_id}/report/{year}/LIMIT/line",
json={"expense_item_id": expense_item_id},
headers=auth_headers(admin_tokens),
)
if response.status_code != 200:
continue
rows = response.json()
input_row = next(
(
row
for row in rows
if row.get("row_type") == "INPUT" and row.get("data", {}).get("line_id")
),
None,
)
if input_row:
return int(input_row["data"]["line_id"])
pytest.skip("Не удалось подобрать expense_item_id для add_form3_line в текущей БД")
def test_create_project_smoke(client, admin_tokens, auth_headers):
name = f"PT_{uuid.uuid4().hex[:10]}"
response = client.post(
"/api/v1/projects",
json={"name": name, "year": None, "branch_id": 1},
headers=auth_headers(admin_tokens),
)
assert response.status_code == 200
payload = response.json()
assert "project_id" in payload
assert (project_id := payload["project_id"]) is not None
response = client.get(
f"/api/v1/projects/{project_id}",
headers=auth_headers(admin_tokens),
)
assert response.status_code == 200
payload = response.json()
assert "result" in payload
assert "id" in payload["result"]
assert payload["result"]["id"] == project_id
assert "years" in payload["result"]
assert isinstance(payload["result"]["years"], list)
assert len(payload["result"]["years"]) == 1
def test_projects_write_smoke(client, admin_tokens, auth_headers):
project_id, name = _create_project(client, admin_tokens, auth_headers)
upd_project_response = client.patch(
f"/api/v1/project/{project_id}",
json={"name": f"{name}_U"},
headers=auth_headers(admin_tokens),
)
assert upd_project_response.status_code == 200
line_id = _add_line(client, admin_tokens, auth_headers, project_id)
upd_cell_response = client.patch(
f"/api/v1/projects/{project_id}/report/2026/LIMIT/cell",
json={"line_id": line_id, "column": "q1.m1", "value": 100},
headers=auth_headers(admin_tokens),
)
assert upd_cell_response.status_code == 200
assert isinstance(upd_cell_response.json(), list)
upd_cells_response = client.patch(
f"/api/v1/projects/{project_id}/report/2026/LIMIT/cells",
json={
"changes": [
{"line_id": line_id, "column": "q1.m2", "value": 200},
{"line_id": line_id, "column": "q1.m3", "value": 300},
]
},
headers=auth_headers(admin_tokens),
)
assert upd_cells_response.status_code == 200
assert isinstance(upd_cells_response.json(), list)
del_line_response = client.delete(
f"/api/v1/projects/{project_id}/report/2026/LIMIT/line/{line_id}",
headers=auth_headers(admin_tokens),
)
assert del_line_response.status_code == 200
def test_projects_write_invalid_report_type_error_shape(client, admin_tokens, auth_headers):
project_id, _ = _create_project(client, admin_tokens, auth_headers)
line_id = _add_line(client, admin_tokens, auth_headers, project_id)
response = client.patch(
f"/api/v1/projects/{project_id}/report/2026/BAD_TYPE/cell",
json={"line_id": line_id, "column": "q1.m1", "value": 100},
headers=auth_headers(admin_tokens),
)
assert response.status_code == 400
payload = response.json()
assert isinstance(payload, dict)
assert "message" in payload
assert isinstance(payload["message"], str)
def test_projects_write_report_not_found_error_shape(client, admin_tokens, auth_headers):
project_id, _ = _create_project(client, admin_tokens, auth_headers)
line_id = _add_line(client, admin_tokens, auth_headers, project_id)
response = client.patch(
f"/api/v1/projects/{project_id}/report/2099/LIMIT/cell",
json={"line_id": line_id, "column": "q1.m1", "value": 100},
headers=auth_headers(admin_tokens),
)
assert response.status_code == 400
payload = response.json()
assert isinstance(payload, dict)
assert payload.get("message") == "Отчёт не найден"
def test_delete_project_smoke(client, admin_tokens, auth_headers):
project_id, _ = _create_project(client, admin_tokens, auth_headers, year=datetime.now().year)
response = client.delete(
f"/api/v1/projects/{project_id}",
headers=auth_headers(admin_tokens),
)
assert response.status_code == 200
payload = response.json()
assert payload["success"] is True
assert payload["message"] == "Проект удалён"
def test_delete_project_verify_gone(client, admin_tokens, auth_headers):
project_id, _ = _create_project(client, admin_tokens, auth_headers, year=datetime.now().year)
del_response = client.delete(
f"/api/v1/projects/{project_id}",
headers=auth_headers(admin_tokens),
)
assert del_response.status_code == 200
get_response = client.get(
f"/api/v1/projects/{project_id}",
headers=auth_headers(admin_tokens),
)
assert get_response.status_code == 404
def test_delete_project_not_found(client, admin_tokens, auth_headers):
response = client.delete(
"/api/v1/projects/99999",
headers=auth_headers(admin_tokens),
)
assert response.status_code == 400
payload = response.json()
assert isinstance(payload, dict)
assert "message" in payload
def test_delete_project_unauthorized(client):
response = client.delete("/api/v1/projects/1")
assert response.status_code == 403
#ToDo: вернуть, когда пофиксим смещение фаз в зависимости от года
# def test_delete_project_with_lines_blocked(client, admin_tokens, auth_headers):
# """Проект со строками отчёта нельзя удалить: can_delete_project → false → RAISE EXCEPTION."""
# year = datetime.now().year - 1
# project_id, _ = _create_project(client, admin_tokens, auth_headers, year=year)
# _add_line(client, admin_tokens, auth_headers, project_id, year=year)
# response = client.delete(
# f"/api/v1/projects/{project_id}",
# headers=auth_headers(admin_tokens),
# )
# assert response.status_code == 400
# payload = response.json()
# assert isinstance(payload, dict)
# assert "code" in payload
# assert "message" in payload
# assert "невозможно удалить" in payload["message"]
def test_delete_project_executor_from_other_org(client, admin_tokens, auth_headers, isp_tokens):
"""ISP1 (привязан к РФ id=2) не может удалить проект на ССП id=1 — ACL."""
project_id, _ = _create_project(client, admin_tokens, auth_headers, branch_id=1, year=datetime.now().year)
response = client.delete(
f"/api/v1/projects/{project_id}",
headers=auth_headers(isp_tokens),
)
assert response.status_code == 400
payload = response.json()
assert "message" in payload
assert payload["message"] == "Проект не найден"
#ToDo: вернуть, когда пофиксим смещение фаз в зависимости от года
# def test_delete_project_executor_from_same_org(client, isp_tokens, auth_headers):
# """ISP1 может удалить проект на своём РФ (id=2), если нет строк отчёта."""
# project_id = _create_project(client, isp_tokens, auth_headers, branch_id=2, year=datetime.now().year)
# response = client.delete(
# f"/api/v1/projects/{project_id}",
# headers=auth_headers(isp_tokens),
# )
# assert response.status_code == 200
# payload = response.json()
# assert payload["success"] is True
# assert payload["message"] == "Проект удалён"
def test_delete_seed_project_smoke(client, admin_tokens, auth_headers):
"""Проект из фикстуры (id=2) удаляется: у него нет строк отчёта, can_delete_project → true."""
response = client.delete(
"/api/v1/projects/2",
headers=auth_headers(admin_tokens),
)
assert response.status_code == 200
payload = response.json()
assert payload["success"] is True
assert payload["message"] == "Проект удалён"
def _get_project_with_reports(client, auth_headers, admin_tokens, project_id):
response = client.get(
"/api/v1/projects/with-reports?limit=1000",
headers=auth_headers(admin_tokens),
)
assert response.status_code == 200
return next((p for p in response.json()["result"] if p["id"] == project_id), None)
def test_upd_project_smoke(client, admin_tokens, auth_headers):
project_id, name = _create_project(client, admin_tokens, auth_headers)
before = _get_project_with_reports(client, auth_headers, admin_tokens, project_id)
assert before is not None
assert before["technical_number"]
technical_number_before = before["technical_number"]
response = client.patch(
f"/api/v1/project/{project_id}",
json={
"name": f"{name}_U",
"status": "agreed",
"technical_number": "9999_999999",
"krf_decision_date": "2026-06-11",
"fk_decision_date": "2026-06-18",
"board_decision_date": "2026-07-02",
"open_relocate_close_date": "2026-07-15",
"funding_by_ko_decision": "prrs_budget",
"placement_type": "Собственность",
},
headers=auth_headers(admin_tokens),
)
assert response.status_code == 200
project = _get_project_with_reports(client, auth_headers, admin_tokens, project_id)
assert project is not None
assert project["name"] == f"{name}_U"
assert project["status"] == "agreed"
# technical_number генерируется автоматически и не редактируется
assert project["technical_number"] == technical_number_before
assert re.fullmatch(r"\d{4}_\d{6}", project["technical_number"])
assert project["krf_decision_date"] == "2026-06-11"
assert project["fk_decision_date"] == "2026-06-18"
assert project["board_decision_date"] == "2026-07-02"
assert project["open_relocate_close_date"] == "2026-07-15"
assert project["funding_by_ko_decision"] == "prrs_budget"
# placement_type сконвертирован в БД-код и обратно в строковое представление
assert project["placement_type"] == "Собственность"
def test_create_project_auto_generates_technical_number(client, admin_tokens, auth_headers):
project_id, _ = _create_project(client, admin_tokens, auth_headers)
project = _get_project_with_reports(client, auth_headers, admin_tokens, project_id)
assert project is not None
assert project["technical_number"]
assert re.fullmatch(r"\d{4}_\d{6}", project["technical_number"])
def test_upd_smeta_smoke(client, admin_tokens, auth_headers):
project_id, _ = _create_project(client, admin_tokens, auth_headers, year=2026)
response = client.patch(
f"/api/v1/projects/{project_id}/smeta/2026",
json={
"is_in_plan": True,
"is_in_plan_q2": True,
"is_in_plan_q3": False,
"is_in_plan_q4": False,
"development_block": "out_of_base_plan",
"reserve_to_prrs_ahr": 1500.5,
"reserve_to_prrs_kv": 2300,
},
headers=auth_headers(admin_tokens),
)
assert response.status_code == 200
payload = response.json()
assert payload["is_in_plan"] is True
assert payload["is_in_plan_q2"] is True
assert payload["is_in_plan_q3"] is False
assert payload["is_in_plan_q4"] is False
assert payload["development_block"] == "out_of_base_plan"
assert payload["reserve_to_prrs_ahr"] == 1500.5
assert payload["reserve_to_prrs_kv"] == 2300
project = _get_project_with_reports(client, auth_headers, admin_tokens, project_id)
assert project is not None
sub_row = next((sr for sr in project["sub_rows"] if sr["year"] == 2026), None)
assert sub_row is not None
assert sub_row["is_in_plan"] is True
assert sub_row["development_block"] == "out_of_base_plan"
def test_upd_smeta_not_found(client, admin_tokens, auth_headers):
project_id, _ = _create_project(client, admin_tokens, auth_headers, year=2026)
response = client.patch(
f"/api/v1/projects/{project_id}/smeta/2099",
json={"is_in_plan": True},
headers=auth_headers(admin_tokens),
)
assert response.status_code == 400
payload = response.json()
assert payload.get("message") == "Смета не найдена"
def test_add_project_year_smoke(client, admin_tokens, auth_headers):
project_id, _ = _create_project(client, admin_tokens, auth_headers, year=2026)
response = client.post(
f"/api/v1/projects/{project_id}/year",
json={"year": 2027},
headers=auth_headers(admin_tokens),
)
assert response.status_code == 200
payload = response.json()
assert payload["year"] == 2027
assert payload["project"] == "Смета_2027"
assert payload["id"] is not None
project = _get_project_with_reports(client, auth_headers, admin_tokens, project_id)
assert project is not None
sub_years = {sr["year"] for sr in project["sub_rows"]}
assert 2026 in sub_years
assert 2027 in sub_years
def test_add_project_year_duplicate(client, admin_tokens, auth_headers):
project_id, _ = _create_project(client, admin_tokens, auth_headers, year=2026)
response = client.post(
f"/api/v1/projects/{project_id}/year",
json={"year": 2026},
headers=auth_headers(admin_tokens),
)
assert response.status_code == 400
payload = response.json()
assert payload.get("message") == "Год 2026 у проекта уже заведён"
def test_add_project_year_project_not_found(client, admin_tokens, auth_headers):
response = client.post(
"/api/v1/projects/99999/year",
json={"year": 2027},
headers=auth_headers(admin_tokens),
)
assert response.status_code == 400
payload = response.json()
assert payload.get("message") == "Проект не найден"
def test_add_project_year_by_executor(client, admin_tokens, isp_tokens, auth_headers):
project_id, _ = _create_project(client, admin_tokens, auth_headers, branch_id=2, year=2026)
response = client.post(
f"/api/v1/projects/{project_id}/year",
json={"year": 2027},
headers=auth_headers(isp_tokens),
)
assert response.status_code == 200
payload = response.json()
assert payload["year"] == 2027
def test_add_project_year_by_executor_from_other_org(client, admin_tokens, isp_tokens, auth_headers):
"""Исполнитель, привязанный к РФ id=2, не может добавить год проекту на ССП id=1 — ACL."""
project_id, _ = _create_project(client, admin_tokens, auth_headers, branch_id=1, year=2026)
response = client.post(
f"/api/v1/projects/{project_id}/year",
json={"year": 2027},
headers=auth_headers(isp_tokens),
)
assert response.status_code == 400
payload = response.json()
assert payload.get("message") == "Проект не найден"