## Funcționalități Principale ### Bulk Upload & Processing - Drag & drop pentru upload bonuri multiple oriunde pe pagină - Batch processing cu job queue și worker pool - Real-time updates via SSE (Server-Sent Events) cu fallback polling - Duplicate detection via SHA-256 file hash - Auto-retry pentru job-uri failed - Cancel individual jobs sau batch complet ### Mobile UX - Android Native Style - Top bar fixă cu hamburger, titlu centrat, acțiuni (search/filter) - Bottom navigation cu 4 tab-uri (Bonuri, Upload, Rapoarte, Setări) - FAB (Floating Action Button) cu hide/show on scroll - Filter chips orizontal scrollabile - Selecție multiplă prin long-press (500ms) - Select All + Bulk Delete cu confirmare - Layout Android pentru Create/Edit/View bon (Gmail compose style) ### Bug Fixes - Refresh individual via SSE în loc de refresh total pagină - Bonurile cu eroare OCR rămân vizibile pentru editare manuală - Afișare nume fișier original pentru toate bonurile - Upload stabil pe mobil (fix race condition File API) - Păstrare ordine bonuri la refresh (nu se reordonează) ### Backend - SSE endpoint pentru status updates real-time - Bulk delete endpoint cu partial success - Auto-cleanup bonuri failed după 7 zile - Batch model cu tracking complet ### Testing - E2E tests cu Playwright - Unit tests pentru bulk upload, auto-create, cleanup ## Commits Squashed: 43 user stories (US-001 → US-043) ## Branch: ralph/bulk-receipt-upload ## Timp dezvoltare: ~3 zile (Ralph autonomous) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
546 lines
22 KiB
Python
546 lines
22 KiB
Python
"""
|
|
Tests for receipt list endpoint with batch info (US-012).
|
|
|
|
US-012 Acceptance Criteria:
|
|
- Response includes new fields for each receipt: batch_id, processing_status,
|
|
processing_error, file_hash, processing_started_at, processing_completed_at
|
|
- Filtering on processing_status works (?processing_status=failed)
|
|
- Filtering on batch_id works (?batch_id=uuid)
|
|
- Sorting on processing_started_at works
|
|
- Response includes processing_stats with counts: {pending_count, processing_count,
|
|
completed_count, failed_count}
|
|
- pytest tests pass for all new filters
|
|
"""
|
|
|
|
import pytest
|
|
from datetime import datetime
|
|
from decimal import Decimal
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
from fastapi import FastAPI
|
|
from httpx import AsyncClient, ASGITransport
|
|
|
|
from backend.modules.data_entry.routers.receipts import router
|
|
from backend.modules.data_entry.db.models.receipt import (
|
|
Receipt, ReceiptStatus, ReceiptDirection, ReceiptType
|
|
)
|
|
from backend.modules.data_entry.schemas.receipt import (
|
|
ReceiptResponse, ReceiptListResponse, ProcessingStats, ReceiptFilter
|
|
)
|
|
from shared.auth.dependencies import get_current_user
|
|
from backend.modules.data_entry.db.database import get_session
|
|
|
|
|
|
# Mock user for authentication
|
|
class MockCurrentUser:
|
|
username = "test_user"
|
|
user_id = 1
|
|
companies = ["1"]
|
|
permissions = ["data_entry"]
|
|
|
|
|
|
def create_mock_receipt(
|
|
receipt_id: int,
|
|
batch_id: str = None,
|
|
processing_status: str = None,
|
|
processing_error: str = None,
|
|
file_hash: str = None,
|
|
processing_started_at: datetime = None,
|
|
processing_completed_at: datetime = None,
|
|
) -> MagicMock:
|
|
"""Create a mock Receipt object with batch fields."""
|
|
receipt = MagicMock(spec=Receipt)
|
|
receipt.id = receipt_id
|
|
receipt.receipt_type = ReceiptType.BON_FISCAL
|
|
receipt.direction = ReceiptDirection.CHELTUIALA
|
|
receipt.receipt_number = f"NR-{receipt_id}"
|
|
receipt.receipt_series = "ABC"
|
|
receipt.receipt_date = datetime(2026, 1, 10).date()
|
|
receipt.amount = Decimal("100.50")
|
|
receipt.description = f"Test receipt {receipt_id}"
|
|
receipt.tva_breakdown = None
|
|
receipt.tva_total = None
|
|
receipt.items_count = None
|
|
receipt.vendor_address = None
|
|
receipt.expense_type_code = "CONSUMABILE"
|
|
receipt.company_id = 1
|
|
receipt.partner_name = "Test Vendor"
|
|
receipt.cui = "12345678"
|
|
receipt.ocr_raw_text = None
|
|
receipt.payment_methods = None
|
|
receipt.cash_register_id = None
|
|
receipt.cash_register_name = None
|
|
receipt.cash_register_account = None
|
|
receipt.payment_mode = "casa"
|
|
receipt.status = ReceiptStatus.DRAFT
|
|
receipt.created_by = "test_user"
|
|
receipt.created_at = datetime.utcnow()
|
|
receipt.updated_at = datetime.utcnow()
|
|
receipt.submitted_at = None
|
|
receipt.reviewed_by = None
|
|
receipt.reviewed_at = None
|
|
receipt.rejection_reason = None
|
|
receipt.oracle_synced_at = None
|
|
receipt.oracle_act_id = None
|
|
receipt.oracle_error = None
|
|
# Batch fields (US-012)
|
|
receipt.batch_id = batch_id
|
|
receipt.processing_status = processing_status
|
|
receipt.processing_error = processing_error
|
|
receipt.file_hash = file_hash
|
|
receipt.processing_started_at = processing_started_at
|
|
receipt.processing_completed_at = processing_completed_at
|
|
# Relationships
|
|
receipt.attachments = []
|
|
receipt.entries = []
|
|
return receipt
|
|
|
|
|
|
def create_test_app(mock_session=None, mock_user=None):
|
|
"""Create a test FastAPI app with the receipts router and dependency overrides."""
|
|
app = FastAPI()
|
|
app.include_router(router, prefix="/api/data-entry/receipts")
|
|
|
|
if mock_user is not None:
|
|
app.dependency_overrides[get_current_user] = lambda: mock_user
|
|
|
|
if mock_session is not None:
|
|
async def override_session():
|
|
yield mock_session
|
|
app.dependency_overrides[get_session] = override_session
|
|
|
|
return app
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_current_user():
|
|
return MockCurrentUser()
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_session():
|
|
"""Create a mock async session."""
|
|
session = AsyncMock()
|
|
return session
|
|
|
|
|
|
# ============================================================================
|
|
# Tests for Response Schema with Batch Fields
|
|
# ============================================================================
|
|
|
|
class TestReceiptResponseBatchFields:
|
|
"""Test that ReceiptResponse includes batch fields."""
|
|
|
|
def test_response_includes_batch_id(self):
|
|
"""Verify ReceiptResponse includes batch_id field."""
|
|
receipt = create_mock_receipt(1, batch_id="test-batch-uuid")
|
|
response = ReceiptResponse.model_validate(receipt)
|
|
assert response.batch_id == "test-batch-uuid"
|
|
|
|
def test_response_includes_processing_status(self):
|
|
"""Verify ReceiptResponse includes processing_status field."""
|
|
receipt = create_mock_receipt(1, processing_status="completed")
|
|
response = ReceiptResponse.model_validate(receipt)
|
|
assert response.processing_status == "completed"
|
|
|
|
def test_response_includes_processing_error(self):
|
|
"""Verify ReceiptResponse includes processing_error field."""
|
|
receipt = create_mock_receipt(1, processing_error="OCR failed: invalid image")
|
|
response = ReceiptResponse.model_validate(receipt)
|
|
assert response.processing_error == "OCR failed: invalid image"
|
|
|
|
def test_response_includes_file_hash(self):
|
|
"""Verify ReceiptResponse includes file_hash field."""
|
|
receipt = create_mock_receipt(1, file_hash="abc123hash")
|
|
response = ReceiptResponse.model_validate(receipt)
|
|
assert response.file_hash == "abc123hash"
|
|
|
|
def test_response_includes_processing_started_at(self):
|
|
"""Verify ReceiptResponse includes processing_started_at field."""
|
|
started_at = datetime(2026, 1, 10, 10, 30, 0)
|
|
receipt = create_mock_receipt(1, processing_started_at=started_at)
|
|
response = ReceiptResponse.model_validate(receipt)
|
|
assert response.processing_started_at == started_at
|
|
|
|
def test_response_includes_processing_completed_at(self):
|
|
"""Verify ReceiptResponse includes processing_completed_at field."""
|
|
completed_at = datetime(2026, 1, 10, 10, 35, 0)
|
|
receipt = create_mock_receipt(1, processing_completed_at=completed_at)
|
|
response = ReceiptResponse.model_validate(receipt)
|
|
assert response.processing_completed_at == completed_at
|
|
|
|
def test_response_batch_fields_are_optional(self):
|
|
"""Verify batch fields can be None."""
|
|
receipt = create_mock_receipt(1)
|
|
response = ReceiptResponse.model_validate(receipt)
|
|
assert response.batch_id is None
|
|
assert response.processing_status is None
|
|
assert response.processing_error is None
|
|
assert response.file_hash is None
|
|
assert response.processing_started_at is None
|
|
assert response.processing_completed_at is None
|
|
|
|
|
|
# ============================================================================
|
|
# Tests for ProcessingStats Schema
|
|
# ============================================================================
|
|
|
|
class TestProcessingStatsSchema:
|
|
"""Test ProcessingStats schema."""
|
|
|
|
def test_processing_stats_structure(self):
|
|
"""Verify ProcessingStats has correct structure."""
|
|
stats = ProcessingStats(
|
|
pending_count=5,
|
|
processing_count=2,
|
|
completed_count=10,
|
|
failed_count=1
|
|
)
|
|
assert stats.pending_count == 5
|
|
assert stats.processing_count == 2
|
|
assert stats.completed_count == 10
|
|
assert stats.failed_count == 1
|
|
|
|
def test_processing_stats_defaults_to_zero(self):
|
|
"""Verify ProcessingStats defaults all counts to 0."""
|
|
stats = ProcessingStats()
|
|
assert stats.pending_count == 0
|
|
assert stats.processing_count == 0
|
|
assert stats.completed_count == 0
|
|
assert stats.failed_count == 0
|
|
|
|
|
|
# ============================================================================
|
|
# Tests for ReceiptListResponse with processing_stats
|
|
# ============================================================================
|
|
|
|
class TestReceiptListResponseWithStats:
|
|
"""Test ReceiptListResponse includes processing_stats."""
|
|
|
|
def test_list_response_includes_processing_stats(self):
|
|
"""Verify ReceiptListResponse includes processing_stats field."""
|
|
receipt = create_mock_receipt(1)
|
|
stats = ProcessingStats(pending_count=1, processing_count=0, completed_count=5, failed_count=2)
|
|
response = ReceiptListResponse(
|
|
items=[ReceiptResponse.model_validate(receipt)],
|
|
total=1,
|
|
page=1,
|
|
page_size=20,
|
|
pages=1,
|
|
processing_stats=stats
|
|
)
|
|
assert response.processing_stats is not None
|
|
assert response.processing_stats.pending_count == 1
|
|
assert response.processing_stats.completed_count == 5
|
|
assert response.processing_stats.failed_count == 2
|
|
|
|
def test_list_response_processing_stats_is_optional(self):
|
|
"""Verify processing_stats can be None."""
|
|
receipt = create_mock_receipt(1)
|
|
response = ReceiptListResponse(
|
|
items=[ReceiptResponse.model_validate(receipt)],
|
|
total=1,
|
|
page=1,
|
|
page_size=20,
|
|
pages=1,
|
|
)
|
|
assert response.processing_stats is None
|
|
|
|
|
|
# ============================================================================
|
|
# Tests for ReceiptFilter with new fields
|
|
# ============================================================================
|
|
|
|
class TestReceiptFilterWithBatchFields:
|
|
"""Test ReceiptFilter includes batch filtering fields."""
|
|
|
|
def test_filter_includes_processing_status(self):
|
|
"""Verify ReceiptFilter includes processing_status field."""
|
|
filter_obj = ReceiptFilter(processing_status="failed")
|
|
assert filter_obj.processing_status == "failed"
|
|
|
|
def test_filter_includes_batch_id(self):
|
|
"""Verify ReceiptFilter includes batch_id field."""
|
|
filter_obj = ReceiptFilter(batch_id="test-batch-uuid")
|
|
assert filter_obj.batch_id == "test-batch-uuid"
|
|
|
|
def test_filter_includes_sort_by(self):
|
|
"""Verify ReceiptFilter includes sort_by field."""
|
|
filter_obj = ReceiptFilter(sort_by="processing_started_at")
|
|
assert filter_obj.sort_by == "processing_started_at"
|
|
|
|
def test_filter_new_fields_are_optional(self):
|
|
"""Verify new filter fields default to None."""
|
|
filter_obj = ReceiptFilter()
|
|
assert filter_obj.processing_status is None
|
|
assert filter_obj.batch_id is None
|
|
assert filter_obj.sort_by is None
|
|
|
|
|
|
# ============================================================================
|
|
# Integration Tests for List Endpoint with Filters
|
|
# ============================================================================
|
|
|
|
class TestListReceiptsEndpointWithFilters:
|
|
"""Test GET /api/data-entry/receipts endpoint with new filters."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_receipts_returns_batch_fields(self, mock_session, mock_current_user):
|
|
"""Test that list endpoint returns batch fields in response."""
|
|
app = create_test_app(mock_session=mock_session, mock_user=mock_current_user)
|
|
|
|
receipt = create_mock_receipt(
|
|
1,
|
|
batch_id="batch-123",
|
|
processing_status="completed",
|
|
file_hash="hash123",
|
|
processing_started_at=datetime(2026, 1, 10, 10, 0, 0),
|
|
processing_completed_at=datetime(2026, 1, 10, 10, 5, 0)
|
|
)
|
|
|
|
with patch("backend.modules.data_entry.services.receipt_service.ReceiptCRUD") as mock_crud:
|
|
mock_crud.get_list = AsyncMock(return_value=([receipt], 1))
|
|
mock_crud.get_processing_stats = AsyncMock(return_value={
|
|
"pending_count": 0,
|
|
"processing_count": 0,
|
|
"completed_count": 1,
|
|
"failed_count": 0
|
|
})
|
|
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
|
response = await client.get("/api/data-entry/receipts/")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert len(data["items"]) == 1
|
|
item = data["items"][0]
|
|
assert item["batch_id"] == "batch-123"
|
|
assert item["processing_status"] == "completed"
|
|
assert item["file_hash"] == "hash123"
|
|
assert "processing_started_at" in item
|
|
assert "processing_completed_at" in item
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_receipts_includes_processing_stats(self, mock_session, mock_current_user):
|
|
"""Test that list endpoint returns processing_stats in response."""
|
|
app = create_test_app(mock_session=mock_session, mock_user=mock_current_user)
|
|
|
|
receipt = create_mock_receipt(1)
|
|
|
|
with patch("backend.modules.data_entry.services.receipt_service.ReceiptCRUD") as mock_crud:
|
|
mock_crud.get_list = AsyncMock(return_value=([receipt], 1))
|
|
mock_crud.get_processing_stats = AsyncMock(return_value={
|
|
"pending_count": 2,
|
|
"processing_count": 1,
|
|
"completed_count": 5,
|
|
"failed_count": 1
|
|
})
|
|
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
|
response = await client.get("/api/data-entry/receipts/")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "processing_stats" in data
|
|
stats = data["processing_stats"]
|
|
assert stats["pending_count"] == 2
|
|
assert stats["processing_count"] == 1
|
|
assert stats["completed_count"] == 5
|
|
assert stats["failed_count"] == 1
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_filter_by_processing_status(self, mock_session, mock_current_user):
|
|
"""Test filtering by processing_status query param."""
|
|
app = create_test_app(mock_session=mock_session, mock_user=mock_current_user)
|
|
|
|
receipt = create_mock_receipt(1, processing_status="failed")
|
|
|
|
with patch("backend.modules.data_entry.services.receipt_service.ReceiptCRUD") as mock_crud:
|
|
mock_crud.get_list = AsyncMock(return_value=([receipt], 1))
|
|
mock_crud.get_processing_stats = AsyncMock(return_value={
|
|
"pending_count": 0,
|
|
"processing_count": 0,
|
|
"completed_count": 0,
|
|
"failed_count": 1
|
|
})
|
|
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
|
response = await client.get("/api/data-entry/receipts/?processing_status=failed")
|
|
|
|
assert response.status_code == 200
|
|
|
|
# Verify filter was passed to CRUD
|
|
call_args = mock_crud.get_list.call_args
|
|
filters = call_args[0][1] # Second positional argument is filters
|
|
assert filters.processing_status == "failed"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_filter_by_batch_id(self, mock_session, mock_current_user):
|
|
"""Test filtering by batch_id query param."""
|
|
app = create_test_app(mock_session=mock_session, mock_user=mock_current_user)
|
|
|
|
batch_uuid = "test-batch-uuid-123"
|
|
receipt = create_mock_receipt(1, batch_id=batch_uuid)
|
|
|
|
with patch("backend.modules.data_entry.services.receipt_service.ReceiptCRUD") as mock_crud:
|
|
mock_crud.get_list = AsyncMock(return_value=([receipt], 1))
|
|
mock_crud.get_processing_stats = AsyncMock(return_value={
|
|
"pending_count": 0,
|
|
"processing_count": 0,
|
|
"completed_count": 1,
|
|
"failed_count": 0
|
|
})
|
|
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
|
response = await client.get(f"/api/data-entry/receipts/?batch_id={batch_uuid}")
|
|
|
|
assert response.status_code == 200
|
|
|
|
# Verify filter was passed to CRUD
|
|
call_args = mock_crud.get_list.call_args
|
|
filters = call_args[0][1]
|
|
assert filters.batch_id == batch_uuid
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_sort_by_processing_started_at(self, mock_session, mock_current_user):
|
|
"""Test sorting by processing_started_at query param."""
|
|
app = create_test_app(mock_session=mock_session, mock_user=mock_current_user)
|
|
|
|
receipt = create_mock_receipt(1)
|
|
|
|
with patch("backend.modules.data_entry.services.receipt_service.ReceiptCRUD") as mock_crud:
|
|
mock_crud.get_list = AsyncMock(return_value=([receipt], 1))
|
|
mock_crud.get_processing_stats = AsyncMock(return_value={
|
|
"pending_count": 0,
|
|
"processing_count": 0,
|
|
"completed_count": 0,
|
|
"failed_count": 0
|
|
})
|
|
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
|
response = await client.get("/api/data-entry/receipts/?sort_by=processing_started_at")
|
|
|
|
assert response.status_code == 200
|
|
|
|
# Verify sort_by was passed to CRUD
|
|
call_args = mock_crud.get_list.call_args
|
|
filters = call_args[0][1]
|
|
assert filters.sort_by == "processing_started_at"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_combined_filters(self, mock_session, mock_current_user):
|
|
"""Test combining batch_id, processing_status, and sort_by filters."""
|
|
app = create_test_app(mock_session=mock_session, mock_user=mock_current_user)
|
|
|
|
batch_uuid = "batch-abc-123"
|
|
receipt = create_mock_receipt(1, batch_id=batch_uuid, processing_status="completed")
|
|
|
|
with patch("backend.modules.data_entry.services.receipt_service.ReceiptCRUD") as mock_crud:
|
|
mock_crud.get_list = AsyncMock(return_value=([receipt], 1))
|
|
mock_crud.get_processing_stats = AsyncMock(return_value={
|
|
"pending_count": 0,
|
|
"processing_count": 0,
|
|
"completed_count": 1,
|
|
"failed_count": 0
|
|
})
|
|
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
|
response = await client.get(
|
|
f"/api/data-entry/receipts/"
|
|
f"?batch_id={batch_uuid}"
|
|
f"&processing_status=completed"
|
|
f"&sort_by=processing_started_at"
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
|
|
# Verify all filters were passed to CRUD
|
|
call_args = mock_crud.get_list.call_args
|
|
filters = call_args[0][1]
|
|
assert filters.batch_id == batch_uuid
|
|
assert filters.processing_status == "completed"
|
|
assert filters.sort_by == "processing_started_at"
|
|
|
|
|
|
# ============================================================================
|
|
# Tests for CRUD get_processing_stats
|
|
# ============================================================================
|
|
|
|
class TestCRUDGetProcessingStats:
|
|
"""Test ReceiptCRUD.get_processing_stats method."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_processing_stats_returns_counts(self):
|
|
"""Test that get_processing_stats returns proper count structure."""
|
|
from backend.modules.data_entry.db.crud.receipt import ReceiptCRUD
|
|
|
|
mock_session = AsyncMock()
|
|
|
|
# Mock query result
|
|
mock_result = MagicMock()
|
|
mock_result.all = MagicMock(return_value=[
|
|
MagicMock(processing_status="pending", count=2),
|
|
MagicMock(processing_status="processing", count=1),
|
|
MagicMock(processing_status="completed", count=5),
|
|
MagicMock(processing_status="failed", count=1),
|
|
])
|
|
mock_session.execute = AsyncMock(return_value=mock_result)
|
|
|
|
stats = await ReceiptCRUD.get_processing_stats(mock_session)
|
|
|
|
assert stats["pending_count"] == 2
|
|
assert stats["processing_count"] == 1
|
|
assert stats["completed_count"] == 5
|
|
assert stats["failed_count"] == 1
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_processing_stats_with_company_filter(self):
|
|
"""Test that get_processing_stats respects company_id filter."""
|
|
from backend.modules.data_entry.db.crud.receipt import ReceiptCRUD
|
|
|
|
mock_session = AsyncMock()
|
|
|
|
mock_result = MagicMock()
|
|
mock_result.all = MagicMock(return_value=[
|
|
MagicMock(processing_status="completed", count=3),
|
|
])
|
|
mock_session.execute = AsyncMock(return_value=mock_result)
|
|
|
|
stats = await ReceiptCRUD.get_processing_stats(mock_session, company_id=1)
|
|
|
|
assert stats["completed_count"] == 3
|
|
assert stats["pending_count"] == 0
|
|
assert stats["processing_count"] == 0
|
|
assert stats["failed_count"] == 0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_processing_stats_with_batch_filter(self):
|
|
"""Test that get_processing_stats respects batch_id filter."""
|
|
from backend.modules.data_entry.db.crud.receipt import ReceiptCRUD
|
|
|
|
mock_session = AsyncMock()
|
|
|
|
mock_result = MagicMock()
|
|
mock_result.all = MagicMock(return_value=[
|
|
MagicMock(processing_status="pending", count=2),
|
|
MagicMock(processing_status="failed", count=1),
|
|
])
|
|
mock_session.execute = AsyncMock(return_value=mock_result)
|
|
|
|
stats = await ReceiptCRUD.get_processing_stats(mock_session, batch_id="batch-123")
|
|
|
|
assert stats["pending_count"] == 2
|
|
assert stats["failed_count"] == 1
|
|
assert stats["completed_count"] == 0
|
|
assert stats["processing_count"] == 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|