Files
NaliiaBot/tests/conftest.py
aserrador 761253a056 fix: webhook test modernization and production bug fix
- Fix production bug in send_whatsapp_message: access 'instance_url' instead
  of 'server_url' to match SendMessageScheme TypedDict definition
- Update webhook test to use test_app with in-memory dependencies
- Mock send_whatsapp_message to avoid real HTTP calls in tests
- Add assertions to verify correct parameters passed to send_whatsapp_message

Bug: send_whatsapp_message accessed request_data['server_url'] but
SendMessageScheme defines field as 'instance_url', causing KeyError when
webhook tries to send WhatsApp messages.
2026-06-10 16:30:28 -05:00

266 lines
7.9 KiB
Python

import pytest
from unittest.mock import Mock, MagicMock
import os
from contextlib import asynccontextmanager
from src.naliiabot.bot.agent.agent import Agent, AgentConfig
from src.naliiabot.bot.factories.llm_factory import LLMFactory
from httpx import ASGITransport, AsyncClient
from fastapi import FastAPI
from langgraph.checkpoint.memory import MemorySaver
from langchain_core.messages import AIMessage
from langchain_anthropic import ChatAnthropic
from langchain_deepseek import ChatDeepSeek
from naliiabot.bot.prompts.load_prompt import get_prompt_template
from naliiabot.bot.tools.naliia_tools import NaliiaTools
import naliiabotapi.api.dependencies as deps
from naliiabotapi.api.v1.endpoints.chat import router as chat_router
from naliiabotapi.api.v1.webhooks.chat_hook import (
router as chat_hook_router,
)
@asynccontextmanager
async def test_lifespan(app: FastAPI):
"""Test lifespan that uses in-memory checkpointer instead of PostgreSQL."""
# Use in-memory checkpointer for tests
checkpointer = MemorySaver()
# Use a mock model for tests to avoid needing API keys
# The mock needs to return proper AIMessage objects
mock_model = MagicMock()
def mock_invoke(*args, **kwargs):
"""Mock invoke that returns a proper AIMessage."""
return AIMessage(
content="This is a test response from the mock model."
)
mock_model.invoke = mock_invoke
mock_model.bind_tools = Mock(return_value=mock_model)
naliia_prompt = get_prompt_template("NALIIA_PROMPT")
naliia_tools = NaliiaTools().get_tools()
config = AgentConfig(system_prompt=naliia_prompt)
deps.agent_instance = Agent(
model=mock_model,
config=config,
checkpointer=checkpointer,
tools=naliia_tools,
)
yield
# Cleanup
deps.agent_instance = None
@pytest.fixture(scope="session")
def test_app():
"""Create a test FastAPI app with in-memory dependencies."""
app = FastAPI(
title="NaliiaBot API - Test",
description="Test API for NaliiaBot",
version="1.0.0",
root_path="/agent/api",
lifespan=test_lifespan,
)
app.include_router(chat_router)
app.include_router(chat_hook_router)
@app.get("/")
def read_root():
return {"Hello": "World"}
@app.get("/health")
def health_check():
return {"status": "ok"}
return app
@pytest.fixture(scope="function")
async def async_client(test_app):
"""Fixture para un cliente HTTP asíncrono."""
async with AsyncClient(
transport=ASGITransport(app=test_app),
base_url="http://localhost:8010",
) as client:
yield client
@pytest.fixture
def mock_tool():
"""Mock de herramienta."""
tool = Mock()
tool.name = "calculator"
tool.invoke = Mock(return_value="42")
return tool
@pytest.fixture
def mock_model():
"""Mock de modelo LLM con interfaz completa."""
model = Mock()
model.bind_tools = Mock(return_value=model)
return model
@pytest.fixture
def default_config():
"""Configuración de test predecible."""
return AgentConfig(
system_prompt="Test prompt", max_iterations=5, timeout_seconds=10.0
)
@pytest.fixture
def agent(mock_model, default_config):
"""Agente básico sin herramientas."""
return Agent(model=mock_model, config=default_config)
@pytest.fixture
def agent_with_tool(mock_model, default_config, mock_tool):
"""Agente con una herramienta mock."""
return Agent(
model=mock_model, config=default_config, tools=[mock_tool]
)
@pytest.fixture
def anthropic_model():
llmFactory = LLMFactory("anthropic")
anthropic_model = llmFactory.get_model()
return anthropic_model
@pytest.fixture
def send_message_payload():
"""Payload de ejemplo para enviar mensajes al webhook."""
return {
"event": "messages.upsert",
"instance": "ALEJANDRO AYALA",
"data": {
"key": {
"remoteJid": "5512992544490@s.whatsapp.net",
"remoteJidAlt": "5512992544490@s.whatsapp.net",
"fromMe": False,
"id": "AC4367597BDC50537133B9C8848B2E62",
"participant": "",
"addressingMode": "lid",
},
"pushName": "Alejandro",
"status": "DELIVERY_ACK",
"message": {
"conversation": "Hola",
"messageContextInfo": {
"threadId": [],
"deviceListMetadata": {
"senderKeyIndexes": [],
"recipientKeyIndexes": [],
"senderKeyHash": {
"0": 154,
"1": 106,
"2": 69,
"3": 189,
"4": 90,
"5": 105,
"6": 172,
"7": 243,
"8": 27,
"9": 98,
},
"senderTimestamp": {
"low": 1772156772,
"high": 0,
"unsigned": "true",
},
"recipientKeyHash": {
"0": 16,
"1": 98,
"2": 75,
"3": 100,
"4": 131,
"5": 43,
"6": 24,
"7": 225,
"8": 244,
"9": 126,
},
"recipientTimestamp": {
"low": 1772155597,
"high": 0,
"unsigned": True,
},
},
"deviceListMetadataVersion": 2,
"messageSecret": {
"0": 100,
"1": 47,
"2": 112,
"3": 133,
"4": 255,
"5": 156,
"6": 129,
"7": 57,
"8": 239,
"9": 219,
"10": 74,
"11": 171,
"12": 212,
"13": 150,
"14": 112,
"15": 83,
"16": 123,
"17": 78,
"18": 189,
"19": 13,
"20": 51,
"21": 133,
"22": 129,
"23": 6,
"24": 197,
"25": 119,
"26": 197,
"27": 56,
"28": 87,
"29": 176,
"30": 135,
"31": 62,
},
},
},
"messageType": "conversation",
"messageTimestamp": 1772168493,
"instanceId": "75a877f1-d772-422b-a6f1-6ac470af5663",
"source": "android",
},
"destination": "http://192.168.58.110:8010/webhook",
"date_time": "2026-02-27T02:01:34.178Z",
"sender": "573016859278@s.whatsapp.net",
"server_url": "http://localhost:8080",
"apikey": "98B6B820AFC3-4544-9A4C-011B0719C2D1",
}
@pytest.fixture
def mock_chat_anthropic():
"""Mock para ChatAnthropic que no requiere API key."""
mock = Mock(spec=ChatAnthropic)
mock.model_name = "claude-sonnet-4-5-20250929"
return mock
@pytest.fixture
def mock_chat_deepseek():
"""Mock para ChatDeepSeek que no requiere API key."""
mock = Mock(spec=ChatDeepSeek)
mock.model_name = "deepseek-chat"
return mock