forked from username/flaskpaste
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.
323 lines
11 KiB
Python
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()
|