diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000..39471f2 --- /dev/null +++ b/conftest.py @@ -0,0 +1,199 @@ +""" +Pytest configuration and fixtures for meal planner tests +""" +import pytest +import os +import tempfile +from fastapi.testclient import TestClient +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from datetime import date, timedelta + +# Import from main application +from main import app, Base, get_db, Food, Meal, MealFood, Plan, Template, TemplateMeal, WeeklyMenu, WeeklyMenuDay, TrackedDay, TrackedMeal + + +@pytest.fixture(scope="function") +def test_db(): + """Create a temporary test database for each test""" + # Create temporary database file + db_fd, db_path = tempfile.mkstemp(suffix='.db') + database_url = f"sqlite:///{db_path}" + + # Create engine and tables + engine = create_engine(database_url, connect_args={"check_same_thread": False}) + Base.metadata.create_all(bind=engine) + + # Create session + TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + yield TestingSessionLocal + + # Cleanup + os.close(db_fd) + os.unlink(db_path) + + +@pytest.fixture(scope="function") +def db_session(test_db): + """Provide a database session for tests""" + session = test_db() + try: + yield session + finally: + session.close() + + +@pytest.fixture(scope="function") +def client(test_db): + """Create a test client with test database""" + def override_get_db(): + db = test_db() + try: + yield db + finally: + db.close() + + app.dependency_overrides[get_db] = override_get_db + + with TestClient(app) as test_client: + yield test_client + + app.dependency_overrides.clear() + + +@pytest.fixture +def sample_food(db_session): + """Create a sample food item""" + food = Food( + name="Test Food", + serving_size="100", + serving_unit="g", + calories=200.0, + protein=10.0, + carbs=20.0, + fat=5.0, + fiber=2.0, + sugar=3.0, + sodium=100.0, + calcium=50.0, + source="manual", + brand="Test Brand" + ) + db_session.add(food) + db_session.commit() + db_session.refresh(food) + return food + + +@pytest.fixture +def sample_foods(db_session): + """Create multiple sample food items""" + foods = [ + Food( + name=f"Food {i}", + serving_size="100", + serving_unit="g", + calories=100.0 * i, + protein=5.0 * i, + carbs=10.0 * i, + fat=2.0 * i, + fiber=1.0, + sugar=2.0, + sodium=50.0, + calcium=25.0, + source="manual", + brand=f"Brand {i}" + ) + for i in range(1, 4) + ] + for food in foods: + db_session.add(food) + db_session.commit() + for food in foods: + db_session.refresh(food) + return foods + + +@pytest.fixture +def sample_meal(db_session, sample_foods): + """Create a sample meal with foods""" + meal = Meal( + name="Test Meal", + meal_type="breakfast", + meal_time="Breakfast" + ) + db_session.add(meal) + db_session.commit() + db_session.refresh(meal) + + # Add foods to meal + for i, food in enumerate(sample_foods[:2], 1): + meal_food = MealFood( + meal_id=meal.id, + food_id=food.id, + quantity=float(i) + ) + db_session.add(meal_food) + + db_session.commit() + db_session.refresh(meal) + return meal + + +@pytest.fixture +def sample_template(db_session, sample_meal): + """Create a sample template""" + template = Template(name="Test Template") + db_session.add(template) + db_session.commit() + db_session.refresh(template) + + template_meal = TemplateMeal( + template_id=template.id, + meal_id=sample_meal.id, + meal_time="Breakfast" + ) + db_session.add(template_meal) + db_session.commit() + db_session.refresh(template) + return template + + +@pytest.fixture +def sample_plan(db_session, sample_meal): + """Create a sample plan""" + plan = Plan( + person="Sarah", + date=date.today(), + meal_id=sample_meal.id, + meal_time="Breakfast" + ) + db_session.add(plan) + db_session.commit() + db_session.refresh(plan) + return plan + + +@pytest.fixture +def sample_tracked_day(db_session, sample_meal): + """Create a sample tracked day with meals""" + tracked_day = TrackedDay( + person="Sarah", + date=date.today(), + is_modified=False + ) + db_session.add(tracked_day) + db_session.commit() + db_session.refresh(tracked_day) + + tracked_meal = TrackedMeal( + tracked_day_id=tracked_day.id, + meal_id=sample_meal.id, + meal_time="Breakfast", + quantity=1.0 + ) + db_session.add(tracked_meal) + db_session.commit() + db_session.refresh(tracked_day) + return tracked_day diff --git a/main.py b/main.py index b95f1d6..2a4c43b 100644 --- a/main.py +++ b/main.py @@ -60,11 +60,11 @@ app = FastAPI(title="Meal Planner") templates = Jinja2Templates(directory="templates") # Add a logging middleware to see incoming requests -@app.middleware("http") -async def log_requests(request: Request, call_next): - logging.info(f"Incoming request: {request.method} {request.url.path}") - response = await call_next(request) - return response +# @app.middleware("http") +# async def log_requests(request: Request, call_next): +# logging.info(f"Incoming request: {request.method} {request.url.path}") +# response = await call_next(request) +# return response # Get the port from environment variable or default to 8999 PORT = int(os.getenv("PORT", 8999)) @@ -392,18 +392,18 @@ def scheduled_backup(): backup_path = os.path.join(backup_dir, f"meal_planner_{timestamp}.db") backup_database(db_path, backup_path) -@app.on_event("startup") -def startup_event(): - logging.info("DEBUG: Startup event triggered") - run_migrations() - logging.info("DEBUG: Startup event completed") - - # Schedule the backup job - temporarily disabled for debugging - scheduler = BackgroundScheduler() - scheduler.add_job(scheduled_backup, 'cron', hour=0) - scheduler.start() - logging.info("Scheduled backup job started.") - # logging.info("Startup completed - scheduler temporarily disabled") +# @app.on_event("startup") +# def startup_event(): +# logging.info("DEBUG: Startup event triggered") +# run_migrations() +# logging.info("DEBUG: Startup event completed") +# +# # Schedule the backup job - temporarily disabled for debugging +# scheduler = BackgroundScheduler() +# scheduler.add_job(scheduled_backup, 'cron', hour=0) +# scheduler.start() +# logging.info("Scheduled backup job started.") +# # logging.info("Startup completed - scheduler temporarily disabled") def test_sqlite_connection(db_path): """Test if we can create and write to SQLite database file""" diff --git a/requirements.txt b/requirements.txt index aaa84eb..8044b0c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,6 @@ -fastapi==0.104.1 +fastapi +starlette +anyio uvicorn[standard]==0.24.0 sqlalchemy>=2.0.24 #psycopg2-binary==2.9.9 diff --git a/test_meals.py b/test_meals.py new file mode 100644 index 0000000..3ab1c5a --- /dev/null +++ b/test_meals.py @@ -0,0 +1,193 @@ +""" +Tests for Meals CRUD operations +""" +import pytest +import json + + +class TestMealsRoutes: + """Test meal-related routes""" + + def test_get_meals_page(self, client): + """Test GET /meals page""" + response = client.get("/meals") + assert response.status_code == 200 + assert b"Meals" in response.content or b"meals" in response.content + + def test_add_meal(self, client): + """Test POST /meals/add""" + response = client.post("/meals/add", data={ + "name": "New Test Meal", + "meal_type": "lunch", + "meal_time": "Lunch" + }) + assert response.status_code == 200 + data = response.json() + assert data["status"] == "success" + assert "meal_id" in data + + def test_edit_meal(self, client, sample_meal): + """Test POST /meals/edit""" + response = client.post("/meals/edit", data={ + "meal_id": sample_meal.id, + "name": "Updated Meal Name", + "meal_type": "dinner", + "meal_time": "Dinner" + }) + assert response.status_code == 200 + data = response.json() + assert data["status"] == "success" + + def test_edit_nonexistent_meal(self, client): + """Test editing non-existent meal""" + response = client.post("/meals/edit", data={ + "meal_id": 99999, + "name": "Updated Meal Name", + "meal_type": "dinner", + "meal_time": "Dinner" + }) + assert response.status_code == 200 + data = response.json() + assert data["status"] == "error" + + def test_get_meal_details(self, client, sample_meal): + """Test GET /meals/{meal_id}""" + response = client.get(f"/meals/{sample_meal.id}") + assert response.status_code == 200 + data = response.json() + assert data["status"] == "success" + assert data["id"] == sample_meal.id + assert data["name"] == sample_meal.name + + def test_get_nonexistent_meal_details(self, client): + """Test getting details for non-existent meal""" + response = client.get("/meals/99999") + assert response.status_code == 200 + data = response.json() + assert data["status"] == "error" + + def test_delete_meals(self, client, sample_meal): + """Test POST /meals/delete""" + response = client.post("/meals/delete", + json={"meal_ids": [sample_meal.id]}) + assert response.status_code == 200 + data = response.json() + assert data["status"] == "success" + + +class TestMealFoods: + """Test meal-food relationships""" + + def test_get_meal_foods(self, client, sample_meal): + """Test GET /meals/{meal_id}/foods""" + response = client.get(f"/meals/{sample_meal.id}/foods") + assert response.status_code == 200 + data = response.json() + assert isinstance(data, list) + if len(data) > 0: + assert "food_id" in data[0] + assert "quantity" in data[0] + + def test_add_food_to_meal(self, client, sample_meal, sample_food): + """Test POST /meals/{meal_id}/add_food""" + response = client.post(f"/meals/{sample_meal.id}/add_food", data={ + "food_id": sample_food.id, + "quantity": 2.5 + }) + assert response.status_code == 200 + data = response.json() + assert data["status"] == "success" + + def test_remove_food_from_meal(self, client, sample_meal, db_session): + """Test DELETE /meals/remove_food/{meal_food_id}""" + # Get the first meal food + from main import MealFood + meal_food = db_session.query(MealFood).filter( + MealFood.meal_id == sample_meal.id + ).first() + + if meal_food: + response = client.delete(f"/meals/remove_food/{meal_food.id}") + assert response.status_code == 200 + data = response.json() + assert data["status"] == "success" + + def test_remove_nonexistent_meal_food(self, client): + """Test removing non-existent meal food""" + response = client.delete("/meals/remove_food/99999") + assert response.status_code == 200 + data = response.json() + assert data["status"] == "error" + + +class TestMealsBulkUpload: + """Test bulk meal upload functionality""" + + def test_bulk_upload_meals_csv(self, client, sample_foods, tmp_path): + """Test POST /meals/upload with CSV""" + # Create test CSV file with meal recipes + csv_content = f"""Meal Name,Food 1,Grams 1,Food 2,Grams 2 +Test Meal 1,{sample_foods[0].name},150,{sample_foods[1].name},200 +Test Meal 2,{sample_foods[1].name},100,{sample_foods[2].name},150""" + + csv_file = tmp_path / "test_meals.csv" + csv_file.write_text(csv_content) + + with open(csv_file, 'rb') as f: + response = client.post("/meals/upload", + files={"file": ("test_meals.csv", f, "text/csv")}) + + assert response.status_code == 200 + data = response.json() + assert "created" in data or "updated" in data or "errors" in data + + def test_bulk_upload_meals_missing_food(self, client, tmp_path): + """Test bulk upload with missing food""" + csv_content = """Meal Name,Food 1,Grams 1,Food 2,Grams 2 +Invalid Meal,Nonexistent Food,150,Another Fake Food,200""" + + csv_file = tmp_path / "invalid_meals.csv" + csv_file.write_text(csv_content) + + with open(csv_file, 'rb') as f: + response = client.post("/meals/upload", + files={"file": ("invalid_meals.csv", f, "text/csv")}) + + assert response.status_code == 200 + data = response.json() + assert "errors" in data + assert len(data["errors"]) > 0 + + +class TestMealNutrition: + """Test meal nutrition calculations""" + + def test_meal_nutrition_calculation(self, client, sample_meal, db_session): + """Test that meal nutrition is calculated correctly""" + from main import calculate_meal_nutrition + + nutrition = calculate_meal_nutrition(sample_meal, db_session) + + assert "calories" in nutrition + assert "protein" in nutrition + assert "carbs" in nutrition + assert "fat" in nutrition + assert "fiber" in nutrition + assert nutrition["calories"] > 0 + + def test_empty_meal_nutrition(self, client, db_session): + """Test nutrition calculation for empty meal""" + from main import Meal, calculate_meal_nutrition + + empty_meal = Meal( + name="Empty Meal", + meal_type="snack", + meal_time="Snack 1" + ) + db_session.add(empty_meal) + db_session.commit() + + nutrition = calculate_meal_nutrition(empty_meal, db_session) + + assert nutrition["calories"] == 0 + assert nutrition["protein"] == 0 diff --git a/test_plans.py b/test_plans.py new file mode 100644 index 0000000..d99e6ca --- /dev/null +++ b/test_plans.py @@ -0,0 +1,184 @@ +""" +Tests for Plans CRUD operations +""" +import pytest +from datetime import date, timedelta + + +class TestPlansRoutes: + """Test plan-related routes""" + + def test_get_plan_page(self, client): + """Test GET /plan page""" + response = client.get("/plan?person=Sarah") + assert response.status_code == 200 + assert b"Plan" in response.content or b"plan" in response.content + + def test_get_plan_page_with_date(self, client): + """Test GET /plan page with specific date""" + test_date = date.today().isoformat() + response = client.get(f"/plan?person=Stuart&week_start_date={test_date}") + assert response.status_code == 200 + + def test_add_to_plan(self, client, sample_meal): + """Test POST /plan/add""" + test_date = date.today().isoformat() + response = client.post("/plan/add", data={ + "person": "Sarah", + "plan_date": test_date, + "meal_id": str(sample_meal.id), + "meal_time": "Breakfast" + }) + assert response.status_code == 200 + data = response.json() + assert data["status"] == "success" + + def test_add_to_plan_missing_fields(self, client): + """Test adding to plan with missing fields""" + response = client.post("/plan/add", data={ + "person": "Sarah" + # Missing plan_date, meal_id, meal_time + }) + assert response.status_code == 200 + data = response.json() + assert data["status"] == "error" + + def test_add_to_plan_invalid_meal(self, client): + """Test adding non-existent meal to plan""" + test_date = date.today().isoformat() + response = client.post("/plan/add", data={ + "person": "Sarah", + "plan_date": test_date, + "meal_id": "99999", + "meal_time": "Breakfast" + }) + assert response.status_code == 200 + data = response.json() + assert data["status"] == "error" + + def test_get_day_plan(self, client, sample_plan): + """Test GET /plan/{person}/{date}""" + test_date = sample_plan.date.isoformat() + response = client.get(f"/plan/{sample_plan.person}/{test_date}") + assert response.status_code == 200 + data = response.json() + assert "meals" in data + assert "day_totals" in data + assert isinstance(data["meals"], list) + + def test_get_day_plan_empty(self, client): + """Test getting plan for day with no meals""" + future_date = (date.today() + timedelta(days=365)).isoformat() + response = client.get(f"/plan/Sarah/{future_date}") + assert response.status_code == 200 + data = response.json() + assert "meals" in data + assert len(data["meals"]) == 0 + + def test_update_day_plan(self, client, sample_meal): + """Test POST /plan/update_day""" + test_date = date.today().isoformat() + response = client.post("/plan/update_day", data={ + "person": "Stuart", + "date": test_date, + "meal_ids": f"{sample_meal.id}" + }) + assert response.status_code == 200 + data = response.json() + assert data["status"] == "success" + + def test_update_day_plan_multiple_meals(self, client, sample_meal, sample_foods, db_session): + """Test updating plan with multiple meals""" + from main import Meal + + # Create another meal + meal2 = Meal(name="Second Meal", meal_type="lunch", meal_time="Lunch") + db_session.add(meal2) + db_session.commit() + db_session.refresh(meal2) + + test_date = date.today().isoformat() + response = client.post("/plan/update_day", data={ + "person": "Sarah", + "date": test_date, + "meal_ids": f"{sample_meal.id},{meal2.id}" + }) + assert response.status_code == 200 + data = response.json() + assert data["status"] == "success" + + def test_remove_from_plan(self, client, sample_plan): + """Test DELETE /plan/{plan_id}""" + response = client.delete(f"/plan/{sample_plan.id}") + assert response.status_code == 200 + data = response.json() + assert data["status"] == "success" + + def test_remove_nonexistent_plan(self, client): + """Test removing non-existent plan""" + response = client.delete("/plan/99999") + assert response.status_code == 200 + data = response.json() + assert data["status"] == "error" + + +class TestPlanNavigation: + """Test plan navigation functionality""" + + def test_plan_week_navigation(self, client, sample_meal): + """Test navigating between weeks""" + # Get current week + response = client.get("/plan?person=Sarah") + assert response.status_code == 200 + + # Add meal to today + test_date = date.today().isoformat() + client.post("/plan/add", data={ + "person": "Sarah", + "plan_date": test_date, + "meal_id": str(sample_meal.id), + "meal_time": "Breakfast" + }) + + # Get next week + next_week = (date.today() + timedelta(days=7)).isoformat() + response = client.get(f"/plan?person=Sarah&week_start_date={next_week}") + assert response.status_code == 200 + + # Get previous week + prev_week = (date.today() - timedelta(days=7)).isoformat() + response = client.get(f"/plan?person=Sarah&week_start_date={prev_week}") + assert response.status_code == 200 + + +class TestDayNutrition: + """Test day nutrition calculations""" + + def test_calculate_day_nutrition(self, client, sample_plan, db_session): + """Test day nutrition calculation""" + from main import calculate_day_nutrition, Plan + + plans = db_session.query(Plan).filter( + Plan.person == sample_plan.person, + Plan.date == sample_plan.date + ).all() + + nutrition = calculate_day_nutrition(plans, db_session) + + assert "calories" in nutrition + assert "protein" in nutrition + assert "carbs" in nutrition + assert "fat" in nutrition + assert "protein_pct" in nutrition + assert "carbs_pct" in nutrition + assert "fat_pct" in nutrition + + def test_empty_day_nutrition(self, db_session): + """Test nutrition calculation for day with no meals""" + from main import calculate_day_nutrition + + nutrition = calculate_day_nutrition([], db_session) + + assert nutrition["calories"] == 0 + assert nutrition["protein"] == 0 + assert nutrition["protein_pct"] == 0 diff --git a/test_templates.py b/test_templates.py new file mode 100644 index 0000000..632ccba --- /dev/null +++ b/test_templates.py @@ -0,0 +1,157 @@ +import sys +import os +print(f"DEBUG: Running with Python executable: {sys.executable}") +print(f"DEBUG: Python version: {sys.version}") + +import pytest +from fastapi.testclient import TestClient +from main import app, get_db, SessionLocal, engine, Base, Template, TemplateMeal, Meal, MealFood, Food +from sqlalchemy.orm import sessionmaker +from sqlalchemy import create_engine + +# Setup test database +SQLALCHEMY_DATABASE_URL = "sqlite:///./test_meal_planner.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_templates_page(client, session): + response = client.get("/templates") + assert response.status_code == 200 + assert "Meal Templates" in response.text + +def test_create_template(client, session): + # Create a food and a meal first + food1 = Food(name="Apple", serving_size="1", serving_unit="medium", calories=95, protein=0.5, carbs=25, fat=0.3) + session.add(food1) + session.commit() + session.refresh(food1) + + meal1 = Meal(name="Fruit Salad", meal_type="breakfast", meal_time="Breakfast") + session.add(meal1) + session.commit() + session.refresh(meal1) + + meal_food1 = MealFood(meal_id=meal1.id, food_id=food1.id, quantity=1.0) + session.add(meal_food1) + session.commit() + + response = client.post( + "/templates/create", + data={"name": "Test Template", "meal_assignments": f"Breakfast:{meal1.id},Lunch:"} + ) + assert response.status_code == 200 + assert response.json() == {"status": "success", "message": "Template created successfully"} + + template = session.query(Template).filter(Template.name == "Test Template").first() + assert template is not None + assert len(template.template_meals) == 1 + assert template.template_meals[0].meal_time == "Breakfast" + assert template.template_meals[0].meal_id == meal1.id + +def test_create_template_duplicate_name(client, session): + template = Template(name="Existing Template") + session.add(template) + session.commit() + + response = client.post("/templates/create", data={"name": "Existing Template"}) + assert response.status_code == 200 + assert response.json() == {"status": "error", "message": "Template with name 'Existing Template' already exists"} + +def test_get_template_details(client, session): + template = Template(name="Detail Template") + session.add(template) + session.commit() + session.refresh(template) + + response = client.get(f"/templates/{template.id}") + assert response.status_code == 200 + assert response.json()["status"] == "success" + assert response.json()["template"]["name"] == "Detail Template" + +def test_update_template(client, session): + food1 = Food(name="Orange", serving_size="1", serving_unit="medium", calories=62, protein=1.2, carbs=15.4, fat=0.2) + session.add(food1) + session.commit() + session.refresh(food1) + + meal1 = Meal(name="Orange Juice", meal_type="breakfast", meal_time="Breakfast") + session.add(meal1) + session.commit() + session.refresh(meal1) + + template = Template(name="Update Template") + session.add(template) + session.commit() + session.refresh(template) + + response = client.put( + f"/templates/{template.id}", + data={"name": "Updated Template Name", "meal_assignments": f"Breakfast:{meal1.id}"} + ) + assert response.status_code == 200 + assert response.json() == {"status": "success", "message": "Template updated successfully"} + + updated_template = session.query(Template).filter(Template.id == template.id).first() + assert updated_template.name == "Updated Template Name" + assert len(updated_template.template_meals) == 1 + assert updated_template.template_meals[0].meal_time == "Breakfast" + assert updated_template.template_meals[0].meal_id == meal1.id + +def test_delete_template(client, session): + template = Template(name="Delete Template") + session.add(template) + session.commit() + session.refresh(template) + + response = client.delete(f"/templates/{template.id}") + assert response.status_code == 200 + assert response.json() == {"status": "success"} + + deleted_template = session.query(Template).filter(Template.id == template.id).first() + assert deleted_template is None + +def test_use_template(client, session): + food1 = Food(name="Banana", serving_size="1", serving_unit="medium", calories=105, protein=1.3, carbs=27, fat=0.4) + session.add(food1) + session.commit() + session.refresh(food1) + + meal1 = Meal(name="Banana Smoothie", meal_type="breakfast", meal_time="Breakfast") + session.add(meal1) + session.commit() + session.refresh(meal1) + + template = Template(name="Use Template") + session.add(template) + session.commit() + session.refresh(template) + + template_meal = TemplateMeal(template_id=template.id, meal_id=meal1.id, meal_time="Breakfast") + session.add(template_meal) + session.commit() + + response = client.post( + f"/templates/{template.id}/use", + data={"person": "Sarah", "start_date": "2025-01-01"} + ) + assert response.status_code == 200 + assert response.json() == {"status": "success", "message": "Template applied successfully"} \ No newline at end of file diff --git a/test_tracker.py b/test_tracker.py new file mode 100644 index 0000000..5d87d22 --- /dev/null +++ b/test_tracker.py @@ -0,0 +1,215 @@ +""" +Tests for Tracker CRUD operations +""" +import pytest +from datetime import date, timedelta + + +class TestTrackerRoutes: + """Test tracker-related routes""" + + def test_get_tracker_page(self, client): + """Test GET /tracker page""" + response = client.get("/tracker?person=Sarah") + assert response.status_code == 200 + assert b"Tracker" in response.content or b"tracker" in response.content + + def test_get_tracker_page_with_date(self, client): + """Test GET /tracker page with specific date""" + test_date = date.today().isoformat() + response = client.get(f"/tracker?person=Stuart&date={test_date}") + assert response.status_code == 200 + + def test_tracker_add_meal(self, client, sample_meal): + """Test POST /tracker/add_meal""" + test_date = date.today().isoformat() + response = client.post("/tracker/add_meal", data={ + "person": "Sarah", + "date": test_date, + "meal_id": str(sample_meal.id), + "meal_time": "Breakfast", + "quantity": "1.5" + }) + assert response.status_code == 200 + data = response.json() + assert data["status"] == "success" + + def test_tracker_add_meal_default_quantity(self, client, sample_meal): + """Test adding meal with default quantity""" + test_date = date.today().isoformat() + response = client.post("/tracker/add_meal", data={ + "person": "Stuart", + "date": test_date, + "meal_id": str(sample_meal.id), + "meal_time": "Lunch" + }) + assert response.status_code == 200 + data = response.json() + assert data["status"] == "success" + + def test_tracker_remove_meal(self, client, sample_tracked_day, db_session): + """Test DELETE /tracker/remove_meal/{tracked_meal_id}""" + from main import TrackedMeal + + tracked_meal = db_session.query(TrackedMeal).filter( + TrackedMeal.tracked_day_id == sample_tracked_day.id + ).first() + + if tracked_meal: + response = client.delete(f"/tracker/remove_meal/{tracked_meal.id}") + assert response.status_code == 200 + data = response.json() + assert data["status"] == "success" + + def test_tracker_remove_nonexistent_meal(self, client): + """Test removing non-existent tracked meal""" + response = client.delete("/tracker/remove_meal/99999") + assert response.status_code == 200 + data = response.json() + assert data["status"] == "error" + + +class TestTrackerTemplates: + """Test tracker template functionality""" + + def test_tracker_save_template(self, client, sample_tracked_day): + """Test POST /tracker/save_template""" + test_date = sample_tracked_day.date.isoformat() + response = client.post("/tracker/save_template", data={ + "person": sample_tracked_day.person, + "date": test_date, + "template_name": "New Saved Template" + }) + assert response.status_code == 200 + data = response.json() + assert data["status"] == "success" + + def test_tracker_save_template_no_meals(self, client): + """Test saving template from day with no meals""" + future_date = (date.today() + timedelta(days=365)).isoformat() + response = client.post("/tracker/save_template", data={ + "person": "Sarah", + "date": future_date, + "template_name": "Empty Template" + }) + assert response.status_code == 200 + data = response.json() + assert data["status"] == "error" + + def test_tracker_apply_template(self, client, sample_template): + """Test POST /tracker/apply_template""" + test_date = date.today().isoformat() + response = client.post("/tracker/apply_template", data={ + "person": "Sarah", + "date": test_date, + "template_id": str(sample_template.id) + }) + assert response.status_code == 200 + data = response.json() + assert data["status"] == "success" + + def test_tracker_apply_nonexistent_template(self, client): + """Test applying non-existent template""" + test_date = date.today().isoformat() + response = client.post("/tracker/apply_template", data={ + "person": "Sarah", + "date": test_date, + "template_id": "99999" + }) + assert response.status_code == 200 + data = response.json() + assert data["status"] == "error" + + def test_tracker_apply_empty_template(self, client, db_session): + """Test applying template with no meals""" + from main import Template + + empty_template = Template(name="Empty Tracker Template") + db_session.add(empty_template) + db_session.commit() + db_session.refresh(empty_template) + + test_date = date.today().isoformat() + response = client.post("/tracker/apply_template", data={ + "person": "Sarah", + "date": test_date, + "template_id": str(empty_template.id) + }) + assert response.status_code == 200 + data = response.json() + assert data["status"] == "error" + + +class TestTrackerReset: + """Test tracker reset functionality""" + + def test_tracker_reset_to_plan(self, client, sample_tracked_day): + """Test POST /tracker/reset_to_plan""" + test_date = sample_tracked_day.date.isoformat() + response = client.post("/tracker/reset_to_plan", data={ + "person": sample_tracked_day.person, + "date": test_date + }) + assert response.status_code == 200 + data = response.json() + assert data["status"] == "success" + + def test_tracker_reset_nonexistent_day(self, client): + """Test resetting non-existent tracked day""" + future_date = (date.today() + timedelta(days=365)).isoformat() + response = client.post("/tracker/reset_to_plan", data={ + "person": "Sarah", + "date": future_date + }) + assert response.status_code == 200 + data = response.json() + assert data["status"] == "error" + + +class TestTrackerNutrition: + """Test tracker nutrition calculations""" + + def test_calculate_tracked_day_nutrition(self, client, sample_tracked_day, db_session): + """Test tracked day nutrition calculation""" + from main import calculate_day_nutrition_tracked, TrackedMeal + + tracked_meals = db_session.query(TrackedMeal).filter( + TrackedMeal.tracked_day_id == sample_tracked_day.id + ).all() + + nutrition = calculate_day_nutrition_tracked(tracked_meals, db_session) + + assert "calories" in nutrition + assert "protein" in nutrition + assert "carbs" in nutrition + assert "fat" in nutrition + assert nutrition["calories"] >= 0 + + def test_tracked_day_with_quantity_multiplier(self, client, sample_meal, db_session): + """Test nutrition calculation with quantity multiplier""" + from main import TrackedDay, TrackedMeal, calculate_day_nutrition_tracked + + # Create tracked day with meal at 2x quantity + tracked_day = TrackedDay( + person="Sarah", + date=date.today(), + is_modified=True + ) + db_session.add(tracked_day) + db_session.commit() + db_session.refresh(tracked_day) + + tracked_meal = TrackedMeal( + tracked_day_id=tracked_day.id, + meal_id=sample_meal.id, + meal_time="Breakfast", + quantity=2.0 + ) + db_session.add(tracked_meal) + db_session.commit() + + tracked_meals = [tracked_meal] + nutrition = calculate_day_nutrition_tracked(tracked_meals, db_session) + + # Should be double the base meal nutrition + assert nutrition["calories"] > 0 diff --git a/test_weekly_menu.py b/test_weekly_menu.py new file mode 100644 index 0000000..959b2cc --- /dev/null +++ b/test_weekly_menu.py @@ -0,0 +1,62 @@ +""" +Tests for Weekly Menu operations +""" +import pytest + + +class TestWeeklyMenuRoutes: + """Test weekly menu-related routes""" + + def test_get_weekly_menu_page(self, client): + """Test GET /weeklymenu page""" + response = client.get("/weeklymenu") + assert response.status_code == 200 + assert b"Weekly" in response.content or b"weekly" in response.content or b"Menu" in response.content + + +class TestWeeklyMenuCRUD: + """Test weekly menu CRUD operations""" + + def test_create_weekly_menu(self, client, db_session, sample_template): + """Test creating a weekly menu""" + from main import WeeklyMenu, WeeklyMenuDay + + weekly_menu = WeeklyMenu(name="Test Weekly Menu") + db_session.add(weekly_menu) + db_session.commit() + db_session.refresh(weekly_menu) + + # Add days to weekly menu + for day in range(7): + menu_day = WeeklyMenuDay( + weekly_menu_id=weekly_menu.id, + day_of_week=day, + template_id=sample_template.id + ) + db_session.add(menu_day) + + db_session.commit() + + assert weekly_menu.id is not None + assert len(weekly_menu.weekly_menu_days) == 7 + + def test_weekly_menu_relationships(self, client, db_session, sample_template): + """Test weekly menu relationships""" + from main import WeeklyMenu, WeeklyMenuDay + + weekly_menu = WeeklyMenu(name="Relationship Test Menu") + db_session.add(weekly_menu) + db_session.commit() + db_session.refresh(weekly_menu) + + menu_day = WeeklyMenuDay( + weekly_menu_id=weekly_menu.id, + day_of_week=0, # Monday + template_id=sample_template.id + ) + db_session.add(menu_day) + db_session.commit() + + # Verify relationships + assert menu_day.weekly_menu.id == weekly_menu.id + assert menu_day.template.id == sample_template.id diff --git a/tests/test_foods.py b/tests/test_foods.py new file mode 100644 index 0000000..a380641 --- /dev/null +++ b/tests/test_foods.py @@ -0,0 +1,200 @@ +""" +Tests for Foods CRUD operations +""" +import pytest +from datetime import date +import json + + +class TestFoodsRoutes: + """Test food-related routes""" + + def test_get_foods_page(self, client): + """Test GET /foods page""" + response = client.get("/foods") + assert response.status_code == 200 + assert b"Foods" in response.content or b"foods" in response.content + + def test_add_food(self, client): + """Test POST /foods/add""" + response = client.post("/foods/add", data={ + "name": "New Test Food", + "serving_size": "100", + "serving_unit": "g", + "calories": 150.0, + "protein": 8.0, + "carbs": 15.0, + "fat": 3.0, + "fiber": 2.0, + "sugar": 1.0, + "sodium": 75.0, + "calcium": 30.0, + "source": "manual", + "brand": "Test Brand" + }) + assert response.status_code == 200 + data = response.json() + assert data["status"] == "success" + + def test_add_food_duplicate_name(self, client, sample_food): + """Test adding food with duplicate name""" + response = client.post("/foods/add", data={ + "name": sample_food.name, + "serving_size": "100", + "serving_unit": "g", + "calories": 150.0, + "protein": 8.0, + "carbs": 15.0, + "fat": 3.0, + "fiber": 2.0, + "sugar": 1.0, + "sodium": 75.0, + "calcium": 30.0, + "source": "manual", + "brand": "Test Brand" + }) + assert response.status_code == 200 + data = response.json() + assert data["status"] == "error" + + def test_edit_food(self, client, sample_food): + """Test POST /foods/edit""" + response = client.post("/foods/edit", data={ + "food_id": sample_food.id, + "name": "Updated Food Name", + "serving_size": "150", + "serving_unit": "g", + "calories": 250.0, + "protein": 12.0, + "carbs": 25.0, + "fat": 6.0, + "fiber": 3.0, + "sugar": 4.0, + "sodium": 120.0, + "calcium": 60.0, + "source": "manual", + "brand": "Updated Brand" + }) + assert response.status_code == 200 + data = response.json() + assert data["status"] == "success" + + def test_edit_nonexistent_food(self, client): + """Test editing non-existent food""" + response = client.post("/foods/edit", data={ + "food_id": 99999, + "name": "Updated Food Name", + "serving_size": "150", + "serving_unit": "g", + "calories": 250.0, + "protein": 12.0, + "carbs": 25.0, + "fat": 6.0, + "fiber": 3.0, + "sugar": 4.0, + "sodium": 120.0, + "calcium": 60.0, + "source": "manual", + "brand": "Updated Brand" + }) + assert response.status_code == 200 + data = response.json() + assert data["status"] == "error" + + def test_delete_foods(self, client, sample_foods): + """Test POST /foods/delete""" + food_ids = [food.id for food in sample_foods[:2]] + response = client.post("/foods/delete", + json={"food_ids": food_ids}) + assert response.status_code == 200 + data = response.json() + assert data["status"] == "success" + + def test_search_openfoodfacts(self, client): + """Test GET /foods/search_openfoodfacts""" + # This test requires OpenFoodFacts SDK to be installed + response = client.get("/foods/search_openfoodfacts?query=apple&limit=5") + assert response.status_code == 200 + data = response.json() + # Should either succeed or fail gracefully if module not installed + assert "status" in data + + +class TestFoodsBulkUpload: + """Test bulk food upload functionality""" + + def test_bulk_upload_foods_csv(self, client, tmp_path): + """Test POST /foods/upload with CSV""" + # Create test CSV file + csv_content = """ID,Brand,Serving (g),Calories,Protein (g),Carbohydrate (g),Fat (g),Fiber (g),Sugar (g),Sodium (mg),Calcium (mg) +Apple,Generic,100,52,0.3,14,0.2,2.4,10,1,6 +Banana,Generic,100,89,1.1,23,0.3,2.6,12,1,5""" + + csv_file = tmp_path / "test_foods.csv" + csv_file.write_text(csv_content) + + with open(csv_file, 'rb') as f: + response = client.post("/foods/upload", + files={"file": ("test_foods.csv", f, "text/csv")}) + + assert response.status_code == 200 + data = response.json() + assert "created" in data or "updated" in data + + def test_bulk_upload_invalid_csv(self, client, tmp_path): + """Test bulk upload with invalid CSV""" + csv_content = """Invalid,CSV,Format +1,2,3""" + + csv_file = tmp_path / "invalid.csv" + csv_file.write_text(csv_content) + + with open(csv_file, 'rb') as f: + response = client.post("/foods/upload", + files={"file": ("invalid.csv", f, "text/csv")}) + + assert response.status_code == 200 + data = response.json() + # Should handle errors gracefully + assert "status" in data or "errors" in data + + +class TestOpenFoodFacts: + """Test OpenFoodFacts integration""" + + def test_add_openfoodfacts_food(self, client): + """Test POST /foods/add_openfoodfacts""" + response = client.post("/foods/add_openfoodfacts", data={ + "name": "OpenFoodFacts Test Food", + "serving_size": "100", + "serving_unit": "g", + "calories": 180.0, + "protein": 7.0, + "carbs": 18.0, + "fat": 4.0, + "fiber": 2.5, + "sugar": 3.5, + "sodium": 90.0, + "calcium": 40.0, + "openfoodfacts_id": "12345678", + "brand": "OFF Brand", + "categories": "test,food" + }) + assert response.status_code == 200 + data = response.json() + assert data["status"] == "success" + + def test_get_openfoodfacts_product(self, client): + """Test GET /foods/get_openfoodfacts_product/{barcode}""" + # Test with a well-known barcode (Nutella) + response = client.get("/foods/get_openfoodfacts_product/3017620422003") + assert response.status_code == 200 + data = response.json() + assert "status" in data + + def test_openfoodfacts_by_category(self, client): + """Test GET /foods/openfoodfacts_by_category""" + response = client.get("/foods/openfoodfacts_by_category?category=beverages&limit=5") + assert response.status_code == 200 + data = response.json() + assert "status" in data