Skip to content

Commit 970fcd5

Browse files
committed
✅(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 eeeaa0c commit 970fcd5

10 files changed

+635
-44
lines changed

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
@@ -37,15 +37,19 @@
3737
from rest_framework.permissions import AllowAny
3838

3939
from core import authentication, choices, enums, models
40+
from core.services import mime_types
4041
from core.services.ai_services import AIService
4142
from core.services.collaboration_services import CollaborationService
4243
from core.services.converter_services import (
4344
ConversionError,
45+
Converter,
46+
)
47+
from core.services.converter_services import (
4448
ServiceUnavailableError as YProviderServiceUnavailableError,
49+
)
50+
from core.services.converter_services import (
4551
ValidationError as YProviderValidationError,
46-
Converter,
4752
)
48-
from core.services import mime_types
4953
from core.tasks.mail import send_ask_for_access_mail
5054
from core.utils import extract_attachments, filter_descendants
5155

@@ -515,7 +519,7 @@ def perform_create(self, serializer):
515519
converted_content = converter.convert(
516520
file_content,
517521
content_type=uploaded_file.content_type,
518-
accept=mime_types.YJS
522+
accept=mime_types.YJS,
519523
)
520524
serializer.validated_data["content"] = converted_content
521525
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)