diff --git a/main.py b/main.py index 04e5349..8dabd4d 100644 --- a/main.py +++ b/main.py @@ -12,7 +12,7 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import HTMLResponse from sqlalchemy import create_engine, Column, Integer, String, Float, DateTime, ForeignKey, Text, Date, Boolean from sqlalchemy import or_ -from sqlalchemy.orm import sessionmaker, Session, relationship +from sqlalchemy.orm import sessionmaker, Session, relationship, declarative_base from pydantic import BaseModel, ConfigDict from typing import List, Optional from datetime import date, datetime @@ -54,7 +54,6 @@ except Exception as e: engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False} if "sqlite" in DATABASE_URL else {}) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) from sqlalchemy.orm import declarative_base -from sqlalchemy.orm import declarative_base Base = declarative_base() # Initialize FastAPI app @@ -1350,11 +1349,10 @@ async def bulk_upload_meals(file: UploadFile = File(...), db: Session = Depends( stats = {'created': 0, 'updated': 0, 'errors': []} - # Skip header rows - next(reader) # First header - next(reader) # Second header + # Skip header + header = next(reader) - for row_num, row in enumerate(reader, 3): # Start at row 3 + for row_num, row in enumerate(reader, 2): # Start at row 2 if not row: continue @@ -1387,10 +1385,12 @@ async def bulk_upload_meals(file: UploadFile = File(...), db: Session = Depends( food = db.query(Food).filter(Food.name.ilike(f"%{search_pattern}%")).first() if not food: + logging.error(f"Food '{food_name}' not found in database.") # Get all food names for debugging all_foods = db.query(Food.name).limit(10).all() food_names = [f[0] for f in all_foods] raise ValueError(f"Food '{food_name}' not found. Available foods include: {', '.join(food_names[:5])}...") + logging.info(f"Found food '{food_name}' with id {food.id}") ingredients.append((food.id, quantity)) # Create/update meal @@ -2206,11 +2206,17 @@ async def create_template(request: Request, db: Session = Depends(get_db)): # Process meal assignments if meal_assignments_str: + logging.info(f"Processing meal assignments: {meal_assignments_str}") assignments = meal_assignments_str.split(',') for assignment in assignments: - meal_time, meal_id_str = assignment.split(':') - meal_id = int(meal_id_str) + meal_time, meal_id_str = assignment.split(':', 1) + logging.info(f"Processing assignment: meal_time='{meal_time}', meal_id_str='{meal_id_str}'") + if not meal_id_str: + logging.warning(f"Skipping empty meal ID for meal_time '{meal_time}'") + continue + + meal_id = int(meal_id_str) meal = db.query(Meal).filter(Meal.id == meal_id).first() if meal: template_meal = TemplateMeal( diff --git a/plan.md b/plan.md new file mode 100644 index 0000000..728489b --- /dev/null +++ b/plan.md @@ -0,0 +1,50 @@ +# Plan for Pytest of Details Tab + +This plan outlines the steps to create a comprehensive pytest for the "details" tab in the Food Planner application. + +## Objective +The goal is to create a suite of tests that verify the functionality of the `/detailed` route, ensuring it correctly handles both plan-based and template-based views, as well as invalid inputs. + +## File to be Created +- `tests/test_detailed.py` + +## Test Cases + +### 1. Test with `plan_date` +- **Description**: This test will check the `/detailed` route when a valid `plan_date` is provided. +- **Steps**: + 1. Create mock data: a `Food`, a `Meal`, a `MealFood`, and a `Plan` for a specific date. + 2. Send a GET request to `/detailed` with the `person` and `plan_date` as query parameters. + 3. Assert that the response status code is 200. + 4. Assert that the response contains the correct data for the plan. + +### 2. Test with `template_id` +- **Description**: This test will check the `/detailed` route when a valid `template_id` is provided. +- **Steps**: + 1. Create mock data: a `Food`, a `Meal`, a `Template`, and a `TemplateMeal`. + 2. Send a GET request to `/detailed` with the `template_id` as a query parameter. + 3. Assert that the response status code is 200. + 4. Assert that the response contains the correct data for the template. + +### 3. Test with Invalid `plan_date` +- **Description**: This test will ensure the route handles an invalid `plan_date` gracefully. +- **Steps**: + 1. Send a GET request to `/detailed` with a non-existent `plan_date`. + 2. Assert that the response status code is 200 (as the page should still render). + 3. Assert that the response contains a message indicating that no plan was found. + +### 4. Test with Invalid `template_id` +- **Description**: This test will ensure the route handles an invalid `template_id` gracefully. +- **Steps**: + 1. Send a GET request to `/detailed` with a non-existent `template_id`. + 2. Assert that the response status code is 200. + 3. Assert that the response contains a message indicating that the template was not found. + +## Implementation Details + +The `tests/test_detailed.py` file should include: +- Imports for `pytest`, `TestClient`, and the necessary models from `main.py`. +- A `TestClient` instance for making requests to the application. +- Fixtures to set up and tear down the test database for each test function to ensure test isolation. + +This plan provides a clear path for a developer to implement the required tests. \ No newline at end of file diff --git a/tests/test_detailed.py b/tests/test_detailed.py new file mode 100644 index 0000000..2cefc1e --- /dev/null +++ b/tests/test_detailed.py @@ -0,0 +1,103 @@ +import pytest +from fastapi.testclient import TestClient +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from main import app, get_db, Base, Food, Meal, MealFood, Plan, Template, TemplateMeal +from datetime import date, timedelta + +# Setup test database +SQLALCHEMY_DATABASE_URL = "sqlite:///./test_detailed.db" +test_engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}) +TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=test_engine) + +@pytest.fixture(name="session") +def session_fixture(): + Base.metadata.create_all(bind=test_engine) + db = TestingSessionLocal() + try: + yield db + finally: + db.close() + Base.metadata.drop_all(bind=test_engine) + +@pytest.fixture(name="client") +def client_fixture(session): + def override_get_db(): + yield session + app.dependency_overrides[get_db] = override_get_db + with TestClient(app) as client: + yield client + app.dependency_overrides.clear() + +def test_detailed_page_no_params(client): + response = client.get("/detailed") + assert response.status_code == 200 + assert "error" in response.text or "Template Not Found" in response.text # Based on the existing code, it returns an error template if neither plan_date nor template_id is provided, and template_id is None. + +def test_detailed_page_with_plan_date(client, session): + # Create mock data + food = Food(name="Apple", serving_size="100", serving_unit="g", calories=52, protein=0.3, carbs=14, fat=0.2) + session.add(food) + session.commit() + session.refresh(food) + + meal = Meal(name="Fruit Snack", meal_type="snack", meal_time="Snack") + session.add(meal) + session.commit() + session.refresh(meal) + + meal_food = MealFood(meal_id=meal.id, food_id=food.id, quantity=1.0) + session.add(meal_food) + session.commit() + + test_date = date.today() + plan = Plan(person="Sarah", date=test_date, meal_id=meal.id, meal_time="Snack") + session.add(plan) + session.commit() + + response = client.get(f"/detailed?person=Sarah&plan_date={test_date.isoformat()}") + assert response.status_code == 200 + assert b"Sarah's Detailed Plan" in response.content + assert b"Fruit Snack" in response.content + +def test_detailed_page_with_template_id(client, session): + # Create mock data + food = Food(name="Banana", serving_size="100", serving_unit="g", calories=89, protein=1.1, carbs=23, fat=0.3) + session.add(food) + session.commit() + session.refresh(food) + + meal = Meal(name="Banana Smoothie", meal_type="breakfast", meal_time="Breakfast") + session.add(meal) + session.commit() + session.refresh(meal) + + meal_food = MealFood(meal_id=meal.id, food_id=food.id, quantity=1.0) + session.add(meal_food) + session.commit() + + template = Template(name="Morning Boost") + session.add(template) + session.commit() + session.refresh(template) + + template_meal = TemplateMeal(template_id=template.id, meal_id=meal.id, meal_time="Breakfast") + session.add(template_meal) + session.commit() + + response = client.get(f"/detailed?template_id={template.id}") + assert response.status_code == 200 + assert b"Morning Boost Template" in response.content + assert b"Banana Smoothie" in response.content + +def test_detailed_page_with_invalid_plan_date(client): + invalid_date = date.today() + timedelta(days=100) # A date far in the future + response = client.get(f"/detailed?person=Sarah&plan_date={invalid_date.isoformat()}") + assert response.status_code == 200 + assert b"Sarah's Detailed Plan" in response.content + assert b"No meals planned for this day." in response.content # Assuming this message is displayed + +def test_detailed_page_with_invalid_template_id(client): + response = client.get(f"/detailed?template_id=99999") + assert response.status_code == 200 + assert b"Template Not Found" in response.content