import asyncio import importlib.util import sys import types from io import BytesIO from pathlib import Path from unittest.mock import AsyncMock import pytest from quart import Blueprint, Quart ROOT = Path(__file__).resolve().parents[1] BROWSER_ACCEPT = 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' PASTE = ( 'Ref Barend: https://i.example.dev/-Q6jmHoRYjFi\n' 'Foreign: https://other.example.dev/-abc\n' 'Script test: \n' ) FILES = { 'paste': { 'file_id': 'paste', 'file_name': 'pasted-2026-10-25T101010Z.txt', 'content_type': 'text/plain', 'db_image_url': 'storage://paste', 'uploaded_at': 1700000000000, 'user_id': 'owner', }, 'Q6jmHoRYjFi': { 'file_id': 'Q6jmHoRYjFi', 'file_name': 'barend.png', 'content_type': 'image/png', 'db_image_url': 'storage://barend', 'uploaded_at': 1700000000000, 'user_id': 'owner', }, } def load_share_module(monkeypatch): stubs = { 'my_modules.decoratory.header': {'login_required': lambda fn: fn}, 'my_modules.functions': {'get_ip': lambda: '192.0.2.1'}, 'my_modules.app.setup': { 'LIMITER': types.SimpleNamespace(limit=lambda *args, **kwargs: lambda fn: fn), }, 'my_modules.app.logger': {'logger': None}, 'my_modules.expiry': {'parse_expires': lambda value: value}, 'quart_common.web.wide_event': {'add_wide_event_context': lambda **kwargs: None}, } for name, values in stubs.items(): module = types.ModuleType(name) module.__dict__.update(values) monkeypatch.setitem(sys.modules, name, module) path = ROOT / 'routes/side/main.py' spec = importlib.util.spec_from_file_location('text_view_route_under_test', path) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module def build_app(monkeypatch, paste_body: bytes = PASTE.encode()): module = load_share_module(monkeypatch) app = Quart( 'text_view_test', template_folder=str(ROOT / 'templates/side'), static_folder=str(ROOT / 'templates/static'), ) app.secret_key = 'test-secret' async def get_file(file_id): return FILES.get(file_id) async def get_from_storage(storage_id): return BytesIO(paste_body if storage_id == 'storage://paste' else b'binary') app.convex = types.SimpleNamespace( get_file=AsyncMock(side_effect=get_file), get_from_storage=AsyncMock(side_effect=get_from_storage), add_file_access=AsyncMock(return_value=None), ) app.register_blueprint(module.side_main_bp) # Minimal endpoints so base.htm/navigation.htm can resolve their url_for calls. stub_bp = Blueprint('basic', __name__) stub_bp.add_url_rule('/favicon.ico', 'favicon', lambda: '') app.register_blueprint(stub_bp) auth_bp = Blueprint('auth_login', __name__) auth_bp.add_url_rule('/login', 'login', lambda: '') app.register_blueprint(auth_bp) composer_bp = Blueprint('link_composer', __name__) composer_bp.add_url_rule('/links', 'link_composer_page', lambda: '') app.register_blueprint(composer_bp) return app def get(app, path, headers=None): async def run(): return await app.test_client().get(path, headers=headers or {}) return asyncio.run(run()) async def body_of(response): return (await response.get_data()).decode() def text_of(response): return asyncio.run(body_of(response)) # --- browser view ------------------------------------------------------------- def test_browser_gets_html_with_clickable_links(monkeypatch): app = build_app(monkeypatch) response = get(app, '/-paste', {'Accept': BROWSER_ACCEPT, 'Host': 'i.example.dev'}) assert response.status_code == 200 assert response.content_type.startswith('text/html') html = text_of(response) assert 'alert(1)' not in html assert '<script>' in html def test_text_view_renders_large_pastes_completely(monkeypatch): marker = 'END-OF-PASTE-MARKER' body = (('filler line\n' * 60_000) + marker).encode() app = build_app(monkeypatch, paste_body=body) html = text_of(get(app, '/-paste', {'Accept': BROWSER_ACCEPT, 'Host': 'i.example.dev'})) assert marker in html assert html.count('filler line') == 60_000 assert 'truncated' not in html.lower() def test_text_view_is_not_cached(monkeypatch): app = build_app(monkeypatch) response = get(app, '/-paste', {'Accept': BROWSER_ACCEPT, 'Host': 'i.example.dev'}) assert response.headers['Cache-Control'] == 'private, no-store' # --- raw fallbacks ------------------------------------------------------------ @pytest.mark.parametrize('path,headers', [ ('/-paste', {'Accept': '*/*'}), ('/-paste', {'User-Agent': 'curl/8.0'}), ('/-paste?raw=1', {'Accept': BROWSER_ACCEPT}), ('/-paste?download=1', {'Accept': BROWSER_ACCEPT}), ]) def test_non_browser_requests_still_get_raw_text(monkeypatch, path, headers): app = build_app(monkeypatch) headers.setdefault('Host', 'i.example.dev') response = get(app, path, headers) assert response.status_code == 200 assert response.content_type.startswith('text/plain') assert text_of(response) == PASTE def test_binary_files_are_untouched(monkeypatch): app = build_app(monkeypatch) response = get(app, '/-Q6jmHoRYjFi', {'Accept': BROWSER_ACCEPT, 'Host': 'i.example.dev'}) assert response.status_code == 200 assert response.content_type.startswith('image/png')