- Introduced CombinedTrackModel, CombinedTrackCreate, and CombinedTrackRepository for managing combined tracks. - Implemented logic to create combined tracks based on existing tracks between two stations. - Added methods to check for existing combined tracks and retrieve constituent track IDs. - Enhanced TrackModel and TrackRepository to support OSM ID and track updates. - Created migration scripts for adding combined tracks table and OSM ID to tracks. - Updated services and API endpoints to handle combined track operations. - Added tests for combined track creation, repository methods, and API interactions.
159 lines
4.5 KiB
Python
159 lines
4.5 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import datetime, timezone
|
|
from typing import Any
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
from backend.app.api import tracks as tracks_api
|
|
from backend.app.main import app
|
|
from backend.app.models import CombinedTrackModel, TrackModel
|
|
|
|
client = TestClient(app)
|
|
|
|
|
|
def _track_model(track_id: str = "track-1") -> TrackModel:
|
|
now = datetime.now(timezone.utc)
|
|
return TrackModel(
|
|
id=track_id,
|
|
start_station_id="station-a",
|
|
end_station_id="station-b",
|
|
length_meters=None,
|
|
max_speed_kph=None,
|
|
status="planned",
|
|
coordinates=[(52.5, 13.4), (52.6, 13.5)],
|
|
is_bidirectional=True,
|
|
created_at=now,
|
|
updated_at=now,
|
|
)
|
|
|
|
|
|
def _combined_model(track_id: str = "combined-1") -> CombinedTrackModel:
|
|
now = datetime.now(timezone.utc)
|
|
return CombinedTrackModel(
|
|
id=track_id,
|
|
start_station_id="station-a",
|
|
end_station_id="station-b",
|
|
length_meters=1000,
|
|
max_speed_kph=120,
|
|
status="operational",
|
|
coordinates=[(52.5, 13.4), (52.6, 13.5)],
|
|
constituent_track_ids=["track-1", "track-2"],
|
|
is_bidirectional=True,
|
|
created_at=now,
|
|
updated_at=now,
|
|
)
|
|
|
|
|
|
def _authenticate() -> str:
|
|
response = client.post(
|
|
"/api/auth/login",
|
|
json={"username": "demo", "password": "railgame123"},
|
|
)
|
|
assert response.status_code == 200
|
|
return response.json()["accessToken"]
|
|
|
|
|
|
def test_list_tracks(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
token = _authenticate()
|
|
monkeypatch.setattr(tracks_api, "list_tracks", lambda db: [_track_model()])
|
|
|
|
response = client.get(
|
|
"/api/tracks",
|
|
headers={"Authorization": f"Bearer {token}"},
|
|
)
|
|
assert response.status_code == 200
|
|
payload = response.json()
|
|
assert isinstance(payload, list)
|
|
assert payload[0]["id"] == "track-1"
|
|
|
|
|
|
def test_get_track_returns_404(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
token = _authenticate()
|
|
monkeypatch.setattr(tracks_api, "get_track", lambda db, track_id: None)
|
|
|
|
response = client.get(
|
|
"/api/tracks/not-found",
|
|
headers={"Authorization": f"Bearer {token}"},
|
|
)
|
|
assert response.status_code == 404
|
|
|
|
|
|
def test_create_track_calls_service(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
token = _authenticate()
|
|
captured: dict[str, Any] = {}
|
|
payload = {
|
|
"startStationId": "station-a",
|
|
"endStationId": "station-b",
|
|
"coordinates": [[52.5, 13.4], [52.6, 13.5]],
|
|
}
|
|
|
|
def fake_create(db: Any, data: Any) -> TrackModel:
|
|
assert data.start_station_id == "station-a"
|
|
captured["payload"] = data
|
|
return _track_model("track-new")
|
|
|
|
monkeypatch.setattr(tracks_api, "create_track", fake_create)
|
|
|
|
response = client.post(
|
|
"/api/tracks",
|
|
json=payload,
|
|
headers={"Authorization": f"Bearer {token}"},
|
|
)
|
|
assert response.status_code == 201
|
|
body = response.json()
|
|
assert body["id"] == "track-new"
|
|
assert captured["payload"].end_station_id == "station-b"
|
|
|
|
|
|
def test_delete_track_returns_404(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
token = _authenticate()
|
|
monkeypatch.setattr(
|
|
tracks_api, "delete_track", lambda db, tid, regenerate=False: False
|
|
)
|
|
|
|
response = client.delete(
|
|
"/api/tracks/missing",
|
|
headers={"Authorization": f"Bearer {token}"},
|
|
)
|
|
assert response.status_code == 404
|
|
|
|
|
|
def test_delete_track_success(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
token = _authenticate()
|
|
seen: dict[str, Any] = {}
|
|
|
|
def fake_delete(db: Any, track_id: str, regenerate: bool = False) -> bool:
|
|
seen["track_id"] = track_id
|
|
seen["regenerate"] = regenerate
|
|
return True
|
|
|
|
monkeypatch.setattr(tracks_api, "delete_track", fake_delete)
|
|
|
|
response = client.delete(
|
|
"/api/tracks/track-99",
|
|
params={"regenerate": "true"},
|
|
headers={"Authorization": f"Bearer {token}"},
|
|
)
|
|
|
|
assert response.status_code == 204
|
|
assert seen["track_id"] == "track-99"
|
|
assert seen["regenerate"] is True
|
|
|
|
|
|
def test_list_combined_tracks(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
token = _authenticate()
|
|
monkeypatch.setattr(
|
|
tracks_api, "list_combined_tracks", lambda db: [_combined_model()]
|
|
)
|
|
|
|
response = client.get(
|
|
"/api/tracks/combined",
|
|
headers={"Authorization": f"Bearer {token}"},
|
|
)
|
|
assert response.status_code == 200
|
|
payload = response.json()
|
|
assert len(payload) == 1
|
|
assert payload[0]["id"] == "combined-1"
|