Getting the frontend and backend wired up correctly.

Signed-off-by: Cliff Hill <xlorep@darkhelm.org>
This commit is contained in:
2025-08-28 08:47:38 -04:00
parent 78aa4aea15
commit ca05a4f26b
9 changed files with 151 additions and 34 deletions

View File

@@ -9,6 +9,7 @@ from contextlib import asynccontextmanager
from typing import Any
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from backend import config
from backend.logging import setup_logging
@@ -45,8 +46,22 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
app_configs["lifespan"] = lifespan
app = FastAPI(**app_configs)
# Allow CORS for frontend (adjust origins as needed for production)
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://localhost:3000", # React dev server
"http://localhost:8000", # Docker Compose frontend (if served on 8000)
"http://frontend:3000", # Docker Compose service name
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(users.router)
app.include_router(rooms.router)
app.include_router(bookings.router)

View File

@@ -134,6 +134,7 @@ async def create_db_and_tables() -> None: # pragma: no cover
await conn.run_sync(Base.metadata.create_all)
# Python type aliases for lists
type UserList = list[User]
type RoomList = list[Room]
type BookingList = list[Booking]

View File

@@ -1,10 +1,10 @@
"""Routes for booking-related operations in the backend."""
import logging
from typing import List
from fastapi import APIRouter
from fastapi import HTTPException
from fastapi import Query
from fastapi import status
from sqlalchemy.exc import NoResultFound
from sqlalchemy.exc import SQLAlchemyError
@@ -30,25 +30,39 @@ logger = logging.getLogger(__name__)
router = APIRouter(prefix="/bookings", tags=["bookings"])
@router.get("/room/{room_id}", response_model=List[BookingResponse])
async def read_bookings_for_room(room_id: int, session: DBSession) -> BookingList:
"""Retrieve all bookings for a specific room.
_default_date = Query(None, description="Filter bookings by date (YYYY-MM-DD)")
@router.get("/room/{room_id}", response_model=list[BookingResponse])
async def read_bookings_for_room(
room_id: int,
session: DBSession,
date: str = _default_date,
) -> BookingList:
"""Retrieve bookings for a specific room, optionally filtered by date.
Args:
room_id: ID of the room to retrieve bookings for.
session: Database session.
Returns:
List of bookings for the room.
room_id (int): The ID of the room to retrieve bookings for.
session (DBSession): The database session.
date (str): Optional date string (YYYY-MM-DD) to filter bookings by start_time.
Raises:
HTTPException: If a database error or unexpected error occurs.
HTTPException: If the date format is invalid.
Returns:
BookingList: List of bookings for the room, filtered by date if provided.
"""
logger.debug(f"Received request to fetch bookings for room_id: {room_id}")
logger.debug(
f"Received request to fetch bookings for room_id: {room_id} and date: {date}"
)
try:
bookings = await get_bookings_for_room(session, room_id)
try:
bookings = await get_bookings_for_room(session, room_id, date)
except ValueError as ve:
raise HTTPException(status_code=422, detail=str(ve)) from ve
logger.info(
f"Successfully retrieved {len(bookings)} bookings for room_id: {room_id}"
f"Successfully retrieved {len(bookings)} bookings for room_id: {room_id} "
f"and date: {date}"
)
return bookings
except SQLAlchemyError as e:

View File

@@ -11,11 +11,13 @@ from datetime import timezone
from typing import Any
from typing import Awaitable
from typing import Callable
from typing import List
from typing import Sequence
from typing import TypedDict
from typing import Unpack
from typing import cast
from sqlalchemy import and_
from sqlalchemy import delete
from sqlalchemy import func
from sqlalchemy import select
@@ -23,6 +25,15 @@ from sqlalchemy import update
from sqlalchemy.exc import NoResultFound
from sqlalchemy.ext.asyncio import AsyncSession
from backend.models import Booking
# BookingList is a type alias for List[Booking]
try:
from backend.models import BookingList
except ImportError:
BookingList = List[Booking]
from backend import config
from backend.models import Booking
from backend.models import BookingList
@@ -43,30 +54,58 @@ class BookingParams(TypedDict):
"""
async def get_bookings_for_room(session: AsyncSession, room_id: int) -> BookingList:
"""Retrieve all bookings for a specific room.
async def get_bookings_for_room(
session: AsyncSession, room_id: int, date: str | None = None
) -> BookingList:
"""Retrieve bookings for a specific room, optionally filtered by date.
If a date string (YYYY-MM-DD) is provided, only bookings whose start_time falls on that date
are returned. If no date is provided, all bookings for the room are returned.
Args:
session: Database session.
room_id: ID of the room to retrieve bookings for.
session: The asynchronous database session.
room_id: The ID of the room to retrieve bookings for.
date: Optional date string (YYYY-MM-DD) to filter bookings by start_time.
Returns:
List of bookings associated with the room.
List of bookings for the room, filtered by date if provided.
Raises:
Exception: If any database error occurs.
ValueError: If the date string is not in YYYY-MM-DD format.
"""
logger.debug(f"Entering get_bookings_for_room with room_id: {room_id}")
logger.debug(
f"Entering get_bookings_for_room with room_id: {room_id} and date: {date}"
)
try:
stmt = select(Booking).where(Booking.room_id == room_id)
result = await session.scalars(stmt)
bookings = cast(BookingList, result.all())
if date:
try:
date_obj = datetime.strptime(date, "%Y-%m-%d")
except ValueError as e:
raise ValueError("Invalid date format. Use YYYY-MM-DD.") from e
start_of_day = date_obj
end_of_day = date_obj + timedelta(days=1)
stmt = select(Booking).where(
Booking.room_id == room_id,
and_(
Booking.start_time >= start_of_day,
Booking.start_time < end_of_day,
),
)
result = await session.scalars(stmt)
bookings = list(result.all())
else:
stmt = select(Booking).where(Booking.room_id == room_id)
result = await session.scalars(stmt)
bookings = cast(BookingList, result.all())
logger.info(
f"Successfully retrieved {len(bookings)} bookings for room_id: {room_id}"
f"Successfully retrieved {len(bookings)} bookings for room_id: {room_id} "
f"and date: {date}"
)
return bookings
except Exception as e:
logger.error(f"Failed to retrieve bookings for room_id {room_id}: {str(e)}")
logger.error(
f"Failed to retrieve bookings for room_id {room_id} and date {date}: {str(e)}"
)
raise
finally:
logger.debug("Exiting get_bookings_for_room")

View File

@@ -50,7 +50,7 @@ async def test_get_bookings_for_room_success(
}
for booking in sample_bookings
]
mock_get_bookings.assert_called_once_with(ANY, room_id)
mock_get_bookings.assert_called_once_with(ANY, room_id, None)
@pytest.mark.asyncio
@@ -76,7 +76,7 @@ async def test_get_bookings_for_room_empty(
assert response.status_code == 200
assert response.json() == []
mock_get_bookings.assert_called_once_with(ANY, room_id)
mock_get_bookings.assert_called_once_with(ANY, room_id, None)
@pytest.mark.asyncio
@@ -102,7 +102,7 @@ async def test_get_bookings_for_room_database_error(
assert response.status_code == 500
assert "detail" in response.json()
mock_get_bookings.assert_called_once_with(ANY, room_id)
mock_get_bookings.assert_called_once_with(ANY, room_id, None)
@pytest.mark.asyncio

View File

@@ -33,10 +33,7 @@ from backend.services.bookings import update_booking
async def test_get_bookings_for_room_success(
async_session: AsyncSession, sample_bookings: BookingList, mock_logger: MagicMock
) -> None:
"""Test successful retrieval of all bookings for a room.
Verifies that get_bookings_for_room returns the expected list of bookings and constructs
the correct SQLAlchemy query.
"""Test retrieval of all bookings for a room with no date filter.
Args:
async_session: The asynchronous database session.
@@ -58,6 +55,52 @@ async def test_get_bookings_for_room_success(
)
@pytest.mark.asyncio
@pytest.mark.parametrize("mock_logger", ["backend.services.bookings"], indirect=True)
async def test_get_bookings_for_room_with_date(
async_session: AsyncSession, sample_bookings: BookingList, mock_logger: MagicMock
) -> None:
"""Test retrieval of bookings for a room filtered by date.
Args:
async_session: The asynchronous database session.
sample_bookings: The mocked list of bookings to return.
mock_logger: The mocked logger instance.
"""
room_id = 1
date = "2025-08-28"
mock_scalars_result = AsyncMock()
mock_scalars_result.all = MagicMock(return_value=sample_bookings)
mock_scalars = AsyncMock(return_value=mock_scalars_result)
with patch.object(async_session, "scalars", mock_scalars) as scalars_mock:
result: BookingList = await get_bookings_for_room(async_session, room_id, date)
assert isinstance(result, list)
assert result == sample_bookings
scalars_mock.assert_called_once()
# Check the query includes the date filter
stmt = scalars_mock.call_args.args[0]
assert stmt.whereclause is not None
assert str(stmt.whereclause).find("start_time >=") != -1
assert str(stmt.whereclause).find("start_time <") != -1
@pytest.mark.asyncio
@pytest.mark.parametrize("mock_logger", ["backend.services.bookings"], indirect=True)
async def test_get_bookings_for_room_invalid_date(
async_session: AsyncSession, mock_logger: MagicMock
) -> None:
"""Test that an invalid date string raises ValueError.
Args:
async_session: The asynchronous database session.
mock_logger: The mocked logger instance.
"""
room_id = 1
invalid_date = "not-a-date"
with pytest.raises(ValueError):
await get_bookings_for_room(async_session, room_id, invalid_date)
@pytest.mark.asyncio
@pytest.mark.parametrize("mock_logger", ["backend.services.bookings"], indirect=True)
async def test_get_bookings_for_room_empty(

View File

@@ -33,6 +33,8 @@ services:
extends:
file: compose.yml
service: frontend
environment:
REACT_APP_API_URL: http://localhost:8000
volumes:
- ./frontend/src:/app/src
- ./frontend/public:/app/public

View File

@@ -5,7 +5,7 @@ services:
build:
context: ./backend
dockerfile: Dockerfile
expose:
ports:
- "8000:8000"
environment:
ENVIRONMENT: production
@@ -22,6 +22,8 @@ services:
build:
context: ./frontend
dockerfile: Dockerfile
environment:
REACT_APP_API_URL: http://localhost:8000
ports:
- "3000:3000"
depends_on:

View File

@@ -7,7 +7,8 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import axios from "axios";
const queryClient = new QueryClient();
axios.defaults.baseURL = "http://backend:8000";
axios.defaults.baseURL =
process.env.REACT_APP_API_URL || "http://localhost:8000";
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement