mirror of
https://github.com/joaovitoriasilva/endurain.git
synced 2026-01-09 15:57:59 -05:00
Refactor health_weight API and add tests
Refactored backend health_weight CRUD, models, router, schema, and utils for improved type safety, docstrings, and response structure. Added HealthWeightListResponse for paginated and total-count responses. Updated router endpoints to return total counts and records, and improved error handling. Added comprehensive unit tests for health_weight CRUD, models, and router. Updated frontend service and view to match new API response format.
This commit is contained in:
@@ -10,21 +10,33 @@ import health_weight.utils as health_weight_utils
|
||||
import core.logger as core_logger
|
||||
|
||||
|
||||
def get_all_health_weight(db: Session):
|
||||
def get_all_health_weight(
|
||||
db: Session,
|
||||
) -> list[health_weight_models.HealthWeight]:
|
||||
"""
|
||||
Retrieve all health weight records from the database.
|
||||
|
||||
Queries the database for all health weight entries and returns them ordered
|
||||
by date in descending order (most recent first).
|
||||
|
||||
Args:
|
||||
db (Session): SQLAlchemy database session for executing queries.
|
||||
|
||||
Returns:
|
||||
list[HealthWeight]: A list of all HealthWeight model instances ordered
|
||||
by date (descending).
|
||||
|
||||
Raises:
|
||||
HTTPException: A 500 Internal Server Error if the database query fails
|
||||
or any other exception occurs during execution.
|
||||
"""
|
||||
try:
|
||||
# Get the health_weight from the database
|
||||
health_weight = (
|
||||
# Get the health_weight from the database and return it
|
||||
return (
|
||||
db.query(health_weight_models.HealthWeight)
|
||||
.order_by(desc(health_weight_models.HealthWeight.date))
|
||||
.all()
|
||||
)
|
||||
|
||||
# Check if there are health_weight if not return None
|
||||
if not health_weight:
|
||||
return None
|
||||
|
||||
# Return the health_weight
|
||||
return health_weight
|
||||
except Exception as err:
|
||||
# Log the exception
|
||||
core_logger.print_to_log(
|
||||
@@ -37,7 +49,22 @@ def get_all_health_weight(db: Session):
|
||||
) from err
|
||||
|
||||
|
||||
def get_health_weight_number(user_id: int, db: Session):
|
||||
def get_health_weight_number(user_id: int, db: Session) -> int:
|
||||
"""
|
||||
Retrieves the total count of health weight records for a specific user.
|
||||
|
||||
Args:
|
||||
user_id (int): The unique identifier of the user whose health weight records are to be counted.
|
||||
db (Session): The database session object used to execute the query.
|
||||
|
||||
Returns:
|
||||
int: The total number of health weight records associated with the specified user.
|
||||
|
||||
Raises:
|
||||
HTTPException: A 500 Internal Server Error is raised if any exception occurs during
|
||||
the database query operation. The original exception is logged before
|
||||
raising the HTTPException.
|
||||
"""
|
||||
try:
|
||||
# Get the number of health_weight from the database
|
||||
return (
|
||||
@@ -57,22 +84,36 @@ def get_health_weight_number(user_id: int, db: Session):
|
||||
) from err
|
||||
|
||||
|
||||
def get_all_health_weight_by_user_id(user_id: int, db: Session):
|
||||
def get_all_health_weight_by_user_id(
|
||||
user_id: int, db: Session
|
||||
) -> list[health_weight_models.HealthWeight]:
|
||||
"""
|
||||
Retrieve all health weight records for a specific user.
|
||||
|
||||
This function queries the database to fetch all health weight entries associated
|
||||
with a given user ID, ordered by date in descending order (most recent first).
|
||||
|
||||
Args:
|
||||
user_id (int): The unique identifier of the user whose health weight records
|
||||
are to be retrieved.
|
||||
db (Session): SQLAlchemy database session object for executing queries.
|
||||
|
||||
Returns:
|
||||
list: A list of HealthWeight model instances containing all weight records
|
||||
for the specified user, ordered by date (newest to oldest).
|
||||
|
||||
Raises:
|
||||
HTTPException: Returns a 500 Internal Server Error if any database operation
|
||||
or unexpected error occurs during the query execution.
|
||||
"""
|
||||
try:
|
||||
# Get the health_weight from the database
|
||||
health_weight = (
|
||||
return (
|
||||
db.query(health_weight_models.HealthWeight)
|
||||
.filter(health_weight_models.HealthWeight.user_id == user_id)
|
||||
.order_by(desc(health_weight_models.HealthWeight.date))
|
||||
.all()
|
||||
)
|
||||
|
||||
# Check if there are health_weight if not return None
|
||||
if not health_weight:
|
||||
return None
|
||||
|
||||
# Return the health_weight
|
||||
return health_weight
|
||||
except Exception as err:
|
||||
# Log the exception
|
||||
core_logger.print_to_log(
|
||||
@@ -87,10 +128,30 @@ def get_all_health_weight_by_user_id(user_id: int, db: Session):
|
||||
|
||||
def get_health_weight_with_pagination(
|
||||
user_id: int, db: Session, page_number: int = 1, num_records: int = 5
|
||||
):
|
||||
) -> list[health_weight_models.HealthWeight]:
|
||||
"""
|
||||
Retrieve paginated health weight records for a specific user.
|
||||
|
||||
This function queries the database to fetch health weight records for a given user
|
||||
with pagination support. Results are ordered by date in descending order (most recent first).
|
||||
|
||||
Args:
|
||||
user_id (int): The unique identifier of the user whose health weight records to retrieve.
|
||||
db (Session): The SQLAlchemy database session used for querying.
|
||||
page_number (int, optional): The page number to retrieve. Defaults to 1.
|
||||
num_records (int, optional): The number of records per page. Defaults to 5.
|
||||
|
||||
Returns:
|
||||
list[health_weight_models.HealthWeight]: A list of HealthWeight model instances
|
||||
for the specified page, ordered by date in descending order.
|
||||
|
||||
Raises:
|
||||
HTTPException: Raises a 500 Internal Server Error if any exception occurs during
|
||||
the database query operation.
|
||||
"""
|
||||
try:
|
||||
# Get the health_weight from the database
|
||||
health_weight = (
|
||||
# Get the health_weight from the database with pagination and return it
|
||||
return (
|
||||
db.query(health_weight_models.HealthWeight)
|
||||
.filter(health_weight_models.HealthWeight.user_id == user_id)
|
||||
.order_by(desc(health_weight_models.HealthWeight.date))
|
||||
@@ -98,13 +159,6 @@ def get_health_weight_with_pagination(
|
||||
.limit(num_records)
|
||||
.all()
|
||||
)
|
||||
|
||||
# Check if there are health_weight if not return None
|
||||
if not health_weight:
|
||||
return None
|
||||
|
||||
# Return the health_weight
|
||||
return health_weight
|
||||
except Exception as err:
|
||||
# Log the exception
|
||||
core_logger.print_to_log(
|
||||
@@ -117,10 +171,30 @@ def get_health_weight_with_pagination(
|
||||
) from err
|
||||
|
||||
|
||||
def get_health_weight_by_date(user_id: int, date: str, db: Session):
|
||||
def get_health_weight_by_date(
|
||||
user_id: int, date: str, db: Session
|
||||
) -> health_weight_models.HealthWeight | None:
|
||||
"""
|
||||
Retrieve a health weight record for a specific user and date.
|
||||
|
||||
This function queries the database to find a health weight entry matching
|
||||
the provided user ID and date.
|
||||
|
||||
Args:
|
||||
user_id (int): The unique identifier of the user.
|
||||
date (str): The date string for which to retrieve the health weight record.
|
||||
db (Session): The database session object for executing queries.
|
||||
|
||||
Returns:
|
||||
HealthWeight | None: The health weight record if found, None otherwise.
|
||||
|
||||
Raises:
|
||||
HTTPException: A 500 Internal Server Error if the database query fails
|
||||
or any other exception occurs during execution.
|
||||
"""
|
||||
try:
|
||||
# Get the health_weight from the database
|
||||
health_weight = (
|
||||
# Get the health_weight from the database and return it
|
||||
return (
|
||||
db.query(health_weight_models.HealthWeight)
|
||||
.filter(
|
||||
health_weight_models.HealthWeight.date == date,
|
||||
@@ -128,13 +202,6 @@ def get_health_weight_by_date(user_id: int, date: str, db: Session):
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
# Check if there are health_weight if not return None
|
||||
if not health_weight:
|
||||
return None
|
||||
|
||||
# Return the health_weight
|
||||
return health_weight
|
||||
except Exception as err:
|
||||
# Log the exception
|
||||
core_logger.print_to_log(
|
||||
@@ -149,7 +216,33 @@ def get_health_weight_by_date(user_id: int, date: str, db: Session):
|
||||
|
||||
def create_health_weight(
|
||||
user_id: int, health_weight: health_weight_schema.HealthWeight, db: Session
|
||||
):
|
||||
) -> health_weight_schema.HealthWeight:
|
||||
"""
|
||||
Create a new health weight entry for a user.
|
||||
|
||||
This function creates a new health weight record in the database. If the date is not provided,
|
||||
it defaults to the current date. If BMI is not provided, it is automatically calculated
|
||||
using the user's height and the provided weight.
|
||||
|
||||
Args:
|
||||
user_id (int): The ID of the user for whom the health weight entry is being created.
|
||||
health_weight (health_weight_schema.HealthWeight): The health weight data to be created,
|
||||
containing fields such as weight, date, and optionally BMI.
|
||||
db (Session): The database session used for database operations.
|
||||
|
||||
Returns:
|
||||
health_weight_schema.HealthWeight: The created health weight entry with its assigned ID.
|
||||
|
||||
Raises:
|
||||
HTTPException:
|
||||
- 409 Conflict: If a duplicate entry exists for the same date.
|
||||
- 500 Internal Server Error: If any other unexpected error occurs during creation.
|
||||
|
||||
Note:
|
||||
- The function automatically sets the date to current timestamp if not provided.
|
||||
- BMI is calculated automatically if not provided in the input.
|
||||
- The database transaction is rolled back in case of any errors.
|
||||
"""
|
||||
try:
|
||||
# Check if date is None
|
||||
if health_weight.date is None:
|
||||
@@ -204,7 +297,33 @@ def create_health_weight(
|
||||
|
||||
def edit_health_weight(
|
||||
user_id, health_weight: health_weight_schema.HealthWeight, db: Session
|
||||
):
|
||||
) -> health_weight_schema.HealthWeight:
|
||||
"""
|
||||
Edit an existing health weight record for a user.
|
||||
|
||||
This function updates a health weight entry in the database for a specific user.
|
||||
It automatically calculates BMI if weight is provided but BMI is not. The function
|
||||
only updates fields that are explicitly set in the input schema.
|
||||
|
||||
Args:
|
||||
user_id: The ID of the user who owns the health weight record.
|
||||
health_weight (health_weight_schema.HealthWeight): The health weight data to update,
|
||||
containing the record ID and fields to be modified.
|
||||
db (Session): The database session for executing queries.
|
||||
|
||||
Returns:
|
||||
health_weight_schema.HealthWeight: The updated health weight object with all current values.
|
||||
|
||||
Raises:
|
||||
HTTPException:
|
||||
- 404 NOT_FOUND if the health weight record doesn't exist or doesn't belong to the user.
|
||||
- 500 INTERNAL_SERVER_ERROR if a database error or unexpected exception occurs.
|
||||
|
||||
Note:
|
||||
- Only fields present in the input schema (exclude_unset=True) will be updated.
|
||||
- If BMI is None but weight is provided, BMI will be automatically calculated.
|
||||
- The database transaction is automatically rolled back on error.
|
||||
"""
|
||||
try:
|
||||
# Get the health_weight from the database
|
||||
db_health_weight = (
|
||||
@@ -257,7 +376,29 @@ def edit_health_weight(
|
||||
) from err
|
||||
|
||||
|
||||
def delete_health_weight(user_id: int, health_weight_id: int, db: Session):
|
||||
def delete_health_weight(user_id: int, health_weight_id: int, db: Session) -> None:
|
||||
"""
|
||||
Delete a health weight record for a specific user.
|
||||
|
||||
This function deletes a health weight entry from the database identified by
|
||||
the health_weight_id and user_id. If the record is not found, it raises a
|
||||
404 HTTPException. Any database errors will trigger a rollback and raise a
|
||||
500 HTTPException.
|
||||
|
||||
Args:
|
||||
user_id (int): The ID of the user who owns the health weight record.
|
||||
health_weight_id (int): The ID of the health weight record to delete.
|
||||
db (Session): The database session object used for querying and committing.
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
Raises:
|
||||
HTTPException:
|
||||
- 404 NOT_FOUND if the health weight record with the specified ID
|
||||
for the given user is not found.
|
||||
- 500 INTERNAL_SERVER_ERROR if a database error occurs during deletion.
|
||||
"""
|
||||
try:
|
||||
# Delete the health_weight
|
||||
num_deleted = (
|
||||
|
||||
@@ -11,6 +11,34 @@ from core.database import Base
|
||||
|
||||
|
||||
class HealthWeight(Base):
|
||||
"""
|
||||
SQLAlchemy model representing health weight measurements and body composition data.
|
||||
|
||||
This model stores comprehensive health metrics including weight, BMI, and various
|
||||
body composition measurements for users. Each record is associated with a specific
|
||||
user and date.
|
||||
|
||||
Attributes:
|
||||
id (int): Primary key, auto-incremented unique identifier.
|
||||
user_id (int): Foreign key referencing the user who owns this health weight record.
|
||||
Indexed for query performance. Cascades on delete.
|
||||
date (Date): The date when the health weight measurement was taken.
|
||||
weight (Decimal): Weight measurement in kilograms (precision: 10, scale: 2).
|
||||
bmi (Decimal, optional): Body Mass Index calculation (precision: 10, scale: 2).
|
||||
body_fat (Decimal, optional): Body fat percentage (precision: 10, scale: 2).
|
||||
body_water (Decimal, optional): Body hydration/water percentage (precision: 10, scale: 2).
|
||||
bone_mass (Decimal, optional): Bone mass percentage (precision: 10, scale: 2).
|
||||
muscle_mass (Decimal, optional): Muscle mass percentage (precision: 10, scale: 2).
|
||||
physique_rating (int, optional): Overall physique rating score.
|
||||
visceral_fat (Decimal, optional): Visceral fat rating (precision: 10, scale: 2).
|
||||
metabolic_age (int, optional): Calculated metabolic age.
|
||||
source (str, optional): Source or origin of the health weight data (max length: 250).
|
||||
|
||||
Relationships:
|
||||
user (User): Many-to-one relationship with the User model. References the user
|
||||
who owns this health weight record.
|
||||
"""
|
||||
|
||||
__tablename__ = "health_weight"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from typing import Annotated, Callable
|
||||
from datetime import date
|
||||
|
||||
from fastapi import APIRouter, Depends, Security
|
||||
from fastapi import APIRouter, Depends, Security, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
import health_weight.schema as health_weight_schema
|
||||
@@ -15,30 +16,9 @@ import core.dependencies as core_dependencies
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get(
|
||||
"/number",
|
||||
response_model=int,
|
||||
)
|
||||
async def read_health_weight_number(
|
||||
_check_scopes: Annotated[
|
||||
Callable, Security(auth_security.check_scopes, scopes=["health:read"])
|
||||
],
|
||||
token_user_id: Annotated[
|
||||
int,
|
||||
Depends(auth_security.get_sub_from_access_token),
|
||||
],
|
||||
db: Annotated[
|
||||
Session,
|
||||
Depends(core_database.get_db),
|
||||
],
|
||||
):
|
||||
# Get the health_weight number from the database
|
||||
return health_weight_crud.get_health_weight_number(token_user_id, db)
|
||||
|
||||
|
||||
@router.get(
|
||||
"",
|
||||
response_model=list[health_weight_schema.HealthWeight] | None,
|
||||
response_model=health_weight_schema.HealthWeightListResponse,
|
||||
)
|
||||
async def read_health_weight_all(
|
||||
_check_scopes: Annotated[
|
||||
@@ -52,14 +32,36 @@ async def read_health_weight_all(
|
||||
Session,
|
||||
Depends(core_database.get_db),
|
||||
],
|
||||
):
|
||||
# Get the health_weight from the database
|
||||
return health_weight_crud.get_all_health_weight_by_user_id(token_user_id, db)
|
||||
) -> health_weight_schema.HealthWeightListResponse:
|
||||
"""
|
||||
Retrieve all health weight records for the authenticated user with total count.
|
||||
|
||||
This endpoint fetches all weight measurements associated with the currently
|
||||
authenticated user from the database, along with the total count of records.
|
||||
|
||||
Args:
|
||||
_check_scopes: Security dependency that verifies the user has 'health:read' scope.
|
||||
token_user_id: The user ID extracted from the access token.
|
||||
db: Database session dependency for executing queries.
|
||||
|
||||
Returns:
|
||||
HealthWeightListResponse: An object containing the total count and list of
|
||||
all health weight records belonging to the authenticated user.
|
||||
|
||||
Raises:
|
||||
HTTPException: May be raised by dependencies if authentication fails or
|
||||
if the user lacks required permissions.
|
||||
"""
|
||||
# Get the total count and records from the database
|
||||
total = health_weight_crud.get_health_weight_number(token_user_id, db)
|
||||
records = health_weight_crud.get_all_health_weight_by_user_id(token_user_id, db)
|
||||
|
||||
return health_weight_schema.HealthWeightListResponse(total=total, records=records)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/page_number/{page_number}/num_records/{num_records}",
|
||||
response_model=list[health_weight_schema.HealthWeight] | None,
|
||||
response_model=health_weight_schema.HealthWeightListResponse,
|
||||
)
|
||||
async def read_health_weight_all_pagination(
|
||||
page_number: int,
|
||||
@@ -78,12 +80,38 @@ async def read_health_weight_all_pagination(
|
||||
Session,
|
||||
Depends(core_database.get_db),
|
||||
],
|
||||
):
|
||||
# Get the health_weight from the database with pagination
|
||||
return health_weight_crud.get_health_weight_with_pagination(
|
||||
) -> health_weight_schema.HealthWeightListResponse:
|
||||
"""
|
||||
Retrieve weight health records for the authenticated user with pagination and total count.
|
||||
|
||||
This endpoint fetches weight health records from the database for the authenticated user,
|
||||
with support for pagination to limit the number of records returned. Also includes the
|
||||
total count of all records.
|
||||
|
||||
Args:
|
||||
page_number (int): The page number to retrieve (1-indexed).
|
||||
num_records (int): The number of records to return per page.
|
||||
_check_scopes (Callable): Security dependency that validates the required scopes.
|
||||
validate_pagination_values (Callable): Dependency that validates pagination parameters.
|
||||
token_user_id (int): The user ID extracted from the access token.
|
||||
db (Session): The database session dependency.
|
||||
|
||||
Returns:
|
||||
HealthWeightListResponse: An object containing the total count and paginated list
|
||||
of health weight records for the user.
|
||||
|
||||
Raises:
|
||||
HTTPException: If authentication fails or user lacks required permissions.
|
||||
HTTPException: If pagination parameters are invalid.
|
||||
"""
|
||||
# Get the total count and paginated records from the database
|
||||
total = health_weight_crud.get_health_weight_number(token_user_id, db)
|
||||
records = health_weight_crud.get_health_weight_with_pagination(
|
||||
token_user_id, db, page_number, num_records
|
||||
)
|
||||
|
||||
return health_weight_schema.HealthWeightListResponse(total=total, records=records)
|
||||
|
||||
|
||||
@router.post("", status_code=201)
|
||||
async def create_health_weight(
|
||||
@@ -99,10 +127,35 @@ async def create_health_weight(
|
||||
Session,
|
||||
Depends(core_database.get_db),
|
||||
],
|
||||
):
|
||||
) -> health_weight_schema.HealthWeight:
|
||||
"""
|
||||
Create or update a health weight record for the authenticated user.
|
||||
|
||||
This endpoint creates a new health weight record if one doesn't exist for the given date,
|
||||
or updates an existing record if one is already present for that date.
|
||||
|
||||
Args:
|
||||
health_weight (health_weight_schema.HealthWeight): The health weight data to create or update.
|
||||
Must include a date field.
|
||||
_check_scopes (Callable): Security dependency that verifies the user has 'health:write' scope.
|
||||
token_user_id (int): The ID of the authenticated user extracted from the access token.
|
||||
db (Session): Database session dependency for performing database operations.
|
||||
|
||||
Returns:
|
||||
health_weight_schema.HealthWeight: The created or updated health weight record.
|
||||
|
||||
Raises:
|
||||
HTTPException: 400 error if the date field is not provided in the request.
|
||||
"""
|
||||
if not health_weight.date:
|
||||
raise HTTPException(status_code=400, detail="Date field is required.")
|
||||
|
||||
# Convert date to string format for CRUD function
|
||||
date_str = health_weight.date.isoformat()
|
||||
|
||||
# Check if health_weight for this date already exists
|
||||
health_for_date = health_weight_crud.get_health_weight_by_date(
|
||||
token_user_id, health_weight.date, db
|
||||
token_user_id, date_str, db
|
||||
)
|
||||
|
||||
if health_for_date:
|
||||
@@ -128,12 +181,34 @@ async def edit_health_weight(
|
||||
Session,
|
||||
Depends(core_database.get_db),
|
||||
],
|
||||
):
|
||||
) -> health_weight_schema.HealthWeight:
|
||||
"""
|
||||
Edit a health weight entry for the authenticated user.
|
||||
|
||||
This endpoint allows users with 'health:write' scope to update an existing
|
||||
health weight record in the database.
|
||||
|
||||
Args:
|
||||
health_weight (health_weight_schema.HealthWeight): The health weight data
|
||||
to be updated, containing the weight information and associated metadata.
|
||||
_check_scopes (Callable): Security dependency that verifies the user has
|
||||
'health:write' scope permission.
|
||||
token_user_id (int): The ID of the authenticated user extracted from the
|
||||
access token.
|
||||
db (Session): Database session dependency for executing database operations.
|
||||
|
||||
Returns:
|
||||
The updated health weight record from the database.
|
||||
|
||||
Raises:
|
||||
HTTPException: If the user doesn't have permission to edit the weight entry
|
||||
or if the entry doesn't exist.
|
||||
"""
|
||||
# Updates the health_weight in the database and returns it
|
||||
return health_weight_crud.edit_health_weight(token_user_id, health_weight, db)
|
||||
|
||||
|
||||
@router.delete("/{health_weight_id}")
|
||||
@router.delete("/{health_weight_id}", status_code=204)
|
||||
async def delete_health_weight(
|
||||
health_weight_id: int,
|
||||
_check_scopes: Annotated[
|
||||
@@ -147,6 +222,26 @@ async def delete_health_weight(
|
||||
Session,
|
||||
Depends(core_database.get_db),
|
||||
],
|
||||
):
|
||||
) -> None:
|
||||
"""
|
||||
Delete a health weight entry for the authenticated user.
|
||||
|
||||
This endpoint allows users to delete their own health weight records. It requires
|
||||
the 'health:write' scope for authorization.
|
||||
|
||||
Args:
|
||||
health_weight_id (int): The unique identifier of the health weight entry to delete.
|
||||
_check_scopes (Callable): Security dependency that verifies the user has 'health:write' scope.
|
||||
token_user_id (int): The user ID extracted from the access token, used to ensure
|
||||
users can only delete their own weight entries.
|
||||
db (Session): Database session dependency for performing database operations.
|
||||
|
||||
Returns:
|
||||
None: This function does not return any value upon successful deletion.
|
||||
|
||||
Raises:
|
||||
HTTPException: May raise various HTTP exceptions (e.g., 404 if entry not found,
|
||||
403 if unauthorized) through the CRUD layer.
|
||||
"""
|
||||
# Deletes entry from database
|
||||
return health_weight_crud.delete_health_weight(token_user_id, health_weight_id, db)
|
||||
health_weight_crud.delete_health_weight(token_user_id, health_weight_id, db)
|
||||
|
||||
@@ -5,16 +5,44 @@ from datetime import date as datetime_date
|
||||
|
||||
class Source(Enum):
|
||||
"""
|
||||
An enumeration representing supported sources for the application.
|
||||
Enumeration representing the source of health weight data.
|
||||
|
||||
Members:
|
||||
GARMIN: Garmin health data source
|
||||
Attributes:
|
||||
GARMIN (str): Indicates that the weight data comes from Garmin devices or services.
|
||||
"""
|
||||
|
||||
GARMIN = "garmin"
|
||||
|
||||
|
||||
class HealthWeight(BaseModel):
|
||||
"""
|
||||
Schema for health weight measurements.
|
||||
|
||||
This class represents a comprehensive set of body composition and health metrics
|
||||
for a user at a specific point in time.
|
||||
|
||||
Attributes:
|
||||
id (int | None): Unique identifier for the health weight record.
|
||||
user_id (int | None): Identifier of the user associated with this measurement.
|
||||
date (datetime_date | None): Date when the measurement was taken.
|
||||
weight (float | None): Body weight measurement.
|
||||
bmi (float | None): Body Mass Index calculated from height and weight.
|
||||
body_fat (float | None): Body fat percentage.
|
||||
body_water (float | None): Body water percentage.
|
||||
bone_mass (float | None): Bone mass measurement.
|
||||
muscle_mass (float | None): Muscle mass measurement.
|
||||
physique_rating (int | None): Overall physique rating score.
|
||||
visceral_fat (float | None): Visceral fat level measurement.
|
||||
metabolic_age (int | None): Estimated metabolic age based on body composition.
|
||||
source (Source | None): Source or origin of the measurement data.
|
||||
|
||||
Configuration:
|
||||
- from_attributes: Enables population from ORM models
|
||||
- extra: Forbids extra fields not defined in the schema
|
||||
- validate_assignment: Validates values on assignment
|
||||
- use_enum_values: Uses enum values instead of enum instances
|
||||
"""
|
||||
|
||||
id: int | None = None
|
||||
user_id: int | None = None
|
||||
date: datetime_date | None = None
|
||||
@@ -35,3 +63,30 @@ class HealthWeight(BaseModel):
|
||||
validate_assignment=True,
|
||||
use_enum_values=True,
|
||||
)
|
||||
|
||||
|
||||
class HealthWeightListResponse(BaseModel):
|
||||
"""
|
||||
Response schema for health weight list with total count.
|
||||
|
||||
This class wraps a list of health weight records along with the total count,
|
||||
providing a complete response for list endpoints.
|
||||
|
||||
Attributes:
|
||||
total (int): Total number of weight records for the user.
|
||||
records (list[HealthWeight]): List of health weight measurements.
|
||||
|
||||
Configuration:
|
||||
- from_attributes: Enables population from ORM models
|
||||
- extra: Forbids extra fields not defined in the schema
|
||||
- validate_assignment: Validates values on assignment
|
||||
"""
|
||||
|
||||
total: int
|
||||
records: list[HealthWeight]
|
||||
|
||||
model_config = ConfigDict(
|
||||
from_attributes=True,
|
||||
extra="forbid",
|
||||
validate_assignment=True,
|
||||
)
|
||||
|
||||
@@ -11,11 +11,32 @@ def calculate_bmi(
|
||||
user_id: int,
|
||||
db: Session,
|
||||
) -> health_weight_schema.HealthWeight:
|
||||
"""
|
||||
Calculate the Body Mass Index (BMI) for a health weight record.
|
||||
|
||||
This function computes the BMI using the formula: weight (kg) / (height (m))^2.
|
||||
The height is retrieved from the user's profile and converted from centimeters to meters.
|
||||
If the user doesn't exist, or if height or weight data is missing, BMI is set to None.
|
||||
|
||||
Args:
|
||||
health_weight (health_weight_schema.HealthWeight): The health weight record containing
|
||||
the weight value and where the calculated BMI will be stored.
|
||||
user_id (int): The unique identifier of the user.
|
||||
db (Session): The database session object for querying user data.
|
||||
|
||||
Returns:
|
||||
health_weight_schema.HealthWeight: The updated health weight record with the calculated
|
||||
BMI value (or None if calculation is not possible).
|
||||
"""
|
||||
# Get the user from the database
|
||||
user = users_crud.get_user_by_id(user_id, db)
|
||||
|
||||
# Check if user is not None and user height is not None
|
||||
if user is not None and user.height is not None:
|
||||
# Check if user is not None and user height is not None and health_weight weight is not None
|
||||
if (
|
||||
user is not None
|
||||
and user.height is not None
|
||||
and health_weight.weight is not None
|
||||
):
|
||||
# Calculate the bmi
|
||||
health_weight.bmi = float(health_weight.weight) / ((user.height / 100) ** 2)
|
||||
else:
|
||||
@@ -26,7 +47,22 @@ def calculate_bmi(
|
||||
return health_weight
|
||||
|
||||
|
||||
def calculate_bmi_all_user_entries(user_id: int, db: Session):
|
||||
def calculate_bmi_all_user_entries(user_id: int, db: Session) -> None:
|
||||
"""
|
||||
Calculate and update BMI for all health weight entries of a specific user.
|
||||
|
||||
This function retrieves all health weight entries for a given user, calculates
|
||||
the BMI for each entry using the calculate_bmi function, and updates the
|
||||
database with the calculated BMI values.
|
||||
|
||||
Args:
|
||||
user_id (int): The unique identifier of the user whose health weight
|
||||
entries should be processed.
|
||||
db (Session): The database session object used for database operations.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
# Get all the health data entries for the user
|
||||
health_weight_entries = health_weight_crud.get_all_health_weight_by_user_id(
|
||||
user_id, db
|
||||
|
||||
@@ -20,11 +20,13 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "app"))
|
||||
import session.router as session_router
|
||||
import auth.password_hasher as auth_password_hasher
|
||||
import auth.token_manager as auth_token_manager
|
||||
import auth.security as auth_security
|
||||
import users.user.schema as user_schema
|
||||
|
||||
# Variables and constants
|
||||
DEFAULT_ROUTER_MODULES = [
|
||||
"session.router",
|
||||
"health_weight.router",
|
||||
]
|
||||
|
||||
|
||||
@@ -128,12 +130,17 @@ def _include_router_if_exists(app: FastAPI, dotted: str):
|
||||
Notes:
|
||||
- If the module does not exist or does not have a 'router' attribute, the function silently ignores the error.
|
||||
- This is useful for conditionally including routers in a modular FastAPI project.
|
||||
- For health_weight router, adds /health_weight prefix to match expected test URLs
|
||||
"""
|
||||
try:
|
||||
mod = import_module(dotted)
|
||||
router = getattr(mod, "router", None)
|
||||
if router is not None:
|
||||
app.include_router(router)
|
||||
# Add prefix for health_weight router
|
||||
if dotted == "health_weight.router":
|
||||
app.include_router(router, prefix="/health_weight")
|
||||
else:
|
||||
app.include_router(router)
|
||||
except Exception:
|
||||
# Silently ignore if module isn't present in this project
|
||||
pass
|
||||
@@ -372,6 +379,15 @@ def fast_api_app(password_hasher, token_manager, mock_db) -> FastAPI:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Override auth_security for health_weight router
|
||||
try:
|
||||
app.dependency_overrides[auth_security.check_scopes] = _mock_check_scopes
|
||||
app.dependency_overrides[auth_security.get_sub_from_access_token] = (
|
||||
_mock_get_sub_from_access_token
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Generic overrides
|
||||
_override_if_exists(
|
||||
app, "auth.password_hasher", "get_password_hasher", lambda: password_hasher
|
||||
|
||||
0
backend/tests/health_weight/__init__.py
Normal file
0
backend/tests/health_weight/__init__.py
Normal file
537
backend/tests/health_weight/test_crud.py
Normal file
537
backend/tests/health_weight/test_crud.py
Normal file
@@ -0,0 +1,537 @@
|
||||
import pytest
|
||||
from datetime import date as datetime_date
|
||||
from unittest.mock import MagicMock, patch
|
||||
from fastapi import HTTPException, status
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
import health_weight.crud as health_weight_crud
|
||||
import health_weight.schema as health_weight_schema
|
||||
import health_weight.models as health_weight_models
|
||||
|
||||
|
||||
class TestGetAllHealthWeight:
|
||||
"""
|
||||
Test suite for get_all_health_weight function.
|
||||
"""
|
||||
|
||||
def test_get_all_health_weight_success(self, mock_db):
|
||||
"""
|
||||
Test successful retrieval of all health weight records.
|
||||
"""
|
||||
# Arrange
|
||||
mock_weight1 = MagicMock(spec=health_weight_models.HealthWeight)
|
||||
mock_weight2 = MagicMock(spec=health_weight_models.HealthWeight)
|
||||
mock_query = mock_db.query.return_value
|
||||
mock_query.order_by.return_value.all.return_value = [
|
||||
mock_weight1,
|
||||
mock_weight2,
|
||||
]
|
||||
|
||||
# Act
|
||||
result = health_weight_crud.get_all_health_weight(mock_db)
|
||||
|
||||
# Assert
|
||||
assert result == [mock_weight1, mock_weight2]
|
||||
mock_db.query.assert_called_once_with(health_weight_models.HealthWeight)
|
||||
|
||||
def test_get_all_health_weight_exception(self, mock_db):
|
||||
"""
|
||||
Test exception handling in get_all_health_weight.
|
||||
"""
|
||||
# Arrange
|
||||
mock_db.query.side_effect = Exception("Database error")
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
health_weight_crud.get_all_health_weight(mock_db)
|
||||
|
||||
assert exc_info.value.status_code == (status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
assert exc_info.value.detail == "Internal Server Error"
|
||||
|
||||
|
||||
class TestGetHealthWeightNumber:
|
||||
"""
|
||||
Test suite for get_health_weight_number function.
|
||||
"""
|
||||
|
||||
def test_get_health_weight_number_success(self, mock_db):
|
||||
"""
|
||||
Test successful count of health weight records for a user.
|
||||
"""
|
||||
# Arrange
|
||||
user_id = 1
|
||||
expected_count = 5
|
||||
mock_query = mock_db.query.return_value
|
||||
mock_query.filter.return_value.count.return_value = expected_count
|
||||
|
||||
# Act
|
||||
result = health_weight_crud.get_health_weight_number(user_id, mock_db)
|
||||
|
||||
# Assert
|
||||
assert result == expected_count
|
||||
mock_db.query.assert_called_once_with(health_weight_models.HealthWeight)
|
||||
|
||||
def test_get_health_weight_number_exception(self, mock_db):
|
||||
"""
|
||||
Test exception handling in get_health_weight_number.
|
||||
"""
|
||||
# Arrange
|
||||
user_id = 1
|
||||
mock_db.query.side_effect = Exception("Database error")
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
health_weight_crud.get_health_weight_number(user_id, mock_db)
|
||||
|
||||
assert exc_info.value.status_code == (status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
class TestGetAllHealthWeightByUserId:
|
||||
"""
|
||||
Test suite for get_all_health_weight_by_user_id function.
|
||||
"""
|
||||
|
||||
def test_get_all_health_weight_by_user_id_success(self, mock_db):
|
||||
"""
|
||||
Test successful retrieval of all health weight records for user.
|
||||
"""
|
||||
# Arrange
|
||||
user_id = 1
|
||||
mock_weight1 = MagicMock(spec=health_weight_models.HealthWeight)
|
||||
mock_weight2 = MagicMock(spec=health_weight_models.HealthWeight)
|
||||
mock_query = mock_db.query.return_value
|
||||
mock_filter = mock_query.filter.return_value
|
||||
mock_filter.order_by.return_value.all.return_value = [
|
||||
mock_weight1,
|
||||
mock_weight2,
|
||||
]
|
||||
|
||||
# Act
|
||||
result = health_weight_crud.get_all_health_weight_by_user_id(user_id, mock_db)
|
||||
|
||||
# Assert
|
||||
assert result == [mock_weight1, mock_weight2]
|
||||
mock_db.query.assert_called_once_with(health_weight_models.HealthWeight)
|
||||
|
||||
def test_get_all_health_weight_by_user_id_exception(self, mock_db):
|
||||
"""
|
||||
Test exception handling in get_all_health_weight_by_user_id.
|
||||
"""
|
||||
# Arrange
|
||||
user_id = 1
|
||||
mock_db.query.side_effect = Exception("Database error")
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
health_weight_crud.get_all_health_weight_by_user_id(user_id, mock_db)
|
||||
|
||||
assert exc_info.value.status_code == (status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
class TestGetHealthWeightWithPagination:
|
||||
"""
|
||||
Test suite for get_health_weight_with_pagination function.
|
||||
"""
|
||||
|
||||
def test_get_health_weight_with_pagination_success(self, mock_db):
|
||||
"""
|
||||
Test successful retrieval of paginated health weight records.
|
||||
"""
|
||||
# Arrange
|
||||
user_id = 1
|
||||
page_number = 2
|
||||
num_records = 5
|
||||
mock_weight1 = MagicMock(spec=health_weight_models.HealthWeight)
|
||||
mock_weight2 = MagicMock(spec=health_weight_models.HealthWeight)
|
||||
mock_query = mock_db.query.return_value
|
||||
mock_filter = mock_query.filter.return_value
|
||||
mock_order = mock_filter.order_by.return_value
|
||||
mock_offset = mock_order.offset.return_value
|
||||
mock_offset.limit.return_value.all.return_value = [
|
||||
mock_weight1,
|
||||
mock_weight2,
|
||||
]
|
||||
|
||||
# Act
|
||||
result = health_weight_crud.get_health_weight_with_pagination(
|
||||
user_id, mock_db, page_number, num_records
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result == [mock_weight1, mock_weight2]
|
||||
mock_order.offset.assert_called_once_with(5)
|
||||
mock_offset.limit.assert_called_once_with(5)
|
||||
|
||||
def test_get_health_weight_with_pagination_defaults(self, mock_db):
|
||||
"""
|
||||
Test pagination with default values.
|
||||
"""
|
||||
# Arrange
|
||||
user_id = 1
|
||||
mock_query = mock_db.query.return_value
|
||||
mock_filter = mock_query.filter.return_value
|
||||
mock_order = mock_filter.order_by.return_value
|
||||
mock_offset = mock_order.offset.return_value
|
||||
mock_offset.limit.return_value.all.return_value = []
|
||||
|
||||
# Act
|
||||
result = health_weight_crud.get_health_weight_with_pagination(user_id, mock_db)
|
||||
|
||||
# Assert
|
||||
mock_order.offset.assert_called_once_with(0)
|
||||
mock_offset.limit.assert_called_once_with(5)
|
||||
|
||||
def test_get_health_weight_with_pagination_exception(self, mock_db):
|
||||
"""
|
||||
Test exception handling in get_health_weight_with_pagination.
|
||||
"""
|
||||
# Arrange
|
||||
user_id = 1
|
||||
mock_db.query.side_effect = Exception("Database error")
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
health_weight_crud.get_health_weight_with_pagination(user_id, mock_db)
|
||||
|
||||
assert exc_info.value.status_code == (status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
class TestGetHealthWeightByDate:
|
||||
"""
|
||||
Test suite for get_health_weight_by_date function.
|
||||
"""
|
||||
|
||||
def test_get_health_weight_by_date_success(self, mock_db):
|
||||
"""
|
||||
Test successful retrieval of health weight by date.
|
||||
"""
|
||||
# Arrange
|
||||
user_id = 1
|
||||
test_date = "2024-01-15"
|
||||
mock_weight = MagicMock(spec=health_weight_models.HealthWeight)
|
||||
mock_query = mock_db.query.return_value
|
||||
mock_query.filter.return_value.first.return_value = mock_weight
|
||||
|
||||
# Act
|
||||
result = health_weight_crud.get_health_weight_by_date(
|
||||
user_id, test_date, mock_db
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result == mock_weight
|
||||
mock_db.query.assert_called_once_with(health_weight_models.HealthWeight)
|
||||
|
||||
def test_get_health_weight_by_date_not_found(self, mock_db):
|
||||
"""
|
||||
Test retrieval when no record exists for date.
|
||||
"""
|
||||
# Arrange
|
||||
user_id = 1
|
||||
test_date = "2024-01-15"
|
||||
mock_query = mock_db.query.return_value
|
||||
mock_query.filter.return_value.first.return_value = None
|
||||
|
||||
# Act
|
||||
result = health_weight_crud.get_health_weight_by_date(
|
||||
user_id, test_date, mock_db
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result is None
|
||||
|
||||
def test_get_health_weight_by_date_exception(self, mock_db):
|
||||
"""
|
||||
Test exception handling in get_health_weight_by_date.
|
||||
"""
|
||||
# Arrange
|
||||
user_id = 1
|
||||
test_date = "2024-01-15"
|
||||
mock_db.query.side_effect = Exception("Database error")
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
health_weight_crud.get_health_weight_by_date(user_id, test_date, mock_db)
|
||||
|
||||
assert exc_info.value.status_code == (status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
class TestCreateHealthWeight:
|
||||
"""
|
||||
Test suite for create_health_weight function.
|
||||
"""
|
||||
|
||||
@patch("health_weight.crud.health_weight_utils.calculate_bmi")
|
||||
def test_create_health_weight_success(self, mock_calculate_bmi, mock_db):
|
||||
"""
|
||||
Test successful creation of health weight entry.
|
||||
"""
|
||||
# Arrange
|
||||
user_id = 1
|
||||
health_weight = health_weight_schema.HealthWeight(
|
||||
date=datetime_date(2024, 1, 15),
|
||||
weight=75.5,
|
||||
bmi=None,
|
||||
)
|
||||
updated_weight = health_weight_schema.HealthWeight(
|
||||
date=datetime_date(2024, 1, 15),
|
||||
weight=75.5,
|
||||
bmi=24.5,
|
||||
)
|
||||
mock_calculate_bmi.return_value = updated_weight
|
||||
|
||||
mock_db_weight = MagicMock()
|
||||
mock_db_weight.id = 1
|
||||
mock_db.add.return_value = None
|
||||
mock_db.commit.return_value = None
|
||||
mock_db.refresh.return_value = None
|
||||
|
||||
with patch.object(
|
||||
health_weight_models,
|
||||
"HealthWeight",
|
||||
return_value=mock_db_weight,
|
||||
):
|
||||
# Act
|
||||
result = health_weight_crud.create_health_weight(
|
||||
user_id, health_weight, mock_db
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result.id == 1
|
||||
assert result.bmi == 24.5
|
||||
mock_db.add.assert_called_once()
|
||||
mock_db.commit.assert_called_once()
|
||||
mock_db.refresh.assert_called_once()
|
||||
|
||||
@patch("health_weight.crud.health_weight_utils.calculate_bmi")
|
||||
@patch("health_weight.crud.func")
|
||||
def test_create_health_weight_with_none_date(
|
||||
self, mock_func, mock_calculate_bmi, mock_db
|
||||
):
|
||||
"""
|
||||
Test creation with None date sets current date.
|
||||
"""
|
||||
# Arrange
|
||||
user_id = 1
|
||||
health_weight = health_weight_schema.HealthWeight(
|
||||
date=None, weight=75.5, bmi=24.5
|
||||
)
|
||||
|
||||
# Mock func.now() to return a proper date object
|
||||
mock_func.now.return_value = datetime_date(2024, 1, 15)
|
||||
|
||||
mock_db_weight = MagicMock()
|
||||
mock_db_weight.id = 1
|
||||
mock_db.add.return_value = None
|
||||
mock_db.commit.return_value = None
|
||||
mock_db.refresh.return_value = None
|
||||
|
||||
with patch.object(
|
||||
health_weight_models,
|
||||
"HealthWeight",
|
||||
return_value=mock_db_weight,
|
||||
):
|
||||
# Act
|
||||
result = health_weight_crud.create_health_weight(
|
||||
user_id, health_weight, mock_db
|
||||
)
|
||||
|
||||
# Assert
|
||||
mock_func.now.assert_called_once()
|
||||
assert result.id == 1
|
||||
assert result.date == datetime_date(2024, 1, 15)
|
||||
|
||||
def test_create_health_weight_duplicate_entry(self, mock_db):
|
||||
"""
|
||||
Test creation with duplicate entry raises conflict error.
|
||||
"""
|
||||
# Arrange
|
||||
user_id = 1
|
||||
health_weight = health_weight_schema.HealthWeight(
|
||||
date=datetime_date(2024, 1, 15), weight=75.5, bmi=24.5
|
||||
)
|
||||
|
||||
mock_db_weight = MagicMock()
|
||||
mock_db.add.return_value = None
|
||||
mock_db.commit.side_effect = IntegrityError("Duplicate entry", None, None)
|
||||
|
||||
with patch.object(
|
||||
health_weight_models,
|
||||
"HealthWeight",
|
||||
return_value=mock_db_weight,
|
||||
):
|
||||
# Act & Assert
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
health_weight_crud.create_health_weight(user_id, health_weight, mock_db)
|
||||
|
||||
assert exc_info.value.status_code == status.HTTP_409_CONFLICT
|
||||
assert "Duplicate entry error" in exc_info.value.detail
|
||||
mock_db.rollback.assert_called_once()
|
||||
|
||||
def test_create_health_weight_exception(self, mock_db):
|
||||
"""
|
||||
Test exception handling in create_health_weight.
|
||||
"""
|
||||
# Arrange
|
||||
user_id = 1
|
||||
health_weight = health_weight_schema.HealthWeight(
|
||||
date=datetime_date(2024, 1, 15), weight=75.5, bmi=24.5
|
||||
)
|
||||
|
||||
mock_db.add.side_effect = Exception("Database error")
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
health_weight_crud.create_health_weight(user_id, health_weight, mock_db)
|
||||
|
||||
assert exc_info.value.status_code == (status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
mock_db.rollback.assert_called_once()
|
||||
|
||||
|
||||
class TestEditHealthWeight:
|
||||
"""
|
||||
Test suite for edit_health_weight function.
|
||||
"""
|
||||
|
||||
@patch("health_weight.crud.health_weight_utils.calculate_bmi")
|
||||
def test_edit_health_weight_success(self, mock_calculate_bmi, mock_db):
|
||||
"""
|
||||
Test successful edit of health weight entry.
|
||||
"""
|
||||
# Arrange
|
||||
user_id = 1
|
||||
health_weight = health_weight_schema.HealthWeight(
|
||||
id=1, date=datetime_date(2024, 1, 15), weight=76.0, bmi=None
|
||||
)
|
||||
updated_weight = health_weight_schema.HealthWeight(
|
||||
id=1, date=datetime_date(2024, 1, 15), weight=76.0, bmi=24.7
|
||||
)
|
||||
mock_calculate_bmi.return_value = updated_weight
|
||||
|
||||
mock_db_weight = MagicMock(spec=health_weight_models.HealthWeight)
|
||||
mock_query = mock_db.query.return_value
|
||||
mock_query.filter.return_value.first.return_value = mock_db_weight
|
||||
|
||||
# Act
|
||||
result = health_weight_crud.edit_health_weight(user_id, health_weight, mock_db)
|
||||
|
||||
# Assert
|
||||
assert result.bmi == 24.7
|
||||
mock_db.commit.assert_called_once()
|
||||
|
||||
def test_edit_health_weight_not_found(self, mock_db):
|
||||
"""
|
||||
Test edit when health weight record not found.
|
||||
"""
|
||||
# Arrange
|
||||
user_id = 1
|
||||
health_weight = health_weight_schema.HealthWeight(
|
||||
id=999, date=datetime_date(2024, 1, 15), weight=76.0
|
||||
)
|
||||
|
||||
mock_query = mock_db.query.return_value
|
||||
mock_query.filter.return_value.first.return_value = None
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
health_weight_crud.edit_health_weight(user_id, health_weight, mock_db)
|
||||
|
||||
assert exc_info.value.status_code == status.HTTP_404_NOT_FOUND
|
||||
assert exc_info.value.detail == "Health weight not found"
|
||||
|
||||
def test_edit_health_weight_without_bmi_calculation(self, mock_db):
|
||||
"""
|
||||
Test edit without BMI calculation when BMI provided.
|
||||
"""
|
||||
# Arrange
|
||||
user_id = 1
|
||||
health_weight = health_weight_schema.HealthWeight(
|
||||
id=1, date=datetime_date(2024, 1, 15), weight=76.0, bmi=25.0
|
||||
)
|
||||
|
||||
mock_db_weight = MagicMock(spec=health_weight_models.HealthWeight)
|
||||
mock_query = mock_db.query.return_value
|
||||
mock_query.filter.return_value.first.return_value = mock_db_weight
|
||||
|
||||
# Act
|
||||
result = health_weight_crud.edit_health_weight(user_id, health_weight, mock_db)
|
||||
|
||||
# Assert
|
||||
mock_db.commit.assert_called_once()
|
||||
|
||||
def test_edit_health_weight_exception(self, mock_db):
|
||||
"""
|
||||
Test exception handling in edit_health_weight.
|
||||
"""
|
||||
# Arrange
|
||||
user_id = 1
|
||||
health_weight = health_weight_schema.HealthWeight(id=1, weight=76.0)
|
||||
|
||||
mock_db.query.side_effect = Exception("Database error")
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
health_weight_crud.edit_health_weight(user_id, health_weight, mock_db)
|
||||
|
||||
assert exc_info.value.status_code == (status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
mock_db.rollback.assert_called_once()
|
||||
|
||||
|
||||
class TestDeleteHealthWeight:
|
||||
"""
|
||||
Test suite for delete_health_weight function.
|
||||
"""
|
||||
|
||||
def test_delete_health_weight_success(self, mock_db):
|
||||
"""
|
||||
Test successful deletion of health weight entry.
|
||||
"""
|
||||
# Arrange
|
||||
user_id = 1
|
||||
health_weight_id = 1
|
||||
|
||||
mock_query = mock_db.query.return_value
|
||||
mock_filter = mock_query.filter.return_value
|
||||
mock_filter.delete.return_value = 1
|
||||
|
||||
# Act
|
||||
health_weight_crud.delete_health_weight(user_id, health_weight_id, mock_db)
|
||||
|
||||
# Assert
|
||||
mock_db.commit.assert_called_once()
|
||||
mock_db.query.assert_called_once_with(health_weight_models.HealthWeight)
|
||||
|
||||
def test_delete_health_weight_not_found(self, mock_db):
|
||||
"""
|
||||
Test deletion when health weight record not found.
|
||||
"""
|
||||
# Arrange
|
||||
user_id = 1
|
||||
health_weight_id = 999
|
||||
|
||||
mock_query = mock_db.query.return_value
|
||||
mock_filter = mock_query.filter.return_value
|
||||
mock_filter.delete.return_value = 0
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
health_weight_crud.delete_health_weight(user_id, health_weight_id, mock_db)
|
||||
|
||||
assert exc_info.value.status_code == status.HTTP_404_NOT_FOUND
|
||||
assert f"Health weight with id {health_weight_id}" in (exc_info.value.detail)
|
||||
|
||||
def test_delete_health_weight_exception(self, mock_db):
|
||||
"""
|
||||
Test exception handling in delete_health_weight.
|
||||
"""
|
||||
# Arrange
|
||||
user_id = 1
|
||||
health_weight_id = 1
|
||||
|
||||
mock_db.query.side_effect = Exception("Database error")
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
health_weight_crud.delete_health_weight(user_id, health_weight_id, mock_db)
|
||||
|
||||
assert exc_info.value.status_code == (status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
mock_db.rollback.assert_called_once()
|
||||
122
backend/tests/health_weight/test_models.py
Normal file
122
backend/tests/health_weight/test_models.py
Normal file
@@ -0,0 +1,122 @@
|
||||
import pytest
|
||||
from datetime import date as datetime_date
|
||||
from decimal import Decimal
|
||||
|
||||
import health_weight.models as health_weight_models
|
||||
|
||||
|
||||
class TestHealthWeightModel:
|
||||
"""
|
||||
Test suite for HealthWeight SQLAlchemy model.
|
||||
"""
|
||||
|
||||
def test_health_weight_model_table_name(self):
|
||||
"""
|
||||
Test HealthWeight model has correct table name.
|
||||
"""
|
||||
# Assert
|
||||
assert health_weight_models.HealthWeight.__tablename__ == "health_weight"
|
||||
|
||||
def test_health_weight_model_columns_exist(self):
|
||||
"""
|
||||
Test HealthWeight model has all expected columns.
|
||||
"""
|
||||
# Assert
|
||||
assert hasattr(health_weight_models.HealthWeight, "id")
|
||||
assert hasattr(health_weight_models.HealthWeight, "user_id")
|
||||
assert hasattr(health_weight_models.HealthWeight, "date")
|
||||
assert hasattr(health_weight_models.HealthWeight, "weight")
|
||||
assert hasattr(health_weight_models.HealthWeight, "bmi")
|
||||
assert hasattr(health_weight_models.HealthWeight, "body_fat")
|
||||
assert hasattr(health_weight_models.HealthWeight, "body_water")
|
||||
assert hasattr(health_weight_models.HealthWeight, "bone_mass")
|
||||
assert hasattr(health_weight_models.HealthWeight, "muscle_mass")
|
||||
assert hasattr(health_weight_models.HealthWeight, "physique_rating")
|
||||
assert hasattr(health_weight_models.HealthWeight, "visceral_fat")
|
||||
assert hasattr(health_weight_models.HealthWeight, "metabolic_age")
|
||||
assert hasattr(health_weight_models.HealthWeight, "source")
|
||||
|
||||
def test_health_weight_model_primary_key(self):
|
||||
"""
|
||||
Test HealthWeight model has correct primary key.
|
||||
"""
|
||||
# Arrange
|
||||
id_column = health_weight_models.HealthWeight.id
|
||||
|
||||
# Assert
|
||||
assert id_column.primary_key is True
|
||||
assert id_column.autoincrement is True
|
||||
|
||||
def test_health_weight_model_foreign_key(self):
|
||||
"""
|
||||
Test HealthWeight model has correct foreign key.
|
||||
"""
|
||||
# Arrange
|
||||
user_id_column = health_weight_models.HealthWeight.user_id
|
||||
|
||||
# Assert
|
||||
assert user_id_column.nullable is False
|
||||
assert user_id_column.index is True
|
||||
|
||||
def test_health_weight_model_nullable_fields(self):
|
||||
"""
|
||||
Test HealthWeight model nullable fields.
|
||||
"""
|
||||
# Assert
|
||||
assert health_weight_models.HealthWeight.bmi.nullable is True
|
||||
assert health_weight_models.HealthWeight.body_fat.nullable is True
|
||||
assert health_weight_models.HealthWeight.body_water.nullable is True
|
||||
assert health_weight_models.HealthWeight.bone_mass.nullable is True
|
||||
assert health_weight_models.HealthWeight.muscle_mass.nullable is True
|
||||
assert health_weight_models.HealthWeight.physique_rating.nullable is True
|
||||
assert health_weight_models.HealthWeight.visceral_fat.nullable is True
|
||||
assert health_weight_models.HealthWeight.metabolic_age.nullable is True
|
||||
assert health_weight_models.HealthWeight.source.nullable is True
|
||||
|
||||
def test_health_weight_model_required_fields(self):
|
||||
"""
|
||||
Test HealthWeight model required fields.
|
||||
"""
|
||||
# Assert
|
||||
assert health_weight_models.HealthWeight.user_id.nullable is False
|
||||
assert health_weight_models.HealthWeight.date.nullable is False
|
||||
assert health_weight_models.HealthWeight.weight.nullable is False
|
||||
|
||||
def test_health_weight_model_column_types(self):
|
||||
"""
|
||||
Test HealthWeight model column types.
|
||||
"""
|
||||
# Assert
|
||||
assert health_weight_models.HealthWeight.id.type.python_type == int
|
||||
assert health_weight_models.HealthWeight.user_id.type.python_type == int
|
||||
assert health_weight_models.HealthWeight.date.type.python_type == datetime_date
|
||||
assert health_weight_models.HealthWeight.physique_rating.type.python_type == int
|
||||
assert health_weight_models.HealthWeight.metabolic_age.type.python_type == int
|
||||
assert health_weight_models.HealthWeight.source.type.python_type == str
|
||||
|
||||
def test_health_weight_model_decimal_precision(self):
|
||||
"""
|
||||
Test HealthWeight model decimal fields precision.
|
||||
"""
|
||||
# Assert
|
||||
assert health_weight_models.HealthWeight.weight.type.precision == 10
|
||||
assert health_weight_models.HealthWeight.weight.type.scale == 2
|
||||
assert health_weight_models.HealthWeight.bmi.type.precision == 10
|
||||
assert health_weight_models.HealthWeight.bmi.type.scale == 2
|
||||
assert health_weight_models.HealthWeight.body_fat.type.precision == 10
|
||||
assert health_weight_models.HealthWeight.body_fat.type.scale == 2
|
||||
|
||||
def test_health_weight_model_has_user_relationship(self):
|
||||
"""
|
||||
Test HealthWeight model has user relationship.
|
||||
"""
|
||||
# Assert
|
||||
assert hasattr(health_weight_models.HealthWeight, "user")
|
||||
|
||||
def test_health_weight_model_docstring(self):
|
||||
"""
|
||||
Test HealthWeight model has docstring.
|
||||
"""
|
||||
# Assert
|
||||
assert health_weight_models.HealthWeight.__doc__ is not None
|
||||
assert "SQLAlchemy model" in health_weight_models.HealthWeight.__doc__
|
||||
341
backend/tests/health_weight/test_router.py
Normal file
341
backend/tests/health_weight/test_router.py
Normal file
@@ -0,0 +1,341 @@
|
||||
import pytest
|
||||
from datetime import date as datetime_date
|
||||
from unittest.mock import MagicMock, patch, ANY
|
||||
from fastapi import HTTPException, status
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
import health_weight.schema as health_weight_schema
|
||||
import health_weight.models as health_weight_models
|
||||
|
||||
|
||||
class TestReadHealthWeightAll:
|
||||
"""
|
||||
Test suite for read_health_weight_all endpoint.
|
||||
"""
|
||||
|
||||
@patch("health_weight.router.health_weight_crud.get_health_weight_number")
|
||||
@patch(
|
||||
"health_weight.router.health_weight_crud." "get_all_health_weight_by_user_id"
|
||||
)
|
||||
def test_read_health_weight_all_success(
|
||||
self, mock_get_all, mock_get_number, fast_api_client, fast_api_app
|
||||
):
|
||||
"""
|
||||
Test successful retrieval of all health weight records with total count.
|
||||
"""
|
||||
# Arrange
|
||||
mock_weight1 = MagicMock(spec=health_weight_models.HealthWeight)
|
||||
mock_weight1.id = 1
|
||||
mock_weight1.user_id = 1
|
||||
mock_weight1.date = datetime_date(2024, 1, 15)
|
||||
mock_weight1.weight = 75.5
|
||||
mock_weight1.bmi = 24.5
|
||||
mock_weight1.source = None
|
||||
|
||||
mock_weight2 = MagicMock(spec=health_weight_models.HealthWeight)
|
||||
mock_weight2.id = 2
|
||||
mock_weight2.user_id = 1
|
||||
mock_weight2.date = datetime_date(2024, 1, 16)
|
||||
mock_weight2.weight = 75.0
|
||||
mock_weight2.bmi = 24.3
|
||||
mock_weight2.source = None
|
||||
|
||||
mock_get_all.return_value = [mock_weight1, mock_weight2]
|
||||
mock_get_number.return_value = 2
|
||||
|
||||
# Act
|
||||
response = fast_api_client.get(
|
||||
"/health_weight",
|
||||
headers={"Authorization": "Bearer mock_token"},
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["total"] == 2
|
||||
assert len(data["records"]) == 2
|
||||
|
||||
@patch("health_weight.router.health_weight_crud.get_health_weight_number")
|
||||
@patch(
|
||||
"health_weight.router.health_weight_crud." "get_all_health_weight_by_user_id"
|
||||
)
|
||||
def test_read_health_weight_all_empty(
|
||||
self, mock_get_all, mock_get_number, fast_api_client, fast_api_app
|
||||
):
|
||||
"""
|
||||
Test retrieval when user has no health weight records.
|
||||
"""
|
||||
# Arrange
|
||||
mock_get_all.return_value = []
|
||||
mock_get_number.return_value = 0
|
||||
|
||||
# Act
|
||||
response = fast_api_client.get(
|
||||
"/health_weight",
|
||||
headers={"Authorization": "Bearer mock_token"},
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["total"] == 0
|
||||
assert data["records"] == []
|
||||
|
||||
|
||||
class TestReadHealthWeightAllPagination:
|
||||
"""
|
||||
Test suite for read_health_weight_all_pagination endpoint.
|
||||
"""
|
||||
|
||||
@patch("health_weight.router.health_weight_crud.get_health_weight_number")
|
||||
@patch(
|
||||
"health_weight.router.health_weight_crud." "get_health_weight_with_pagination"
|
||||
)
|
||||
def test_read_health_weight_all_pagination_success(
|
||||
self, mock_get_paginated, mock_get_number, fast_api_client, fast_api_app
|
||||
):
|
||||
"""
|
||||
Test successful retrieval of paginated health weight records with total count.
|
||||
"""
|
||||
# Arrange
|
||||
mock_weight1 = MagicMock(spec=health_weight_models.HealthWeight)
|
||||
mock_weight1.id = 1
|
||||
mock_weight1.user_id = 1
|
||||
mock_weight1.date = datetime_date(2024, 1, 15)
|
||||
mock_weight1.weight = 75.5
|
||||
mock_weight1.bmi = 24.5
|
||||
mock_weight1.source = None
|
||||
|
||||
mock_get_paginated.return_value = [mock_weight1]
|
||||
mock_get_number.return_value = 10
|
||||
|
||||
# Act
|
||||
response = fast_api_client.get(
|
||||
"/health_weight/page_number/1/num_records/5",
|
||||
headers={"Authorization": "Bearer mock_token"},
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["total"] == 10
|
||||
assert len(data["records"]) == 1
|
||||
|
||||
@patch("health_weight.router.health_weight_crud.get_health_weight_number")
|
||||
@patch(
|
||||
"health_weight.router.health_weight_crud." "get_health_weight_with_pagination"
|
||||
)
|
||||
def test_read_health_weight_all_pagination_different_page(
|
||||
self, mock_get_paginated, mock_get_number, fast_api_client, fast_api_app
|
||||
):
|
||||
"""
|
||||
Test paginated retrieval with different page numbers.
|
||||
"""
|
||||
# Arrange
|
||||
mock_get_paginated.return_value = []
|
||||
mock_get_number.return_value = 20
|
||||
|
||||
# Act
|
||||
response = fast_api_client.get(
|
||||
"/health_weight/page_number/2/num_records/10",
|
||||
headers={"Authorization": "Bearer mock_token"},
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["total"] == 20
|
||||
assert data["records"] == []
|
||||
mock_get_paginated.assert_called_once_with(1, ANY, 2, 10)
|
||||
|
||||
|
||||
class TestCreateHealthWeight:
|
||||
"""
|
||||
Test suite for create_health_weight endpoint.
|
||||
"""
|
||||
|
||||
@patch("health_weight.router.health_weight_crud.create_health_weight")
|
||||
@patch("health_weight.router.health_weight_crud.get_health_weight_by_date")
|
||||
def test_create_health_weight_success(
|
||||
self,
|
||||
mock_get_by_date,
|
||||
mock_create,
|
||||
fast_api_client,
|
||||
fast_api_app,
|
||||
):
|
||||
"""
|
||||
Test successful creation of health weight entry.
|
||||
"""
|
||||
# Arrange
|
||||
mock_get_by_date.return_value = None
|
||||
created_weight = health_weight_schema.HealthWeight(
|
||||
id=1,
|
||||
user_id=1,
|
||||
date=datetime_date(2024, 1, 15),
|
||||
weight=75.5,
|
||||
bmi=24.5,
|
||||
)
|
||||
mock_create.return_value = created_weight
|
||||
|
||||
# Act
|
||||
response = fast_api_client.post(
|
||||
"/health_weight",
|
||||
json={
|
||||
"date": "2024-01-15",
|
||||
"weight": 75.5,
|
||||
},
|
||||
headers={"Authorization": "Bearer mock_token"},
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["weight"] == 75.5
|
||||
|
||||
@patch("health_weight.router.health_weight_crud.edit_health_weight")
|
||||
@patch("health_weight.router.health_weight_crud.get_health_weight_by_date")
|
||||
def test_create_health_weight_updates_existing(
|
||||
self, mock_get_by_date, mock_edit, fast_api_client, fast_api_app
|
||||
):
|
||||
"""
|
||||
Test creating health weight when entry exists updates it.
|
||||
"""
|
||||
# Arrange
|
||||
existing_weight = MagicMock()
|
||||
existing_weight.id = 1
|
||||
mock_get_by_date.return_value = existing_weight
|
||||
|
||||
updated_weight = health_weight_schema.HealthWeight(
|
||||
id=1,
|
||||
user_id=1,
|
||||
date=datetime_date(2024, 1, 15),
|
||||
weight=76.0,
|
||||
bmi=24.7,
|
||||
)
|
||||
mock_edit.return_value = updated_weight
|
||||
|
||||
# Act
|
||||
response = fast_api_client.post(
|
||||
"/health_weight",
|
||||
json={
|
||||
"date": "2024-01-15",
|
||||
"weight": 76.0,
|
||||
},
|
||||
headers={"Authorization": "Bearer mock_token"},
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 201
|
||||
mock_edit.assert_called_once()
|
||||
|
||||
|
||||
class TestEditHealthWeight:
|
||||
"""
|
||||
Test suite for edit_health_weight endpoint.
|
||||
"""
|
||||
|
||||
@patch("health_weight.router.health_weight_crud.edit_health_weight")
|
||||
def test_edit_health_weight_success(self, mock_edit, fast_api_client, fast_api_app):
|
||||
"""
|
||||
Test successful edit of health weight entry.
|
||||
"""
|
||||
# Arrange
|
||||
updated_weight = health_weight_schema.HealthWeight(
|
||||
id=1,
|
||||
user_id=1,
|
||||
date=datetime_date(2024, 1, 15),
|
||||
weight=76.0,
|
||||
bmi=24.7,
|
||||
)
|
||||
mock_edit.return_value = updated_weight
|
||||
|
||||
# Act
|
||||
response = fast_api_client.put(
|
||||
"/health_weight",
|
||||
json={
|
||||
"id": 1,
|
||||
"date": "2024-01-15",
|
||||
"weight": 76.0,
|
||||
},
|
||||
headers={"Authorization": "Bearer mock_token"},
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["weight"] == 76.0
|
||||
|
||||
@patch("health_weight.router.health_weight_crud.edit_health_weight")
|
||||
def test_edit_health_weight_not_found(
|
||||
self, mock_edit, fast_api_client, fast_api_app
|
||||
):
|
||||
"""
|
||||
Test edit when health weight not found.
|
||||
"""
|
||||
# Arrange
|
||||
mock_edit.side_effect = HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Health weight not found",
|
||||
)
|
||||
|
||||
# Act
|
||||
response = fast_api_client.put(
|
||||
"/health_weight",
|
||||
json={
|
||||
"id": 999,
|
||||
"date": "2024-01-15",
|
||||
"weight": 76.0,
|
||||
},
|
||||
headers={"Authorization": "Bearer mock_token"},
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
class TestDeleteHealthWeight:
|
||||
"""
|
||||
Test suite for delete_health_weight endpoint.
|
||||
"""
|
||||
|
||||
@patch("health_weight.router.health_weight_crud.delete_health_weight")
|
||||
def test_delete_health_weight_success(
|
||||
self, mock_delete, fast_api_client, fast_api_app
|
||||
):
|
||||
"""
|
||||
Test successful deletion of health weight entry.
|
||||
"""
|
||||
# Arrange
|
||||
mock_delete.return_value = None
|
||||
|
||||
# Act
|
||||
response = fast_api_client.delete(
|
||||
"/health_weight/1",
|
||||
headers={"Authorization": "Bearer mock_token"},
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 204
|
||||
mock_delete.assert_called_once_with(1, 1, ANY)
|
||||
|
||||
@patch("health_weight.router.health_weight_crud.delete_health_weight")
|
||||
def test_delete_health_weight_not_found(
|
||||
self, mock_delete, fast_api_client, fast_api_app
|
||||
):
|
||||
"""
|
||||
Test deletion when health weight not found.
|
||||
"""
|
||||
# Arrange
|
||||
mock_delete.side_effect = HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Health weight with id 999 for user 1 not found",
|
||||
)
|
||||
|
||||
# Act
|
||||
response = fast_api_client.delete(
|
||||
"/health_weight/999",
|
||||
headers={"Authorization": "Bearer mock_token"},
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 404
|
||||
253
backend/tests/health_weight/test_schema.py
Normal file
253
backend/tests/health_weight/test_schema.py
Normal file
@@ -0,0 +1,253 @@
|
||||
import pytest
|
||||
from datetime import date as datetime_date
|
||||
from pydantic import ValidationError
|
||||
|
||||
import health_weight.schema as health_weight_schema
|
||||
|
||||
|
||||
class TestHealthWeightSchema:
|
||||
"""
|
||||
Test suite for HealthWeight Pydantic schema.
|
||||
"""
|
||||
|
||||
def test_health_weight_valid_full_data(self):
|
||||
"""
|
||||
Test HealthWeight schema with all valid fields.
|
||||
"""
|
||||
# Arrange & Act
|
||||
health_weight = health_weight_schema.HealthWeight(
|
||||
id=1,
|
||||
user_id=1,
|
||||
date=datetime_date(2024, 1, 15),
|
||||
weight=75.5,
|
||||
bmi=24.5,
|
||||
body_fat=18.5,
|
||||
body_water=60.0,
|
||||
bone_mass=3.5,
|
||||
muscle_mass=62.0,
|
||||
physique_rating=7,
|
||||
visceral_fat=5.0,
|
||||
metabolic_age=25,
|
||||
source=health_weight_schema.Source.GARMIN,
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert health_weight.id == 1
|
||||
assert health_weight.user_id == 1
|
||||
assert health_weight.date == datetime_date(2024, 1, 15)
|
||||
assert health_weight.weight == 75.5
|
||||
assert health_weight.bmi == 24.5
|
||||
assert health_weight.body_fat == 18.5
|
||||
assert health_weight.body_water == 60.0
|
||||
assert health_weight.bone_mass == 3.5
|
||||
assert health_weight.muscle_mass == 62.0
|
||||
assert health_weight.physique_rating == 7
|
||||
assert health_weight.visceral_fat == 5.0
|
||||
assert health_weight.metabolic_age == 25
|
||||
assert health_weight.source == "garmin"
|
||||
|
||||
def test_health_weight_minimal_data(self):
|
||||
"""
|
||||
Test HealthWeight schema with minimal required fields.
|
||||
"""
|
||||
# Arrange & Act
|
||||
health_weight = health_weight_schema.HealthWeight()
|
||||
|
||||
# Assert
|
||||
assert health_weight.id is None
|
||||
assert health_weight.user_id is None
|
||||
assert health_weight.date is None
|
||||
assert health_weight.weight is None
|
||||
assert health_weight.bmi is None
|
||||
|
||||
def test_health_weight_with_none_values(self):
|
||||
"""
|
||||
Test HealthWeight schema allows None for optional fields.
|
||||
"""
|
||||
# Arrange & Act
|
||||
health_weight = health_weight_schema.HealthWeight(
|
||||
id=1,
|
||||
user_id=1,
|
||||
date=datetime_date(2024, 1, 15),
|
||||
weight=75.5,
|
||||
bmi=None,
|
||||
body_fat=None,
|
||||
body_water=None,
|
||||
bone_mass=None,
|
||||
muscle_mass=None,
|
||||
physique_rating=None,
|
||||
visceral_fat=None,
|
||||
metabolic_age=None,
|
||||
source=None,
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert health_weight.id == 1
|
||||
assert health_weight.bmi is None
|
||||
assert health_weight.body_fat is None
|
||||
|
||||
def test_health_weight_with_float_values(self):
|
||||
"""
|
||||
Test HealthWeight schema with various float values.
|
||||
"""
|
||||
# Arrange & Act
|
||||
health_weight = health_weight_schema.HealthWeight(
|
||||
weight=75.567, bmi=24.523, body_fat=18.234
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert health_weight.weight == 75.567
|
||||
assert health_weight.bmi == 24.523
|
||||
assert health_weight.body_fat == 18.234
|
||||
|
||||
def test_health_weight_with_integer_values(self):
|
||||
"""
|
||||
Test HealthWeight schema with integer values for float fields.
|
||||
"""
|
||||
# Arrange & Act
|
||||
health_weight = health_weight_schema.HealthWeight(
|
||||
weight=75, bmi=24, body_fat=18
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert health_weight.weight == 75
|
||||
assert health_weight.bmi == 24
|
||||
assert health_weight.body_fat == 18
|
||||
|
||||
def test_health_weight_forbid_extra_fields(self):
|
||||
"""
|
||||
Test that HealthWeight schema forbids extra fields.
|
||||
"""
|
||||
# Arrange & Act & Assert
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
health_weight_schema.HealthWeight(weight=75.5, extra_field="not allowed")
|
||||
|
||||
assert "extra_field" in str(exc_info.value)
|
||||
|
||||
def test_health_weight_from_attributes(self):
|
||||
"""
|
||||
Test HealthWeight schema can be created from ORM model.
|
||||
"""
|
||||
|
||||
# Arrange
|
||||
class MockORMModel:
|
||||
"""Mock ORM model for testing."""
|
||||
|
||||
id = 1
|
||||
user_id = 1
|
||||
date = datetime_date(2024, 1, 15)
|
||||
weight = 75.5
|
||||
bmi = 24.5
|
||||
body_fat = 18.5
|
||||
body_water = 60.0
|
||||
bone_mass = 3.5
|
||||
muscle_mass = 62.0
|
||||
physique_rating = 7
|
||||
visceral_fat = 5.0
|
||||
metabolic_age = 25
|
||||
source = "garmin"
|
||||
|
||||
# Act
|
||||
health_weight = health_weight_schema.HealthWeight.model_validate(MockORMModel())
|
||||
|
||||
# Assert
|
||||
assert health_weight.id == 1
|
||||
assert health_weight.weight == 75.5
|
||||
assert health_weight.source == "garmin"
|
||||
|
||||
def test_health_weight_validate_assignment(self):
|
||||
"""
|
||||
Test that validate_assignment works correctly.
|
||||
"""
|
||||
# Arrange
|
||||
health_weight = health_weight_schema.HealthWeight(weight=75.5)
|
||||
|
||||
# Act
|
||||
health_weight.weight = 80.0
|
||||
health_weight.bmi = 25.5
|
||||
|
||||
# Assert
|
||||
assert health_weight.weight == 80.0
|
||||
assert health_weight.bmi == 25.5
|
||||
|
||||
def test_health_weight_date_validation(self):
|
||||
"""
|
||||
Test date field validation.
|
||||
"""
|
||||
# Arrange & Act
|
||||
health_weight = health_weight_schema.HealthWeight(
|
||||
date=datetime_date(2024, 12, 31)
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert health_weight.date == datetime_date(2024, 12, 31)
|
||||
|
||||
def test_health_weight_physique_rating_integer(self):
|
||||
"""
|
||||
Test physique_rating accepts integer values.
|
||||
"""
|
||||
# Arrange & Act
|
||||
health_weight = health_weight_schema.HealthWeight(physique_rating=8)
|
||||
|
||||
# Assert
|
||||
assert health_weight.physique_rating == 8
|
||||
assert isinstance(health_weight.physique_rating, int)
|
||||
|
||||
def test_health_weight_metabolic_age_integer(self):
|
||||
"""
|
||||
Test metabolic_age accepts integer values.
|
||||
"""
|
||||
# Arrange & Act
|
||||
health_weight = health_weight_schema.HealthWeight(metabolic_age=30)
|
||||
|
||||
# Assert
|
||||
assert health_weight.metabolic_age == 30
|
||||
assert isinstance(health_weight.metabolic_age, int)
|
||||
|
||||
|
||||
class TestSourceEnum:
|
||||
"""
|
||||
Test suite for Source enum.
|
||||
"""
|
||||
|
||||
def test_source_enum_garmin(self):
|
||||
"""
|
||||
Test Source enum has GARMIN value.
|
||||
"""
|
||||
# Arrange & Act
|
||||
source = health_weight_schema.Source.GARMIN
|
||||
|
||||
# Assert
|
||||
assert source.value == "garmin"
|
||||
|
||||
def test_source_enum_use_in_schema(self):
|
||||
"""
|
||||
Test Source enum can be used in HealthWeight schema.
|
||||
"""
|
||||
# Arrange & Act
|
||||
health_weight = health_weight_schema.HealthWeight(
|
||||
source=health_weight_schema.Source.GARMIN
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert health_weight.source == "garmin"
|
||||
|
||||
def test_source_enum_string_value(self):
|
||||
"""
|
||||
Test Source enum accepts string value directly.
|
||||
"""
|
||||
# Arrange & Act
|
||||
health_weight = health_weight_schema.HealthWeight(source="garmin")
|
||||
|
||||
# Assert
|
||||
assert health_weight.source == "garmin"
|
||||
|
||||
def test_source_enum_invalid_value(self):
|
||||
"""
|
||||
Test Source enum rejects invalid values.
|
||||
"""
|
||||
# Arrange & Act & Assert
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
health_weight_schema.HealthWeight(source="invalid")
|
||||
|
||||
assert "source" in str(exc_info.value)
|
||||
286
backend/tests/health_weight/test_utils.py
Normal file
286
backend/tests/health_weight/test_utils.py
Normal file
@@ -0,0 +1,286 @@
|
||||
import pytest
|
||||
from datetime import date as datetime_date
|
||||
from unittest.mock import MagicMock, patch
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
import health_weight.utils as health_weight_utils
|
||||
import health_weight.schema as health_weight_schema
|
||||
import users.user.schema as user_schema
|
||||
|
||||
|
||||
class TestCalculateBMI:
|
||||
"""
|
||||
Test suite for calculate_bmi function.
|
||||
"""
|
||||
|
||||
@patch("health_weight.utils.users_crud.get_user_by_id")
|
||||
def test_calculate_bmi_success(self, mock_get_user):
|
||||
"""
|
||||
Test successful BMI calculation.
|
||||
"""
|
||||
# Arrange
|
||||
user_id = 1
|
||||
mock_db = MagicMock(spec=Session)
|
||||
|
||||
mock_user = MagicMock()
|
||||
mock_user.height = 175
|
||||
mock_get_user.return_value = mock_user
|
||||
|
||||
health_weight = health_weight_schema.HealthWeight(
|
||||
date=datetime_date(2024, 1, 15), weight=75.0, bmi=None
|
||||
)
|
||||
|
||||
# Act
|
||||
result = health_weight_utils.calculate_bmi(health_weight, user_id, mock_db)
|
||||
|
||||
# Assert
|
||||
assert result.bmi is not None
|
||||
expected_bmi = 75.0 / ((175 / 100) ** 2)
|
||||
assert abs(result.bmi - expected_bmi) < 0.01
|
||||
mock_get_user.assert_called_once_with(user_id, mock_db)
|
||||
|
||||
@patch("health_weight.utils.users_crud.get_user_by_id")
|
||||
def test_calculate_bmi_user_not_found(self, mock_get_user):
|
||||
"""
|
||||
Test BMI calculation when user not found.
|
||||
"""
|
||||
# Arrange
|
||||
user_id = 1
|
||||
mock_db = MagicMock(spec=Session)
|
||||
mock_get_user.return_value = None
|
||||
|
||||
health_weight = health_weight_schema.HealthWeight(
|
||||
date=datetime_date(2024, 1, 15), weight=75.0, bmi=None
|
||||
)
|
||||
|
||||
# Act
|
||||
result = health_weight_utils.calculate_bmi(health_weight, user_id, mock_db)
|
||||
|
||||
# Assert
|
||||
assert result.bmi is None
|
||||
|
||||
@patch("health_weight.utils.users_crud.get_user_by_id")
|
||||
def test_calculate_bmi_no_height(self, mock_get_user):
|
||||
"""
|
||||
Test BMI calculation when user has no height.
|
||||
"""
|
||||
# Arrange
|
||||
user_id = 1
|
||||
mock_db = MagicMock(spec=Session)
|
||||
|
||||
mock_user = MagicMock()
|
||||
mock_user.height = None
|
||||
mock_get_user.return_value = mock_user
|
||||
|
||||
health_weight = health_weight_schema.HealthWeight(
|
||||
date=datetime_date(2024, 1, 15), weight=75.0, bmi=None
|
||||
)
|
||||
|
||||
# Act
|
||||
result = health_weight_utils.calculate_bmi(health_weight, user_id, mock_db)
|
||||
|
||||
# Assert
|
||||
assert result.bmi is None
|
||||
|
||||
@patch("health_weight.utils.users_crud.get_user_by_id")
|
||||
def test_calculate_bmi_no_weight(self, mock_get_user):
|
||||
"""
|
||||
Test BMI calculation when health weight has no weight.
|
||||
"""
|
||||
# Arrange
|
||||
user_id = 1
|
||||
mock_db = MagicMock(spec=Session)
|
||||
|
||||
mock_user = MagicMock()
|
||||
mock_user.height = 175
|
||||
mock_get_user.return_value = mock_user
|
||||
|
||||
health_weight = health_weight_schema.HealthWeight(
|
||||
date=datetime_date(2024, 1, 15), weight=None, bmi=None
|
||||
)
|
||||
|
||||
# Act
|
||||
result = health_weight_utils.calculate_bmi(health_weight, user_id, mock_db)
|
||||
|
||||
# Assert
|
||||
assert result.bmi is None
|
||||
|
||||
@patch("health_weight.utils.users_crud.get_user_by_id")
|
||||
def test_calculate_bmi_various_heights_and_weights(self, mock_get_user):
|
||||
"""
|
||||
Test BMI calculation with various heights and weights.
|
||||
"""
|
||||
# Arrange
|
||||
user_id = 1
|
||||
mock_db = MagicMock(spec=Session)
|
||||
|
||||
test_cases = [
|
||||
(180, 80.0, 80.0 / ((180 / 100) ** 2)),
|
||||
(165, 60.0, 60.0 / ((165 / 100) ** 2)),
|
||||
(190, 95.0, 95.0 / ((190 / 100) ** 2)),
|
||||
]
|
||||
|
||||
for height, weight, expected_bmi in test_cases:
|
||||
# Arrange
|
||||
mock_user = MagicMock()
|
||||
mock_user.height = height
|
||||
mock_get_user.return_value = mock_user
|
||||
|
||||
health_weight = health_weight_schema.HealthWeight(
|
||||
date=datetime_date(2024, 1, 15),
|
||||
weight=weight,
|
||||
bmi=None,
|
||||
)
|
||||
|
||||
# Act
|
||||
result = health_weight_utils.calculate_bmi(health_weight, user_id, mock_db)
|
||||
|
||||
# Assert
|
||||
assert result.bmi is not None
|
||||
assert abs(result.bmi - expected_bmi) < 0.01
|
||||
|
||||
|
||||
class TestCalculateBMIAllUserEntries:
|
||||
"""
|
||||
Test suite for calculate_bmi_all_user_entries function.
|
||||
"""
|
||||
|
||||
@patch("health_weight.utils.health_weight_crud.edit_health_weight")
|
||||
@patch("health_weight.utils.health_weight_crud." "get_all_health_weight_by_user_id")
|
||||
@patch("health_weight.utils.calculate_bmi")
|
||||
def test_calculate_bmi_all_user_entries_success(
|
||||
self, mock_calculate_bmi, mock_get_all, mock_edit
|
||||
):
|
||||
"""
|
||||
Test successful BMI calculation for all user entries.
|
||||
"""
|
||||
# Arrange
|
||||
user_id = 1
|
||||
mock_db = MagicMock(spec=Session)
|
||||
|
||||
mock_weight1 = MagicMock()
|
||||
mock_weight1.id = 1
|
||||
mock_weight1.user_id = user_id
|
||||
mock_weight1.date = datetime_date(2024, 1, 15)
|
||||
mock_weight1.weight = 75.0
|
||||
mock_weight1.bmi = None
|
||||
mock_weight1.source = "garmin"
|
||||
|
||||
mock_weight2 = MagicMock()
|
||||
mock_weight2.id = 2
|
||||
mock_weight2.user_id = user_id
|
||||
mock_weight2.date = datetime_date(2024, 1, 16)
|
||||
mock_weight2.weight = 74.5
|
||||
mock_weight2.bmi = None
|
||||
mock_weight2.source = "garmin"
|
||||
|
||||
mock_get_all.return_value = [mock_weight1, mock_weight2]
|
||||
|
||||
calculated_weight1 = health_weight_schema.HealthWeight(
|
||||
id=1,
|
||||
user_id=user_id,
|
||||
date=datetime_date(2024, 1, 15),
|
||||
weight=75.0,
|
||||
bmi=24.5,
|
||||
source="garmin",
|
||||
)
|
||||
calculated_weight2 = health_weight_schema.HealthWeight(
|
||||
id=2,
|
||||
user_id=user_id,
|
||||
date=datetime_date(2024, 1, 16),
|
||||
weight=74.5,
|
||||
bmi=24.3,
|
||||
source="garmin",
|
||||
)
|
||||
|
||||
mock_calculate_bmi.side_effect = [
|
||||
calculated_weight1,
|
||||
calculated_weight2,
|
||||
]
|
||||
|
||||
# Act
|
||||
health_weight_utils.calculate_bmi_all_user_entries(user_id, mock_db)
|
||||
|
||||
# Assert
|
||||
mock_get_all.assert_called_once_with(user_id, mock_db)
|
||||
assert mock_calculate_bmi.call_count == 2
|
||||
assert mock_edit.call_count == 2
|
||||
|
||||
@patch("health_weight.utils.health_weight_crud." "get_all_health_weight_by_user_id")
|
||||
def test_calculate_bmi_all_user_entries_no_entries(self, mock_get_all):
|
||||
"""
|
||||
Test BMI calculation when user has no entries.
|
||||
"""
|
||||
# Arrange
|
||||
user_id = 1
|
||||
mock_db = MagicMock(spec=Session)
|
||||
mock_get_all.return_value = None
|
||||
|
||||
# Act
|
||||
health_weight_utils.calculate_bmi_all_user_entries(user_id, mock_db)
|
||||
|
||||
# Assert
|
||||
mock_get_all.assert_called_once_with(user_id, mock_db)
|
||||
|
||||
@patch("health_weight.utils.health_weight_crud." "get_all_health_weight_by_user_id")
|
||||
def test_calculate_bmi_all_user_entries_empty_list(self, mock_get_all):
|
||||
"""
|
||||
Test BMI calculation when user has empty list of entries.
|
||||
"""
|
||||
# Arrange
|
||||
user_id = 1
|
||||
mock_db = MagicMock(spec=Session)
|
||||
mock_get_all.return_value = []
|
||||
|
||||
# Act
|
||||
health_weight_utils.calculate_bmi_all_user_entries(user_id, mock_db)
|
||||
|
||||
# Assert
|
||||
mock_get_all.assert_called_once_with(user_id, mock_db)
|
||||
|
||||
@patch("health_weight.utils.health_weight_crud.edit_health_weight")
|
||||
@patch("health_weight.utils.health_weight_crud." "get_all_health_weight_by_user_id")
|
||||
@patch("health_weight.utils.calculate_bmi")
|
||||
def test_calculate_bmi_all_user_entries_with_all_fields(
|
||||
self, mock_calculate_bmi, mock_get_all, mock_edit
|
||||
):
|
||||
"""
|
||||
Test BMI calculation for entries with all fields populated.
|
||||
"""
|
||||
# Arrange
|
||||
user_id = 1
|
||||
mock_db = MagicMock(spec=Session)
|
||||
|
||||
mock_weight = MagicMock()
|
||||
mock_weight.id = 1
|
||||
mock_weight.user_id = user_id
|
||||
mock_weight.date = datetime_date(2024, 1, 15)
|
||||
mock_weight.weight = 75.0
|
||||
mock_weight.bmi = 24.0
|
||||
mock_weight.body_fat = 18.5
|
||||
mock_weight.body_water = 60.0
|
||||
mock_weight.bone_mass = 3.5
|
||||
mock_weight.muscle_mass = 62.0
|
||||
mock_weight.physique_rating = 7
|
||||
mock_weight.visceral_fat = 5.0
|
||||
mock_weight.metabolic_age = 25
|
||||
mock_weight.source = "garmin"
|
||||
|
||||
mock_get_all.return_value = [mock_weight]
|
||||
|
||||
calculated_weight = health_weight_schema.HealthWeight(
|
||||
id=1,
|
||||
user_id=user_id,
|
||||
date=datetime_date(2024, 1, 15),
|
||||
weight=75.0,
|
||||
bmi=24.5,
|
||||
source="garmin",
|
||||
)
|
||||
mock_calculate_bmi.return_value = calculated_weight
|
||||
|
||||
# Act
|
||||
health_weight_utils.calculate_bmi_all_user_entries(user_id, mock_db)
|
||||
|
||||
# Assert
|
||||
assert mock_calculate_bmi.call_count == 1
|
||||
assert mock_edit.call_count == 1
|
||||
@@ -6,9 +6,6 @@ import {
|
||||
} from '@/utils/serviceUtils'
|
||||
|
||||
export const health_weight = {
|
||||
getUserHealthWeightNumber() {
|
||||
return fetchGetRequest('health/weight/number')
|
||||
},
|
||||
getUserHealthWeight() {
|
||||
return fetchGetRequest('health/weight')
|
||||
},
|
||||
|
||||
@@ -95,7 +95,7 @@ function updateActiveSection(section) {
|
||||
if (pageNumberWeight.value !== 1 || pageNumberSteps.value !== 1) {
|
||||
pageNumberWeight.value = 1
|
||||
pageNumberSteps.value = 1
|
||||
updateHealthWeight()
|
||||
updateHealthWeightPagination()
|
||||
updateHealthSteps()
|
||||
}
|
||||
}
|
||||
@@ -135,13 +135,14 @@ function setPageNumberRHR(page) {
|
||||
}
|
||||
|
||||
// Weight functions
|
||||
async function updateHealthWeight() {
|
||||
async function updateHealthWeightPagination() {
|
||||
try {
|
||||
isHealthWeightUpdatingLoading.value = true
|
||||
userHealthWeightPagination.value = await health_weight.getUserHealthWeightWithPagination(
|
||||
const weightDataPagination = await health_weight.getUserHealthWeightWithPagination(
|
||||
pageNumberWeight.value,
|
||||
numRecords
|
||||
)
|
||||
userHealthWeightPagination.value = weightDataPagination.records
|
||||
isHealthWeightUpdatingLoading.value = false
|
||||
} catch (error) {
|
||||
push.error(`${t('healthView.errorFetchingHealthWeight')} - ${error}`)
|
||||
@@ -150,9 +151,10 @@ async function updateHealthWeight() {
|
||||
|
||||
async function fetchHealthWeight() {
|
||||
try {
|
||||
userHealthWeightNumber.value = await health_weight.getUserHealthWeightNumber()
|
||||
userHealthWeight.value = await health_weight.getUserHealthWeight()
|
||||
await updateHealthWeight()
|
||||
const weightData = await health_weight.getUserHealthWeight()
|
||||
userHealthWeight.value = weightData.records
|
||||
userHealthWeightNumber.value = weightData.total
|
||||
await updateHealthWeightPagination()
|
||||
totalPagesWeight.value = Math.ceil(userHealthWeightNumber.value / numRecords)
|
||||
} catch (error) {
|
||||
push.error(`${t('healthView.errorFetchingHealthWeight')} - ${error}`)
|
||||
@@ -359,7 +361,7 @@ function setSleepTarget(sleepTarget) {
|
||||
// Watch functions
|
||||
watch(pageNumberSleep, updateHealthSleep, { immediate: false })
|
||||
watch(pageNumberSteps, updateHealthSteps, { immediate: false })
|
||||
watch(pageNumberWeight, updateHealthWeight, { immediate: false })
|
||||
watch(pageNumberWeight, updateHealthWeightPagination, { immediate: false })
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchHealthSleep()
|
||||
|
||||
Reference in New Issue
Block a user