forked from claw/flaskpaste
add CLI enhancements and scheduled cleanup
CLI commands: - list: show user's pastes with pagination - search: filter by type (glob), after/before timestamps - update: modify content, password, or extend expiry - export: save pastes to directory with optional decryption API changes: - PUT /<id>: update paste content and metadata - GET /pastes: add type, after, before query params Scheduled tasks: - Thread-safe cleanup with per-task intervals - Activate cleanup_expired_hashes (15min) - Activate cleanup_rate_limits (5min) Tests: 205 passing
This commit is contained in:
169
tests/test_scheduled_cleanup.py
Normal file
169
tests/test_scheduled_cleanup.py
Normal file
@@ -0,0 +1,169 @@
|
||||
"""Tests for scheduled cleanup functionality."""
|
||||
|
||||
import time
|
||||
|
||||
|
||||
class TestScheduledCleanup:
|
||||
"""Tests for scheduled cleanup in before_request hook."""
|
||||
|
||||
def test_cleanup_times_reset(self, app, client):
|
||||
"""Verify cleanup times can be reset for testing."""
|
||||
from app.api import _cleanup_times, reset_cleanup_times
|
||||
|
||||
# Set some cleanup times
|
||||
with app.app_context():
|
||||
reset_cleanup_times()
|
||||
for key in _cleanup_times:
|
||||
assert _cleanup_times[key] == 0
|
||||
|
||||
def test_cleanup_runs_on_request(self, app, client, auth_header):
|
||||
"""Verify cleanup runs during request handling."""
|
||||
from app.api import _cleanup_times, reset_cleanup_times
|
||||
|
||||
with app.app_context():
|
||||
reset_cleanup_times()
|
||||
|
||||
# Make a request to trigger cleanup
|
||||
client.get("/health")
|
||||
|
||||
# Cleanup times should be set
|
||||
with app.app_context():
|
||||
for key in _cleanup_times:
|
||||
assert _cleanup_times[key] > 0
|
||||
|
||||
def test_cleanup_respects_intervals(self, app, client):
|
||||
"""Verify cleanup respects configured intervals."""
|
||||
from app.api import _cleanup_times, reset_cleanup_times
|
||||
|
||||
with app.app_context():
|
||||
reset_cleanup_times()
|
||||
|
||||
# First request triggers cleanup
|
||||
client.get("/health")
|
||||
|
||||
with app.app_context():
|
||||
first_times = {k: v for k, v in _cleanup_times.items()}
|
||||
|
||||
# Immediate second request should not reset times
|
||||
client.get("/health")
|
||||
|
||||
with app.app_context():
|
||||
for key in _cleanup_times:
|
||||
assert _cleanup_times[key] == first_times[key]
|
||||
|
||||
def test_expired_paste_cleanup(self, app, client, auth_header):
|
||||
"""Test that expired pastes are cleaned up."""
|
||||
import json
|
||||
|
||||
from app.database import get_db
|
||||
|
||||
# Create paste with short expiry
|
||||
response = client.post(
|
||||
"/",
|
||||
data="test content",
|
||||
content_type="text/plain",
|
||||
headers={**auth_header, "X-Expiry": "1"}, # 1 second
|
||||
)
|
||||
assert response.status_code == 201
|
||||
paste_id = json.loads(response.data)["id"]
|
||||
|
||||
# Wait for expiry
|
||||
time.sleep(1.5)
|
||||
|
||||
# Trigger cleanup via database function
|
||||
with app.app_context():
|
||||
from app.database import cleanup_expired_pastes
|
||||
|
||||
count = cleanup_expired_pastes()
|
||||
assert count >= 1
|
||||
|
||||
# Verify paste is gone
|
||||
db = get_db()
|
||||
row = db.execute("SELECT id FROM pastes WHERE id = ?", (paste_id,)).fetchone()
|
||||
assert row is None
|
||||
|
||||
def test_rate_limit_cleanup(self, app, client):
|
||||
"""Test that rate limit entries are cleaned up."""
|
||||
from app.api.routes import (
|
||||
_rate_limit_requests,
|
||||
check_rate_limit,
|
||||
cleanup_rate_limits,
|
||||
reset_rate_limits,
|
||||
)
|
||||
|
||||
with app.app_context():
|
||||
reset_rate_limits()
|
||||
|
||||
# Add some rate limit entries
|
||||
check_rate_limit("192.168.1.1", authenticated=False)
|
||||
check_rate_limit("192.168.1.2", authenticated=False)
|
||||
|
||||
assert len(_rate_limit_requests) == 2
|
||||
|
||||
# Cleanup with very large window (nothing should be removed)
|
||||
count = cleanup_rate_limits(window=3600)
|
||||
assert count == 0
|
||||
|
||||
# Wait a bit and cleanup with tiny window
|
||||
time.sleep(0.1)
|
||||
count = cleanup_rate_limits(window=0) # Immediate cleanup
|
||||
assert count == 2
|
||||
assert len(_rate_limit_requests) == 0
|
||||
|
||||
|
||||
class TestCleanupThreadSafety:
|
||||
"""Tests for thread-safety of cleanup operations."""
|
||||
|
||||
def test_cleanup_lock_exists(self, app):
|
||||
"""Verify cleanup lock exists."""
|
||||
import threading
|
||||
|
||||
from app.api import _cleanup_lock
|
||||
|
||||
assert isinstance(_cleanup_lock, type(threading.Lock()))
|
||||
|
||||
def test_concurrent_cleanup_access(self, app, client):
|
||||
"""Test that concurrent requests don't corrupt cleanup state."""
|
||||
import threading
|
||||
|
||||
from app.api import reset_cleanup_times
|
||||
|
||||
with app.app_context():
|
||||
reset_cleanup_times()
|
||||
|
||||
errors = []
|
||||
results = []
|
||||
|
||||
def make_request():
|
||||
try:
|
||||
resp = client.get("/health")
|
||||
results.append(resp.status_code)
|
||||
except Exception as e:
|
||||
errors.append(str(e))
|
||||
|
||||
# Simulate concurrent requests
|
||||
threads = [threading.Thread(target=make_request) for _ in range(10)]
|
||||
for t in threads:
|
||||
t.start()
|
||||
for t in threads:
|
||||
t.join()
|
||||
|
||||
assert not errors
|
||||
assert all(r == 200 for r in results)
|
||||
|
||||
|
||||
class TestCleanupConfiguration:
|
||||
"""Tests for cleanup configuration."""
|
||||
|
||||
def test_cleanup_intervals_configured(self, app):
|
||||
"""Verify cleanup intervals are properly configured."""
|
||||
from app.api import _CLEANUP_INTERVALS
|
||||
|
||||
assert "pastes" in _CLEANUP_INTERVALS
|
||||
assert "hashes" in _CLEANUP_INTERVALS
|
||||
assert "rate_limits" in _CLEANUP_INTERVALS
|
||||
|
||||
# Verify reasonable intervals
|
||||
assert _CLEANUP_INTERVALS["pastes"] >= 60 # At least 1 minute
|
||||
assert _CLEANUP_INTERVALS["hashes"] >= 60
|
||||
assert _CLEANUP_INTERVALS["rate_limits"] >= 60
|
||||
Reference in New Issue
Block a user