Files
flaskpaste/tests/test_paste_update.py
Username bfc238b5cf 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
2025-12-20 20:13:00 +01:00

243 lines
8.4 KiB
Python

"""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()