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
196 lines
6.6 KiB
Python
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
|