📄 test_groceries_clear.py 9,701 bytes Apr 27, 2026 📋 Raw

!/usr/bin/env python3

"""Integration test: /groceries clear and clear:groceries callback.

Tests:
1. handle_groceries_command returns formatted list with Clear button
2. handle_clear_groceries via text command clears list
3. handle_clear_groceries via callback clears list and edits message
4. Empty list after clear
"""

import json
import sys
import os
import tempfile
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock

ICARUS_ROOT = Path(file).parent.parent
sys.path.insert(0, str(ICARUS_ROOT))
os.environ["ICARUS_ENV"] = "staging"

from core.db.grocery_list import init_db, add_items, clear_list, list_items, DB_PATH
from core.handlers.recipe_toggle import handle_groceries_command, handle_clear_groceries
import core.db.grocery_list as gl

RESULTS = {"pass": 0, "fail": 0, "tests": []}

def record(name, passed, detail=""):
status = "✅ PASS" if passed else "❌ FAIL"
RESULTS["tests"].append({"name": name, "passed": passed, "detail": detail})
if passed:
RESULTS["pass"] += 1
else:
RESULTS["fail"] += 1
print(f" {status} — {name}" + (f" ({detail})" if detail else ""))

async def test_groceries_command():
"""Test /groceries returns formatted list with Clear List button."""
print("\n=== Test 1: /groceries command output ===\n")

# Setup: add some items
with tempfile.TemporaryDirectory() as tmpdir:
    tmp_db = Path(tmpdir) / "test_icarus.db"
    original_db_path = gl.DB_PATH
    gl.DB_PATH = tmp_db
    try:
        init_db()
        add_items(
            items=[{"text": "2 cups flour"}, {"text": "1 lb butter"}],
            recipe_source="Test Recipe",
            recipe_url="https://example.com/test/",
            requested_by="matt",
        )

        # Test list is not empty
        items = list_items()
        record("2 items added to grocery list", len(items) == 2, f"actual={len(items)}")

        # Mock bot
        bot = AsyncMock()
        sent_messages = []
        async def mock_send(chat_id, text, reply_markup=None, parse_mode="unset"):
            sent_messages.append({"chat_id": chat_id, "text": text, "reply_markup": reply_markup, "parse_mode": parse_mode})
            return {"ok": True, "result": {"message_id": 42}}
        bot.send_message = mock_send

        message = {
            "chat": {"id": 12345},
            "from": {"id": "999", "first_name": "Matt", "username": "matt"},
            "text": "/groceries",
        }

        await handle_groceries_command(message, bot)

        record("bot.send_message called", len(sent_messages) == 1, f"calls={len(sent_messages)}")
        msg = sent_messages[0]
        record("message contains Grocery List", "Grocery List" in msg["text"])
        record("message contains Test Recipe", "Test Recipe" in msg["text"])
        record("message contains flour", "flour" in msg["text"])
        record("message contains butter", "butter" in msg["text"])
        record("reply_markup has inline_keyboard", msg["reply_markup"] is not None and "inline_keyboard" in msg["reply_markup"])
        kb = msg["reply_markup"]["inline_keyboard"]
        record("Clear List button present", any(btn.get("callback_data") == "clear:groceries" for row in kb for btn in row))
        record("parse_mode is 'none' (plain text)", msg["parse_mode"] == "none", f"actual={msg['parse_mode']}")

    finally:
        gl.DB_PATH = original_db_path

async def test_clear_via_text_command():
"""Test /groceries clear via text command."""
print("\n=== Test 2: /groceries clear text command ===\n")

with tempfile.TemporaryDirectory() as tmpdir:
    tmp_db = Path(tmpdir) / "test_icarus.db"
    original_db_path = gl.DB_PATH
    gl.DB_PATH = tmp_db
    try:
        init_db()
        add_items(
            items=[{"text": "2 cups flour"}, {"text": "1 lb butter"}, {"text": "3 eggs"}],
            recipe_source="Test Recipe",
            recipe_url="https://example.com/test/",
            requested_by="matt",
        )

        items = list_items()
        record("3 items added", len(items) == 3, f"actual={len(items)}")

        # Mock bot
        bot = AsyncMock()
        sent_messages = []
        async def mock_send(chat_id, text, reply_markup=None, parse_mode="unset"):
            sent_messages.append({"chat_id": chat_id, "text": text, "reply_markup": reply_markup})
            return {"ok": True, "result": {"message_id": 42}}
        bot.send_message = mock_send

        message = {
            "chat": {"id": 12345},
            "from": {"id": "999", "first_name": "Matt", "username": "matt"},
            "text": "/groceries clear",
        }

        await handle_clear_groceries(message, bot)

        record("bot.send_message called", len(sent_messages) == 1)
        msg = sent_messages[0]
        record("confirmation mentions 3 items", "3 items" in msg["text"], f"actual={msg['text']}")

        # Verify list is now empty
        items_after = list_items(requested_by="matt")
        record("grocery list is empty after clear", len(items_after) == 0, f"actual={len(items_after)}")

    finally:
        gl.DB_PATH = original_db_path

async def test_clear_via_callback():
"""Test clear:groceries via callback query."""
print("\n=== Test 3: clear:groceries callback ===\n")

with tempfile.TemporaryDirectory() as tmpdir:
    tmp_db = Path(tmpdir) / "test_icarus.db"
    original_db_path = gl.DB_PATH
    gl.DB_PATH = tmp_db
    try:
        init_db()
        add_items(
            items=[{"text": "1 cup sugar"}, {"text": "2 tsp vanilla"}],
            recipe_source="Another Recipe",
            recipe_url="https://example.com/another/",
            requested_by="matt",
        )

        items = list_items()
        record("2 items added", len(items) == 2, f"actual={len(items)}")

        bot = AsyncMock()
        edit_messages = []
        async def mock_edit(chat_id, message_id, text, reply_markup=None):
            edit_messages.append({"chat_id": chat_id, "message_id": message_id, "text": text, "reply_markup": reply_markup})
            return {"ok": True}
        bot.edit_message_text = mock_edit
        bot.answer_callback_query = AsyncMock()

        callback = {
            "id": "callback_123",
            "from": {"id": 999, "first_name": "Matt", "username": "matt"},
            "data": "clear:groceries",
            "message": {"message_id": 42, "chat": {"id": 12345}},
        }

        await handle_clear_groceries(callback, bot)

        record("bot.edit_message_text called", len(edit_messages) == 1)
        msg = edit_messages[0]
        record("edited message mentions 2 items", "2 items" in msg["text"], f"actual={msg['text']}")
        record("reply_markup removed (None)", msg["reply_markup"] is None)

        # Verify list is empty
        items_after = list_items(requested_by="matt")
        record("grocery list is empty after callback clear", len(items_after) == 0, f"actual={len(items_after)}")

        record("answer_callback_query called", bot.answer_callback_query.called)

    finally:
        gl.DB_PATH = original_db_path

async def test_empty_groceries():
"""Test /groceries when list is empty."""
print("\n=== Test 4: /groceries when empty ===\n")

with tempfile.TemporaryDirectory() as tmpdir:
    tmp_db = Path(tmpdir) / "test_icarus.db"
    original_db_path = gl.DB_PATH
    gl.DB_PATH = tmp_db
    try:
        init_db()

        items = list_items()
        record("grocery list starts empty", len(items) == 0)

        bot = AsyncMock()
        sent_messages = []
        async def mock_send(chat_id, text, reply_markup=None, parse_mode="unset"):
            sent_messages.append({"chat_id": chat_id, "text": text, "reply_markup": reply_markup})
            return {"ok": True, "result": {"message_id": 42}}
        bot.send_message = mock_send

        message = {
            "chat": {"id": 12345},
            "from": {"id": "999", "first_name": "Matt", "username": "matt"},
            "text": "/groceries",
        }

        await handle_groceries_command(message, bot)

        record("bot.send_message called", len(sent_messages) == 1)
        msg = sent_messages[0]
        record("message says 'empty'", "empty" in msg["text"].lower(), f"actual={msg['text']}")
        record("no reply_markup when empty", msg["reply_markup"] is None, f"actual={msg['reply_markup']}")

    finally:
        gl.DB_PATH = original_db_path

async def main():
print("=" * 60)
print("INTEGRATION TEST: Grocery List View + Clear")
print("=" * 60)

await test_groceries_command()
await test_clear_via_text_command()
await test_clear_via_callback()
await test_empty_groceries()

print("\n" + "=" * 60)
print(f"RESULTS: {RESULTS['pass']} passed, {RESULTS['fail']} failed")
print("=" * 60)

failures = [t for t in RESULTS["tests"] if not t["passed"]]
if failures:
    print("\nFAILURES:")
    for f in failures:
        print(f"  ❌ {f['name']}" + (f" — {f['detail']}" if f['detail'] else ""))

return RESULTS["fail"] == 0

if name == "main":
import asyncio
success = asyncio.run(main())
sys.exit(0 if success else 1)