Rednote / backend /tests /integration /test_document_processor.py
orakzai-io
Intial 2
3dbd619
Raw History Blame Contribute Delete
5.42 kB
"""Unit tests for DocumentProcessor with mocked dependencies."""
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(__file__)), ".."))
import uuid
import hashlib
import tempfile
from unittest.mock import AsyncMock, MagicMock
import pytest
from models.db import Document
from services.document_processor import DocumentProcessor
@pytest.fixture
def mock_db():
"""Create a mocked async database session."""
db = AsyncMock()
# Make db.add a no-op side effect that actually stores the object
db.add = MagicMock()
# Default execute to return a result that yields None for scalars().first()
mock_result = MagicMock()
mock_result.scalars.return_value.first.return_value = None
db.execute.return_value = mock_result
return db
@pytest.fixture
def mock_embedding_service():
"""Create a mocked embedding service that returns fake embeddings."""
service = MagicMock()
service.embed_chunks.return_value = [[0.1] * 384, [0.2] * 384]
return service
@pytest.fixture
def sample_txt():
"""Create a temporary text file for testing."""
with tempfile.NamedTemporaryFile(
mode="w", suffix=".txt", delete=False, encoding="utf-8"
) as f:
f.write("This is a test document with enough words to create multiple chunks for testing the document processor pipeline.")
f.write(" " + "word " * 500) # Add more words to ensure chunking
tmp_path = f.name
yield tmp_path
if os.path.exists(tmp_path):
os.unlink(tmp_path)
@pytest.mark.asyncio
async def test_process_document_success(mock_db, mock_embedding_service, sample_txt):
"""Should successfully process a txt file through the full pipeline."""
user_id = uuid.uuid4()
processor = DocumentProcessor(mock_db, mock_embedding_service, user_id)
# Configure mock flush to set document.id
async def flush_side_effect():
pass
mock_db.flush = AsyncMock(side_effect=flush_side_effect)
doc = await processor.process_document(sample_txt, "test.txt")
assert doc.filename == "test.txt"
assert doc.status == "ready"
assert doc.content_hash is not None
assert doc.user_id == user_id
# Verify that chunks were added
chunk_add_calls = [call for call in mock_db.add.call_args_list if call[0][0].__class__.__name__ == "Chunk"]
assert len(chunk_add_calls) > 0
mock_db.commit.assert_awaited_once()
@pytest.mark.asyncio
async def test_process_document_empty_file(mock_db, mock_embedding_service):
"""Should raise ValueError on empty document."""
with tempfile.NamedTemporaryFile(
mode="w", suffix=".txt", delete=False, encoding="utf-8"
) as f:
f.write(" ")
empty_path = f.name
user_id = uuid.uuid4()
processor = DocumentProcessor(mock_db, mock_embedding_service, user_id)
try:
with pytest.raises(ValueError, match="empty"):
await processor.process_document(empty_path, "empty.txt")
finally:
os.unlink(empty_path)
@pytest.mark.asyncio
async def test_process_document_unsupported_extension(mock_db, mock_embedding_service):
"""Should raise ValueError for unsupported file extensions."""
with tempfile.NamedTemporaryFile(
mode="w", suffix=".xyz", delete=False
) as f:
f.write("some text")
bad_path = f.name
user_id = uuid.uuid4()
processor = DocumentProcessor(mock_db, mock_embedding_service, user_id)
try:
with pytest.raises(ValueError, match="Unsupported"):
await processor.process_document(bad_path, "test.xyz")
finally:
os.unlink(bad_path)
@pytest.mark.asyncio
async def test_process_document_dedup(mock_db, mock_embedding_service, sample_txt):
"""Should return existing document if content hash matches."""
user_id = uuid.uuid4()
processor = DocumentProcessor(mock_db, mock_embedding_service, user_id)
with open(sample_txt, "r") as f:
text = f.read()
content_hash = hashlib.sha256(text.encode("utf-8")).hexdigest()
# Create an "existing" document that the DB will return
existing_doc = Document(
id=uuid.uuid4(),
user_id=user_id,
filename="existing.txt",
content_hash=content_hash,
file_path=sample_txt,
status="ready",
)
# Mock the DB to return existing_doc on the first select query
mock_result = MagicMock()
mock_result.scalars.return_value.first.return_value = existing_doc
mock_db.execute.return_value = mock_result
result = await processor.process_document(sample_txt, "test.txt")
# Should return the existing document without creating a new one
assert result.id == existing_doc.id
assert result.filename == "existing.txt"
# The select query should have been executed
mock_db.execute.assert_awaited_once()
@pytest.mark.asyncio
async def test_process_document_user_id_none(mock_db, mock_embedding_service, sample_txt):
"""Should handle user_id=None gracefully (guest users)."""
processor = DocumentProcessor(mock_db, mock_embedding_service, user_id=None)
async def flush_side_effect():
pass
mock_db.flush = AsyncMock(side_effect=flush_side_effect)
doc = await processor.process_document(sample_txt, "guest_test.txt")
assert doc.filename == "guest_test.txt"
assert doc.status == "ready"
mock_db.commit.assert_awaited_once()