diff --git a/server/routers/stations.py b/server/routers/stations.py index 5920610..f44da27 100644 --- a/server/routers/stations.py +++ b/server/routers/stations.py @@ -11,7 +11,7 @@ from schemas.station import ( StationResponse, StationRecipeAssignmentCreate, StationRecipeAssignmentResponse, - _RecipeSummary, + RecipeSummary, ) from services import station_service @@ -40,7 +40,11 @@ async def create_new_station( return StationResponse.model_validate(station) -@router.get("/by-code/{code}/recipes", response_model=list[_RecipeSummary]) +# NOTE: this literal-prefix route must stay above the /{station_id} routes. +# The int-typed station_id param already guards against "by-code" being +# matched as a station id, but keeping the explicit order avoids surprises +# during refactors (e.g. if someone regroups handlers by HTTP method). +@router.get("/by-code/{code}/recipes", response_model=list[RecipeSummary]) async def list_recipes_by_station_code( code: str, user: User = Depends(get_current_user), @@ -56,10 +60,10 @@ async def list_recipes_by_station_code( if station is None or not station.active: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail=f"Station '{code}' not found or inactive", + detail="Station not found", ) recipes = await station_service.list_station_recipes(db, station.id) - return [_RecipeSummary.model_validate(r) for r in recipes] + return [RecipeSummary.model_validate(r) for r in recipes] @router.get("/{station_id}", response_model=StationResponse) @@ -95,7 +99,7 @@ async def remove_station( await station_service.delete_station(db, station_id) -@router.get("/{station_id}/recipes", response_model=list[_RecipeSummary]) +@router.get("/{station_id}/recipes", response_model=list[RecipeSummary]) async def list_assigned_recipes( station_id: int, admin: User = Depends(require_admin_user), @@ -103,7 +107,7 @@ async def list_assigned_recipes( ): """Admin view: recipes assigned to this station (active only).""" recipes = await station_service.list_station_recipes(db, station_id) - return [_RecipeSummary.model_validate(r) for r in recipes] + return [RecipeSummary.model_validate(r) for r in recipes] @router.post( diff --git a/server/schemas/station.py b/server/schemas/station.py index d44e870..7b0c6b0 100644 --- a/server/schemas/station.py +++ b/server/schemas/station.py @@ -45,7 +45,7 @@ class StationRecipeAssignmentResponse(BaseModel): assigned_at: datetime -class _RecipeSummary(BaseModel): +class RecipeSummary(BaseModel): model_config = ConfigDict(from_attributes=True) id: int code: str @@ -54,4 +54,4 @@ class _RecipeSummary(BaseModel): class StationWithRecipesResponse(StationResponse): - recipes: list[_RecipeSummary] = Field(default_factory=list) + recipes: list[RecipeSummary] = Field(default_factory=list) diff --git a/server/tests/test_stations_api.py b/server/tests/test_stations_api.py index 31058a1..c936ee8 100644 --- a/server/tests/test_stations_api.py +++ b/server/tests/test_stations_api.py @@ -134,3 +134,70 @@ async def test_list_recipes_by_unknown_code_404( headers=auth_headers(measurement_tec_user), ) assert resp.status_code == 404 + + +async def test_admin_can_list_stations(client: AsyncClient, admin_user): + await client.post( + "/api/stations", + headers=auth_headers(admin_user), + json={"code": "ST-L1", "name": "A"}, + ) + await client.post( + "/api/stations", + headers=auth_headers(admin_user), + json={"code": "ST-L2", "name": "B", "active": False}, + ) + resp = await client.get("/api/stations", headers=auth_headers(admin_user)) + assert resp.status_code == 200 + codes = {s["code"] for s in resp.json()} + assert {"ST-L1", "ST-L2"}.issubset(codes) + + resp_active = await client.get( + "/api/stations?active_only=true", headers=auth_headers(admin_user), + ) + assert resp_active.status_code == 200 + active_codes = {s["code"] for s in resp_active.json()} + assert "ST-L1" in active_codes + assert "ST-L2" not in active_codes + + +async def test_assign_recipe_not_found_returns_404( + client: AsyncClient, admin_user, +): + created = await client.post( + "/api/stations", + headers=auth_headers(admin_user), + json={"code": "ST-NR", "name": "NR"}, + ) + sid = created.json()["id"] + resp = await client.post( + f"/api/stations/{sid}/recipes", + headers=auth_headers(admin_user), + json={"recipe_id": 99999}, + ) + assert resp.status_code == 404 + + +async def test_duplicate_assignment_returns_409( + client: AsyncClient, admin_user, db_session, +): + recipe = await create_test_recipe(db_session, user_id=admin_user.id, code="REC-DUP") + await db_session.commit() + created = await client.post( + "/api/stations", + headers=auth_headers(admin_user), + json={"code": "ST-DUP-A", "name": "Dup"}, + ) + sid = created.json()["id"] + first = await client.post( + f"/api/stations/{sid}/recipes", + headers=auth_headers(admin_user), + json={"recipe_id": recipe.id}, + ) + assert first.status_code == 201 + second = await client.post( + f"/api/stations/{sid}/recipes", + headers=auth_headers(admin_user), + json={"recipe_id": recipe.id}, + ) + assert second.status_code == 409