feat: Initialize frontend and backend structure with essential configurations
Some checks failed
Backend CI / lint-and-test (push) Failing after 2m15s
Frontend CI / lint-and-build (push) Successful in 1m1s

- 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:
2025-10-11 15:25:32 +02:00
commit fc1e874309
74 changed files with 9477 additions and 0 deletions

View File

View 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"

View 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"}

View 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

View 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

View 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"])

View 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