All checks were successful
CI / Security Scan (push) Successful in 20s
CI / Lint & Format (push) Successful in 23s
CI / Advanced Security Tests (push) Successful in 16s
CI / Memory Leak Check (push) Successful in 20s
CI / Security Tests (push) Successful in 26s
CI / Unit Tests (push) Successful in 34s
CI / Fuzz Testing (push) Successful in 25s
CI / SBOM Generation (push) Successful in 19s
CI / Build & Push Image (push) Successful in 17s
CI / Harbor Vulnerability Scan (push) Successful in 33s
290 lines
10 KiB
Python
290 lines
10 KiB
Python
"""Tests for FlaskPaste API endpoints."""
|
|
|
|
import json
|
|
|
|
from app.config import VERSION
|
|
|
|
|
|
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["version"] == VERSION
|
|
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 returns octet-stream (magic detection disabled)."""
|
|
response = client.post(
|
|
"/",
|
|
data=png_bytes,
|
|
content_type="application/octet-stream",
|
|
)
|
|
assert response.status_code == 201
|
|
data = json.loads(response.data)
|
|
# Magic byte detection disabled - binary content is octet-stream
|
|
assert data["mime_type"] == "application/octet-stream"
|
|
|
|
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
|
|
|
|
def test_head_paste_metadata(self, client, sample_text):
|
|
"""HEAD returns headers without body."""
|
|
create = client.post("/", data=sample_text, content_type="text/plain")
|
|
paste_id = json.loads(create.data)["id"]
|
|
|
|
response = client.head(f"/{paste_id}")
|
|
assert response.status_code == 200
|
|
assert response.content_type == "application/json"
|
|
assert response.data == b"" # No body for HEAD
|
|
|
|
def test_head_paste_not_found(self, client):
|
|
"""HEAD on nonexistent paste returns 404."""
|
|
response = client.head("/abcd12345678")
|
|
assert response.status_code == 404
|
|
|
|
|
|
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
|
|
# Magic byte detection disabled - binary served as octet-stream
|
|
assert response.content_type == "application/octet-stream"
|
|
|
|
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"
|
|
|
|
def test_head_paste_raw(self, client, sample_text):
|
|
"""HEAD on raw returns content-type without body."""
|
|
create = client.post("/", data=sample_text, content_type="text/plain")
|
|
paste_id = json.loads(create.data)["id"]
|
|
|
|
response = client.head(f"/{paste_id}/raw")
|
|
assert response.status_code == 200
|
|
assert response.content_type == "text/plain; charset=utf-8"
|
|
assert response.data == b"" # No body for HEAD
|
|
|
|
|
|
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
|