11"""Tests for the PDF Block"""
22
33import json
4- from typing import Any
4+ from dataclasses import dataclass
5+ from typing import Any , TypedDict
56from unittest .mock import MagicMock , patch
67
8+ import pytest
9+ from django .contrib .auth .models import User
710from django .test import override_settings
11+ from requests import Response
12+ from requests .exceptions import HTTPError
813from xblock .field_data import DictFieldData
914from xblock .fields import ScopeIds
1015from xblock .test .toy_runtime import ToyRuntime
1116
1217from xblock_pdf import PDFBlock
18+ from xblock_pdf .utils import error_response
1319
20+ MockOptValues = TypedDict ("MockOptValues" , {"edx-platform.user_is_staff" : bool , "edx-platform.user_id" : int })
1421
15- def make_block (** fields : str ) -> PDFBlock :
22+
23+ @dataclass
24+ class MockUser :
25+ opt_attrs : MockOptValues
26+
27+
28+ class ToyUserService :
29+ """
30+ Toy version of the user service that implements just enough for us to work with.
31+ """
32+
33+ def __init__ (self , * , user_id : int , is_staff = False ):
34+ self ._user = MockUser (opt_attrs = {"edx-platform.user_is_staff" : is_staff , "edx-platform.user_id" : user_id })
35+
36+ def get_current_user (self ):
37+ return self ._user
38+
39+
40+ class ToyPermissionsService :
41+ """
42+ Toy version of the studio_user_permissions service.
43+ """
44+
45+ def __init__ (self , can_read = True , can_write = False ):
46+ self ._can_read = can_read
47+ self ._can_write = can_write
48+
49+ def can_read (self , _context_key ):
50+ return self ._can_read
51+
52+ def can_write (self , _context_key ):
53+ return self ._can_write
54+
55+
56+ class ToyServiceRuntime (ToyRuntime ):
57+ """
58+ Modified toy runtime that includes custom services for mocking/testing.
59+ """
60+
61+ def __init__ (self , * , services : dict [str , Any ] | None = None ):
62+ super ().__init__ ()
63+ if services is not None :
64+ self ._services .update (services )
65+
66+
67+ def make_block (* , services : dict [str , Any ] | None = None , ** fields : str ) -> PDFBlock :
1668 """Build a block with specific fields set."""
1769 scope_ids = ScopeIds ("1" , "2" , "3" , "4" )
18- return PDFBlock (ToyRuntime ( ), scope_ids = scope_ids , field_data = DictFieldData (data = fields ))
70+ return PDFBlock (ToyServiceRuntime ( services = services ), scope_ids = scope_ids , field_data = DictFieldData (data = fields ))
1971
2072
2173def get_student_content (block : PDFBlock ) -> str :
@@ -52,7 +104,7 @@ def test_download_button():
52104
53105
54106def test_source_url ():
55- """Test rendering based on whether or not there's a source URL"""
107+ """Test rendering based on whether there's a source URL"""
56108 block = make_block ()
57109 get_student_content (block )
58110 content = get_student_content (block )
@@ -62,60 +114,6 @@ def test_source_url():
62114 assert "Download the source document" in content
63115
64116
65- @override_settings (PDFXBLOCK_DISABLE_ALL_DOWNLOAD = False )
66- def test_saves_settings ():
67- """Test that PDF settings are saved."""
68- block = make_block ()
69- request = mock_handle_request (
70- {
71- "display_name" : "Novel application of theory" ,
72- "url" : "https://example.com/nature_article.pdf" ,
73- "allow_download" : "false" ,
74- "source_text" : "Get educated" ,
75- "source_url" : "https://example.com/nature_article.tex" ,
76- }
77- )
78- block .save_pdf (request )
79- assert block .display_name == "Novel application of theory"
80- assert block .url == "https://example.com/nature_article.pdf"
81- assert not block .allow_download
82- assert block .source_text == "Get educated"
83- assert block .source_url == "https://example.com/nature_article.tex"
84-
85-
86- @override_settings (PDFXBLOCK_DISABLE_ALL_DOWNLOAD = True )
87- def test_saves_settings_omits_on_download_disabled_flag ():
88- """
89- Test that fields relating to download are ignored when the universal
90- downloads disabled flag is set.
91- """
92- block = make_block ()
93- request = mock_handle_request (
94- {
95- "display_name" : "Novel application of theory" ,
96- "url" : "https://example.com/nature_article.pdf" ,
97- # These fields shouldn't be visible on the front end,
98- # but should be dropped if they somehow are.
99- #
100- # Potential future improvement would be saving these
101- # but ignoring them when rendering. This is not currently
102- # the case since the fields are entirely absent from the studio
103- # render, and so would send blank data which would error out.
104- "allow_download" : "false" ,
105- "source_text" : "Get educated" ,
106- "source_url" : "https://example.com/nature_article.tex" ,
107- }
108- )
109- block .save_pdf (request )
110- assert block .display_name == "Novel application of theory"
111- assert block .url == "https://example.com/nature_article.pdf"
112- # Flag will be the default, which is True, even though download will be
113- # disabled in practice.
114- assert block .allow_download
115- assert block .source_text == ""
116- assert block .source_url == ""
117-
118-
119117@patch .object (ToyRuntime , "publish" )
120118def test_download_event_fires (mock_publish ):
121119 """Test that we fire a download event."""
@@ -138,3 +136,92 @@ def test_get_settings():
138136 request = mock_handle_request ({}, method = "GET" )
139137 result = json .loads (block .load_pdf (request ).body )
140138 assert result ["display_name" ] == "PDF"
139+
140+
141+ @override_settings (GOTENBERG_HOST = None )
142+ def test_convert_pdf_fails_no_gotenberg ():
143+ """
144+ Test that PDF conversion fails if Gotenberg is not available.
145+ """
146+ block = make_block (services = {"user" : ToyUserService (is_staff = True , user_id = 1 )})
147+ request = mock_handle_request ({"url" : "https://example.com/thing.doc" })
148+ result = block .convert_pdf (request )
149+ assert result .status_code == 400
150+ assert b"Gotenberg not enabled. PDF Conversion unavailable." in result .body
151+
152+
153+ @override_settings (GOTENBERG_HOST = "https://gotenberg/" )
154+ def test_convert_fails_not_staff ():
155+ """
156+ Test that PDF conversion fails if user is not staff.
157+ """
158+ block = make_block (services = {"user" : ToyUserService (is_staff = False , user_id = 1 )})
159+ request = mock_handle_request ({"url" : "https://example.com/thing.doc" })
160+ result = block .convert_pdf (request )
161+ assert result .status_code == 403
162+ assert b"You do not have permission to manage files for this block." in result .body
163+
164+
165+ @override_settings (GOTENBERG_HOST = "https://gotenberg/" )
166+ @patch ("xblock_pdf.pdf.logger" )
167+ @patch ("xblock_pdf.pdf.fetch_source_asset" )
168+ @pytest .mark .django_db
169+ def test_failed_fetch_logs (mock_fetch , mock_log ):
170+ block = make_block (
171+ services = {
172+ "user" : ToyUserService (
173+ is_staff = True , user_id = User .objects .create (username = "beep" , email = "beep@example.com" ).id
174+ )
175+ }
176+ )
177+ mock_fetch .side_effect = HTTPError (response = error_response ({"error" : "Failed." }, status = 400 ))
178+ request = mock_handle_request ({"url" : "https://example.com/thing.doc" })
179+ result = block .convert_pdf (request )
180+ assert mock_log .exception .has_been_called ()
181+ assert result .status_code == 502
182+ assert b"Could not fetch source document." in result .body
183+
184+
185+ @override_settings (GOTENBERG_HOST = "https://gotenberg/" )
186+ @patch ("xblock_pdf.pdf.convert_to_pdf" )
187+ @patch ("xblock_pdf.pdf.fetch_source_asset" )
188+ @pytest .mark .django_db
189+ def test_failed_conversion (mock_fetch , mock_convert ):
190+ block = make_block (
191+ services = {
192+ "user" : ToyUserService (
193+ is_staff = True , user_id = User .objects .create (username = "beep" , email = "beep@example.com" ).id
194+ )
195+ }
196+ )
197+ mock_fetch .return_value = b"beep"
198+ mock_convert .return_value = None
199+ request = mock_handle_request ({"url" : "https://example.com/thing.doc" })
200+ result = block .convert_pdf (request )
201+ assert result .status_code == 500
202+ assert b"PDF Conversion failed." in result .body
203+
204+
205+ @override_settings (GOTENBERG_HOST = "https://gotenberg/" )
206+ @patch ("xblock_pdf.pdf.add_asset" )
207+ @patch ("xblock_pdf.utils.requests" )
208+ @patch ("xblock_pdf.pdf.fetch_source_asset" )
209+ @pytest .mark .django_db
210+ def test_successful_conversion_with_perms_service (mock_fetch , mock_requests , mock_add_asset ):
211+ block = make_block (
212+ services = {
213+ "user" : ToyUserService (
214+ is_staff = False , user_id = User .objects .create (username = "beep" , email = "beep@example.com" ).id
215+ ),
216+ "studio_user_permissions" : ToyPermissionsService (can_write = True ),
217+ }
218+ )
219+ mock_fetch .return_value = b"beep"
220+ mock_response = Response ()
221+ mock_response .__setstate__ ({"status_code" : 200 , "_content" : b"boop" })
222+ mock_requests .post .return_value = mock_response
223+ mock_add_asset .return_value = "https://example.com/exported.pdf"
224+ request = mock_handle_request ({"url" : "https://example.com/thing.doc" })
225+ result = block .convert_pdf (request )
226+ assert result .status_code == 200
227+ assert b"https://example.com/exported.pdf" in result .body
0 commit comments