feat: Initialize frontend and backend structure with essential configurations
- Added TypeScript build info for frontend. - Created Vite configuration for React application. - Implemented pre-commit hook to run checks before commits. - Set up PostgreSQL Dockerfile with PostGIS support and initialization scripts. - Added database creation script for PostgreSQL with necessary extensions. - Established Python project configuration with dependencies and development tools. - Developed pre-commit script to enforce code quality checks for backend and frontend. - Created PowerShell script to set up Git hooks path.
This commit is contained in:
0
backend/tests/__init__.py
Normal file
0
backend/tests/__init__.py
Normal file
81
backend/tests/test_auth_api.py
Normal file
81
backend/tests/test_auth_api.py
Normal file
@@ -0,0 +1,81 @@
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from backend.app.main import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_login_returns_token_and_user() -> None:
|
||||
response = client.post(
|
||||
"/api/auth/login",
|
||||
json={"username": "demo", "password": "railgame123"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
payload = response.json()
|
||||
assert "accessToken" in payload
|
||||
assert payload["tokenType"] == "bearer"
|
||||
assert payload["user"]["username"] == "demo"
|
||||
|
||||
|
||||
def test_login_with_invalid_credentials_fails() -> None:
|
||||
response = client.post(
|
||||
"/api/auth/login",
|
||||
json={"username": "demo", "password": "wrong"},
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_me_endpoint_returns_current_user() -> None:
|
||||
login = client.post(
|
||||
"/api/auth/login",
|
||||
json={"username": "demo", "password": "railgame123"},
|
||||
)
|
||||
token = login.json()["accessToken"]
|
||||
|
||||
response = client.get(
|
||||
"/api/auth/me", headers={"Authorization": f"Bearer {token}"}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["username"] == "demo"
|
||||
|
||||
|
||||
def test_register_creates_user_and_returns_token() -> None:
|
||||
username = f"player_{uuid4().hex[:8]}"
|
||||
response = client.post(
|
||||
"/api/auth/register",
|
||||
json={
|
||||
"username": username,
|
||||
"password": "testpass123",
|
||||
"fullName": "Test Player",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 201
|
||||
payload = response.json()
|
||||
assert payload["user"]["username"] == username
|
||||
assert payload["tokenType"] == "bearer"
|
||||
|
||||
me = client.get(
|
||||
"/api/auth/me",
|
||||
headers={"Authorization": f"Bearer {payload['accessToken']}"},
|
||||
)
|
||||
assert me.status_code == 200
|
||||
assert me.json()["username"] == username
|
||||
|
||||
|
||||
def test_register_duplicate_username_returns_conflict() -> None:
|
||||
username = f"dupe_{uuid4().hex[:8]}"
|
||||
first = client.post(
|
||||
"/api/auth/register",
|
||||
json={"username": username, "password": "firstpass"},
|
||||
)
|
||||
assert first.status_code == 201
|
||||
|
||||
duplicate = client.post(
|
||||
"/api/auth/register",
|
||||
json={"username": username, "password": "secondpass"},
|
||||
)
|
||||
assert duplicate.status_code == 409
|
||||
assert duplicate.json()["detail"] == "Username already exists"
|
||||
11
backend/tests/test_health.py
Normal file
11
backend/tests/test_health.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from backend.app.main import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_health_check() -> None:
|
||||
response = client.get("/api/health")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"status": "ok"}
|
||||
51
backend/tests/test_models.py
Normal file
51
backend/tests/test_models.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from backend.app.models import StationModel, TrackModel, TrainModel
|
||||
|
||||
|
||||
def _now() -> datetime:
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
def test_station_model_round_trip() -> None:
|
||||
timestamp = _now()
|
||||
station = StationModel(
|
||||
id="station-1",
|
||||
name="Central",
|
||||
latitude=52.52,
|
||||
longitude=13.405,
|
||||
created_at=timestamp,
|
||||
updated_at=timestamp,
|
||||
)
|
||||
assert station.name == "Central"
|
||||
assert station.model_dump()["id"] == "station-1"
|
||||
|
||||
|
||||
def test_track_model_properties() -> None:
|
||||
timestamp = _now()
|
||||
track = TrackModel(
|
||||
id="track-1",
|
||||
start_station_id="station-1",
|
||||
end_station_id="station-2",
|
||||
length_meters=1500.0,
|
||||
max_speed_kph=120.0,
|
||||
created_at=timestamp,
|
||||
updated_at=timestamp,
|
||||
)
|
||||
assert track.length_meters > 0
|
||||
assert track.start_station_id != track.end_station_id
|
||||
|
||||
|
||||
def test_train_model_operating_tracks() -> None:
|
||||
timestamp = _now()
|
||||
train = TrainModel(
|
||||
id="train-1",
|
||||
designation="Express",
|
||||
capacity=350,
|
||||
max_speed_kph=200.0,
|
||||
operating_track_ids=["track-1", "track-2"],
|
||||
created_at=timestamp,
|
||||
updated_at=timestamp,
|
||||
)
|
||||
assert train.capacity >= 0
|
||||
assert len(train.operating_track_ids) == 2
|
||||
35
backend/tests/test_network_api.py
Normal file
35
backend/tests/test_network_api.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from backend.app.main import app
|
||||
|
||||
AUTH_CREDENTIALS = {"username": "demo", "password": "railgame123"}
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def _authenticate() -> str:
|
||||
response = client.post("/api/auth/login", json=AUTH_CREDENTIALS)
|
||||
assert response.status_code == 200
|
||||
return response.json()["accessToken"]
|
||||
|
||||
|
||||
def test_network_snapshot_requires_authentication() -> None:
|
||||
response = client.get("/api/network")
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_network_snapshot_endpoint_returns_collections() -> None:
|
||||
token = _authenticate()
|
||||
response = client.get(
|
||||
"/api/network", headers={"Authorization": f"Bearer {token}"}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
payload = response.json()
|
||||
assert set(payload.keys()) == {"stations", "tracks", "trains"}
|
||||
assert all(isinstance(payload[key], list) for key in payload)
|
||||
assert payload["stations"], "Expected sample station data"
|
||||
assert payload["trains"], "Expected sample train data"
|
||||
|
||||
station = payload["stations"][0]
|
||||
assert "name" in station and "createdAt" in station
|
||||
70
backend/tests/test_network_service.py
Normal file
70
backend/tests/test_network_service.py
Normal file
@@ -0,0 +1,70 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from types import SimpleNamespace
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from backend.app.repositories import StationRepository, TrackRepository, TrainRepository
|
||||
from backend.app.services.network import get_network_snapshot
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_entities() -> dict[str, SimpleNamespace]:
|
||||
timestamp = datetime.now(timezone.utc)
|
||||
station = SimpleNamespace(
|
||||
id=uuid4(),
|
||||
name="Test Station",
|
||||
location=None,
|
||||
created_at=timestamp,
|
||||
updated_at=timestamp,
|
||||
)
|
||||
track = SimpleNamespace(
|
||||
id=uuid4(),
|
||||
start_station_id=station.id,
|
||||
end_station_id=station.id,
|
||||
length_meters=1234.5,
|
||||
max_speed_kph=160,
|
||||
created_at=timestamp,
|
||||
updated_at=timestamp,
|
||||
)
|
||||
train = SimpleNamespace(
|
||||
id=uuid4(),
|
||||
designation="Test Express",
|
||||
capacity=200,
|
||||
max_speed_kph=220,
|
||||
created_at=timestamp,
|
||||
updated_at=timestamp,
|
||||
)
|
||||
return {"station": station, "track": track, "train": train}
|
||||
|
||||
|
||||
def test_network_snapshot_prefers_repository_data(monkeypatch: pytest.MonkeyPatch, sample_entities: dict[str, SimpleNamespace]) -> None:
|
||||
station = sample_entities["station"]
|
||||
track = sample_entities["track"]
|
||||
train = sample_entities["train"]
|
||||
|
||||
monkeypatch.setattr(StationRepository, "list_active", lambda self: [station])
|
||||
monkeypatch.setattr(TrackRepository, "list_all", lambda self: [track])
|
||||
monkeypatch.setattr(TrainRepository, "list_all", lambda self: [train])
|
||||
|
||||
snapshot = get_network_snapshot(session=None) # type: ignore[arg-type]
|
||||
|
||||
assert snapshot["stations"]
|
||||
assert snapshot["stations"][0]["name"] == station.name
|
||||
assert snapshot["tracks"][0]["lengthMeters"] == pytest.approx(track.length_meters)
|
||||
assert snapshot["trains"][0]["designation"] == train.designation
|
||||
assert snapshot["trains"][0]["operatingTrackIds"] == []
|
||||
|
||||
|
||||
def test_network_snapshot_falls_back_when_repositories_empty(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(StationRepository, "list_active", lambda self: [])
|
||||
monkeypatch.setattr(TrackRepository, "list_all", lambda self: [])
|
||||
monkeypatch.setattr(TrainRepository, "list_all", lambda self: [])
|
||||
|
||||
snapshot = get_network_snapshot(session=None) # type: ignore[arg-type]
|
||||
|
||||
assert snapshot["stations"]
|
||||
assert snapshot["trains"]
|
||||
assert any(station["name"] == "Central" for station in snapshot["stations"])
|
||||
106
backend/tests/test_repositories.py
Normal file
106
backend/tests/test_repositories.py
Normal file
@@ -0,0 +1,106 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, List
|
||||
|
||||
import pytest
|
||||
|
||||
from backend.app.models import StationCreate, TrackCreate, TrainCreate
|
||||
from backend.app.repositories import StationRepository, TrackRepository, TrainRepository
|
||||
|
||||
|
||||
@dataclass
|
||||
class DummySession:
|
||||
added: List[Any] = field(default_factory=list)
|
||||
|
||||
def add(self, instance: Any) -> None:
|
||||
self.added.append(instance)
|
||||
|
||||
def add_all(self, instances: list[Any]) -> None:
|
||||
self.added.extend(instances)
|
||||
|
||||
def scalars(self, _statement: Any) -> list[Any]: # pragma: no cover - not used here
|
||||
return []
|
||||
|
||||
def flush(self, _objects: list[Any] | None = None) -> None: # pragma: no cover - optional
|
||||
return None
|
||||
|
||||
|
||||
def test_station_repository_create_generates_geometry() -> None:
|
||||
session = DummySession()
|
||||
repo = StationRepository(session) # type: ignore[arg-type]
|
||||
|
||||
station = repo.create(
|
||||
StationCreate(
|
||||
name="Central",
|
||||
latitude=52.52,
|
||||
longitude=13.405,
|
||||
osm_id="123",
|
||||
code="BER",
|
||||
elevation_m=34.5,
|
||||
)
|
||||
)
|
||||
|
||||
assert station.name == "Central"
|
||||
assert session.added and session.added[0] is station
|
||||
assert getattr(station.location, "srid", None) == 4326
|
||||
assert "POINT" in str(station.location)
|
||||
|
||||
|
||||
def test_track_repository_requires_geometry() -> None:
|
||||
session = DummySession()
|
||||
repo = TrackRepository(session) # type: ignore[arg-type]
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
repo.create(
|
||||
TrackCreate(
|
||||
start_station_id="00000000-0000-0000-0000-000000000001",
|
||||
end_station_id="00000000-0000-0000-0000-000000000002",
|
||||
coordinates=[(52.0, 13.0)],
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_track_repository_create_builds_linestring() -> None:
|
||||
session = DummySession()
|
||||
repo = TrackRepository(session) # type: ignore[arg-type]
|
||||
|
||||
track = repo.create(
|
||||
TrackCreate(
|
||||
name="Main Line",
|
||||
start_station_id="00000000-0000-0000-0000-000000000001",
|
||||
end_station_id="00000000-0000-0000-0000-000000000002",
|
||||
coordinates=[(52.0, 13.0), (53.0, 14.0)],
|
||||
length_meters=1000.5,
|
||||
max_speed_kph=160,
|
||||
is_bidirectional=False,
|
||||
status="operational",
|
||||
)
|
||||
)
|
||||
|
||||
assert session.added and session.added[0] is track
|
||||
assert track.status == "operational"
|
||||
geom_repr = str(track.track_geometry)
|
||||
assert "LINESTRING" in geom_repr
|
||||
assert "13.0 52.0" in geom_repr
|
||||
|
||||
|
||||
def test_train_repository_create_supports_optional_ids() -> None:
|
||||
session = DummySession()
|
||||
repo = TrainRepository(session) # type: ignore[arg-type]
|
||||
|
||||
train = repo.create(
|
||||
TrainCreate(
|
||||
designation="ICE 123",
|
||||
capacity=400,
|
||||
max_speed_kph=300,
|
||||
operator_id=None,
|
||||
home_station_id="00000000-0000-0000-0000-000000000001",
|
||||
consist="locomotive+cars",
|
||||
)
|
||||
)
|
||||
|
||||
assert session.added and session.added[0] is train
|
||||
assert train.designation == "ICE 123"
|
||||
assert str(train.home_station_id).endswith("1")
|
||||
assert train.operator_id is None
|
||||
Reference in New Issue
Block a user