Files
flaskpaste/tests/test_paste_update.py
Username 8ebabfe102
All checks were successful
CI / Lint & Security Scan (push) Successful in 47s
CI / Build & Push Image (push) Successful in 22s
CI / Harbor Vulnerability Scan (push) Successful in 37s
pastes: add display_name field
Authenticated users can tag pastes with a human-readable label
via X-Display-Name header. Supports create, update, remove, and
listing. Max 128 chars, control characters rejected.
2026-02-24 12:55:44 +01:00

323 lines
11 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 TestPasteUpdateDisplayName:
"""Tests for display_name update via PUT."""
def test_update_set_display_name(self, client, auth_header):
"""Set display_name on existing paste."""
create = client.post("/", data="content", content_type="text/plain", headers=auth_header)
paste_id = json.loads(create.data)["id"]
response = client.put(
f"/{paste_id}",
data="",
headers={**auth_header, "X-Display-Name": "my label"},
)
assert response.status_code == 200
data = json.loads(response.data)
assert data["display_name"] == "my label"
def test_update_change_display_name(self, client, auth_header):
"""Change existing display_name."""
create = client.post(
"/",
data="content",
content_type="text/plain",
headers={**auth_header, "X-Display-Name": "old name"},
)
paste_id = json.loads(create.data)["id"]
response = client.put(
f"/{paste_id}",
data="",
headers={**auth_header, "X-Display-Name": "new name"},
)
assert response.status_code == 200
data = json.loads(response.data)
assert data["display_name"] == "new name"
def test_update_remove_display_name(self, client, auth_header):
"""Remove display_name via X-Remove-Display-Name header."""
create = client.post(
"/",
data="content",
content_type="text/plain",
headers={**auth_header, "X-Display-Name": "to remove"},
)
paste_id = json.loads(create.data)["id"]
response = client.put(
f"/{paste_id}",
data="",
headers={**auth_header, "X-Remove-Display-Name": "true"},
)
assert response.status_code == 200
data = json.loads(response.data)
assert "display_name" not in data
def test_update_display_name_too_long(self, client, auth_header):
"""Reject display_name exceeding 128 chars in update."""
create = client.post("/", data="content", content_type="text/plain", headers=auth_header)
paste_id = json.loads(create.data)["id"]
response = client.put(
f"/{paste_id}",
data="",
headers={**auth_header, "X-Display-Name": "x" * 129},
)
assert response.status_code == 400
def test_update_display_name_control_chars(self, client, auth_header):
"""Reject display_name with control characters in update."""
create = client.post("/", data="content", content_type="text/plain", headers=auth_header)
paste_id = json.loads(create.data)["id"]
response = client.put(
f"/{paste_id}",
data="",
headers={**auth_header, "X-Display-Name": "bad\x01name"},
)
assert response.status_code == 400
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()