car-tracker/tests/test_diagnostics.py
Antoine Vandeplanque 5d68f0f776
All checks were successful
CI / Lint, PostgreSQL tests, and package (push) Successful in 15s
CI / Build and publish tested image (push) Successful in 1m19s
enable Playwright Stealth with French locale for collection
2026-10-13 14:16:13 +02:00

92 lines
4.9 KiB
Python

import json
from contextlib import nullcontext
from pathlib import Path
import tempfile
from types import SimpleNamespace
import unittest
from unittest.mock import Mock, patch
from car_tracker.diagnostics import capture_403
from car_tracker.collect import collect_browser
class DiagnosticTests(unittest.TestCase):
def setUp(self):
self.root = Path(self.enterContext(tempfile.TemporaryDirectory())) / "diagnostics/http-403"
request = SimpleNamespace(url="https://www.leboncoin.fr/ad/voitures/123", method="GET",
redirected_from=None, all_headers=lambda: {"cookie": "private-cookie"})
self.response = SimpleNamespace(status=403, url=request.url, request=request,
all_headers=lambda: {"set-cookie": "private-response-cookie"},
body=lambda: b"<html>Forbidden synthetic response</html>")
self.page = SimpleNamespace(url=request.url, content=lambda: "<html>synthetic DOM</html>",
context=SimpleNamespace(cookies=Mock(return_value=[{"name": "datadome", "value": "secret"}])),
screenshot=Mock(return_value=b"synthetic-png"))
self.browser = SimpleNamespace(version="synthetic-browser")
self.enterContext(patch("car_tracker.diagnostics.version", return_value="synthetic-playwright"))
def capture(self):
return capture_403(self.root, self.response, self.page, self.browser, {"delay_seconds": 5})
def test_capture_is_private_and_does_not_navigate(self):
report = self.capture()
self.assertFalse(report["errors"])
self.assertEqual(self.root.stat().st_mode & 0o777, 0o700)
self.assertEqual(self.root.parent.stat().st_mode & 0o777, 0o700)
for path in self.root.iterdir():
self.assertEqual(path.stat().st_mode & 0o777, 0o600)
self.assertEqual(json.loads((self.root / "request-headers.json").read_text())["cookie"], "private-cookie")
self.assertEqual((self.root / "response-body.html").read_bytes(), self.response.body())
self.page.context.cookies.assert_called_once_with(["https://www.leboncoin.fr"])
self.page.screenshot.assert_called_once_with(timeout=3000, animations="allow")
self.assertNotIn("secret", json.dumps(report))
def test_one_capture_failure_does_not_prevent_other_artifacts(self):
self.response.body = Mock(side_effect=RuntimeError("private body with token"))
report = self.capture()
self.assertEqual(report["errors"], {"response-body.html": "RuntimeError"})
self.assertTrue((self.root / "viewport.png").exists())
self.assertNotIn("token", (self.root / "capture.json").read_text())
def test_documents_are_disk_bounded_and_truncation_is_recorded(self):
with patch("car_tracker.diagnostics.MAX_DOCUMENT_BYTES", 10):
report = self.capture()
self.assertEqual((self.root / "response-body.html").stat().st_size, 10)
self.assertIn("response-body.html", report["truncated"])
self.assertIn("rendered-page.html", report["truncated"])
def test_existing_capture_is_not_overwritten(self):
self.capture()
with self.assertRaises(FileExistsError):
self.capture()
def test_browser_403_captures_before_close_and_never_retries(self):
self.page.goto = Mock(return_value=self.response)
context = self.page.context
context.new_page = Mock(return_value=self.page)
self.browser.new_context = Mock(return_value=context)
self.browser.close = Mock()
def body_before_close():
self.browser.close.assert_not_called()
return b"synthetic forbidden response"
self.response.body = body_before_close
playwright = SimpleNamespace(chromium=SimpleNamespace(launch=Mock(return_value=self.browser)))
args = SimpleNamespace(headed=True, delay=5, max_pages=1, max_details=None,
search_only=False, output=self.root.parent.parent,
searches=[{"family": "synthetic", "text": "synthetic"}])
module = SimpleNamespace(sync_playwright=lambda: nullcontext(playwright))
with patch("playwright_stealth.Stealth") as stealth, patch.dict(
"sys.modules", {"playwright.sync_api": module},
):
stealth.return_value.use_sync.side_effect = lambda manager: manager
path, status = collect_browser(args)
manifest = json.loads(path.read_text())
self.assertEqual(status, "failed")
self.assertEqual(manifest["diagnostics"]["capture_status"], "complete")
self.assertTrue((path.parent / "diagnostics/http-403/response-body.html").exists())
metadata = json.loads((path.parent / "diagnostics/http-403/metadata.json").read_text())
self.assertTrue(metadata["settings"]["stealth"])
self.page.goto.assert_called_once()
self.browser.close.assert_called_once()