Skip to content

Commit bdffa76

Browse files
StephanMeijerAntoLC
authored andcommitted
✅(backend) add tests for document import feature
Added comprehensive tests covering DocSpec converter service, converter orchestration, and document creation with file uploads. Tests validate DOCX and Markdown conversion workflows, error handling, service availability, and edge cases including empty files and Unicode filenames. Signed-off-by: Stephan Meijer <me@stephanmeijer.com>
1 parent 4ea5aac commit bdffa76

10 files changed

Lines changed: 635 additions & 44 deletions

src/backend/core/api/serializers.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@
1111
from django.utils.text import slugify
1212
from django.utils.translation import gettext_lazy as _
1313

14-
from core.services import mime_types
1514
import magic
1615
from rest_framework import serializers
1716

1817
from core import choices, enums, models, utils, validators
18+
from core.services import mime_types
1919
from core.services.ai_services import AI_ACTIONS
2020
from core.services.converter_services import (
2121
ConversionError,
@@ -465,9 +465,7 @@ def create(self, validated_data):
465465

466466
try:
467467
document_content = Converter().convert(
468-
validated_data["content"],
469-
mime_types.MARKDOWN,
470-
mime_types.YJS
468+
validated_data["content"], mime_types.MARKDOWN, mime_types.YJS
471469
)
472470
except ConversionError as err:
473471
raise serializers.ValidationError(

src/backend/core/api/viewsets.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,20 @@
4040
from rest_framework.permissions import AllowAny
4141

4242
from core import authentication, choices, enums, models
43+
from core.services import mime_types
4344
from core.api.filters import remove_accents
4445
from core.services.ai_services import AIService
4546
from core.services.collaboration_services import CollaborationService
4647
from core.services.converter_services import (
4748
ConversionError,
49+
Converter,
50+
)
51+
from core.services.converter_services import (
4852
ServiceUnavailableError as YProviderServiceUnavailableError,
53+
)
54+
from core.services.converter_services import (
4955
ValidationError as YProviderValidationError,
50-
Converter,
5156
)
52-
from core.services import mime_types
5357
from core.services.search_indexers import (
5458
get_document_indexer,
5559
get_visited_document_ids_of,
@@ -536,7 +540,7 @@ def perform_create(self, serializer):
536540
converted_content = converter.convert(
537541
file_content,
538542
content_type=uploaded_file.content_type,
539-
accept=mime_types.YJS
543+
accept=mime_types.YJS,
540544
)
541545
serializer.validated_data["content"] = converted_content
542546
serializer.validated_data["title"] = uploaded_file.name

src/backend/core/services/converter_services.py

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
"""Y-Provider API services."""
22

3+
import typing
34
from base64 import b64encode
45

56
from django.conf import settings
67

78
import requests
8-
import typing
99

1010
from core.services import mime_types
1111

12+
1213
class ConversionError(Exception):
1314
"""Base exception for conversion-related errors."""
1415

@@ -22,28 +23,33 @@ class ServiceUnavailableError(ConversionError):
2223

2324

2425
class ConverterProtocol(typing.Protocol):
25-
def convert(self, text, content_type, accept): ...
26+
"""Protocol for converter classes."""
27+
28+
def convert(self, text, content_type, accept):
29+
"""Convert content from one format to another."""
2630

2731

2832
class Converter:
33+
"""Orchestrates conversion between different formats using specialized converters."""
34+
2935
docspec: ConverterProtocol
3036
ydoc: ConverterProtocol
3137

3238
def __init__(self):
3339
self.docspec = DocSpecConverter()
3440
self.ydoc = YdocConverter()
3541

36-
def convert(self, input, content_type, accept):
42+
def convert(self, data, content_type, accept):
3743
"""Convert input into other formats using external microservices."""
38-
44+
3945
if content_type == mime_types.DOCX and accept == mime_types.YJS:
4046
return self.convert(
41-
self.docspec.convert(input, mime_types.DOCX, mime_types.BLOCKNOTE),
47+
self.docspec.convert(data, mime_types.DOCX, mime_types.BLOCKNOTE),
4248
mime_types.BLOCKNOTE,
43-
mime_types.YJS
49+
mime_types.YJS,
4450
)
45-
46-
return self.ydoc.convert(input, content_type, accept)
51+
52+
return self.ydoc.convert(data, content_type, accept)
4753

4854

4955
class DocSpecConverter:
@@ -61,15 +67,17 @@ def _request(self, url, data, content_type):
6167
)
6268
response.raise_for_status()
6369
return response
64-
70+
6571
def convert(self, data, content_type, accept):
6672
"""Convert a Document to BlockNote."""
6773
if not data:
6874
raise ValidationError("Input data cannot be empty")
69-
75+
7076
if content_type != mime_types.DOCX or accept != mime_types.BLOCKNOTE:
71-
raise ValidationError(f"Conversion from {content_type} to {accept} is not supported.")
72-
77+
raise ValidationError(
78+
f"Conversion from {content_type} to {accept} is not supported."
79+
)
80+
7381
try:
7482
return self._request(settings.DOCSPEC_API_URL, data, content_type).content
7583
except requests.RequestException as err:
@@ -103,9 +111,7 @@ def _request(self, url, data, content_type, accept):
103111
response.raise_for_status()
104112
return response
105113

106-
def convert(
107-
self, text, content_type=mime_types.MARKDOWN, accept=mime_types.YJS
108-
):
114+
def convert(self, text, content_type=mime_types.MARKDOWN, accept=mime_types.YJS):
109115
"""Convert a Markdown text into our internal format using an external microservice."""
110116

111117
if not text:

src/backend/core/services/mime_types.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"""MIME type constants for document conversion."""
2+
13
BLOCKNOTE = "application/vnd.blocknote+json"
24
YJS = "application/vnd.yjs.doc"
35
MARKDOWN = "text/markdown"

src/backend/core/tests/documents/test_api_documents_create_for_owner.py

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from core import factories
1717
from core.api.serializers import ServerCreateDocumentSerializer
1818
from core.models import Document, Invitation, User
19+
from core.services import mime_types
1920
from core.services.converter_services import ConversionError, YdocConverter
2021

2122
pytestmark = pytest.mark.django_db
@@ -191,7 +192,9 @@ def test_api_documents_create_for_owner_existing(mock_convert_md):
191192

192193
assert response.status_code == 201
193194

194-
mock_convert_md.assert_called_once_with("Document content")
195+
mock_convert_md.assert_called_once_with(
196+
"Document content", mime_types.MARKDOWN, mime_types.YJS
197+
)
195198

196199
document = Document.objects.get()
197200
assert response.json() == {"id": str(document.id)}
@@ -236,7 +239,9 @@ def test_api_documents_create_for_owner_new_user(mock_convert_md):
236239

237240
assert response.status_code == 201
238241

239-
mock_convert_md.assert_called_once_with("Document content")
242+
mock_convert_md.assert_called_once_with(
243+
"Document content", mime_types.MARKDOWN, mime_types.YJS
244+
)
240245

241246
document = Document.objects.get()
242247
assert response.json() == {"id": str(document.id)}
@@ -297,7 +302,9 @@ def test_api_documents_create_for_owner_existing_user_email_no_sub_with_fallback
297302

298303
assert response.status_code == 201
299304

300-
mock_convert_md.assert_called_once_with("Document content")
305+
mock_convert_md.assert_called_once_with(
306+
"Document content", mime_types.MARKDOWN, mime_types.YJS
307+
)
301308

302309
document = Document.objects.get()
303310
assert response.json() == {"id": str(document.id)}
@@ -393,7 +400,9 @@ def test_api_documents_create_for_owner_new_user_no_sub_no_fallback_allow_duplic
393400
HTTP_AUTHORIZATION="Bearer DummyToken",
394401
)
395402
assert response.status_code == 201
396-
mock_convert_md.assert_called_once_with("Document content")
403+
mock_convert_md.assert_called_once_with(
404+
"Document content", mime_types.MARKDOWN, mime_types.YJS
405+
)
397406

398407
document = Document.objects.get()
399408
assert response.json() == {"id": str(document.id)}
@@ -474,7 +483,9 @@ def test_api_documents_create_for_owner_with_default_language(
474483
)
475484
assert response.status_code == 201
476485

477-
mock_convert_md.assert_called_once_with("Document content")
486+
mock_convert_md.assert_called_once_with(
487+
"Document content", mime_types.MARKDOWN, mime_types.YJS
488+
)
478489
assert mock_send.call_args[0][3] == "de-de"
479490

480491

@@ -501,7 +512,9 @@ def test_api_documents_create_for_owner_with_custom_language(mock_convert_md):
501512

502513
assert response.status_code == 201
503514

504-
mock_convert_md.assert_called_once_with("Document content")
515+
mock_convert_md.assert_called_once_with(
516+
"Document content", mime_types.MARKDOWN, mime_types.YJS
517+
)
505518

506519
assert len(mail.outbox) == 1
507520
email = mail.outbox[0]
@@ -537,7 +550,9 @@ def test_api_documents_create_for_owner_with_custom_subject_and_message(
537550

538551
assert response.status_code == 201
539552

540-
mock_convert_md.assert_called_once_with("Document content")
553+
mock_convert_md.assert_called_once_with(
554+
"Document content", mime_types.MARKDOWN, mime_types.YJS
555+
)
541556

542557
assert len(mail.outbox) == 1
543558
email = mail.outbox[0]
@@ -571,7 +586,9 @@ def test_api_documents_create_for_owner_with_converter_exception(
571586
format="json",
572587
HTTP_AUTHORIZATION="Bearer DummyToken",
573588
)
574-
mock_convert_md.assert_called_once_with("Document content")
589+
mock_convert_md.assert_called_once_with(
590+
"Document content", mime_types.MARKDOWN, mime_types.YJS
591+
)
575592

576593
assert response.status_code == 400
577594
assert response.json() == {"content": ["Could not convert content"]}

0 commit comments

Comments
 (0)