feat(telegram): bot bonuri fiscale — OCR → preview → Oracle write
- US-001: mută queue_client.py în data_entry/services/ocr/ - US-002/003/004: oracle_receipt_writer + oracle_server_id în DB - US-005: receipt_handlers.py (PDF/photo/callback flow) - US-006: wire handlers în main.py, per-schema connect, seq_cod.nextval - US-007: .gitignore secrets/*.oracle_pass - US-008/009/010: teste unit + integration + E2E - setup-secrets.sh helper + template - docs/telegram/README.md actualizat cu arhitectura nouă Testat E2E pe DB live (MARIUSM_AUTO). COD din seq_cod.nextval. pypdfium2 fallback pentru PDF decode (fără poppler). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
229
tests/backend/test_telegram_oracle_server_id.py
Normal file
229
tests/backend/test_telegram_oracle_server_id.py
Normal file
@@ -0,0 +1,229 @@
|
||||
"""Unit tests for US-004: oracle_server_id in telegram_users.
|
||||
|
||||
Tests cover:
|
||||
- link_user_to_oracle() persists server_id
|
||||
- link_user_to_oracle() works without server_id (backwards compat)
|
||||
- get_user() returns oracle_server_id from stored row
|
||||
- get_user_auth_data() includes server_id in returned dict
|
||||
- Round-trip: link with server_id → auth data returns same server_id
|
||||
"""
|
||||
import asyncio
|
||||
import sys
|
||||
import os
|
||||
import pytest
|
||||
import aiosqlite
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../'))
|
||||
|
||||
# python-telegram-bot is not installed in the test environment; mock it so that
|
||||
# linking.py can be imported (it has `from telegram import User` at top-level).
|
||||
from unittest.mock import MagicMock
|
||||
sys.modules.setdefault('telegram', MagicMock())
|
||||
import backend.modules.telegram.auth.linking # noqa: F401 — registers module for patch() resolution
|
||||
|
||||
from backend.modules.telegram.db.operations import (
|
||||
create_or_update_user,
|
||||
link_user_to_oracle,
|
||||
get_user,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _make_test_db(tmp_path: Path) -> Path:
|
||||
"""Create a minimal telegram_users table in a temp SQLite file."""
|
||||
db_file = tmp_path / "test_app.db"
|
||||
async with aiosqlite.connect(db_file) as db:
|
||||
await db.execute("""CREATE TABLE telegram_users (
|
||||
telegram_user_id INTEGER PRIMARY KEY,
|
||||
username TEXT, first_name TEXT NOT NULL, last_name TEXT,
|
||||
oracle_username TEXT, oracle_server_id TEXT,
|
||||
jwt_token TEXT, jwt_refresh_token TEXT,
|
||||
token_expires_at TIMESTAMP, linked_at TIMESTAMP,
|
||||
last_active_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
is_active BOOLEAN DEFAULT 1
|
||||
)""")
|
||||
await db.commit()
|
||||
return db_file
|
||||
|
||||
|
||||
async def _insert_user(db_path: Path, telegram_user_id: int):
|
||||
async with aiosqlite.connect(db_path) as db:
|
||||
await db.execute(
|
||||
"INSERT INTO telegram_users (telegram_user_id, first_name) VALUES (?, ?)",
|
||||
(telegram_user_id, "Test")
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: link_user_to_oracle persists server_id
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_link_user_persists_server_id(tmp_path):
|
||||
db_file = await _make_test_db(tmp_path)
|
||||
await _insert_user(db_file, 12345)
|
||||
|
||||
with patch("backend.modules.telegram.db.operations.DB_PATH", db_file):
|
||||
result = await link_user_to_oracle(
|
||||
telegram_user_id=12345,
|
||||
oracle_username="TESTUSER",
|
||||
jwt_token="tok",
|
||||
jwt_refresh_token="rtok",
|
||||
token_expires_at=datetime.now() + timedelta(minutes=30),
|
||||
server_id="SERVER1"
|
||||
)
|
||||
|
||||
assert result is True
|
||||
|
||||
async with aiosqlite.connect(db_file) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
cursor = await db.execute(
|
||||
"SELECT oracle_server_id FROM telegram_users WHERE telegram_user_id = ?",
|
||||
(12345,)
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
|
||||
assert row["oracle_server_id"] == "SERVER1"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_link_user_without_server_id(tmp_path):
|
||||
"""Backwards compat: omitting server_id stores NULL."""
|
||||
db_file = await _make_test_db(tmp_path)
|
||||
await _insert_user(db_file, 22222)
|
||||
|
||||
with patch("backend.modules.telegram.db.operations.DB_PATH", db_file):
|
||||
result = await link_user_to_oracle(
|
||||
telegram_user_id=22222,
|
||||
oracle_username="TESTUSER2",
|
||||
jwt_token="tok",
|
||||
jwt_refresh_token="rtok",
|
||||
token_expires_at=datetime.now() + timedelta(minutes=30),
|
||||
)
|
||||
|
||||
assert result is True
|
||||
|
||||
async with aiosqlite.connect(db_file) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
cursor = await db.execute(
|
||||
"SELECT oracle_server_id FROM telegram_users WHERE telegram_user_id = ?",
|
||||
(22222,)
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
|
||||
assert row["oracle_server_id"] is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: get_user returns oracle_server_id
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_returns_oracle_server_id(tmp_path):
|
||||
db_file = await _make_test_db(tmp_path)
|
||||
await _insert_user(db_file, 33333)
|
||||
|
||||
with patch("backend.modules.telegram.db.operations.DB_PATH", db_file):
|
||||
await link_user_to_oracle(
|
||||
telegram_user_id=33333,
|
||||
oracle_username="TESTUSER3",
|
||||
jwt_token="tok",
|
||||
jwt_refresh_token="rtok",
|
||||
token_expires_at=datetime.now() + timedelta(minutes=30),
|
||||
server_id="PROD_SERVER"
|
||||
)
|
||||
user = await get_user(33333)
|
||||
|
||||
assert user is not None
|
||||
assert user["oracle_server_id"] == "PROD_SERVER"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: get_user_auth_data returns server_id (round-trip)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_auth_data_includes_server_id(tmp_path):
|
||||
"""Round-trip: link with server_id → get_user_auth_data returns same server_id."""
|
||||
db_file = await _make_test_db(tmp_path)
|
||||
await _insert_user(db_file, 44444)
|
||||
|
||||
expires_at = datetime.now() + timedelta(hours=1)
|
||||
|
||||
with patch("backend.modules.telegram.db.operations.DB_PATH", db_file):
|
||||
await link_user_to_oracle(
|
||||
telegram_user_id=44444,
|
||||
oracle_username="TESTUSER4",
|
||||
jwt_token="valid_token",
|
||||
jwt_refresh_token="rtoken",
|
||||
token_expires_at=expires_at,
|
||||
server_id="ALPHA_SERVER"
|
||||
)
|
||||
|
||||
mock_companies = [{"id": 1, "name": "Test Co"}]
|
||||
mock_backend = AsyncMock()
|
||||
mock_backend.__aenter__ = AsyncMock(return_value=mock_backend)
|
||||
mock_backend.__aexit__ = AsyncMock(return_value=False)
|
||||
mock_backend.get_user_companies = AsyncMock(return_value=mock_companies)
|
||||
|
||||
with (
|
||||
patch("backend.modules.telegram.db.operations.DB_PATH", db_file),
|
||||
patch("backend.modules.telegram.auth.linking.get_user") as mock_get_user,
|
||||
patch("backend.modules.telegram.auth.linking.get_backend_client", return_value=mock_backend),
|
||||
):
|
||||
# Return stored user row (simulating what get_user() would return)
|
||||
mock_get_user.return_value = {
|
||||
"telegram_user_id": 44444,
|
||||
"oracle_username": "TESTUSER4",
|
||||
"oracle_server_id": "ALPHA_SERVER",
|
||||
"jwt_token": "valid_token",
|
||||
"jwt_refresh_token": "rtoken",
|
||||
"token_expires_at": expires_at.isoformat(),
|
||||
}
|
||||
|
||||
from backend.modules.telegram.auth.linking import get_user_auth_data
|
||||
auth_data = await get_user_auth_data(44444)
|
||||
|
||||
assert auth_data is not None
|
||||
assert "server_id" in auth_data
|
||||
assert auth_data["server_id"] == "ALPHA_SERVER"
|
||||
assert auth_data["username"] == "TESTUSER4"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_auth_data_server_id_none_when_not_set(tmp_path):
|
||||
"""server_id is None in auth data when user was linked without a server_id."""
|
||||
mock_companies = []
|
||||
mock_backend = AsyncMock()
|
||||
mock_backend.__aenter__ = AsyncMock(return_value=mock_backend)
|
||||
mock_backend.__aexit__ = AsyncMock(return_value=False)
|
||||
mock_backend.get_user_companies = AsyncMock(return_value=mock_companies)
|
||||
|
||||
expires_at = datetime.now() + timedelta(hours=1)
|
||||
|
||||
with (
|
||||
patch("backend.modules.telegram.auth.linking.get_user") as mock_get_user,
|
||||
patch("backend.modules.telegram.auth.linking.get_backend_client", return_value=mock_backend),
|
||||
):
|
||||
mock_get_user.return_value = {
|
||||
"telegram_user_id": 55555,
|
||||
"oracle_username": "TESTUSER5",
|
||||
"oracle_server_id": None,
|
||||
"jwt_token": "valid_token",
|
||||
"jwt_refresh_token": "rtoken",
|
||||
"token_expires_at": expires_at.isoformat(),
|
||||
}
|
||||
|
||||
from backend.modules.telegram.auth.linking import get_user_auth_data
|
||||
auth_data = await get_user_auth_data(55555)
|
||||
|
||||
assert auth_data is not None
|
||||
assert "server_id" in auth_data
|
||||
assert auth_data["server_id"] is None
|
||||
Reference in New Issue
Block a user