733 lines
22 KiB
Python
733 lines
22 KiB
Python
from datetime import datetime, timedelta, timezone
|
|
from unittest.mock import AsyncMock
|
|
|
|
import pytest
|
|
from httpx import ASGITransport, AsyncClient
|
|
|
|
from app.api.v1 import visualization
|
|
from app.api.v1.visualization import convert_vessels_to_geojson
|
|
from app.db.session import get_db
|
|
from app.main import app
|
|
from app.models.vessel import AISRawObservation, VesselPosition, VesselStatic
|
|
from app.services import barentswatch
|
|
from app.services.collectors.aisstream import AISStreamCollector
|
|
from app.services.collectors.vessel_ais import VesselAISCollector
|
|
from app.services.vessel_ais_aggregation import (
|
|
aggregate_vessel_observations,
|
|
build_field_conflict_candidates,
|
|
build_observation_hash,
|
|
record_vessel_ais_observation,
|
|
)
|
|
|
|
|
|
def test_vessel_collector_transforms_barentswatch_like_records():
|
|
collector = VesselAISCollector()
|
|
records = collector.transform(
|
|
[
|
|
{
|
|
"mmsi": "257123000",
|
|
"lat": "59.91",
|
|
"lon": "10.73",
|
|
"sog": 12.4,
|
|
"cog": 214,
|
|
"nav_status": 0,
|
|
"shipType": 70,
|
|
"name": "OSLO TRADER",
|
|
},
|
|
{"mmsi": "bad", "lat": 120, "lon": 10},
|
|
]
|
|
)
|
|
|
|
assert len(records) == 1
|
|
assert records[0]["mmsi"] == 257123000
|
|
assert records[0]["vessel_type_name"] == "Cargo"
|
|
assert records[0]["lat"] == pytest.approx(59.91)
|
|
|
|
|
|
def test_vessel_observation_hash_is_stable_for_same_payload():
|
|
observed_at = datetime(2026, 4, 30, 12, 0, tzinfo=timezone.utc)
|
|
payload = {
|
|
"mmsi": 257123000,
|
|
"lat": 59.91,
|
|
"lon": 10.73,
|
|
"received_at": observed_at,
|
|
}
|
|
|
|
first = build_observation_hash(
|
|
source="barentswatch_vessels",
|
|
entity_key="257123000",
|
|
message_type="PositionReport",
|
|
observed_at=observed_at,
|
|
normalized_payload=payload,
|
|
)
|
|
second = build_observation_hash(
|
|
source="barentswatch_vessels",
|
|
entity_key="257123000",
|
|
message_type="PositionReport",
|
|
observed_at=observed_at,
|
|
normalized_payload=dict(reversed(payload.items())),
|
|
)
|
|
|
|
assert first == second
|
|
assert len(first) == 64
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_record_vessel_ais_observation_skips_existing_hash():
|
|
observed_at = datetime(2026, 4, 30, 12, 0, tzinfo=timezone.utc)
|
|
|
|
class _Result:
|
|
def scalar_one_or_none(self):
|
|
return 123
|
|
|
|
class _Session:
|
|
def __init__(self):
|
|
self.added = []
|
|
|
|
async def execute(self, _stmt):
|
|
return _Result()
|
|
|
|
def add(self, item):
|
|
self.added.append(item)
|
|
|
|
db = _Session()
|
|
observation = await record_vessel_ais_observation(
|
|
db,
|
|
source="barentswatch_vessels",
|
|
normalized_payload={
|
|
"mmsi": 257123000,
|
|
"lat": 59.91,
|
|
"lon": 10.73,
|
|
"received_at": observed_at,
|
|
},
|
|
delivery_mode="polling",
|
|
transport="http",
|
|
observed_at=observed_at.isoformat(),
|
|
)
|
|
|
|
assert observation is None
|
|
assert db.added == []
|
|
|
|
|
|
def test_build_field_conflict_candidates_from_raw_observations():
|
|
observations = [
|
|
AISRawObservation(
|
|
source="barentswatch_vessels",
|
|
normalized_payload={"name": "OSLO TRADER", "flag": "NO"},
|
|
),
|
|
AISRawObservation(
|
|
source="aisstream_vessels",
|
|
normalized_payload={"name": "OSLO TRADER II", "flag": "NO"},
|
|
),
|
|
]
|
|
|
|
conflicts = build_field_conflict_candidates(observations)
|
|
|
|
assert conflicts == [
|
|
{
|
|
"field": "name",
|
|
"candidates": {
|
|
"aisstream_vessels": "OSLO TRADER II",
|
|
"barentswatch_vessels": "OSLO TRADER",
|
|
},
|
|
"status": "candidate",
|
|
}
|
|
]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_aggregate_vessel_observations_prefers_realtime_and_records_conflict():
|
|
observed_at = datetime.now(timezone.utc) - timedelta(minutes=5)
|
|
|
|
class _Result:
|
|
def scalar_one_or_none(self):
|
|
return None
|
|
|
|
class _Session:
|
|
def __init__(self):
|
|
self.added = []
|
|
|
|
async def execute(self, _stmt):
|
|
return _Result()
|
|
|
|
def add(self, item):
|
|
self.added.append(item)
|
|
|
|
db = _Session()
|
|
observations = [
|
|
AISRawObservation(
|
|
id=1,
|
|
source="barentswatch_vessels",
|
|
entity_key="257123000",
|
|
delivery_mode="polling",
|
|
transport="http",
|
|
observed_at=observed_at,
|
|
collected_at=observed_at,
|
|
normalized_payload={
|
|
"mmsi": 257123000,
|
|
"name": "OSLO TRADER",
|
|
"lat": 59.91,
|
|
"lon": 10.73,
|
|
},
|
|
),
|
|
AISRawObservation(
|
|
id=2,
|
|
source="aisstream_vessels",
|
|
entity_key="257123000",
|
|
delivery_mode="realtime_stream",
|
|
transport="websocket",
|
|
observed_at=observed_at + timedelta(seconds=10),
|
|
collected_at=observed_at + timedelta(seconds=10),
|
|
normalized_payload={
|
|
"mmsi": 257123000,
|
|
"vessel_type": 79,
|
|
"lat": 59.92,
|
|
"lon": 10.74,
|
|
},
|
|
raw_payload={"MetaData": {"ShipName": "OSLO TRADER II "}},
|
|
),
|
|
]
|
|
|
|
vessels = await aggregate_vessel_observations(db, observations)
|
|
|
|
assert vessels[0]["lat"] == pytest.approx(59.92)
|
|
assert vessels[0]["field_sources"]["lat"] == "aisstream_vessels"
|
|
assert vessels[0]["name"] == "OSLO TRADER II"
|
|
assert vessels[0]["vessel_type_name"] == "Cargo"
|
|
assert vessels[0]["source_summary"]["aisstream_vessels"]["observation_count"] == 1
|
|
assert vessels[0]["source_summary"]["barentswatch_vessels"]["delivery_mode"] == "polling"
|
|
assert vessels[0]["conflict_count"] == 0
|
|
assert db.added == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_vessel_collector_writes_raw_observations_only(monkeypatch):
|
|
collector = VesselAISCollector()
|
|
collector.update_progress = AsyncMock()
|
|
record_observation = AsyncMock()
|
|
update_health = AsyncMock()
|
|
broadcast_custom = AsyncMock()
|
|
monkeypatch.setattr(
|
|
"app.services.collectors.vessel_ais.record_vessel_ais_observation",
|
|
record_observation,
|
|
)
|
|
monkeypatch.setattr(
|
|
"app.services.collectors.vessel_ais.update_ais_source_health",
|
|
update_health,
|
|
)
|
|
monkeypatch.setattr(
|
|
"app.services.collectors.vessel_ais.broadcaster.broadcast_custom",
|
|
broadcast_custom,
|
|
)
|
|
|
|
class _Session:
|
|
def __init__(self):
|
|
self.added = []
|
|
self.committed = False
|
|
|
|
async def get(self, *_args):
|
|
return None
|
|
|
|
def add(self, item):
|
|
self.added.append(item)
|
|
|
|
async def execute(self, _stmt):
|
|
return None
|
|
|
|
async def commit(self):
|
|
self.committed = True
|
|
|
|
db = _Session()
|
|
observed_at = datetime(2026, 4, 30, 12, 0, tzinfo=timezone.utc)
|
|
|
|
saved = await collector._save_data(
|
|
db,
|
|
[
|
|
{
|
|
"mmsi": 257123000,
|
|
"name": "OSLO TRADER",
|
|
"lat": 59.91,
|
|
"lon": 10.73,
|
|
"received_at": observed_at,
|
|
}
|
|
],
|
|
)
|
|
|
|
assert saved == 1
|
|
assert db.committed is True
|
|
# BarentsWatch must funnel through the unified AIS pipeline only — no legacy writes.
|
|
assert not any(isinstance(item, VesselStatic) for item in db.added)
|
|
assert not any(isinstance(item, VesselPosition) for item in db.added)
|
|
record_observation.assert_awaited_once()
|
|
assert record_observation.await_args.kwargs["source"] == "barentswatch_vessels"
|
|
assert record_observation.await_args.kwargs["normalized_payload"]["mmsi"] == 257123000
|
|
update_health.assert_awaited_once()
|
|
broadcast_custom.assert_awaited_once()
|
|
assert broadcast_custom.await_args.args[0] == "vessels"
|
|
assert broadcast_custom.await_args.args[1]["action"] == "upsert"
|
|
assert broadcast_custom.await_args.args[1]["vessels"][0]["mmsi_display"] == "257123000"
|
|
|
|
|
|
def test_aisstream_collector_normalizes_position_report():
|
|
collector = AISStreamCollector()
|
|
|
|
records = collector.transform(
|
|
[
|
|
{
|
|
"MessageType": "PositionReport",
|
|
"MetaData": {
|
|
"MMSI": 257123000,
|
|
"ShipName": "OSLO TRADER ",
|
|
"time_utc": "2026-04-30T12:00:00Z",
|
|
},
|
|
"Message": {
|
|
"PositionReport": {
|
|
"Latitude": 59.91,
|
|
"Longitude": 10.73,
|
|
"Sog": 12.4,
|
|
"Cog": 214,
|
|
"TrueHeading": 215,
|
|
"NavigationalStatus": 0,
|
|
}
|
|
},
|
|
}
|
|
]
|
|
)
|
|
|
|
assert len(records) == 1
|
|
assert records[0]["mmsi"] == 257123000
|
|
assert records[0]["lat"] == pytest.approx(59.91)
|
|
assert records[0]["name"] == "OSLO TRADER"
|
|
assert records[0]["_message_type"] == "PositionReport"
|
|
|
|
|
|
def test_aisstream_collector_maps_ship_static_type_name():
|
|
collector = AISStreamCollector()
|
|
|
|
records = collector.transform(
|
|
[
|
|
{
|
|
"MessageType": "ShipStaticData",
|
|
"MetaData": {
|
|
"MMSI": 257123000,
|
|
"time_utc": "2026-04-30T12:00:00Z",
|
|
},
|
|
"Message": {
|
|
"ShipStaticData": {
|
|
"Name": "OSLO TRADER",
|
|
"Type": 79,
|
|
"CallSign": "LAAB",
|
|
}
|
|
},
|
|
}
|
|
]
|
|
)
|
|
|
|
assert len(records) == 1
|
|
assert records[0]["vessel_type"] == 79
|
|
assert records[0]["vessel_type_name"] == "Cargo"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_aisstream_collector_writes_only_raw_observations(monkeypatch):
|
|
collector = AISStreamCollector()
|
|
collector.update_progress = AsyncMock()
|
|
record_observation = AsyncMock(return_value=object())
|
|
update_health = AsyncMock()
|
|
monkeypatch.setattr(
|
|
"app.services.collectors.aisstream.record_vessel_ais_observation",
|
|
record_observation,
|
|
)
|
|
monkeypatch.setattr(
|
|
"app.services.collectors.aisstream.update_ais_source_health",
|
|
update_health,
|
|
)
|
|
|
|
class _Session:
|
|
def __init__(self):
|
|
self.added = []
|
|
self.committed = False
|
|
|
|
def add(self, item):
|
|
self.added.append(item)
|
|
|
|
async def commit(self):
|
|
self.committed = True
|
|
|
|
db = _Session()
|
|
saved = await collector._save_data(
|
|
db,
|
|
[
|
|
{
|
|
"mmsi": 257123000,
|
|
"lat": 59.91,
|
|
"lon": 10.73,
|
|
"received_at": datetime(2026, 4, 30, 12, 0, tzinfo=timezone.utc),
|
|
"_message_type": "PositionReport",
|
|
}
|
|
],
|
|
)
|
|
|
|
assert saved == 1
|
|
assert db.added == []
|
|
assert db.committed is True
|
|
record_observation.assert_awaited_once()
|
|
assert record_observation.await_args.kwargs["source"] == "aisstream_vessels"
|
|
update_health.assert_awaited_once()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_aisstream_stream_record_broadcasts_vessel_delta(monkeypatch):
|
|
collector = AISStreamCollector()
|
|
record_observation = AsyncMock(return_value=object())
|
|
update_health = AsyncMock()
|
|
broadcast_custom = AsyncMock()
|
|
monkeypatch.setattr(
|
|
"app.services.collectors.aisstream.record_vessel_ais_observation",
|
|
record_observation,
|
|
)
|
|
monkeypatch.setattr(
|
|
"app.services.collectors.aisstream.update_ais_source_health",
|
|
update_health,
|
|
)
|
|
monkeypatch.setattr(
|
|
"app.services.collectors.aisstream.broadcaster.broadcast_custom",
|
|
broadcast_custom,
|
|
)
|
|
|
|
class _Session:
|
|
async def commit(self):
|
|
pass
|
|
|
|
created = await collector._save_stream_record(
|
|
_Session(),
|
|
{
|
|
"mmsi": 257123000,
|
|
"lat": 59.91,
|
|
"lon": 10.73,
|
|
"cog": 214,
|
|
"received_at": datetime(2026, 4, 30, 12, 0, tzinfo=timezone.utc),
|
|
},
|
|
)
|
|
|
|
assert created is True
|
|
record_observation.assert_awaited_once()
|
|
broadcast_custom.assert_awaited_once()
|
|
assert broadcast_custom.await_args.args[0] == "vessels"
|
|
assert broadcast_custom.await_args.args[1]["action"] == "upsert"
|
|
assert broadcast_custom.await_args.args[1]["vessels"][0]["mmsi_display"] == "257123000"
|
|
|
|
|
|
def test_barentswatch_reads_credentials_from_zshrc(tmp_path):
|
|
zshrc = tmp_path / ".zshrc"
|
|
zshrc.write_text(
|
|
"\n".join(
|
|
[
|
|
"export BARENTSWATCH_CLIENT_ID='client-from-zshrc'",
|
|
'export BARENTSWATCH_CLIENT_SECRET="secret-from-zshrc" # local dev credential',
|
|
]
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
values = barentswatch._read_zshrc_env(zshrc)
|
|
|
|
assert values["BARENTSWATCH_CLIENT_ID"] == "client-from-zshrc"
|
|
assert values["BARENTSWATCH_CLIENT_SECRET"] == "secret-from-zshrc"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_barentswatch_resolves_config_from_zshrc(tmp_path, monkeypatch):
|
|
zshrc = tmp_path / ".zshrc"
|
|
zshrc.write_text(
|
|
"\n".join(
|
|
[
|
|
"export BARENTSWATCH_CLIENT_ID=client-from-zshrc",
|
|
"export BARENTSWATCH_CLIENT_SECRET=secret-from-zshrc",
|
|
]
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
monkeypatch.delenv("BARENTSWATCH_CLIENT_ID", raising=False)
|
|
monkeypatch.delenv("BARENTSWATCH_CLIENT_SECRET", raising=False)
|
|
monkeypatch.delenv("BARRENTSWATCH_CLIENT_ID", raising=False)
|
|
monkeypatch.delenv("BARRENTSWATCH_CLIENT_SECRET", raising=False)
|
|
monkeypatch.setattr(barentswatch.Path, "home", lambda: tmp_path)
|
|
|
|
config = await barentswatch.resolve_barentswatch_config(None)
|
|
|
|
assert config.client_id == "client-from-zshrc"
|
|
assert config.client_secret == "secret-from-zshrc"
|
|
assert config.credential_source == "~/.zshrc"
|
|
|
|
|
|
def test_convert_vessels_to_geojson():
|
|
position = VesselPosition(
|
|
mmsi=257123000,
|
|
lat=59.91,
|
|
lon=10.73,
|
|
sog=12.4,
|
|
cog=214,
|
|
heading=215,
|
|
nav_status=0,
|
|
received_at=datetime(2026, 4, 28, 1, 0, tzinfo=timezone.utc),
|
|
)
|
|
static = VesselStatic(
|
|
mmsi=257123000,
|
|
name="OSLO TRADER",
|
|
vessel_type=70,
|
|
vessel_type_name="Cargo",
|
|
flag="NO",
|
|
length=185,
|
|
)
|
|
|
|
payload = convert_vessels_to_geojson([(position, static)])
|
|
|
|
assert payload["type"] == "FeatureCollection"
|
|
assert payload["features"][0]["geometry"]["coordinates"] == [10.73, 59.91]
|
|
assert payload["features"][0]["properties"]["mmsi"] == 257123000
|
|
assert payload["features"][0]["properties"]["vessel_type_name"] == "Cargo"
|
|
|
|
|
|
def test_convert_vessels_to_geojson_dedupes_mmsi_rows():
|
|
first = VesselPosition(
|
|
mmsi=257123000,
|
|
lat=59.91,
|
|
lon=10.73,
|
|
received_at=datetime(2026, 4, 28, 1, 0, tzinfo=timezone.utc),
|
|
)
|
|
duplicate = VesselPosition(
|
|
mmsi=257123000,
|
|
lat=60.01,
|
|
lon=10.83,
|
|
received_at=datetime(2026, 4, 28, 1, 0, tzinfo=timezone.utc),
|
|
)
|
|
other = VesselPosition(
|
|
mmsi=257456000,
|
|
lat=60.3,
|
|
lon=5.3,
|
|
received_at=datetime(2026, 4, 28, 0, 59, tzinfo=timezone.utc),
|
|
)
|
|
|
|
payload = convert_vessels_to_geojson(
|
|
[
|
|
(first, VesselStatic(mmsi=257123000, name="OSLO TRADER")),
|
|
(duplicate, VesselStatic(mmsi=257123000, name="OSLO TRADER DUP")),
|
|
(other, VesselStatic(mmsi=257456000, name="BERGEN FERRY")),
|
|
]
|
|
)
|
|
|
|
mmsis = [feature["properties"]["mmsi"] for feature in payload["features"]]
|
|
assert mmsis == [257123000, 257456000]
|
|
assert payload["features"][0]["geometry"]["coordinates"] == [10.73, 59.91]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_vessel_snapshot_filters_type_and_bbox(monkeypatch):
|
|
now = datetime(2026, 4, 28, 1, 0, tzinfo=timezone.utc)
|
|
monkeypatch.setattr(
|
|
visualization,
|
|
"get_aggregated_vessels_snapshot",
|
|
AsyncMock(
|
|
return_value=[
|
|
{
|
|
"mmsi": 1,
|
|
"lat": 59.9,
|
|
"lon": 10.7,
|
|
"received_at": now,
|
|
"name": "Cargo Ship",
|
|
"vessel_type": 70,
|
|
"vessel_type_name": "Cargo",
|
|
},
|
|
{
|
|
"mmsi": 2,
|
|
"lat": 60.3,
|
|
"lon": 5.3,
|
|
"received_at": now - timedelta(minutes=1),
|
|
"name": "Passenger Ship",
|
|
"vessel_type": 60,
|
|
"vessel_type_name": "Passenger",
|
|
},
|
|
]
|
|
),
|
|
)
|
|
|
|
async def override_get_db():
|
|
yield object()
|
|
|
|
app.dependency_overrides[get_db] = override_get_db
|
|
transport = ASGITransport(app=app)
|
|
try:
|
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
|
response = await client.get(
|
|
"/api/v1/vessels/snapshot",
|
|
params={"bbox": "0,50,20,70", "zoom": 12, "type": "cargo", "limit": 1000},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["count"] == 1
|
|
assert data["features"][0]["properties"]["name"] == "Cargo Ship"
|
|
assert data["stats"]["by_type"]["Cargo"] == 1
|
|
finally:
|
|
app.dependency_overrides.clear()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_legacy_vessels_geojson_endpoint_is_gone():
|
|
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"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_vessel_snapshot_requires_bbox():
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
|
response = await client.get("/api/v1/vessels/snapshot", params={"zoom": 12})
|
|
|
|
assert response.status_code == 400
|
|
assert response.json()["detail"] == "bbox is required"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_vessel_snapshot_filters_bbox_and_caps_limit(monkeypatch):
|
|
now = datetime(2026, 4, 28, 1, 0, tzinfo=timezone.utc)
|
|
captured = {}
|
|
|
|
async def fake_get_aggregated_vessels_snapshot(db, *, bbox, limit, observed_since):
|
|
captured["bbox"] = bbox
|
|
captured["limit"] = limit
|
|
captured["observed_since"] = observed_since
|
|
return [
|
|
{
|
|
"mmsi": 1,
|
|
"lat": 59.9,
|
|
"lon": 10.7,
|
|
"received_at": now,
|
|
"name": "Cargo Ship",
|
|
"vessel_type": 70,
|
|
"vessel_type_name": "Cargo",
|
|
},
|
|
{
|
|
"mmsi": 2,
|
|
"lat": 60.3,
|
|
"lon": 5.3,
|
|
"received_at": now,
|
|
"name": "Passenger Ship",
|
|
"vessel_type": 60,
|
|
"vessel_type_name": "Passenger",
|
|
},
|
|
]
|
|
|
|
monkeypatch.setattr(
|
|
visualization,
|
|
"get_aggregated_vessels_snapshot",
|
|
fake_get_aggregated_vessels_snapshot,
|
|
)
|
|
|
|
class _Result:
|
|
def all(self):
|
|
return []
|
|
|
|
class _FakeSession:
|
|
async def execute(self, _query):
|
|
return _Result()
|
|
|
|
async def override_get_db():
|
|
yield _FakeSession()
|
|
|
|
app.dependency_overrides[get_db] = override_get_db
|
|
transport = ASGITransport(app=app)
|
|
try:
|
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
|
response = await client.get(
|
|
"/api/v1/vessels/snapshot",
|
|
params={
|
|
"bbox": "10,59,11,60",
|
|
"zoom": 12,
|
|
"type": "cargo",
|
|
"limit": 5000,
|
|
"since_minutes": 30,
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["count"] == 1
|
|
assert data["features"][0]["properties"]["name"] == "Cargo Ship"
|
|
assert captured["bbox"] == (10.0, 59.0, 11.0, 60.0)
|
|
assert captured["limit"] == 5000
|
|
assert data["diagnostics"]["bbox_applied"] is True
|
|
assert data["diagnostics"]["legacy_feature_count"] == 0
|
|
assert data["diagnostics"]["legacy_backfilled_mmsi"] == 0
|
|
finally:
|
|
app.dependency_overrides.clear()
|
|
|
|
|
|
@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},
|
|
)
|
|
|
|
assert response.status_code == 410
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_vessel_name_fallbacks_reports_mmsi_display_names(monkeypatch):
|
|
now = datetime(2026, 4, 28, 1, 0, tzinfo=timezone.utc)
|
|
monkeypatch.setattr(
|
|
visualization,
|
|
"get_aggregated_vessels",
|
|
AsyncMock(
|
|
return_value=[
|
|
{
|
|
"mmsi": 257123000,
|
|
"lat": 59.9,
|
|
"lon": 10.7,
|
|
"received_at": now,
|
|
"name": "MMSI 257123000",
|
|
"vessel_type_name": "Other",
|
|
"source_summary": {
|
|
"aisstream_vessels": {
|
|
"latest_observed_at": now,
|
|
"message_types": ["PositionReport"],
|
|
}
|
|
},
|
|
}
|
|
]
|
|
),
|
|
)
|
|
|
|
class _Result:
|
|
def all(self):
|
|
return []
|
|
|
|
class _FakeSession:
|
|
async def execute(self, _query):
|
|
return _Result()
|
|
|
|
async def override_get_db():
|
|
yield _FakeSession()
|
|
|
|
app.dependency_overrides[get_db] = override_get_db
|
|
transport = ASGITransport(app=app)
|
|
try:
|
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
|
response = await client.get("/api/v1/visualization/vessels/name-fallbacks")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["count"] == 1
|
|
assert data["items"][0]["mmsi"] == "257123000"
|
|
assert data["items"][0]["message_types"] == ["PositionReport"]
|
|
finally:
|
|
app.dependency_overrides.clear()
|