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')