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:
João Vitória Silva
2025-12-05 12:01:36 +00:00
parent 72b3263a43
commit 99396433f5
14 changed files with 2005 additions and 96 deletions

View File

@@ -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 = (

View File

@@ -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)

View File

@@ -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)

View File

@@ -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,
)

View File

@@ -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

View File

@@ -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

View File

View 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()

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

View 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

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

View 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

View File

@@ -6,9 +6,6 @@ import {
} from '@/utils/serviceUtils'
export const health_weight = {
getUserHealthWeightNumber() {
return fetchGetRequest('health/weight/number')
},
getUserHealthWeight() {
return fetchGetRequest('health/weight')
},

View File

@@ -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()