forked from username/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:
242
tests/test_paste_update.py
Normal file
242
tests/test_paste_update.py
Normal file
@@ -0,0 +1,242 @@
|
||||
"""Tests for paste update endpoint (PUT /<id>)."""
|
||||
|
||||
import json
|
||||
|
||||
|
||||
class TestPasteUpdateEndpoint:
|
||||
"""Tests for PUT /<id> endpoint."""
|
||||
|
||||
def test_update_requires_auth(self, client, sample_text, auth_header):
|
||||
"""Update requires authentication."""
|
||||
# Create paste
|
||||
create = client.post("/", data=sample_text, content_type="text/plain", headers=auth_header)
|
||||
paste_id = json.loads(create.data)["id"]
|
||||
|
||||
# Try to update without auth
|
||||
response = client.put(f"/{paste_id}", data="updated")
|
||||
assert response.status_code == 401
|
||||
|
||||
def test_update_requires_ownership(self, client, sample_text, auth_header, other_auth_header):
|
||||
"""Update requires paste ownership."""
|
||||
# Create paste as user A
|
||||
create = client.post("/", data=sample_text, content_type="text/plain", headers=auth_header)
|
||||
paste_id = json.loads(create.data)["id"]
|
||||
|
||||
# Try to update as user B
|
||||
response = client.put(
|
||||
f"/{paste_id}",
|
||||
data="updated content",
|
||||
content_type="text/plain",
|
||||
headers=other_auth_header,
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
def test_update_content(self, client, auth_header):
|
||||
"""Update paste content."""
|
||||
# Create paste
|
||||
create = client.post("/", data="original", content_type="text/plain", headers=auth_header)
|
||||
paste_id = json.loads(create.data)["id"]
|
||||
|
||||
# Update content
|
||||
response = client.put(
|
||||
f"/{paste_id}",
|
||||
data="updated content",
|
||||
content_type="text/plain",
|
||||
headers=auth_header,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data["size"] == len("updated content")
|
||||
|
||||
# Verify content changed
|
||||
raw = client.get(f"/{paste_id}/raw")
|
||||
assert raw.data == b"updated content"
|
||||
|
||||
def test_update_password_set(self, client, auth_header):
|
||||
"""Set password on paste."""
|
||||
# Create paste without password
|
||||
create = client.post("/", data="content", content_type="text/plain", headers=auth_header)
|
||||
paste_id = json.loads(create.data)["id"]
|
||||
|
||||
# Add password
|
||||
response = client.put(
|
||||
f"/{paste_id}",
|
||||
data="",
|
||||
headers={**auth_header, "X-Paste-Password": "secret123"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data.get("password_protected") is True
|
||||
|
||||
# Verify password required
|
||||
raw = client.get(f"/{paste_id}/raw")
|
||||
assert raw.status_code == 401
|
||||
|
||||
# Verify correct password works
|
||||
raw = client.get(f"/{paste_id}/raw", headers={"X-Paste-Password": "secret123"})
|
||||
assert raw.status_code == 200
|
||||
|
||||
def test_update_password_remove(self, client, auth_header):
|
||||
"""Remove password from paste."""
|
||||
# Create paste with password
|
||||
create = client.post(
|
||||
"/",
|
||||
data="content",
|
||||
content_type="text/plain",
|
||||
headers={**auth_header, "X-Paste-Password": "secret123"},
|
||||
)
|
||||
paste_id = json.loads(create.data)["id"]
|
||||
|
||||
# Remove password
|
||||
response = client.put(
|
||||
f"/{paste_id}",
|
||||
data="",
|
||||
headers={**auth_header, "X-Remove-Password": "true"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data.get("password_protected") is not True
|
||||
|
||||
# Verify no password required
|
||||
raw = client.get(f"/{paste_id}/raw")
|
||||
assert raw.status_code == 200
|
||||
|
||||
def test_update_extend_expiry(self, client, auth_header):
|
||||
"""Extend paste expiry."""
|
||||
# Create paste with expiry
|
||||
create = client.post(
|
||||
"/",
|
||||
data="content",
|
||||
content_type="text/plain",
|
||||
headers={**auth_header, "X-Expiry": "3600"},
|
||||
)
|
||||
paste_id = json.loads(create.data)["id"]
|
||||
original_expiry = json.loads(create.data)["expires_at"]
|
||||
|
||||
# Extend expiry
|
||||
response = client.put(
|
||||
f"/{paste_id}",
|
||||
data="",
|
||||
headers={**auth_header, "X-Extend-Expiry": "7200"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data["expires_at"] == original_expiry + 7200
|
||||
|
||||
def test_update_add_expiry(self, client, auth_header):
|
||||
"""Add expiry to paste without one."""
|
||||
# Create paste without expiry
|
||||
create = client.post("/", data="content", content_type="text/plain", headers=auth_header)
|
||||
paste_id = json.loads(create.data)["id"]
|
||||
|
||||
# Add expiry
|
||||
response = client.put(
|
||||
f"/{paste_id}",
|
||||
data="",
|
||||
headers={**auth_header, "X-Extend-Expiry": "3600"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert "expires_at" in data
|
||||
|
||||
def test_update_burn_after_read_forbidden(self, client, auth_header):
|
||||
"""Cannot update burn-after-read pastes."""
|
||||
# Create burn-after-read paste
|
||||
create = client.post(
|
||||
"/",
|
||||
data="content",
|
||||
content_type="text/plain",
|
||||
headers={**auth_header, "X-Burn-After-Read": "true"},
|
||||
)
|
||||
paste_id = json.loads(create.data)["id"]
|
||||
|
||||
# Try to update
|
||||
response = client.put(
|
||||
f"/{paste_id}",
|
||||
data="updated",
|
||||
content_type="text/plain",
|
||||
headers=auth_header,
|
||||
)
|
||||
assert response.status_code == 400
|
||||
data = json.loads(response.data)
|
||||
assert "burn" in data["error"].lower()
|
||||
|
||||
def test_update_not_found(self, client, auth_header):
|
||||
"""Update non-existent paste returns 404."""
|
||||
response = client.put(
|
||||
"/000000000000",
|
||||
data="updated",
|
||||
content_type="text/plain",
|
||||
headers=auth_header,
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_update_no_changes(self, client, auth_header):
|
||||
"""Update with no changes returns error."""
|
||||
# Create paste
|
||||
create = client.post("/", data="content", content_type="text/plain", headers=auth_header)
|
||||
paste_id = json.loads(create.data)["id"]
|
||||
|
||||
# Try to update with empty request
|
||||
response = client.put(f"/{paste_id}", data="", headers=auth_header)
|
||||
assert response.status_code == 400
|
||||
data = json.loads(response.data)
|
||||
assert "no updates" in data["error"].lower()
|
||||
|
||||
def test_update_combined(self, client, auth_header):
|
||||
"""Update content and metadata together."""
|
||||
# Create paste
|
||||
create = client.post("/", data="original", content_type="text/plain", headers=auth_header)
|
||||
paste_id = json.loads(create.data)["id"]
|
||||
|
||||
# Update content and add password
|
||||
response = client.put(
|
||||
f"/{paste_id}",
|
||||
data="new content",
|
||||
content_type="text/plain",
|
||||
headers={**auth_header, "X-Paste-Password": "secret"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data["size"] == len("new content")
|
||||
assert data.get("password_protected") is True
|
||||
|
||||
|
||||
class TestPasteUpdatePrivacy:
|
||||
"""Privacy-focused tests for paste update."""
|
||||
|
||||
def test_cannot_update_anonymous_paste(self, client, sample_text, auth_header):
|
||||
"""Cannot update paste without owner."""
|
||||
# Create anonymous paste
|
||||
create = client.post("/", data=sample_text, content_type="text/plain")
|
||||
paste_id = json.loads(create.data)["id"]
|
||||
|
||||
# Try to update
|
||||
response = client.put(
|
||||
f"/{paste_id}",
|
||||
data="updated",
|
||||
content_type="text/plain",
|
||||
headers=auth_header,
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
def test_cannot_update_other_user_paste(
|
||||
self, client, sample_text, auth_header, other_auth_header
|
||||
):
|
||||
"""Cannot update paste owned by another user."""
|
||||
# Create paste as user A
|
||||
create = client.post("/", data=sample_text, content_type="text/plain", headers=auth_header)
|
||||
paste_id = json.loads(create.data)["id"]
|
||||
|
||||
# Try to update as user B
|
||||
response = client.put(
|
||||
f"/{paste_id}",
|
||||
data="hijacked",
|
||||
content_type="text/plain",
|
||||
headers=other_auth_header,
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
# Verify original content unchanged
|
||||
raw = client.get(f"/{paste_id}/raw")
|
||||
assert raw.data == sample_text.encode()
|
||||
Reference in New Issue
Block a user