release: bump version to 0.53.0
Some checks failed
ci / backend (push) Has been cancelled
ci / frontend (push) Has been cancelled
ci / delivery (push) Has been cancelled
release / images (push) Has been cancelled

This commit is contained in:
rayd1o
2026-05-13 08:05:43 +08:00
parent b87cb310fd
commit d9efd98d26
56 changed files with 2318 additions and 243 deletions

View File

@@ -18,6 +18,17 @@ from app.schemas.ai import (
)
class _FakeRedisClient:
def sismember(self, *_args, **_kwargs):
return False
@pytest.fixture(autouse=True)
def fake_token_blacklist(monkeypatch):
"""Keep API auth tests independent from an external Redis service."""
monkeypatch.setattr("app.core.security.redis_client", _FakeRedisClient())
@pytest.fixture
def auth_headers():
"""Create authentication headers"""
@@ -62,20 +73,43 @@ async def test_dashboard_stats_without_auth():
@pytest.mark.asyncio
async def test_dashboard_stats_with_auth(auth_headers):
"""Test dashboard stats with authentication"""
with patch("app.api.v1.dashboard.cache.get", return_value=None):
with patch("app.api.v1.dashboard.cache.set", return_value=True):
with patch("app.db.session.get_db") as mock_get_db:
mock_session = AsyncMock()
mock_result = AsyncMock()
mock_result.scalar.return_value = 0
mock_result.fetchall.return_value = []
mock_session.execute.return_value = mock_result
class _StatsResult:
def __init__(self, row):
self._row = row
async def mock_db_context():
yield mock_session
def one(self):
return self._row
mock_get_db.return_value = mock_db_context()
class _FakeStatsSession:
def __init__(self):
self._rows = [
type("DatasourceStats", (), {"custom_count": 0, "custom_active": 0})(),
type("TaskStats", (), {"tasks_today": 0, "success_tasks": 0})(),
type(
"AlertStats",
(),
{"critical_alerts": 0, "warning_alerts": 0, "info_alerts": 0},
)(),
]
async def execute(self, _query):
return _StatsResult(self._rows.pop(0))
def override_get_current_user():
return User(id=1, username="testuser", email="test@example.com", role="admin", is_active=True)
async def override_get_db():
yield _FakeStatsSession()
app.dependency_overrides.update(
{
__import__("app.core.security", fromlist=["get_current_user"]).get_current_user: override_get_current_user,
get_db: override_get_db,
}
)
try:
with patch("app.api.v1.dashboard.cache.get", return_value=None):
with patch("app.api.v1.dashboard.cache.set", return_value=True):
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.get(
@@ -85,6 +119,8 @@ async def test_dashboard_stats_with_auth(auth_headers):
assert response.status_code == 200
data = response.json()
assert "total_datasources" in data
finally:
app.dependency_overrides.clear()
@pytest.mark.asyncio

View File

@@ -1,4 +1,5 @@
from fastapi import HTTPException
import pytest
from app.api.v1 import layers
@@ -42,3 +43,28 @@ def test_layer_guard_filters_bbox_and_clamps_low_zoom_limit():
assert result["diagnostics"]["limit"] == layers.LOW_ZOOM_FEATURE_LIMIT
assert result["diagnostics"]["limit_clamped"] is True
assert result["diagnostics"]["degraded"] is True
@pytest.mark.asyncio
async def test_vessel_layer_snapshot_passes_type_filter(monkeypatch):
captured = {}
async def fake_build_vessel_snapshot_response(db, **kwargs):
captured.update(kwargs)
return {"type": "FeatureCollection", "features": []}
monkeypatch.setattr(layers, "build_vessel_snapshot_response", fake_build_vessel_snapshot_response)
result = await layers.get_vessel_layer_snapshot(
bbox="10,59,11,60",
zoom=12,
limit=1000,
vessel_type="cargo",
since_minutes=30,
db=object(),
)
assert result["features"] == []
assert captured["bbox"] == (10.0, 59.0, 11.0, 60.0)
assert captured["type_filter"] == "cargo"
assert "vessel_type" not in captured

View File

@@ -0,0 +1,101 @@
from datetime import UTC, datetime
from unittest.mock import AsyncMock
import pytest
from fastapi import HTTPException
from app.api.v1 import realtime_sources
from app.models.datasource import DataSource
from app.models.datasource_config import DataSourceConfig
from app.models.vessel import AISSourceHealth
@pytest.mark.asyncio
async def test_serialize_builtin_aisstream_includes_health_config_and_stats(monkeypatch):
datasource = DataSource(
id=28,
name="AISStream Vessels",
source="aisstream_vessels",
module="L4",
priority="P1",
collector_class="aisstream_vessels",
is_active=True,
)
config = DataSourceConfig(
id=3,
name="aisstream_vessels",
source_type="websocket",
endpoint="wss://stream.aisstream.io/v0/stream",
auth_type="api_key",
auth_config={"api_key": "test-key"},
config={
"message_types": ["PositionReport"],
"bounding_boxes": [[[-10, 50], [35, 75]]],
},
is_active=True,
)
health = AISSourceHealth(
source="aisstream_vessels",
connection_state="connected",
last_seen_at=datetime(2026, 5, 13, 1, 0, tzinfo=UTC),
)
class _Session:
async def get(self, _model, key):
assert key == "aisstream_vessels"
return health
monkeypatch.setattr(
realtime_sources,
"_load_realtime_stats",
AsyncMock(
return_value={
"total_observations": 10,
"observations_24h": 4,
"observations_1h": 1,
"unique_mmsi_total": 8,
"unique_mmsi_24h": 3,
"latest_observed_at": "2026-05-13T01:00:00Z",
"latest_collected_at": "2026-05-13T01:00:01Z",
}
),
)
monkeypatch.setattr(realtime_sources, "is_collector_running", lambda source: False)
payload = await realtime_sources._serialize_builtin_aisstream(_Session(), datasource, config)
assert payload["source"] == "aisstream_vessels"
assert payload["kind"] == "builtin"
assert payload["credential_configured"] is True
assert payload["message_types"] == ["PositionReport"]
assert payload["runtime"]["running"] is False
assert payload["health"]["connection_state"] == "connected"
assert payload["stats"]["total_observations"] == 10
@pytest.mark.asyncio
async def test_start_builtin_realtime_source_rejects_disabled(monkeypatch):
datasource = DataSource(
id=28,
name="AISStream Vessels",
source="aisstream_vessels",
module="L4",
priority="P1",
collector_class="aisstream_vessels",
is_active=False,
)
monkeypatch.setattr(
realtime_sources,
"_load_builtin_aisstream",
AsyncMock(return_value=(datasource, None)),
)
with pytest.raises(HTTPException) as excinfo:
await realtime_sources.start_realtime_source(
"aisstream_vessels",
current_user=object(),
db=object(),
)
assert excinfo.value.status_code == 400
assert "disabled" in excinfo.value.detail

View File

@@ -574,13 +574,12 @@ async def test_vessel_snapshot_filters_type_and_bbox(monkeypatch):
@pytest.mark.asyncio
async def test_legacy_vessels_geojson_endpoint_is_gone():
async def test_legacy_vessels_geojson_route_is_not_registered():
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.get("/api/v1/visualization/geo/vessels")
assert response.status_code == 410
assert "/api/v1/vessels/snapshot" in response.json()["detail"]
assert response.status_code == 404
@pytest.mark.asyncio
@@ -669,15 +668,49 @@ async def test_vessel_snapshot_filters_bbox_and_caps_limit(monkeypatch):
@pytest.mark.asyncio
async def test_legacy_vessels_geojson_rejects_even_with_bbox():
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.get(
"/api/v1/visualization/geo/vessels",
params={"bbox": "10,59,11,60", "type": "cargo", "limit": 1000},
)
async def test_vessel_snapshot_uses_legacy_fallback_when_raw_window_is_empty(monkeypatch):
now = datetime(2026, 4, 28, 1, 0, tzinfo=timezone.utc)
monkeypatch.setattr(
visualization,
"get_aggregated_vessels_snapshot",
AsyncMock(return_value=[]),
)
monkeypatch.setattr(
visualization,
"_load_legacy_vessel_snapshot_features",
AsyncMock(
return_value=[
{
"type": "Feature",
"id": 257123000,
"geometry": {"type": "Point", "coordinates": [10.73, 59.91]},
"properties": {
"mmsi": 257123000,
"name": "OSLO TRADER",
"vessel_type": 70,
"vessel_type_name": "Cargo",
"received_at": now.isoformat(),
},
}
]
),
)
assert response.status_code == 410
result = await visualization.build_vessel_snapshot_response(
object(),
bbox=(10.0, 59.0, 11.0, 60.0),
zoom=12,
type_filter=None,
limit=1000,
since_minutes=60,
)
assert result["count"] == 1
assert result["features"][0]["properties"]["name"] == "OSLO TRADER"
assert result["diagnostics"]["raw_feature_count"] == 0
assert result["diagnostics"]["legacy_feature_count"] == 1
assert result["diagnostics"]["legacy_backfilled_mmsi"] == 1
assert result["diagnostics"]["legacy_fallback_used"] is True
@pytest.mark.asyncio