Удаление строки в формах 1/2/4 не проверяло владельца: любой исполнитель мог удалить чужую строку мгновенно, без подтверждения. - v3.del_budget_line (миграция 0050): не-admin может удалить только budget_line, где created_by совпадает с current_user_id() (проверка и для обычных листов, и для AHR_RENT/UTILITY/SECURITY — через created_by связанной budget_line). Нарушение -> role_not_allowed: (403), по существующей конвенции PG_403_ERRORS. - RealtimeTable: кнопка «Удалить строку» теперь открывает ConfirmDeleteRowModal, реальное удаление — только после подтверждения. - Интеграционные тесты: новый исполнитель isp2 (fixture.sql) в том же org_unit, что и budget_form id=1 — проверяют 403 при чужой строке, 200 при своей и при удалении администратором. Issue: #249 (AURORA-1936, эпик AURORA-1876, Высокий приоритет) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
94 lines
2.6 KiB
Python
94 lines
2.6 KiB
Python
import asyncio
|
|
from contextlib import contextmanager
|
|
import os
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
|
|
from src.db.base import SessionLocal
|
|
from src.db.session import get_db
|
|
from src.main import app
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
def client():
|
|
db_ref = SessionLocal()
|
|
loop_ref = None
|
|
|
|
async def _get_db():
|
|
nonlocal loop_ref
|
|
loop_ref = asyncio.get_running_loop()
|
|
try:
|
|
yield db_ref
|
|
await db_ref.flush()
|
|
except:
|
|
await db_ref.rollback()
|
|
raise
|
|
|
|
app.dependency_overrides[get_db] = _get_db
|
|
with TestClient(app) as test_client:
|
|
try:
|
|
yield test_client
|
|
finally:
|
|
if db_ref is not None and loop_ref:
|
|
async def cleanup():
|
|
await db_ref.rollback()
|
|
await db_ref.close()
|
|
|
|
future = asyncio.run_coroutine_threadsafe(cleanup(), loop_ref)
|
|
future.result()
|
|
app.dependency_overrides.clear()
|
|
|
|
|
|
@pytest.fixture
|
|
def admin_password() -> str:
|
|
# Пароль админа берём из окружения, чтобы не хардкодить локальные отличия.
|
|
return os.getenv("OPENBAO__TEST_ADMIN_PASSWORD", "admin123")
|
|
|
|
|
|
@pytest.fixture
|
|
def isp_password() -> str:
|
|
# Пароль админа берём из окружения, чтобы не хардкодить локальные отличия.
|
|
return os.getenv("OPENBAO__TEST_EXECUTOR_PASSWORD", "admin123")
|
|
|
|
|
|
@pytest.fixture
|
|
def admin_tokens(client, admin_password: str):
|
|
response = client.post(
|
|
"/api/v1/auth/login",
|
|
json={"username": "admin", "password": admin_password},
|
|
)
|
|
assert response.status_code == 200
|
|
return response.json()
|
|
|
|
|
|
@pytest.fixture
|
|
def auth_headers():
|
|
def _build(tokens: dict) -> dict:
|
|
return {"Authorization": f"Bearer {tokens['access_token']}"}
|
|
|
|
return _build
|
|
|
|
@pytest.fixture
|
|
def isp_tokens(client, isp_password: str):
|
|
response = client.post(
|
|
"/api/v1/auth/login",
|
|
json={"username": "isp1", "password": isp_password},
|
|
)
|
|
assert response.status_code == 200
|
|
return response.json()
|
|
|
|
|
|
@pytest.fixture
|
|
def isp2_tokens(client, isp_password: str):
|
|
# Второй исполнитель в том же org_unit, что и budget_form id=1 — нужен,
|
|
# чтобы проверять, что исполнитель не может удалить чужую строку.
|
|
response = client.post(
|
|
"/api/v1/auth/login",
|
|
json={"username": "isp2", "password": isp_password},
|
|
)
|
|
assert response.status_code == 200
|
|
return response.json()
|