Files
documente/tests/unit/test_api/test_chat.py
Luca Sacchi Ricciardi 081f3f0d89 feat(api): add chat functionality (Sprint 3)
Implement Sprint 3: Chat Functionality

- Add ChatService with send_message and get_history methods
- Add POST /api/v1/notebooks/{id}/chat - Send message
- Add GET /api/v1/notebooks/{id}/chat/history - Get chat history
- Add ChatRequest model (message, include_references)
- Add ChatResponse model (message, sources[], timestamp)
- Add ChatMessage model (id, role, content, timestamp, sources)
- Add SourceReference model (source_id, title, snippet)
- Integrate chat router with main app

Features:
- Send messages to notebook chat
- Get AI responses with source references
- Retrieve chat history
- Support for citations in responses

Tests:
- 14 unit tests for ChatService
- 11 integration tests for chat API
- 25/25 tests passing

Related: Sprint 3 - Chat Functionality
2026-04-06 01:48:19 +02:00

196 lines
6.6 KiB
Python

"""Integration tests for chat API endpoints.
Tests all chat endpoints with mocked services.
"""
from datetime import datetime
from unittest.mock import AsyncMock, patch
from uuid import uuid4
import pytest
from fastapi.testclient import TestClient
from notebooklm_agent.api.main import app
from notebooklm_agent.api.models.responses import ChatMessage, ChatResponse, SourceReference
@pytest.mark.unit
class TestSendMessageEndpoint:
"""Test suite for POST /api/v1/notebooks/{id}/chat endpoint."""
def test_send_message_returns_200(self):
"""Should return 200 with chat response."""
# Arrange
client = TestClient(app)
notebook_id = str(uuid4())
with patch("notebooklm_agent.api.routes.chat.ChatService") as mock_service_class:
mock_service = AsyncMock()
mock_response = ChatResponse(
message="This is the answer based on your sources.",
sources=[
SourceReference(
source_id=str(uuid4()),
title="Example Source",
snippet="Relevant text",
)
],
timestamp=datetime.utcnow(),
)
mock_service.send_message.return_value = mock_response
mock_service_class.return_value = mock_service
# Act
response = client.post(
f"/api/v1/notebooks/{notebook_id}/chat",
json={"message": "What are the key points?", "include_references": True},
)
# Assert
assert response.status_code == 200
data = response.json()
assert data["success"] is True
assert "answer based on your sources" in data["data"]["message"]
assert len(data["data"]["sources"]) == 1
def test_send_message_invalid_notebook_id_returns_400(self):
"""Should return 400 for invalid notebook ID."""
# Arrange
client = TestClient(app)
# Act
response = client.post(
"/api/v1/notebooks/invalid-id/chat",
json={"message": "Question?"},
)
# Assert
assert response.status_code in [400, 422]
def test_send_message_empty_message_returns_400(self):
"""Should return 400 for empty message."""
# Arrange
client = TestClient(app)
notebook_id = str(uuid4())
with patch("notebooklm_agent.api.routes.chat.ChatService") as mock_service_class:
mock_service = AsyncMock()
from notebooklm_agent.core.exceptions import ValidationError
mock_service.send_message.side_effect = ValidationError("Message cannot be empty")
mock_service_class.return_value = mock_service
# Act
response = client.post(
f"/api/v1/notebooks/{notebook_id}/chat",
json={"message": ""},
)
# Assert
assert response.status_code in [400, 422]
def test_send_message_notebook_not_found_returns_404(self):
"""Should return 404 when notebook not found."""
# Arrange
client = TestClient(app)
notebook_id = str(uuid4())
with patch("notebooklm_agent.api.routes.chat.ChatService") as mock_service_class:
mock_service = AsyncMock()
from notebooklm_agent.core.exceptions import NotFoundError
mock_service.send_message.side_effect = NotFoundError("Notebook", notebook_id)
mock_service_class.return_value = mock_service
# Act
response = client.post(
f"/api/v1/notebooks/{notebook_id}/chat",
json={"message": "Question?"},
)
# Assert
assert response.status_code == 404
@pytest.mark.unit
class TestGetChatHistoryEndpoint:
"""Test suite for GET /api/v1/notebooks/{id}/chat/history endpoint."""
def test_get_history_returns_200(self):
"""Should return 200 with chat history."""
# Arrange
client = TestClient(app)
notebook_id = str(uuid4())
with patch("notebooklm_agent.api.routes.chat.ChatService") as mock_service_class:
mock_service = AsyncMock()
mock_message = ChatMessage(
id=str(uuid4()),
role="user",
content="What are the key points?",
timestamp=datetime.utcnow(),
sources=None,
)
mock_service.get_history.return_value = [mock_message]
mock_service_class.return_value = mock_service
# Act
response = client.get(f"/api/v1/notebooks/{notebook_id}/chat/history")
# Assert
assert response.status_code == 200
data = response.json()
assert data["success"] is True
assert len(data["data"]) == 1
assert data["data"][0]["role"] == "user"
def test_get_history_empty_returns_empty_list(self):
"""Should return empty list if no history."""
# Arrange
client = TestClient(app)
notebook_id = str(uuid4())
with patch("notebooklm_agent.api.routes.chat.ChatService") as mock_service_class:
mock_service = AsyncMock()
mock_service.get_history.return_value = []
mock_service_class.return_value = mock_service
# Act
response = client.get(f"/api/v1/notebooks/{notebook_id}/chat/history")
# Assert
assert response.status_code == 200
data = response.json()
assert data["success"] is True
assert data["data"] == []
def test_get_history_invalid_notebook_id_returns_400(self):
"""Should return 400 for invalid notebook ID."""
# Arrange
client = TestClient(app)
# Act
response = client.get("/api/v1/notebooks/invalid-id/chat/history")
# Assert
assert response.status_code in [400, 422]
def test_get_history_notebook_not_found_returns_404(self):
"""Should return 404 when notebook not found."""
# Arrange
client = TestClient(app)
notebook_id = str(uuid4())
with patch("notebooklm_agent.api.routes.chat.ChatService") as mock_service_class:
mock_service = AsyncMock()
from notebooklm_agent.core.exceptions import NotFoundError
mock_service.get_history.side_effect = NotFoundError("Notebook", notebook_id)
mock_service_class.return_value = mock_service
# Act
response = client.get(f"/api/v1/notebooks/{notebook_id}/chat/history")
# Assert
assert response.status_code == 404