import os # The app refuses to start without SECRET_KEY outside development; declare that # these are tests before importing it. os.environ.setdefault('PDF_OCR_TESTING', '1') import unittest import tempfile import shutil import json import time import threading import re from unittest.mock import patch, MagicMock from PIL import Image import colorama from colorama import Fore, Style from app import ( # noqa: E402 app, allowed_file, secure_clean_filename, check_dependency, sanitize_text, enhance_image, process_image, fix_common_ocr_errors, save_as_markdown, save_as_html, is_within_upload_folder, looks_like_pdf, save_output, cleanup_old_files, process_pdf_with_progress, parse_preprocess_options, otsu_threshold, run_task_in_background, env_int, log_safe, ConversionCancelled, TASKS, TASK_TIMEOUT, STALE_TASK_TIMEOUT, detect_and_correct_orientation, estimate_skew_angle, extract_native_page_text ) def _temp_path(suffix: str) -> str: """Reserve a temporary path without tempfile.mktemp. `tempfile.mktemp` only returns a name, leaving a window in which another process can create the file first; mkstemp creates it atomically. """ fd, path = tempfile.mkstemp(suffix=suffix) os.close(fd) return path # Initialize colorama for colored terminal output colorama.init(autoreset=True) # Custom test result class for colored output class ColorTextTestResult(unittest.TextTestResult): def addSuccess(self, test): self.stream.write(f"{Fore.GREEN}✓{Style.RESET_ALL} ") super().addSuccess(test) def addError(self, test, err): self.stream.write(f"{Fore.RED}✗{Style.RESET_ALL} ") super().addError(test, err) def addFailure(self, test, err): self.stream.write(f"{Fore.RED}✗{Style.RESET_ALL} ") super().addFailure(test, err) def addSkip(self, test, reason): self.stream.write(f"{Fore.YELLOW}s{Style.RESET_ALL} ") super().addSkip(test, reason) def printErrorList(self, flavour, errors): for test, err in errors: self.stream.writeln(self.separator1) self.stream.writeln(f"{Fore.RED if flavour == 'ERROR' else Fore.YELLOW}{flavour}: {self.getDescription(test)}{Style.RESET_ALL}") self.stream.writeln(self.separator2) self.stream.writeln(f"{err}") # Custom test runner that uses our colored result class class ColorTextTestRunner(unittest.TextTestRunner): def __init__(self, **kwargs): kwargs.setdefault('resultclass', ColorTextTestResult) super().__init__(**kwargs) class TestOCRApp(unittest.TestCase): def setUp(self): app.config['TESTING'] = True app.config['WTF_CSRF_ENABLED'] = False self.app = app.test_client() self.app_context = app.app_context() self.app_context.push() # Create temp upload folder for testing self.test_upload_folder = tempfile.mkdtemp() self.original_upload_folder = app.config['UPLOAD_FOLDER'] app.config['UPLOAD_FOLDER'] = self.test_upload_folder def tearDown(self): # Clean up after tests TASKS.clear() shutil.rmtree(self.test_upload_folder, ignore_errors=True) app.config['UPLOAD_FOLDER'] = self.original_upload_folder self.app_context.pop() TASKS.clear() def test_allowed_file(self): self.assertTrue(allowed_file('test.pdf')) self.assertFalse(allowed_file('test.docx')) self.assertFalse(allowed_file('')) self.assertFalse(allowed_file(None)) def test_secure_clean_filename(self): self.assertEqual(secure_clean_filename('test.pdf'), 'test.pdf') self.assertEqual(secure_clean_filename('test file.pdf'), 'test_file.pdf') self.assertEqual(secure_clean_filename('../dangerous.pdf'), 'dangerous.pdf') self.assertEqual(secure_clean_filename('file!@#$%^&*.pdf'), 'file.pdf') def test_sanitize_text(self): # Test with control characters self.assertEqual(sanitize_text("Hello\x00World"), "HelloWorld") self.assertEqual(sanitize_text("Line1\x0BLine2"), "Line1Line2") # Test with normal text self.assertEqual(sanitize_text("Normal text"), "Normal text") # Test with empty or None self.assertEqual(sanitize_text(""), "") self.assertEqual(sanitize_text(None), "") def test_fix_common_ocr_errors(self): # Punctuation spacing is normalised. self.assertEqual(fix_common_ocr_errors("Hello , world ."), "Hello, world.") # Words hyphenated across a line break are re-joined. self.assertEqual(fix_common_ocr_errors("exam-\nple"), "example") # Trailing whitespace is stripped, excessive blank lines collapsed. self.assertEqual(fix_common_ocr_errors("Para1 \n\n\n\nPara2"), "Para1\n\nPara2") # Line structure is preserved by default (no reflow). self.assertEqual(fix_common_ocr_errors("Line1\nLine2"), "Line1\nLine2") # Reflow is opt-in. self.assertEqual(fix_common_ocr_errors("Line1\nLine2", reflow=True), "Line1 Line2") # Test with empty self.assertEqual(fix_common_ocr_errors(""), "") self.assertEqual(fix_common_ocr_errors(None), None) def test_fix_common_ocr_errors_never_corrupts_content(self): """Regression guard: the clean-up pass must not rewrite characters. A previous implementation applied blind substitutions ('0'->'O', '1'->'I', '5'->'S', 'rn'->'m', 'cl'->'d'), silently destroying every digit in every converted document. Nothing may reintroduce that. """ invoice = ( "ACME Corporation\n" "Invoice No. 2024-0042\n" "IBAN: IT60X0542811101000000123456\n" "Total due: 1,250.00 EUR" ) self.assertEqual(fix_common_ocr_errors(invoice), invoice) # The specific substitutions that used to corrupt output. self.assertEqual(fix_common_ocr_errors("015"), "015") self.assertEqual(fix_common_ocr_errors("modern class vvv"), "modern class vvv") @patch('PIL.ImageEnhance.Contrast') @patch('PIL.Image.Image.filter') @patch('PIL.Image.Image.convert') def test_enhance_image(self, mock_convert, mock_filter, mock_contrast): # Setup mocks test_image = Image.new('RGB', (100, 100)) mock_filter.return_value = test_image mock_enhancer = MagicMock() mock_enhancer.enhance.return_value = test_image mock_contrast.return_value = mock_enhancer mock_convert.return_value = test_image # Call the function result = enhance_image(test_image) # Verify mocks were called mock_filter.assert_called_once() mock_contrast.assert_called_once() # For non-L mode image, convert should be called mock_convert.assert_called_once_with('L') # Result should be the test image self.assertEqual(result, test_image) @patch('app.logger') def test_enhance_image_error_handling(self, mock_logger): # Setup mock test_image = Image.new('RGB', (100, 100)) mock_logger.warning = MagicMock() # Mock an error when enhancing with patch('PIL.Image.Image.filter', side_effect=Exception("Test error")): # Call the function result = enhance_image(test_image) # Should return original image on error self.assertEqual(result, test_image) # Should log a warning mock_logger.warning.assert_called_once() def test_parse_preprocess_options(self): opts = parse_preprocess_options({ 'pre-grayscale': '1', 'pre-sharpen': '1', 'pre-threshold': '1', 'pre-contrast': '1.8', 'pre-deskew': '1', }) self.assertEqual(opts, { "grayscale": True, "sharpen": True, "threshold": True, "contrast": 1.8, "deskew": True, }) # Unchecked boxes are absent from the form, not sent as '0'. opts = parse_preprocess_options({'pre-contrast': '1.0'}) self.assertFalse(opts["grayscale"]) self.assertFalse(opts["threshold"]) self.assertFalse(opts["deskew"]) # A hand-crafted request cannot ask for an absurd contrast factor. self.assertEqual(parse_preprocess_options({'pre-contrast': '999'})["contrast"], 2.5) self.assertEqual(parse_preprocess_options({'pre-contrast': '-5'})["contrast"], 0.5) self.assertEqual(parse_preprocess_options({'pre-contrast': 'abc'})["contrast"], 1.5) def test_estimate_skew_angle(self): from PIL import ImageDraw # Create synthetic image with horizontal text bars im = Image.new('L', (400, 300), color=255) draw = ImageDraw.Draw(im) for y in range(40, 260, 30): draw.rectangle([50, y, 350, y + 10], fill=0) # Straight image should yield 0 angle angle_straight = estimate_skew_angle(im) self.assertEqual(angle_straight, 0.0) # Skewed by 4 degrees counter-clockwise skewed = im.rotate(4.0, fillcolor=255, expand=False) corr_angle = estimate_skew_angle(skewed) self.assertAlmostEqual(corr_angle, -4.0, delta=1.0) def test_detect_and_correct_orientation(self): test_img = Image.new('RGB', (100, 100), color='white') # Mock pytesseract.image_to_osd returning 90 degrees rotation with patch('pytesseract.image_to_osd', return_value={'rotate': 90, 'orientation_conf': 15.0}): rotated_img, angle = detect_and_correct_orientation(test_img) self.assertEqual(angle, 90) self.assertIsNotNone(rotated_img) # When OSD fails, original image is returned with angle 0 with patch('pytesseract.image_to_osd', side_effect=Exception("OSD error")): orig_img, angle = detect_and_correct_orientation(test_img) self.assertEqual(angle, 0) self.assertEqual(orig_img, test_img) def test_extract_native_page_text(self): mock_proc = MagicMock() mock_proc.returncode = 0 mock_proc.stdout = "This is a digital text document page with enough words to be substantial." with patch('subprocess.run', return_value=mock_proc): text = extract_native_page_text('/dummy/path.pdf', 1) self.assertIsNotNone(text) self.assertIn("substantial", text) # Test when page is scanned (empty text) mock_proc.stdout = " \n " with patch('subprocess.run', return_value=mock_proc): text = extract_native_page_text('/dummy/path.pdf', 1) self.assertIsNone(text) def test_save_output_docx_paragraphs(self): out = _temp_path('.docx') try: results = {0: "First paragraph\n\nSecond paragraph", 1: "Page two paragraph"} save_output(results, "docx", out, "test") self.assertTrue(os.path.exists(out)) from docx import Document as ReadDoc doc = ReadDoc(out) # Both paragraphs on page 0 + page 1 paragraph texts = [p.text for p in doc.paragraphs if p.text.strip()] self.assertIn("First paragraph", texts) self.assertIn("Second paragraph", texts) self.assertIn("Page two paragraph", texts) finally: if os.path.exists(out): os.remove(out) def test_otsu_threshold_separates_two_populations(self): # Half the pixels black, half white: the cutoff must land between them. image = Image.new('L', (10, 10), color=0) for y in range(5): for x in range(10): image.putpixel((x, y), 220) cutoff = otsu_threshold(image) # The cutoff is the top of the dark class: `p > cutoff` must send the # 0-valued pixels to black and the 220-valued ones to white. self.assertGreaterEqual(cutoff, 0) self.assertLess(cutoff, 220) # A uniform image has no split to find; the fallback must not divide by # zero or blank the page. self.assertEqual(otsu_threshold(Image.new('L', (4, 4), color=0)), 128) self.assertEqual(otsu_threshold(Image.new('L', (0, 0), color=0)), 128) def test_enhance_image_threshold_binarises(self): image = Image.new('L', (20, 20), color=0) for x in range(20): for y in range(10): image.putpixel((x, y), 200) result = enhance_image(image, { "grayscale": True, "sharpen": False, "contrast": 1.0, "threshold": True, }) self.assertEqual(set(result.convert('L').tobytes()), {0, 255}) def test_enhance_image_honours_disabled_options(self): image = Image.new('RGB', (10, 10), color='white') result = enhance_image(image, { "grayscale": False, "sharpen": False, "contrast": 1.0, "threshold": False, }) # Nothing was asked for, so the mode is untouched. self.assertEqual(result.mode, 'RGB') @patch('app.logger') def test_preprocessing_reaches_path_based_engines(self, mock_logger): """EasyOCR/PaddleOCR read the file from disk, not the PIL object. Without writing the enhanced page back out they silently received the untouched render, so the preprocessing checkbox did nothing for them. """ img = Image.new('RGB', (40, 40), color='white') img_path = os.path.join(self.test_upload_folder, 'page.png') img.save(img_path) fake_reader = MagicMock() fake_reader.readtext.return_value = ["text"] fake_module = MagicMock() fake_module.Reader = MagicMock(return_value=fake_reader) import sys with patch.dict(sys.modules, {'easyocr': fake_module}): process_image(0, img_path, "easyocr", "eng", preprocess=True) used_path = fake_reader.readtext.call_args[0][0] self.assertNotEqual(used_path, img_path) self.assertTrue(used_path.endswith('.pre.png')) # The temporary preprocessed copy is cleaned up afterwards. self.assertFalse(os.path.exists(used_path)) def test_save_as_markdown(self): test_results = {0: "Test page 1", 1: "Test page 2\n\nParagraph 2"} test_output = _temp_path('.md') try: save_as_markdown(test_results, test_output) # Check file exists self.assertTrue(os.path.exists(test_output)) # Check file content with open(test_output, 'r') as f: content = f.read() self.assertIn("Test page 1", content) self.assertIn("Test page 2", content) self.assertIn("Paragraph 2", content) self.assertIn("---", content) # Page separator finally: # Clean up if os.path.exists(test_output): os.remove(test_output) def test_save_as_html(self): test_results = {0: "Test page 1", 1: "Test page 2\n\nParagraph 2", 2: "Test with & entities"} test_output = _temp_path('.html') test_title = "Test Document" try: save_as_html(test_results, test_output, test_title) # Check file exists self.assertTrue(os.path.exists(test_output)) # Check file content with open(test_output, 'r') as f: content = f.read() self.assertIn("", content) self.assertIn(f"{test_title}", content) self.assertIn("

Test page 1

", content) self.assertIn("

Test page 2

", content) self.assertIn("

Paragraph 2

", content) self.assertIn("
", content) # Check HTML entities are escaped self.assertIn("<html> & entities", content) finally: # Clean up if os.path.exists(test_output): os.remove(test_output) @patch('app.subprocess.check_output') def test_check_dependency_tesseract(self, mock_check_output): # Mock tesseract version output mock_check_output.side_effect = [ "tesseract 4.1.1\n Released version", "List of languages:\neng\nfra\ndeu" ] installed, data = check_dependency('tesseract') self.assertTrue(installed) self.assertEqual(data["installed"], True) self.assertIn("tesseract 4.1.1", data["version"]) self.assertIn("eng", data["languages"]) self.assertIn("fra", data["languages"]) @patch('app.subprocess.check_output') def test_check_dependency_poppler(self, mock_check_output): # Mock poppler version output mock_check_output.return_value = "pdftoppm version 22.02.0" installed, data = check_dependency('poppler') self.assertTrue(installed) self.assertEqual(data["installed"], True) self.assertIn("pdftoppm version", data["version"]) @patch('app.subprocess.check_output') def test_check_dependency_not_installed(self, mock_check_output): # Mock subprocess raising FileNotFoundError mock_check_output.side_effect = FileNotFoundError("No such file") installed, data = check_dependency('tesseract') self.assertFalse(installed) self.assertEqual(data["installed"], False) self.assertIn("not installed", data["message"]) def test_check_dependency_unknown(self): installed, data = check_dependency('unknown') self.assertFalse(installed) self.assertEqual(data["installed"], False) self.assertIn("Unknown dependency", data["message"]) def test_check_dependency_easyocr(self): with patch.dict('sys.modules', {'easyocr': MagicMock(__version__='1.7.1')}): installed, data = check_dependency('easyocr') self.assertTrue(installed) self.assertEqual(data["version"], "1.7.1") self.assertIn("EasyOCR is installed", data["message"]) def test_check_dependency_pyocr(self): mock_tool = MagicMock() mock_tool.get_name.return_value = "Tesseract" mock_pyocr = MagicMock() mock_pyocr.get_available_tools.return_value = [mock_tool] with patch.dict('sys.modules', {'pyocr': mock_pyocr}): installed, data = check_dependency('pyocr') self.assertTrue(installed) self.assertIn("Tesseract", data["tools"]) self.assertIn("PyOCR is installed", data["message"]) def test_fix_common_ocr_errors_reflow(self): text = "This is a sentence\nthat wraps across lines.\n\nThis is a new paragraph." reflowed = fix_common_ocr_errors(text, reflow=True) self.assertEqual(reflowed, "This is a sentence that wraps across lines.\n\nThis is a new paragraph.") def test_index_route(self): # Mock check_dependencies to return success with patch('app.check_dependencies', return_value=(True, "All good")): response = self.app.get('/') self.assertEqual(response.status_code, 200) def test_index_route_with_dependency_error(self): # Mock check_dependencies to return failure with patch('app.check_dependencies', return_value=(False, "Missing tesseract")): with self.app.session_transaction(): pass # Setup session if needed response = self.app.get('/', follow_redirects=True) self.assertEqual(response.status_code, 200) # Flash message content would be in response data self.assertIn(b"Missing tesseract", response.data) @patch('app.logger') def test_process_image_tesseract(self, mock_logger): # Create a test image img = Image.new('RGB', (100, 100), color='white') img_path = os.path.join(self.test_upload_folder, 'test_image.png') img.save(img_path) # Mock pytesseract to return known text with patch('pytesseract.image_to_string', return_value="Test OCR result"): with patch('pytesseract.get_tesseract_version', return_value="4.1.1"): idx, text = process_image(0, img_path, "tesseract", "eng") self.assertEqual(idx, 0) self.assertEqual(text, "Test OCR result") mock_logger.info.assert_called() @patch('app.logger') def test_process_image_unsupported_engine(self, mock_logger): # Create a test image img = Image.new('RGB', (100, 100), color='white') img_path = os.path.join(self.test_upload_folder, 'test_image.png') img.save(img_path) idx, text = process_image(0, img_path, "unsupported", "eng") self.assertEqual(idx, 0) self.assertIn("Error: Unsupported OCR engine", text) @patch('app.logger') def test_process_image_file_not_found(self, mock_logger): # Non-existent image path img_path = os.path.join(self.test_upload_folder, 'nonexistent.png') idx, text = process_image(0, img_path, "tesseract", "eng") self.assertEqual(idx, 0) self.assertIn("Error: File not found", text) mock_logger.error.assert_called() # --- helpers ------------------------------------------------------- TASK_ID = "11111111-2222-3333-4444-555555555555" def _own(self, *task_ids): """Mark the given tasks as started by this test client's session.""" with self.app.session_transaction() as sess: sess['owned_tasks'] = list(task_ids) def _make_task(self, task_id=None, **fields): """Create a task record owned by this session and return its id.""" task_id = task_id or self.TASK_ID TASKS.set(task_id, fields) self._own(task_id) return task_id # --- task status --------------------------------------------------- def test_api_task_status_not_found(self): response = self.app.get(f'/api/task_status/{self.TASK_ID}') self.assertEqual(response.status_code, 404) data = json.loads(response.data) self.assertEqual(data["status"], "not_found") def test_api_task_status_processing(self): task_id = self._make_task(status="processing", step="converting", progress=50) response = self.app.get(f'/api/task_status/{task_id}') self.assertEqual(response.status_code, 200) data = json.loads(response.data) self.assertEqual(data["status"], "processing") self.assertEqual(data["progress"], 50) def test_api_task_status_completed(self): task_id = self._make_task( status="completed", progress=100, result_path="/path/to/result.docx", output_filename="result.docx", ) response = self.app.get(f'/api/task_status/{task_id}') self.assertEqual(response.status_code, 200) data = json.loads(response.data) self.assertEqual(data["status"], "completed") self.assertEqual(data["progress"], 100) self.assertIn("redirect", data) # Should have redirect URL # The on-disk location of the result must never reach the browser. self.assertNotIn("result_path", data) def test_task_status_of_another_session_is_not_readable(self): """A task id alone must not grant access to someone else's conversion.""" TASKS.set(self.TASK_ID, {"status": "completed", "output_filename": "secret.docx"}) # No session ownership recorded for this client. self.assertEqual(self.app.get(f'/api/task_status/{self.TASK_ID}').status_code, 404) self.assertEqual( self.app.get(f'/download/{self.TASK_ID}', follow_redirects=False).status_code, 302 ) def test_task_store_rejects_traversal_ids(self): """A crafted task id must not escape the task directory.""" self.assertIsNone(TASKS.get('../../etc/passwd')) TASKS.set('../../evil', {"status": "completed"}) self.assertFalse(os.path.exists(os.path.join(self.test_upload_folder, '..', '..', 'evil.json'))) def test_task_store_roundtrip_and_update(self): TASKS.set(self.TASK_ID, {"status": "processing", "progress": 10}) TASKS.update(self.TASK_ID, progress=55) record = TASKS.get(self.TASK_ID) self.assertEqual(record["status"], "processing") self.assertEqual(record["progress"], 55) self.assertIn("timestamp", record) TASKS.delete(self.TASK_ID) self.assertIsNone(TASKS.get(self.TASK_ID)) def test_is_within_upload_folder(self): inside = os.path.join(self.test_upload_folder, 'result.docx') self.assertTrue(is_within_upload_folder(inside)) self.assertFalse(is_within_upload_folder('/etc/passwd')) # A sibling directory sharing the prefix must not pass (the old check # used str.startswith and would have accepted this). self.assertFalse(is_within_upload_folder(self.test_upload_folder + '_evil/x')) def test_looks_like_pdf(self): import io self.assertTrue(looks_like_pdf(io.BytesIO(b'%PDF-1.7\nrest'))) self.assertFalse(looks_like_pdf(io.BytesIO(b''))) self.assertFalse(looks_like_pdf(io.BytesIO(b''))) # The stream must be rewound so the file can still be saved. stream = io.BytesIO(b'%PDF-1.7\nrest') looks_like_pdf(stream) self.assertEqual(stream.tell(), 0) def test_healthz(self): response = self.app.get('/healthz') self.assertEqual(response.status_code, 200) self.assertEqual(json.loads(response.data)["status"], "ok") def test_security_headers_are_set(self): with patch('app.check_dependencies', return_value=(True, "All good")): response = self.app.get('/') self.assertEqual(response.headers['X-Content-Type-Options'], 'nosniff') self.assertEqual(response.headers['X-Frame-Options'], 'DENY') self.assertIn("default-src 'self'", response.headers['Content-Security-Policy']) def test_upload_rejects_non_pdf_content(self): """A file named .pdf but not containing a PDF must not reach Poppler.""" import io with patch('app.check_dependencies', return_value=(True, "All good")): response = self.app.post( '/upload', data={'file': (io.BytesIO(b''), 'payload.pdf')}, content_type='multipart/form-data', follow_redirects=True, ) self.assertEqual(response.status_code, 200) self.assertIn(b'not a PDF', response.data) def test_upload_rejects_unknown_output_format(self): """`output-format` used to be interpolated straight into the file path.""" import io with patch('app.check_dependencies', return_value=(True, "All good")): response = self.app.post( '/upload', data={ 'file': (io.BytesIO(b'%PDF-1.7\n'), 'doc.pdf'), 'output-format': '../../../etc/cron.d/x', }, content_type='multipart/form-data', follow_redirects=True, ) self.assertIn(b'Unknown output format', response.data) def test_run_task_in_background_records_success(self): """Cover the thread wrapper itself, not just the function it calls. Nothing exercised this path, so an AttributeError inside it surfaced only as a generic "error starting the OCR process" flash at runtime. """ done = threading.Event() def fake_conversion(): return True, os.path.join(self.test_upload_folder, "r.txt"), "r.txt" TASKS.set(self.TASK_ID, {"status": "processing", "progress": 0}) run_task_in_background(lambda: (done.set(), fake_conversion())[1], self.TASK_ID) self.assertTrue(done.wait(timeout=5)) for _ in range(50): record = TASKS.get(self.TASK_ID) if record and record.get("status") == "completed": break time.sleep(0.05) record = TASKS.get(self.TASK_ID) self.assertEqual(record["status"], "completed") self.assertEqual(record["progress"], 100) self.assertEqual(record["output_filename"], "r.txt") def test_run_task_in_background_records_failure(self): def boom(): raise RuntimeError("conversion exploded") TASKS.set(self.TASK_ID, {"status": "processing", "progress": 0}) with patch('app.logger'): run_task_in_background(boom, self.TASK_ID) for _ in range(50): record = TASKS.get(self.TASK_ID) if record and record.get("status") == "failed": break time.sleep(0.05) record = TASKS.get(self.TASK_ID) self.assertEqual(record["status"], "failed") self.assertIn("conversion exploded", record["error"]) def test_stale_processing_task_is_marked_failed(self): """A conversion whose worker died must not stay pending forever. The record refreshes after every page, so a "processing" task with an old timestamp is abandoned. Nothing used to move it out of that state and the progress page polled it indefinitely. """ task_file = os.path.join(self.test_upload_folder, '.tasks', f'{self.TASK_ID}.json') os.makedirs(os.path.dirname(task_file), exist_ok=True) with open(task_file, 'w') as f: json.dump({ "status": "processing", "progress": 40, "timestamp": time.time() - (STALE_TASK_TIMEOUT + 60), }, f) self._own(self.TASK_ID) with patch('app.logger'): response = self.app.get(f'/api/task_status/{self.TASK_ID}') data = json.loads(response.data) self.assertEqual(data["status"], "failed") self.assertIn("stopped unexpectedly", data["error"]) def test_recent_processing_task_is_left_alone(self): task_id = self._make_task(status="processing", progress=40) response = self.app.get(f'/api/task_status/{task_id}') self.assertEqual(json.loads(response.data)["status"], "processing") def test_health_and_diagnostic_endpoints_do_not_set_a_cookie(self): """An uptime monitor polling /healthz was handed a session cookie.""" for path in ('/healthz', '/system-check'): with patch('app.check_dependency', return_value=(True, {"installed": True})): response = self.app.get(path) self.assertIsNone(response.headers.get('Set-Cookie'), msg=path) def test_save_as_html_escapes_the_title(self): out = _temp_path('.html') try: save_as_html({0: "body"}, out, title='x">') with open(out, encoding='utf-8') as fh: content = fh.read() self.assertNotIn("