Files
flaskpaste/tests/test_api.py
Username 8f9868f0d9 flaskpaste: initial commit with security hardening
Features:
- REST API for text/binary pastes with MIME detection
- Client certificate auth via X-SSL-Client-SHA1 header
- SQLite with WAL mode for concurrent access
- Automatic paste expiry with LRU cleanup

Security:
- HSTS, CSP, X-Frame-Options, X-Content-Type-Options
- Cache-Control: no-store for sensitive responses
- X-Request-ID tracing for log correlation
- X-Proxy-Secret validation for defense-in-depth
- Parameterized queries, input validation
- Size limits (3 MiB anon, 50 MiB auth)

Includes /health endpoint, container support, and 70 tests.
2025-12-16 04:42:18 +01:00

264 lines
9.2 KiB
Python

"""Tests for FlaskPaste API endpoints."""
import json
class TestIndex:
"""Tests for GET / endpoint."""
def test_get_api_info(self, client):
"""GET / returns API information."""
response = client.get("/")
assert response.status_code == 200
data = json.loads(response.data)
assert data["name"] == "FlaskPaste"
assert data["version"] == "1.0.0"
assert "endpoints" in data
assert "usage" in data
def test_api_info_contains_endpoints(self, client):
"""API info lists all endpoints."""
response = client.get("/")
data = json.loads(response.data)
endpoints = data["endpoints"]
assert "GET /" in endpoints
assert "POST /" in endpoints
assert "GET /<id>" in endpoints
assert "GET /<id>/raw" in endpoints
assert "DELETE /<id>" in endpoints
class TestHealth:
"""Tests for GET /health endpoint."""
def test_health_endpoint_returns_ok(self, client):
"""Health endpoint returns healthy status."""
response = client.get("/health")
assert response.status_code == 200
data = json.loads(response.data)
assert data["status"] == "healthy"
assert data["database"] == "ok"
def test_health_endpoint_json_response(self, client):
"""Health endpoint returns JSON content type."""
response = client.get("/health")
assert "application/json" in response.content_type
class TestCreatePaste:
"""Tests for POST / endpoint."""
def test_create_paste_raw(self, client, sample_text):
"""Create paste with raw text body."""
response = client.post(
"/",
data=sample_text,
content_type="text/plain",
)
assert response.status_code == 201
data = json.loads(response.data)
assert "id" in data
assert len(data["id"]) == 12
assert data["mime_type"] == "text/plain"
assert "url" in data
assert "raw" in data
def test_create_paste_json(self, client, sample_json):
"""Create paste with JSON body."""
response = client.post(
"/",
data=json.dumps(sample_json),
content_type="application/json",
)
assert response.status_code == 201
data = json.loads(response.data)
assert "id" in data
assert data["mime_type"] == "text/plain"
def test_create_paste_binary(self, client, png_bytes):
"""Create paste with binary content detects MIME type."""
response = client.post(
"/",
data=png_bytes,
content_type="application/octet-stream",
)
assert response.status_code == 201
data = json.loads(response.data)
assert data["mime_type"] == "image/png"
def test_create_paste_empty_fails(self, client):
"""Create paste with empty content fails."""
response = client.post("/", data="")
assert response.status_code == 400
data = json.loads(response.data)
assert "error" in data
def test_create_paste_with_auth(self, client, sample_text, auth_header):
"""Create paste with authentication includes owner."""
response = client.post(
"/",
data=sample_text,
content_type="text/plain",
headers=auth_header,
)
assert response.status_code == 201
data = json.loads(response.data)
assert "owner" in data
assert data["owner"] == "a" * 40
def test_create_paste_invalid_auth_ignored(self, client, sample_text):
"""Invalid auth header is ignored (treated as anonymous)."""
response = client.post(
"/",
data=sample_text,
content_type="text/plain",
headers={"X-SSL-Client-SHA1": "invalid"},
)
assert response.status_code == 201
data = json.loads(response.data)
assert "owner" not in data
class TestGetPaste:
"""Tests for GET /<id> endpoint."""
def test_get_paste_metadata(self, client, sample_text):
"""Get paste returns metadata."""
create = client.post("/", data=sample_text, content_type="text/plain")
paste_id = json.loads(create.data)["id"]
response = client.get(f"/{paste_id}")
assert response.status_code == 200
data = json.loads(response.data)
assert data["id"] == paste_id
assert data["mime_type"] == "text/plain"
assert data["size"] == len(sample_text)
assert "created_at" in data
assert "raw" in data
def test_get_paste_not_found(self, client):
"""Get nonexistent paste returns 404."""
response = client.get("/abcd12345678")
assert response.status_code == 404
data = json.loads(response.data)
assert "error" in data
def test_get_paste_invalid_id(self, client):
"""Get paste with invalid ID returns 400."""
response = client.get("/invalid!")
assert response.status_code == 400
def test_get_paste_wrong_length_id(self, client):
"""Get paste with wrong length ID returns 400."""
response = client.get("/abc")
assert response.status_code == 400
class TestGetPasteRaw:
"""Tests for GET /<id>/raw endpoint."""
def test_get_paste_raw_text(self, client, sample_text):
"""Get raw paste returns content with correct MIME type."""
create = client.post("/", data=sample_text, content_type="text/plain")
paste_id = json.loads(create.data)["id"]
response = client.get(f"/{paste_id}/raw")
assert response.status_code == 200
assert response.data.decode("utf-8") == sample_text
assert response.content_type == "text/plain; charset=utf-8"
def test_get_paste_raw_binary(self, client, png_bytes):
"""Get raw binary paste returns correct content."""
create = client.post(
"/",
data=png_bytes,
content_type="application/octet-stream",
)
paste_id = json.loads(create.data)["id"]
response = client.get(f"/{paste_id}/raw")
assert response.status_code == 200
assert response.data == png_bytes
assert response.content_type == "image/png"
def test_get_paste_raw_not_found(self, client):
"""Get raw nonexistent paste returns 404."""
response = client.get("/abcd12345678/raw")
assert response.status_code == 404
def test_get_paste_raw_inline_disposition(self, client, sample_text):
"""Text and image pastes have inline disposition."""
create = client.post("/", data=sample_text, content_type="text/plain")
paste_id = json.loads(create.data)["id"]
response = client.get(f"/{paste_id}/raw")
assert response.headers.get("Content-Disposition") == "inline"
class TestDeletePaste:
"""Tests for DELETE /<id> endpoint."""
def test_delete_paste_success(self, client, sample_text, auth_header):
"""Delete owned paste succeeds."""
create = client.post(
"/",
data=sample_text,
content_type="text/plain",
headers=auth_header,
)
paste_id = json.loads(create.data)["id"]
response = client.delete(f"/{paste_id}", headers=auth_header)
assert response.status_code == 200
data = json.loads(response.data)
assert "message" in data
# Verify deletion
get_response = client.get(f"/{paste_id}")
assert get_response.status_code == 404
def test_delete_paste_no_auth(self, client, sample_text):
"""Delete without authentication fails."""
create = client.post("/", data=sample_text, content_type="text/plain")
paste_id = json.loads(create.data)["id"]
response = client.delete(f"/{paste_id}")
assert response.status_code == 401
data = json.loads(response.data)
assert "error" in data
def test_delete_paste_wrong_owner(
self, client, sample_text, auth_header, other_auth_header
):
"""Delete paste owned by another user fails."""
create = client.post(
"/",
data=sample_text,
content_type="text/plain",
headers=auth_header,
)
paste_id = json.loads(create.data)["id"]
response = client.delete(f"/{paste_id}", headers=other_auth_header)
assert response.status_code == 403
data = json.loads(response.data)
assert "error" in data
def test_delete_anonymous_paste_fails(self, client, sample_text, auth_header):
"""Cannot delete anonymous paste (no owner)."""
create = client.post("/", data=sample_text, content_type="text/plain")
paste_id = json.loads(create.data)["id"]
response = client.delete(f"/{paste_id}", headers=auth_header)
assert response.status_code == 403
def test_delete_paste_not_found(self, client, auth_header):
"""Delete nonexistent paste returns 404."""
response = client.delete("/abcd12345678", headers=auth_header)
assert response.status_code == 404
def test_delete_paste_invalid_id(self, client, auth_header):
"""Delete with invalid ID returns 400."""
response = client.delete("/invalid!", headers=auth_header)
assert response.status_code == 400