forked from username/flaskpaste
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
243 lines
8.4 KiB
Python
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()
|