kin: BATON-SEC-003-backend_dev

This commit is contained in:
Gros Frumos 2026-03-21 08:12:01 +02:00
parent 097b7af949
commit f17ee79edb
13 changed files with 593 additions and 125 deletions

View file

@ -5,6 +5,10 @@ Acceptance criteria:
1. No asyncio task for the aggregator is created at lifespan startup.
2. POST /api/signal calls telegram.send_message directly (no aggregator intermediary).
3. SignalAggregator class in telegram.py is preserved with '# v2.0 feature' marker.
UUID notes: all UUIDs satisfy the UUID v4 pattern.
BATON-SEC-003: POST /api/signal now requires Authorization: Bearer <api_key>.
Tests that send signals register first and use the returned api_key.
"""
from __future__ import annotations
@ -15,6 +19,7 @@ os.environ.setdefault("CHAT_ID", "-1001234567890")
os.environ.setdefault("WEBHOOK_SECRET", "test-webhook-secret")
os.environ.setdefault("WEBHOOK_URL", "https://example.com/api/webhook/telegram")
os.environ.setdefault("FRONTEND_ORIGIN", "http://localhost:3000")
os.environ.setdefault("ADMIN_TOKEN", "test-admin-token")
from pathlib import Path
from unittest.mock import AsyncMock, patch
@ -25,6 +30,20 @@ from tests.conftest import make_app_client
_BACKEND_DIR = Path(__file__).parent.parent / "backend"
# Valid UUID v4 constants
_UUID_S1 = "a0100001-0000-4000-8000-000000000001"
_UUID_S2 = "a0100002-0000-4000-8000-000000000002"
_UUID_S3 = "a0100003-0000-4000-8000-000000000003"
_UUID_S4 = "a0100004-0000-4000-8000-000000000004"
_UUID_S5 = "a0100005-0000-4000-8000-000000000005"
async def _register(client, uuid: str, name: str) -> str:
"""Register user and return api_key."""
r = await client.post("/api/register", json={"uuid": uuid, "name": name})
assert r.status_code == 200
return r.json()["api_key"]
# ---------------------------------------------------------------------------
# Criterion 1 — No asyncio task for aggregator created at startup (static)
@ -72,11 +91,12 @@ def test_aggregator_instantiation_commented_out_in_main():
async def test_signal_calls_telegram_send_message_directly():
"""POST /api/signal must call telegram.send_message directly, not via aggregator (ADR-004)."""
async with make_app_client() as client:
await client.post("/api/register", json={"uuid": "adr-uuid-s1", "name": "Tester"})
api_key = await _register(client, _UUID_S1, "Tester")
with patch("backend.telegram.send_message", new_callable=AsyncMock) as mock_send:
resp = await client.post(
"/api/signal",
json={"user_id": "adr-uuid-s1", "timestamp": 1742478000000},
json={"user_id": _UUID_S1, "timestamp": 1742478000000},
headers={"Authorization": f"Bearer {api_key}"},
)
assert resp.status_code == 200
mock_send.assert_called_once()
@ -86,11 +106,12 @@ async def test_signal_calls_telegram_send_message_directly():
async def test_signal_message_contains_registered_username():
"""Message passed to send_message must include the registered user's name."""
async with make_app_client() as client:
await client.post("/api/register", json={"uuid": "adr-uuid-s2", "name": "Alice"})
api_key = await _register(client, _UUID_S2, "Alice")
with patch("backend.telegram.send_message", new_callable=AsyncMock) as mock_send:
await client.post(
"/api/signal",
json={"user_id": "adr-uuid-s2", "timestamp": 1742478000000},
json={"user_id": _UUID_S2, "timestamp": 1742478000000},
headers={"Authorization": f"Bearer {api_key}"},
)
text = mock_send.call_args[0][0]
assert "Alice" in text
@ -100,11 +121,12 @@ async def test_signal_message_contains_registered_username():
async def test_signal_message_without_geo_contains_bez_geolocatsii():
"""When geo is None, message must contain 'Без геолокации'."""
async with make_app_client() as client:
await client.post("/api/register", json={"uuid": "adr-uuid-s3", "name": "Bob"})
api_key = await _register(client, _UUID_S3, "Bob")
with patch("backend.telegram.send_message", new_callable=AsyncMock) as mock_send:
await client.post(
"/api/signal",
json={"user_id": "adr-uuid-s3", "timestamp": 1742478000000, "geo": None},
json={"user_id": _UUID_S3, "timestamp": 1742478000000, "geo": None},
headers={"Authorization": f"Bearer {api_key}"},
)
text = mock_send.call_args[0][0]
assert "Без геолокации" in text
@ -114,15 +136,16 @@ async def test_signal_message_without_geo_contains_bez_geolocatsii():
async def test_signal_message_with_geo_contains_coordinates():
"""When geo is provided, message must contain lat and lon values."""
async with make_app_client() as client:
await client.post("/api/register", json={"uuid": "adr-uuid-s4", "name": "Charlie"})
api_key = await _register(client, _UUID_S4, "Charlie")
with patch("backend.telegram.send_message", new_callable=AsyncMock) as mock_send:
await client.post(
"/api/signal",
json={
"user_id": "adr-uuid-s4",
"user_id": _UUID_S4,
"timestamp": 1742478000000,
"geo": {"lat": 55.7558, "lon": 37.6173, "accuracy": 15.0},
},
headers={"Authorization": f"Bearer {api_key}"},
)
text = mock_send.call_args[0][0]
assert "55.7558" in text
@ -133,29 +156,17 @@ async def test_signal_message_with_geo_contains_coordinates():
async def test_signal_message_contains_utc_marker():
"""Message passed to send_message must contain 'UTC' timestamp marker."""
async with make_app_client() as client:
await client.post("/api/register", json={"uuid": "adr-uuid-s5", "name": "Dave"})
api_key = await _register(client, _UUID_S5, "Dave")
with patch("backend.telegram.send_message", new_callable=AsyncMock) as mock_send:
await client.post(
"/api/signal",
json={"user_id": "adr-uuid-s5", "timestamp": 1742478000000},
json={"user_id": _UUID_S5, "timestamp": 1742478000000},
headers={"Authorization": f"Bearer {api_key}"},
)
text = mock_send.call_args[0][0]
assert "UTC" in text
@pytest.mark.asyncio
async def test_signal_unknown_user_message_uses_uuid_prefix():
"""When user is not registered, message uses first 8 chars of uuid as name."""
async with make_app_client() as client:
with patch("backend.telegram.send_message", new_callable=AsyncMock) as mock_send:
await client.post(
"/api/signal",
json={"user_id": "unknown-uuid-xyz", "timestamp": 1742478000000},
)
text = mock_send.call_args[0][0]
assert "unknown-" in text # "unknown-uuid-xyz"[:8] == "unknown-"
# ---------------------------------------------------------------------------
# Criterion 3 — SignalAggregator preserved with '# v2.0 feature' marker (static)
# ---------------------------------------------------------------------------