flaskpaste: initial commit with security hardening
Features: - REST API for text/binary pastes with MIME detection - Client certificate auth via X-SSL-Client-SHA1 header - SQLite with WAL mode for concurrent access - Automatic paste expiry with LRU cleanup Security: - HSTS, CSP, X-Frame-Options, X-Content-Type-Options - Cache-Control: no-store for sensitive responses - X-Request-ID tracing for log correlation - X-Proxy-Secret validation for defense-in-depth - Parameterized queries, input validation - Size limits (3 MiB anon, 50 MiB auth) Includes /health endpoint, container support, and 70 tests.
This commit is contained in:
90
tests/test_database.py
Normal file
90
tests/test_database.py
Normal file
@@ -0,0 +1,90 @@
|
||||
"""Tests for database operations."""
|
||||
|
||||
import json
|
||||
import time
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
class TestDatabaseOperations:
|
||||
"""Tests for database functionality."""
|
||||
|
||||
def test_paste_persists(self, client, sample_text):
|
||||
"""Paste persists in database and can be retrieved."""
|
||||
create = client.post("/", data=sample_text, content_type="text/plain")
|
||||
paste_id = json.loads(create.data)["id"]
|
||||
|
||||
response = client.get(f"/{paste_id}/raw")
|
||||
assert response.data.decode("utf-8") == sample_text
|
||||
|
||||
def test_multiple_pastes_independent(self, client):
|
||||
"""Multiple pastes have unique IDs and content."""
|
||||
create1 = client.post("/", data="paste one", content_type="text/plain")
|
||||
create2 = client.post("/", data="paste two", content_type="text/plain")
|
||||
|
||||
id1 = json.loads(create1.data)["id"]
|
||||
id2 = json.loads(create2.data)["id"]
|
||||
|
||||
assert id1 != id2
|
||||
|
||||
raw1 = client.get(f"/{id1}/raw")
|
||||
raw2 = client.get(f"/{id2}/raw")
|
||||
|
||||
assert raw1.data.decode("utf-8") == "paste one"
|
||||
assert raw2.data.decode("utf-8") == "paste two"
|
||||
|
||||
def test_last_accessed_updated_on_get(self, client, sample_text):
|
||||
"""Last accessed timestamp updates on retrieval."""
|
||||
create = client.post("/", data=sample_text, content_type="text/plain")
|
||||
paste_id = json.loads(create.data)["id"]
|
||||
|
||||
# Get the paste twice with a small delay
|
||||
client.get(f"/{paste_id}")
|
||||
first_access = time.time()
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
client.get(f"/{paste_id}")
|
||||
second_access = time.time()
|
||||
|
||||
# Timestamps should be close to current time (within 2 seconds)
|
||||
assert second_access > first_access
|
||||
|
||||
|
||||
class TestCleanupExpiredPastes:
|
||||
"""Tests for paste expiry and cleanup."""
|
||||
|
||||
def test_expired_paste_cleaned_up(self, app, client, sample_text):
|
||||
"""Expired pastes are removed by cleanup."""
|
||||
from app.database import cleanup_expired_pastes
|
||||
|
||||
# Create a paste
|
||||
create = client.post("/", data=sample_text, content_type="text/plain")
|
||||
paste_id = json.loads(create.data)["id"]
|
||||
|
||||
# Verify it exists
|
||||
assert client.get(f"/{paste_id}").status_code == 200
|
||||
|
||||
# Mock time to simulate expiry (paste expiry + 1 second)
|
||||
future_time = time.time() + app.config["PASTE_EXPIRY_SECONDS"] + 1
|
||||
|
||||
with patch("time.time", return_value=future_time):
|
||||
with app.app_context():
|
||||
deleted = cleanup_expired_pastes()
|
||||
|
||||
assert deleted >= 1
|
||||
|
||||
# Paste should now be gone
|
||||
assert client.get(f"/{paste_id}").status_code == 404
|
||||
|
||||
def test_non_expired_paste_kept(self, app, client, sample_text):
|
||||
"""Non-expired pastes are preserved by cleanup."""
|
||||
from app.database import cleanup_expired_pastes
|
||||
|
||||
create = client.post("/", data=sample_text, content_type="text/plain")
|
||||
paste_id = json.loads(create.data)["id"]
|
||||
|
||||
with app.app_context():
|
||||
deleted = cleanup_expired_pastes()
|
||||
|
||||
assert deleted == 0
|
||||
assert client.get(f"/{paste_id}").status_code == 200
|
||||
Reference in New Issue
Block a user