Files
lux_fiscal/backend/tests/test_orders_router.py
lauadminandClaude Opus 5.5 b8f2fe6b6e Translate UI and user-facing messages to Ukrainian (#5)
- All frontend pages, labels, notices and errors; html lang=uk, uk-UA money format
- Brand "Assistant System" in the top bar and page title
- Backend error details returned to the UI (auth, orders, receipts,
  cash registers, Checkbox/CRM/NP errors) and CLI output
- Tests updated for the new messages

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 15:27:39 +03:00

260 lines
9.1 KiB
Python
Raw Permalink 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.
"""Тесты роутера заказов.
Auth подменена через dependency_overrides, сервисный слой (`app.services.orders`)
— через monkeypatch. `SessionDep` создаёт `AsyncSession`, но соединение с БД
открывается лениво только при первом запросе — раз сервисные функции его не
трогают, реальная БД не нужна.
"""
from __future__ import annotations
import uuid
from datetime import UTC, datetime
import pytest
from fastapi.testclient import TestClient
from app.api.deps import get_crm_client, get_current_user
from app.db.models.order import Order
from app.db.models.user import User, UserRole
from app.main import app
from app.services.crm.stub_client import StubCrmClient
pytestmark = pytest.mark.usefixtures("_patch_orders_service")
def _user(role: UserRole) -> User:
return User(
id=uuid.uuid4(),
email="test@example.com",
password_hash="x",
full_name="Тест",
role=role,
is_active=True,
)
def _order(order_id: str = "1") -> Order:
return Order(
id=order_id,
create_date_time=datetime(2026, 9, 20, 10, 0, 0, tzinfo=UTC),
recipient_name="Тест Тестов",
recipient_phone="+380501112233",
recipient_email=None,
waybill_number="20450123456789",
notes=None,
total_amount_kopecks=120000,
goods=[
{
"id": "1",
"sku": "SKU-1",
"name": "Товар 1",
"price": "1200.00",
"quantity": "1.000",
"discount_amount": "0.00",
"discount_percent": "0.00",
"amount": "1200.00",
}
],
is_deleted=False,
deleted_at=None,
receipt_created_at=None,
)
@pytest.fixture
def _patch_orders_service(monkeypatch: pytest.MonkeyPatch) -> None:
import app.api.v1.orders as orders_router
async def fake_sync(session: object, crm: object) -> None:
return None
async def fake_list(session: object, *, tab: str) -> list[Order]:
return [_order()] if tab == "no_receipt" else []
async def fake_delete(session: object, order_id: str) -> Order | None:
return _order(order_id) if order_id == "1" else None
monkeypatch.setattr(orders_router.orders_service, "sync_orders_from_crm", fake_sync)
monkeypatch.setattr(orders_router.orders_service, "list_orders", fake_list)
monkeypatch.setattr(orders_router.orders_service, "delete_order", fake_delete)
async def fake_cod_in_transit(session: object) -> tuple[int, int]:
return 3, 245050
monkeypatch.setattr(orders_router.orders_service, "cod_in_transit", fake_cod_in_transit)
async def fake_update(
session: object, order_id: str, data: object
) -> tuple[Order, list[str]] | None:
if order_id == "locked":
raise orders_router.orders_service.OrderEditError("За замовленням уже створено чек")
if order_id != "1":
return None
order = _order(order_id)
order.edited_at = datetime(2026, 9, 24, tzinfo=UTC)
return order, ["notes"]
monkeypatch.setattr(orders_router.orders_service, "update_order", fake_update)
async def fake_latest(session: object, order_ids: list[str]) -> dict:
return {}
monkeypatch.setattr(
orders_router.receipts_service, "latest_receipts_by_order", fake_latest
)
async def fake_record(session: object, **kwargs: object) -> None:
return None
monkeypatch.setattr(orders_router.audit, "record", fake_record)
# session.commit() вызывается напрямую роутером после audit.record — сама
# сессия не трогает БД до первого execute/commit, поэтому патчим только
# commit, а не всю SessionDep.
from sqlalchemy.ext.asyncio import AsyncSession
async def fake_commit(self: AsyncSession) -> None:
return None
monkeypatch.setattr(AsyncSession, "commit", fake_commit)
@pytest.fixture
def client() -> TestClient:
app.dependency_overrides[get_crm_client] = lambda: StubCrmClient()
try:
yield TestClient(app)
finally:
app.dependency_overrides.pop(get_crm_client, None)
app.dependency_overrides.pop(get_current_user, None)
class TestListOrders:
@pytest.mark.parametrize("role", [UserRole.ADMIN, UserRole.CASHIER, UserRole.VIEWER])
def test_any_authenticated_role_can_list_no_receipt_orders(
self, client: TestClient, role: UserRole
) -> None:
app.dependency_overrides[get_current_user] = lambda: _user(role)
response = client.get("/api/v1/orders", params={"tab": "no_receipt"})
assert response.status_code == 200
body = response.json()
assert len(body) == 1
assert body[0]["id"] == "1"
assert body[0]["total_amount"] == "1200.00"
assert body[0]["has_receipt"] is False
assert body[0]["goods"][0]["sku"] == "SKU-1"
@pytest.mark.parametrize("tab", ["has_receipt", "refused"])
def test_other_tabs_are_passed_to_service(self, client: TestClient, tab: str) -> None:
app.dependency_overrides[get_current_user] = lambda: _user(UserRole.CASHIER)
response = client.get("/api/v1/orders", params={"tab": tab})
assert response.status_code == 200
assert response.json() == []
def test_unknown_tab_is_rejected(self, client: TestClient) -> None:
app.dependency_overrides[get_current_user] = lambda: _user(UserRole.CASHIER)
response = client.get("/api/v1/orders", params={"tab": "bogus"})
assert response.status_code == 422
def test_requires_authentication(self, client: TestClient) -> None:
response = client.get("/api/v1/orders")
assert response.status_code == 401
class TestDeleteOrder:
@pytest.mark.parametrize("role", [UserRole.ADMIN, UserRole.CASHIER])
def test_admin_and_cashier_can_delete(self, client: TestClient, role: UserRole) -> None:
app.dependency_overrides[get_current_user] = lambda: _user(role)
response = client.delete("/api/v1/orders/1")
assert response.status_code == 204
def test_viewer_cannot_delete(self, client: TestClient) -> None:
app.dependency_overrides[get_current_user] = lambda: _user(UserRole.VIEWER)
response = client.delete("/api/v1/orders/1")
assert response.status_code == 403
def test_deleting_unknown_order_is_404(self, client: TestClient) -> None:
app.dependency_overrides[get_current_user] = lambda: _user(UserRole.CASHIER)
response = client.delete("/api/v1/orders/does-not-exist")
assert response.status_code == 404
_UPDATE_BODY = {
"recipient_name": "Тест Тестов",
"recipient_phone": "+380501112233",
"waybill_number": "20450123456789",
"notes": "правка",
"total_amount": "1200.00",
"goods": [
{"id": "1", "sku": "SKU-1", "name": "Товар 1", "price": "1200.00", "quantity": "1"}
],
}
class TestUpdateOrder:
@pytest.mark.parametrize("role", [UserRole.ADMIN, UserRole.CASHIER])
def test_admin_and_cashier_can_update(self, client: TestClient, role: UserRole) -> None:
app.dependency_overrides[get_current_user] = lambda: _user(role)
response = client.patch("/api/v1/orders/1", json=_UPDATE_BODY)
assert response.status_code == 200
body = response.json()
assert body["edited"] is True
assert body["goods"][0]["sku"] == "SKU-1"
def test_viewer_cannot_update(self, client: TestClient) -> None:
app.dependency_overrides[get_current_user] = lambda: _user(UserRole.VIEWER)
response = client.patch("/api/v1/orders/1", json=_UPDATE_BODY)
assert response.status_code == 403
def test_unknown_order_is_404(self, client: TestClient) -> None:
app.dependency_overrides[get_current_user] = lambda: _user(UserRole.CASHIER)
response = client.patch("/api/v1/orders/nope", json=_UPDATE_BODY)
assert response.status_code == 404
def test_order_with_receipt_is_409(self, client: TestClient) -> None:
app.dependency_overrides[get_current_user] = lambda: _user(UserRole.CASHIER)
response = client.patch("/api/v1/orders/locked", json=_UPDATE_BODY)
assert response.status_code == 409
def test_empty_goods_is_rejected(self, client: TestClient) -> None:
app.dependency_overrides[get_current_user] = lambda: _user(UserRole.CASHIER)
response = client.patch("/api/v1/orders/1", json={**_UPDATE_BODY, "goods": []})
assert response.status_code == 422
class TestSummary:
@pytest.mark.parametrize("role", [UserRole.ADMIN, UserRole.CASHIER, UserRole.VIEWER])
def test_returns_cod_in_transit(self, client: TestClient, role: UserRole) -> None:
app.dependency_overrides[get_current_user] = lambda: _user(role)
response = client.get("/api/v1/orders/summary")
assert response.status_code == 200
assert response.json() == {
"cod_in_transit_count": 3,
"cod_in_transit_amount": "2450.50",
}