2026-03-20 20:44:00 +02:00
|
|
|
"""
|
|
|
|
|
Tests for backend/models.py (Pydantic v2 validation).
|
|
|
|
|
No DB or network calls — pure unit tests.
|
|
|
|
|
"""
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
|
|
|
|
|
os.environ.setdefault("BOT_TOKEN", "test-bot-token")
|
|
|
|
|
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")
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
from pydantic import ValidationError
|
|
|
|
|
|
|
|
|
|
from backend.models import GeoData, RegisterRequest, SignalRequest
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# RegisterRequest
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
def test_register_request_valid():
|
|
|
|
|
req = RegisterRequest(uuid="550e8400-e29b-41d4-a716-446655440000", name="Alice")
|
|
|
|
|
assert req.uuid == "550e8400-e29b-41d4-a716-446655440000"
|
|
|
|
|
assert req.name == "Alice"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_register_request_empty_name():
|
|
|
|
|
with pytest.raises(ValidationError):
|
|
|
|
|
RegisterRequest(uuid="550e8400-e29b-41d4-a716-446655440000", name="")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_register_request_missing_uuid():
|
|
|
|
|
with pytest.raises(ValidationError):
|
|
|
|
|
RegisterRequest(name="Alice") # type: ignore[call-arg]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_register_request_empty_uuid():
|
|
|
|
|
with pytest.raises(ValidationError):
|
|
|
|
|
RegisterRequest(uuid="", name="Alice")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_register_request_name_max_length():
|
|
|
|
|
"""name longer than 100 chars raises ValidationError."""
|
|
|
|
|
with pytest.raises(ValidationError):
|
2026-03-21 08:12:01 +02:00
|
|
|
RegisterRequest(uuid="550e8400-e29b-41d4-a716-446655440000", name="x" * 101)
|
2026-03-20 20:44:00 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_register_request_name_exactly_100():
|
2026-03-21 08:12:01 +02:00
|
|
|
req = RegisterRequest(uuid="550e8400-e29b-41d4-a716-446655440000", name="x" * 100)
|
2026-03-20 20:44:00 +02:00
|
|
|
assert len(req.name) == 100
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# GeoData
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
def test_geo_data_valid():
|
|
|
|
|
geo = GeoData(lat=55.7558, lon=37.6173, accuracy=15.0)
|
|
|
|
|
assert geo.lat == 55.7558
|
|
|
|
|
assert geo.lon == 37.6173
|
|
|
|
|
assert geo.accuracy == 15.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_geo_data_lat_out_of_range_high():
|
|
|
|
|
with pytest.raises(ValidationError):
|
|
|
|
|
GeoData(lat=90.1, lon=0.0, accuracy=10.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_geo_data_lat_out_of_range_low():
|
|
|
|
|
with pytest.raises(ValidationError):
|
|
|
|
|
GeoData(lat=-90.1, lon=0.0, accuracy=10.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_geo_data_lon_out_of_range_high():
|
|
|
|
|
with pytest.raises(ValidationError):
|
|
|
|
|
GeoData(lat=0.0, lon=180.1, accuracy=10.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_geo_data_lon_out_of_range_low():
|
|
|
|
|
with pytest.raises(ValidationError):
|
|
|
|
|
GeoData(lat=0.0, lon=-180.1, accuracy=10.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_geo_data_accuracy_zero():
|
|
|
|
|
"""accuracy must be strictly > 0."""
|
|
|
|
|
with pytest.raises(ValidationError):
|
|
|
|
|
GeoData(lat=0.0, lon=0.0, accuracy=0.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_geo_data_boundary_values():
|
|
|
|
|
"""Boundary values -90/90 lat and -180/180 lon are valid."""
|
|
|
|
|
geo = GeoData(lat=90.0, lon=180.0, accuracy=1.0)
|
|
|
|
|
assert geo.lat == 90.0
|
|
|
|
|
assert geo.lon == 180.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# SignalRequest
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
def test_signal_request_valid():
|
|
|
|
|
req = SignalRequest(
|
|
|
|
|
user_id="550e8400-e29b-41d4-a716-446655440000",
|
|
|
|
|
timestamp=1742478000000,
|
|
|
|
|
geo={"lat": 55.7558, "lon": 37.6173, "accuracy": 15.0},
|
|
|
|
|
)
|
|
|
|
|
assert req.user_id == "550e8400-e29b-41d4-a716-446655440000"
|
|
|
|
|
assert req.timestamp == 1742478000000
|
|
|
|
|
assert req.geo is not None
|
|
|
|
|
assert req.geo.lat == 55.7558
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_signal_request_no_geo():
|
|
|
|
|
req = SignalRequest(
|
2026-03-21 08:12:01 +02:00
|
|
|
user_id="550e8400-e29b-41d4-a716-446655440000",
|
2026-03-20 20:44:00 +02:00
|
|
|
timestamp=1742478000000,
|
|
|
|
|
geo=None,
|
|
|
|
|
)
|
|
|
|
|
assert req.geo is None
|
|
|
|
|
|
|
|
|
|
|
2026-03-21 14:14:12 +02:00
|
|
|
def test_signal_request_without_user_id():
|
|
|
|
|
"""user_id is optional (JWT auth sends signals without it)."""
|
|
|
|
|
req = SignalRequest(timestamp=1742478000000)
|
|
|
|
|
assert req.user_id is None
|
2026-03-20 20:44:00 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_signal_request_empty_user_id():
|
2026-03-21 14:14:12 +02:00
|
|
|
"""Empty string user_id is accepted (treated as None at endpoint level)."""
|
|
|
|
|
req = SignalRequest(user_id="", timestamp=1742478000000)
|
|
|
|
|
assert req.user_id == ""
|
2026-03-20 20:44:00 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_signal_request_timestamp_zero():
|
|
|
|
|
"""timestamp must be > 0."""
|
|
|
|
|
with pytest.raises(ValidationError):
|
2026-03-21 08:12:01 +02:00
|
|
|
SignalRequest(user_id="550e8400-e29b-41d4-a716-446655440000", timestamp=0)
|
2026-03-20 20:44:00 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_signal_request_timestamp_negative():
|
|
|
|
|
with pytest.raises(ValidationError):
|
2026-03-21 08:12:01 +02:00
|
|
|
SignalRequest(user_id="550e8400-e29b-41d4-a716-446655440000", timestamp=-1)
|