- Validate paste IDs against ^[a-f0-9]{16}$ before filesystem access
(blocks read/delete traversal via crafted URLs)
- Remove duplicate ALLOWED_IMAGE_EXTENSIONS that re-allowed SVG
(XSS via stored SVG); keep magic-byte validation
- Remove duplicate cleanup thread (two background loops were started)
- Unknown expiry keys now default to 1 day instead of never
- request.secure -> request.is_secure (werkzeug 3.x)
- Add gunicorn to requirements (Dockerfile CMD referenced it)
- 11 unit tests, ruff clean, MIT LICENSE, full README (was empty)
120 lines
3.6 KiB
Python
120 lines
3.6 KiB
Python
"""Unit tests for paste-bin."""
|
|
|
|
import io
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
|
import app as app_module # noqa: E402
|
|
|
|
|
|
@pytest.fixture()
|
|
def client(tmp_path, monkeypatch):
|
|
app_module.app.config['TESTING'] = True
|
|
app_module.app.config['UPLOAD_FOLDER'] = str(tmp_path / 'uploads')
|
|
app_module.app.config['STORE_FOLDER'] = str(tmp_path / 'store')
|
|
app_module.ensure_dirs()
|
|
with app_module.app.test_client() as c:
|
|
yield c
|
|
|
|
|
|
def test_index_returns_200(client):
|
|
resp = client.get('/')
|
|
assert resp.status_code == 200
|
|
|
|
|
|
def test_text_paste_roundtrip(client):
|
|
resp = client.post('/paste', data={'paste_type': 'text', 'content': 'hello world', 'expiry': '1d'})
|
|
assert resp.status_code == 302
|
|
paste_id = resp.headers['Location'].rsplit('/', 1)[-1]
|
|
assert len(paste_id) == 16
|
|
|
|
view = client.get(f'/{paste_id}')
|
|
assert view.status_code == 200
|
|
assert b'hello world' in view.data
|
|
|
|
raw = client.get(f'/{paste_id}/raw')
|
|
assert raw.status_code == 200
|
|
assert raw.data == b'hello world'
|
|
|
|
dl = client.get(f'/{paste_id}/download')
|
|
assert dl.status_code == 200
|
|
assert dl.data == b'hello world'
|
|
|
|
|
|
def test_empty_content_rejected(client):
|
|
resp = client.post('/paste', data={'paste_type': 'text', 'content': ' ', 'expiry': '1d'})
|
|
assert resp.status_code == 400
|
|
|
|
|
|
def test_invalid_paste_type_rejected(client):
|
|
resp = client.post('/paste', data={'paste_type': 'bogus', 'content': 'x'})
|
|
assert resp.status_code == 400
|
|
|
|
|
|
def test_unknown_paste_404(client):
|
|
assert client.get('/' + 'a' * 16).status_code == 404
|
|
|
|
|
|
def test_path_traversal_paste_id_blocked(client, tmp_path):
|
|
outside = tmp_path / 'secret.txt'
|
|
outside.write_text('top secret')
|
|
for evil in ['..%2F..%2Fsecret.txt', '....//secret.txt', 'a' * 14 + '/../../secret.txt']:
|
|
resp = client.get('/' + evil)
|
|
assert resp.status_code == 404
|
|
assert outside.exists()
|
|
|
|
|
|
def test_svg_upload_rejected(client):
|
|
png_bytes = b'<svg xmlns="http://www.w3.org/2000/svg"><script>alert(1)</script></svg>'
|
|
resp = client.post(
|
|
'/paste',
|
|
data={'paste_type': 'image', 'file': (io.BytesIO(png_bytes), 'x.svg', 'image/svg+xml')},
|
|
content_type='multipart/form-data',
|
|
)
|
|
assert resp.status_code == 400
|
|
|
|
|
|
def test_png_magic_validation(client):
|
|
fake = b'not-a-real-png'
|
|
resp = client.post(
|
|
'/paste',
|
|
data={'paste_type': 'image', 'file': (io.BytesIO(fake), 'x.png', 'image/png')},
|
|
content_type='multipart/form-data',
|
|
)
|
|
assert resp.status_code == 400
|
|
|
|
|
|
def test_valid_png_upload(client):
|
|
png = b'\x89PNG\r\n\x1a\n' + b'\x00' * 32
|
|
resp = client.post(
|
|
'/paste',
|
|
data={'paste_type': 'image', 'file': (io.BytesIO(png), 'pic.png', 'image/png')},
|
|
content_type='multipart/form-data',
|
|
)
|
|
assert resp.status_code == 302
|
|
paste_id = resp.headers['Location'].rsplit('/', 1)[-1]
|
|
assert client.get(f'/{paste_id}').status_code == 200
|
|
|
|
|
|
def test_expiry_parsing():
|
|
assert app_module.parse_expiry('forever') is None
|
|
for key in ['1h', '1d', '1w', '1m', 'bogus-key']:
|
|
result = app_module.parse_expiry(key)
|
|
assert result is not None
|
|
from datetime import datetime
|
|
|
|
datetime.fromisoformat(result)
|
|
|
|
|
|
def test_store_and_load_paste(tmp_path, monkeypatch):
|
|
app_module.app.config['STORE_FOLDER'] = str(tmp_path / 'store')
|
|
app_module.ensure_dirs()
|
|
app_module.store_paste('ab' * 8, {'type': 'text', 'title': 't'})
|
|
paste = app_module.load_paste('ab' * 8)
|
|
assert paste['title'] == 't'
|
|
assert app_module.load_paste('../etc/passwd') is None
|