87 lines
4.6 KiB
Python
87 lines
4.6 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.dict("sys.modules", {"playwright.sync_api": module}):
|
|
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())
|
|
self.page.goto.assert_called_once()
|
|
self.browser.close.assert_called_once()
|