mirror of
https://github.com/xlorepdarkhelm/numinar-coding-project.git
synced 2026-08-24 00:06:26 -04:00
Getting environment configured better.
Signed-off-by: Cliff Hill <xlorep@darkhelm.org>
This commit is contained in:
72
.env.example
72
.env.example
@@ -1,22 +1,62 @@
|
||||
ENVIRONMENT=development
|
||||
|
||||
BACKEND_PROTOCOL=http
|
||||
BACKEND_HOST=localhost
|
||||
BACKEND_PORT=8000
|
||||
BACKEND_LOG_LEVEL=debug
|
||||
|
||||
FRONTEND_HOST=frontend
|
||||
FRONTEND_PORT=3000
|
||||
FRONTEND_LOG_LEVEL=debug
|
||||
|
||||
DATABASE_PROTOCOL=postgresql+asyncpg
|
||||
DATABASE_USER=user
|
||||
DATABASE_PASSWORD=password
|
||||
DATABASE_NAME=mydb
|
||||
# =============================
|
||||
# Backend Database Configuration
|
||||
# =============================
|
||||
# These variables configure the backend database connection.
|
||||
DATABASE_PROTOCOL=postgresql
|
||||
DATABASE_USER=your_db_user
|
||||
DATABASE_PASSWORD=your_db_password
|
||||
DATABASE_NAME=your_db_name
|
||||
DATABASE_HOST=localhost
|
||||
DATABASE_PORT=5432
|
||||
DATABASE_URL=postgresql://your_db_user:your_db_password@localhost:5432/your_db_name
|
||||
|
||||
DEFAULT_BOOKING_INTERVAL_MINUTES=30
|
||||
# =============================
|
||||
# Backend Logging Configuration
|
||||
# =============================
|
||||
# Controls logging level and format for backend services.
|
||||
BACKEND_LOG_LEVEL=INFO
|
||||
BACKEND_LOG_FORMAT="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
BACKEND_LOG_DATE_FORMAT="%Y-%m-%d %H:%M:%S"
|
||||
|
||||
# =============================
|
||||
# Backend SSE (Server-Sent Events) Configuration
|
||||
# =============================
|
||||
# Controls SSE test mode and timeout for streaming endpoints.
|
||||
SSE_TEST_MODE=false
|
||||
SSE_TIMEOUT=15
|
||||
|
||||
# =============================
|
||||
# Backend Server Configuration
|
||||
# =============================
|
||||
# Host and port settings for backend and frontend services.
|
||||
BACKEND_HOST=localhost
|
||||
BACKEND_PORT=8000
|
||||
FRONTEND_HOST=frontend
|
||||
FRONTEND_PORT=3000
|
||||
|
||||
# =============================
|
||||
# Backend Application Settings
|
||||
# =============================
|
||||
# Miscellaneous backend settings.
|
||||
BOOKING_MAX_MONTHS=12
|
||||
# ENVIRONMENT: Set to 'development', 'production', etc.
|
||||
BACKEND_ENVIRONMENT=development
|
||||
|
||||
# DEBUG: Set to 'true' to enable debug mode, 'false' otherwise
|
||||
BACKEND_DEBUG=false
|
||||
|
||||
# Protocol settings for backend and frontend services.
|
||||
BACKEND_PROTOCOL=http
|
||||
FRONTEND_PROTOCOL=http
|
||||
|
||||
# =============================
|
||||
# Frontend Environment Variables
|
||||
# =============================
|
||||
# Variables used by the frontend application and for frontend-backend integration.
|
||||
FRONTEND_LOG_LEVEL=info
|
||||
# Frontend environment: 'development', 'production', etc.
|
||||
FRONTEND_ENVIRONMENT=development
|
||||
DEFAULT_BOOKING_INTERVAL_MINUTES=30
|
||||
BUSINESS_START=08:00
|
||||
BUSINESS_END=17:00
|
||||
BUSINESS_END=18:00
|
||||
|
||||
@@ -36,7 +36,13 @@ LOG_DATE_FORMAT = config(
|
||||
"BACKEND_LOG_DATE_FORMAT", cast=str, default="%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
|
||||
# SSE configuration
|
||||
SSE_TEST_MODE = config("SSE_TEST_MODE", cast=bool, default=False)
|
||||
SSE_TIMEOUT = config("SSE_TIMEOUT", cast=int, default=15)
|
||||
|
||||
# Server configuration
|
||||
BACKEND_PROTOCOL = config("BACKEND_PROTOCOL", cast=str, default="http")
|
||||
FRONTEND_PROTOCOL = config("FRONTEND_PROTOCOL", cast=str, default="http")
|
||||
BACKEND_HOST = config("BACKEND_HOST", cast=str, default="localhost")
|
||||
BACKEND_PORT = config("BACKEND_PORT", cast=int, default=8000)
|
||||
FRONTEND_HOST = config("FRONTEND_HOST", cast=str, default="frontend")
|
||||
@@ -44,6 +50,6 @@ FRONTEND_PORT = config("FRONTEND_PORT", cast=int, default=3000)
|
||||
|
||||
# Other environment variables
|
||||
BOOKING_MAX_MONTHS = config("BOOKING_MAX_MONTHS", cast=int, default=12)
|
||||
DEBUG = config("DEBUG", cast=bool, default=False)
|
||||
DEBUG = config("BACKEND_DEBUG", cast=bool, default=False)
|
||||
ENVIRONMENT = config("ENVIRONMENT", cast=str, default="development")
|
||||
ECHO = ENVIRONMENT in {"development"}
|
||||
|
||||
@@ -7,8 +7,10 @@ from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from backend.env import BACKEND_HOST
|
||||
from backend.env import BACKEND_PORT
|
||||
from backend.env import BACKEND_PROTOCOL
|
||||
from backend.env import FRONTEND_HOST
|
||||
from backend.env import FRONTEND_PORT
|
||||
from backend.env import FRONTEND_PROTOCOL
|
||||
from backend.logging import setup_logging
|
||||
from backend.routers import bookings
|
||||
from backend.routers import rooms
|
||||
@@ -22,9 +24,9 @@ app = FastAPI(**app_configs)
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=[
|
||||
f"http://{BACKEND_HOST}:{FRONTEND_PORT}", # React dev server
|
||||
f"http://{BACKEND_HOST}:{BACKEND_PORT}", # Docker Compose frontend
|
||||
f"http://{FRONTEND_HOST}:{FRONTEND_PORT}", # Docker Compose service name
|
||||
f"{FRONTEND_PROTOCOL}://{BACKEND_HOST}:{FRONTEND_PORT}", # React dev server
|
||||
f"{BACKEND_PROTOCOL}://{BACKEND_HOST}:{BACKEND_PORT}", # Docker Compose frontend
|
||||
f"{FRONTEND_PROTOCOL}://{FRONTEND_HOST}:{FRONTEND_PORT}", # Docker Compose service name
|
||||
],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
|
||||
@@ -57,12 +57,14 @@ async def read_booking(
|
||||
logger.info(f"Successfully retrieved booking with id: {booking_id}")
|
||||
# Optionally, enrich booking object here if needed, but return the model object
|
||||
return BookingResponse.model_validate(booking)
|
||||
|
||||
except NoResultFound as err:
|
||||
logger.warning(f"Booking with id {booking_id} not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Booking with id {booking_id} not found",
|
||||
) from err
|
||||
|
||||
except (SQLAlchemyError, InterfaceError) as err:
|
||||
logger.exception(
|
||||
f"Database error while fetching booking with id {booking_id}: {str(err)}"
|
||||
@@ -71,6 +73,7 @@ async def read_booking(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Database error occurred",
|
||||
) from err
|
||||
|
||||
except Exception as err:
|
||||
logger.exception(
|
||||
f"Unexpected error while fetching booking with id {booking_id}: {str(err)}"
|
||||
@@ -111,6 +114,7 @@ async def create_booking(
|
||||
session, publish_room_availability_event, **booking.model_dump()
|
||||
)
|
||||
return BookingResponse.model_validate(created_booking)
|
||||
|
||||
except ValueError as err:
|
||||
logger.warning(f"Validation error while creating booking: {str(err)}")
|
||||
# Detect overlap/conflict error
|
||||
@@ -124,12 +128,14 @@ async def create_booking(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(err),
|
||||
) from err
|
||||
|
||||
except SQLAlchemyError as err:
|
||||
logger.exception(f"Database error while creating booking: {str(err)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Internal server error",
|
||||
) from err
|
||||
|
||||
except Exception as err:
|
||||
logger.exception(f"Unexpected error while creating booking: {str(err)}")
|
||||
raise HTTPException(
|
||||
@@ -172,14 +178,17 @@ async def update_existing_booking(
|
||||
)
|
||||
logger.info(f"Successfully updated booking with id: {booking_id}")
|
||||
return BookingResponse.model_validate(updated_booking)
|
||||
|
||||
except NoResultFound as err:
|
||||
logger.error(f"Booking with id {booking_id} not found: {err}")
|
||||
raise HTTPException(status_code=404, detail="Booking not found") from err
|
||||
|
||||
except (SQLAlchemyError, InterfaceError) as err:
|
||||
logger.error(
|
||||
f"Database error during booking update: {booking_id}: {err}", exc_info=True
|
||||
)
|
||||
raise HTTPException(status_code=500, detail="Database error") from err
|
||||
|
||||
except ValueError as err:
|
||||
logger.warning(
|
||||
f"Validation error while updating booking with id {booking_id}: {str(err)}"
|
||||
@@ -190,11 +199,13 @@ async def update_existing_booking(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="Booking times overlap with an existing booking for this room.",
|
||||
) from err
|
||||
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=str(err),
|
||||
) from err
|
||||
|
||||
except Exception as err:
|
||||
logger.exception(
|
||||
f"Unexpected error while updating booking with id {booking_id}: {str(err)}"
|
||||
@@ -226,20 +237,24 @@ async def delete_existing_booking(
|
||||
try:
|
||||
await delete_booking_func(session, booking_id, publish_room_availability_event)
|
||||
logger.info(f"Successfully deleted booking with id: {booking_id}")
|
||||
|
||||
except NoResultFound as err:
|
||||
logger.error(f"Booking with id {booking_id} not found: {err}")
|
||||
raise HTTPException(status_code=404, detail="Booking not found") from err
|
||||
|
||||
except (SQLAlchemyError, InterfaceError) as err:
|
||||
logger.error(
|
||||
f"Database error during booking delete: {booking_id}: {err}", exc_info=True
|
||||
)
|
||||
raise HTTPException(status_code=500, detail="Database error") from err
|
||||
|
||||
except ValueError as err:
|
||||
logger.warning(f"Booking with id {booking_id} not found (ValueError)")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Booking with id {booking_id} not found",
|
||||
) from err
|
||||
|
||||
except Exception as err:
|
||||
logger.exception(
|
||||
f"Unexpected error while deleting booking with id {booking_id}: {str(err)}"
|
||||
@@ -278,6 +293,7 @@ async def read_bookings_for_month(
|
||||
f"Successfully retrieved {len(bookings)} bookings for month: {month}"
|
||||
)
|
||||
return [BookingResponse.model_validate(booking) for booking in bookings]
|
||||
|
||||
except Exception as err:
|
||||
logger.exception(f"Error fetching bookings for month {month}: {str(err)}")
|
||||
raise HTTPException(
|
||||
|
||||
@@ -23,6 +23,8 @@ from backend.dependencies.rooms import GetRoomService
|
||||
from backend.dependencies.rooms import GetRoomsService
|
||||
from backend.dependencies.rooms import NewRoomService
|
||||
from backend.dependencies.rooms import UpdateRoomService
|
||||
from backend.env import SSE_TEST_MODE
|
||||
from backend.env import SSE_TIMEOUT
|
||||
|
||||
# from backend.models import Room
|
||||
from backend.schemas.rooms import RoomCreate
|
||||
@@ -31,13 +33,7 @@ from backend.schemas.rooms import RoomUpdate
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_TEST_MODE = False
|
||||
DEFAULT_TIMEOUT = 15.0
|
||||
|
||||
|
||||
router = APIRouter(prefix="/rooms", tags=["rooms"])
|
||||
|
||||
room_availability_subscribers: list[asyncio.Queue[dict[str, Any]]] = []
|
||||
|
||||
|
||||
@@ -60,11 +56,14 @@ async def event_generator(
|
||||
while True:
|
||||
if await request.is_disconnected():
|
||||
break
|
||||
|
||||
try:
|
||||
event = await asyncio.wait_for(queue.get(), timeout=timeout)
|
||||
yield f"data: {json.dumps(event)}\n\n"
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
yield ": keep-alive\n\n"
|
||||
|
||||
finally:
|
||||
if queue in room_availability_subscribers:
|
||||
room_availability_subscribers.remove(queue)
|
||||
@@ -84,10 +83,10 @@ async def publish_room_availability_event(event: dict[str, Any]) -> None:
|
||||
async def stream_room_availability(
|
||||
request: Request,
|
||||
test_mode: bool = Query( # noqa: B008
|
||||
DEFAULT_TEST_MODE, description="Force immediate keep-alive for testing"
|
||||
SSE_TEST_MODE, description="Force immediate keep-alive for testing"
|
||||
),
|
||||
timeout: float = Query( # noqa: B008
|
||||
DEFAULT_TIMEOUT, description="Keep-alive timeout in seconds"
|
||||
SSE_TIMEOUT, description="Keep-alive timeout in seconds"
|
||||
),
|
||||
) -> StreamingResponse:
|
||||
"""Stream real-time room availability changes using SSE.
|
||||
@@ -119,6 +118,7 @@ async def _test_event_generator(
|
||||
if test_mode:
|
||||
yield ": keep-alive\n\n"
|
||||
return
|
||||
|
||||
async for chunk in event_generator(request, queue, timeout):
|
||||
yield chunk
|
||||
|
||||
@@ -148,15 +148,18 @@ async def read_room(
|
||||
room = await get_room_func(session, room_id)
|
||||
logger.info(f"Successfully retrieved room with id: {room_id}")
|
||||
return RoomResponse.model_validate(room)
|
||||
|
||||
except NoResultFound as err:
|
||||
logger.warning(f"Room not found: {str(err)}")
|
||||
raise HTTPException(status_code=404, detail="Room not found") from err
|
||||
|
||||
except (SQLAlchemyError, asyncpg.InterfaceError) as err:
|
||||
logger.error(f"Database error while fetching room: {str(err)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Database error occurred",
|
||||
) from err
|
||||
|
||||
except Exception as err:
|
||||
logger.error(f"Unexpected error while fetching room: {str(err)}")
|
||||
raise HTTPException(
|
||||
@@ -186,15 +189,18 @@ async def read_rooms(
|
||||
rooms = await get_rooms_func(session)
|
||||
logger.info(f"Successfully retrieved {len(rooms)} rooms")
|
||||
return [RoomResponse.model_validate(room) for room in rooms]
|
||||
|
||||
except NoResultFound as err:
|
||||
logger.warning(f"Room not found: {str(err)}")
|
||||
raise HTTPException(status_code=404, detail="Room not found") from err
|
||||
|
||||
except (SQLAlchemyError, asyncpg.InterfaceError) as err:
|
||||
logger.error(f"Database error while fetching rooms: {str(err)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Database error occurred",
|
||||
) from err
|
||||
|
||||
except Exception as err:
|
||||
logger.error(f"Unexpected error while fetching rooms: {str(err)}")
|
||||
raise HTTPException(
|
||||
@@ -230,15 +236,18 @@ async def create_room(
|
||||
created_room = await new_room_func(session, **room.model_dump())
|
||||
logger.info(f"Successfully created room with id: {created_room.id}")
|
||||
return RoomResponse.model_validate(created_room)
|
||||
|
||||
except NoResultFound as err:
|
||||
logger.warning(f"Room not found: {str(err)}")
|
||||
raise HTTPException(status_code=404, detail="Room not found") from err
|
||||
|
||||
except (SQLAlchemyError, asyncpg.InterfaceError) as err:
|
||||
logger.error(f"Database error while creating room: {str(err)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Database error occurred",
|
||||
) from err
|
||||
|
||||
except Exception as err:
|
||||
logger.error(f"Unexpected error while creating room: {str(err)}")
|
||||
raise HTTPException(
|
||||
@@ -276,17 +285,21 @@ async def patch_room(
|
||||
)
|
||||
logger.info(f"Successfully updated room with id: {room_id}")
|
||||
return RoomResponse.model_validate(updated_room)
|
||||
|
||||
except ValueError as err:
|
||||
logger.warning(str(err))
|
||||
raise HTTPException(status_code=400, detail=str(err)) from err
|
||||
|
||||
except NoResultFound as err:
|
||||
logger.warning(f"Room not found: {str(err)}")
|
||||
raise HTTPException(status_code=404, detail="Room not found") from err
|
||||
|
||||
except (SQLAlchemyError, asyncpg.InterfaceError) as err:
|
||||
logger.error(
|
||||
f"Database error while updating room with id {room_id}: {str(err)}"
|
||||
)
|
||||
raise HTTPException(status_code=500, detail="Database error occurred") from err
|
||||
|
||||
except Exception as err:
|
||||
logger.error(
|
||||
f"Unexpected error while updating room with id {room_id}: {str(err)}"
|
||||
@@ -318,17 +331,21 @@ async def delete_existing_room(
|
||||
try:
|
||||
await delete_room_func(session, room_id)
|
||||
logger.info(f"Successfully deleted room with id: {room_id}")
|
||||
|
||||
except ValueError as err:
|
||||
logger.warning(str(err))
|
||||
raise HTTPException(status_code=404, detail=str(err)) from err
|
||||
|
||||
except NoResultFound as err:
|
||||
logger.warning(f"Room not found: {str(err)}")
|
||||
raise HTTPException(status_code=404, detail="Room not found") from err
|
||||
|
||||
except (SQLAlchemyError, asyncpg.InterfaceError) as err:
|
||||
logger.error(
|
||||
f"Database error while deleting room with id {room_id}: {str(err)}"
|
||||
)
|
||||
raise HTTPException(status_code=500, detail="Database error occurred") from err
|
||||
|
||||
except Exception as err:
|
||||
logger.error(
|
||||
f"Unexpected error while deleting room with id {room_id}: {str(err)}"
|
||||
|
||||
@@ -45,9 +45,11 @@ async def read_users(
|
||||
users = await get_users_func(session)
|
||||
logger.info(f"Successfully retrieved {len(users)} users")
|
||||
return [UserResponse.model_validate(user) for user in users]
|
||||
|
||||
except NoResultFound as err:
|
||||
logger.warning(f"User not found: {str(err)}")
|
||||
raise HTTPException(status_code=404, detail="User not found") from err
|
||||
|
||||
except Exception as err:
|
||||
logger.error(f"Unexpected error while fetching users: {str(err)}")
|
||||
raise HTTPException(
|
||||
|
||||
@@ -37,9 +37,11 @@ async def get_rooms(session: AsyncSession) -> list[Room]:
|
||||
rooms = cast(list[Room], result.all())
|
||||
logger.info(f"Successfully retrieved {len(rooms)} rooms")
|
||||
return rooms
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to retrieve rooms: {str(e)}")
|
||||
raise
|
||||
|
||||
finally:
|
||||
logger.debug("Exiting get_rooms")
|
||||
|
||||
@@ -65,9 +67,11 @@ async def get_room(session: AsyncSession, room_id: int) -> Room:
|
||||
raise NoResultFound(f"Room with id {room_id} not found")
|
||||
logger.info(f"Successfully retrieved room with id: {room_id}")
|
||||
return room
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to retrieve room with id {room_id}: {str(e)}")
|
||||
raise
|
||||
|
||||
finally:
|
||||
logger.debug("Exiting get_room")
|
||||
|
||||
@@ -92,10 +96,12 @@ async def new_room(session: AsyncSession, **kwargs: Unpack[RoomData]) -> Room:
|
||||
await session.commit()
|
||||
logger.info(f"Successfully created new room with id: {room.id}")
|
||||
return room
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create new room: {str(e)}")
|
||||
await session.rollback()
|
||||
raise
|
||||
|
||||
finally:
|
||||
logger.debug("Exiting new_room")
|
||||
|
||||
@@ -127,10 +133,12 @@ async def update_room(
|
||||
room = await get_room(session, room_id)
|
||||
logger.info(f"Successfully updated room with id: {room_id}")
|
||||
return room
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update room with id {room_id}: {str(e)}")
|
||||
await session.rollback()
|
||||
raise
|
||||
|
||||
finally:
|
||||
logger.debug("Exiting update_room")
|
||||
|
||||
@@ -154,9 +162,11 @@ async def delete_room(session: AsyncSession, room_id: int) -> None:
|
||||
raise ValueError(f"No room found with id {room_id}")
|
||||
await session.commit()
|
||||
logger.info(f"Successfully deleted room with id: {room_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete room with id {room_id}: {str(e)}")
|
||||
await session.rollback()
|
||||
raise
|
||||
|
||||
finally:
|
||||
logger.debug("Exiting delete_room")
|
||||
|
||||
@@ -31,6 +31,7 @@ async def get_users(session: AsyncSession) -> list[User]:
|
||||
users: list[User] = cast(list[User], result.all())
|
||||
logger.info("Retrieved %d users", len(users))
|
||||
return users
|
||||
|
||||
except Exception:
|
||||
logger.exception("Failed to retrieve users")
|
||||
raise
|
||||
@@ -58,6 +59,7 @@ async def get_user(session: AsyncSession, email: str) -> User:
|
||||
raise NoResultFound(f"User with email {email} not found")
|
||||
logger.info("Retrieved user with email: %s", email)
|
||||
return user
|
||||
|
||||
except Exception:
|
||||
logger.exception("Failed to retrieve user with email '%s'", email)
|
||||
raise
|
||||
|
||||
@@ -5,20 +5,17 @@ from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.exc import NoResultFound
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from backend.models import User
|
||||
from backend.models import UserList
|
||||
from backend.services.users import get_user
|
||||
from backend.models import User # Add this import for the User model
|
||||
from backend.services.users import get_users
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("mock_logger", ["backend.services.users"], indirect=True)
|
||||
async def test_get_users_success(
|
||||
async_session: AsyncSession, sample_users: UserList, mock_logger: MagicMock
|
||||
async_session: AsyncSession, sample_users: list[User], mock_logger: MagicMock
|
||||
) -> None:
|
||||
"""Test successful retrieval of all users from the database.
|
||||
|
||||
@@ -32,7 +29,7 @@ async def test_get_users_success(
|
||||
mock_scalars = AsyncMock(return_value=mock_scalars_result)
|
||||
async_session.scalars = mock_scalars # type: ignore [method-assign]
|
||||
|
||||
result: UserList = await get_users(async_session)
|
||||
result: list[User] = await get_users(async_session)
|
||||
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 2
|
||||
@@ -57,7 +54,7 @@ async def test_get_users_empty(
|
||||
mock_scalars = AsyncMock(return_value=mock_scalars_result)
|
||||
async_session.scalars = mock_scalars # type: ignore [method-assign]
|
||||
|
||||
result: UserList = await get_users(async_session)
|
||||
result: list[User] = await get_users(async_session)
|
||||
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 0
|
||||
@@ -85,95 +82,3 @@ async def test_get_users_database_error(
|
||||
with pytest.raises(SQLAlchemyError):
|
||||
await get_users(async_session)
|
||||
async_session.scalars.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("mock_logger", ["backend.services.users"], indirect=True)
|
||||
@pytest.mark.parametrize(
|
||||
"email, expected_name",
|
||||
[
|
||||
("user1@example.com", "User One"),
|
||||
("user2@example.com", "User Two"),
|
||||
],
|
||||
ids=["user1", "user2"],
|
||||
)
|
||||
async def test_get_user_success(
|
||||
async_session: AsyncSession, email: str, expected_name: str, mock_logger: MagicMock
|
||||
) -> None:
|
||||
"""Test successful retrieval of a user by email.
|
||||
|
||||
Verifies that get_user returns the correct user and constructs the correct query.
|
||||
|
||||
Args:
|
||||
async_session: The asynchronous database session.
|
||||
email: The email of the user to retrieve.
|
||||
expected_name: The expected name of the user.
|
||||
mock_logger: The mocked logger instance.
|
||||
"""
|
||||
user = User(email=email, name=expected_name)
|
||||
async_session.scalar = AsyncMock(return_value=user) # type: ignore [method-assign]
|
||||
|
||||
result: User = await get_user(async_session, email)
|
||||
|
||||
assert result.email == email
|
||||
assert result.name == expected_name
|
||||
async_session.scalar.assert_called_once()
|
||||
assert async_session.scalar.call_args.args[0].compare(
|
||||
select(User).where(User.email == email)
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("mock_logger", ["backend.services.users"], indirect=True)
|
||||
@pytest.mark.parametrize(
|
||||
"email",
|
||||
[
|
||||
"nonexistent@example.com",
|
||||
"invalid@domain.com",
|
||||
],
|
||||
ids=["nonexistent_email", "invalid_email"],
|
||||
)
|
||||
async def test_get_user_not_found(
|
||||
async_session: AsyncSession, mock_logger: MagicMock, email: str
|
||||
) -> None:
|
||||
"""Test handling of non-existent user in get_user.
|
||||
|
||||
Verifies that get_user raises NoResultFound when the user is not found.
|
||||
|
||||
Args:
|
||||
async_session: The asynchronous database session.
|
||||
mock_logger: The mocked logger instance.
|
||||
email: The email of the user to retrieve.
|
||||
"""
|
||||
async_session.scalar = AsyncMock(return_value=None) # type: ignore [method-assign]
|
||||
|
||||
with pytest.raises(NoResultFound):
|
||||
await get_user(async_session, email)
|
||||
async_session.scalar.assert_called_once()
|
||||
assert async_session.scalar.call_args.args[0].compare(
|
||||
select(User).where(User.email == email)
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("mock_logger", ["backend.services.users"], indirect=True)
|
||||
async def test_get_user_database_error(
|
||||
async_session: AsyncSession, mock_logger: MagicMock
|
||||
) -> None:
|
||||
"""Test handling of database errors in get_users.
|
||||
|
||||
Args:
|
||||
async_session: The asynchronous database session.
|
||||
mock_logger: The mocked logger instance.
|
||||
"""
|
||||
email: str = "user1@example.com"
|
||||
async_session.scalar = AsyncMock( # type: ignore [method-assign]
|
||||
side_effect=SQLAlchemyError("Database error")
|
||||
)
|
||||
|
||||
with pytest.raises(SQLAlchemyError):
|
||||
await get_user(async_session, email)
|
||||
async_session.scalar.assert_called_once()
|
||||
assert async_session.scalar.call_args.args[0].compare(
|
||||
select(User).where(User.email == email)
|
||||
)
|
||||
|
||||
@@ -26,7 +26,7 @@ export const ENV = {
|
||||
BACKEND_HOST: process.env.BACKEND_HOST || "localhost",
|
||||
BACKEND_PORT: process.env.BACKEND_PORT || "8000",
|
||||
FRONTEND_LOG_LEVEL: process.env.FRONTEND_LOG_LEVEL || "info",
|
||||
NODE_ENV: process.env.NODE_ENV || "development",
|
||||
NODE_ENV: process.env.FRONTEND_ENVIRONMENT || "development",
|
||||
DEFAULT_BOOKING_INTERVAL_MINUTES:
|
||||
Number(process.env.DEFAULT_BOOKING_INTERVAL_MINUTES) || 30,
|
||||
BUSINESS_START: process.env.BUSINESS_START || "08:00",
|
||||
|
||||
Reference in New Issue
Block a user