"""Tests for paste listing endpoint (GET /pastes).""" import json class TestPastesListEndpoint: """Tests for GET /pastes endpoint.""" def test_list_pastes_requires_auth(self, client): """List pastes requires authentication.""" response = client.get("/pastes") assert response.status_code == 401 data = json.loads(response.data) assert "error" in data def test_list_pastes_empty(self, client, auth_header): """List pastes returns empty when user has no pastes.""" response = client.get("/pastes", headers=auth_header) assert response.status_code == 200 data = json.loads(response.data) assert data["pastes"] == [] assert data["count"] == 0 assert data["total"] == 0 def test_list_pastes_returns_own_pastes(self, client, sample_text, auth_header): """List pastes returns only user's own pastes.""" # Create a paste create = client.post( "/", data=sample_text, content_type="text/plain", headers=auth_header, ) paste_id = json.loads(create.data)["id"] # List pastes response = client.get("/pastes", headers=auth_header) assert response.status_code == 200 data = json.loads(response.data) assert data["count"] == 1 assert data["total"] == 1 assert data["pastes"][0]["id"] == paste_id def test_list_pastes_excludes_others(self, client, sample_text, auth_header, other_auth_header): """List pastes does not include other users' pastes.""" # Create paste as user A client.post( "/", data=sample_text, content_type="text/plain", headers=auth_header, ) # List pastes as user B response = client.get("/pastes", headers=other_auth_header) assert response.status_code == 200 data = json.loads(response.data) assert data["count"] == 0 assert data["total"] == 0 def test_list_pastes_excludes_anonymous(self, client, sample_text, auth_header): """List pastes does not include anonymous pastes.""" # Create anonymous paste client.post("/", data=sample_text, content_type="text/plain") # List pastes as authenticated user response = client.get("/pastes", headers=auth_header) assert response.status_code == 200 data = json.loads(response.data) assert data["count"] == 0 def test_list_pastes_metadata_only(self, client, sample_text, auth_header): """List pastes returns metadata, not content.""" # Create a paste client.post( "/", data=sample_text, content_type="text/plain", headers=auth_header, ) # List pastes response = client.get("/pastes", headers=auth_header) data = json.loads(response.data) paste = data["pastes"][0] # Verify metadata fields assert "id" in paste assert "mime_type" in paste assert "size" in paste assert "created_at" in paste assert "last_accessed" in paste assert "url" in paste assert "raw" in paste # Verify content is NOT included assert "content" not in paste def test_list_pastes_pagination(self, client, auth_header): """List pastes supports pagination.""" # Create multiple pastes for i in range(5): client.post( "/", data=f"paste {i}", content_type="text/plain", headers=auth_header, ) # Get first page response = client.get("/pastes?limit=2&offset=0", headers=auth_header) data = json.loads(response.data) assert data["count"] == 2 assert data["total"] == 5 assert data["limit"] == 2 assert data["offset"] == 0 # Get second page response = client.get("/pastes?limit=2&offset=2", headers=auth_header) data = json.loads(response.data) assert data["count"] == 2 assert data["offset"] == 2 def test_list_pastes_max_limit(self, client, auth_header): """List pastes enforces maximum limit.""" response = client.get("/pastes?limit=500", headers=auth_header) data = json.loads(response.data) assert data["limit"] == 200 # Max limit enforced def test_list_pastes_invalid_pagination(self, client, auth_header): """List pastes handles invalid pagination gracefully.""" response = client.get("/pastes?limit=abc&offset=-1", headers=auth_header) assert response.status_code == 200 data = json.loads(response.data) # Should use defaults assert data["limit"] == 50 assert data["offset"] == 0 def test_list_pastes_includes_special_fields(self, client, auth_header): """List pastes includes burn_after_read, expires_at, password_protected.""" # Create paste with burn-after-read client.post( "/", data="burn test", content_type="text/plain", headers={**auth_header, "X-Burn-After-Read": "true"}, ) response = client.get("/pastes", headers=auth_header) data = json.loads(response.data) paste = data["pastes"][0] assert paste.get("burn_after_read") is True def test_list_pastes_ordered_by_created_at(self, client, auth_header): """List pastes returns all created pastes ordered by created_at DESC.""" # Create pastes ids = set() for i in range(3): create = client.post( "/", data=f"paste {i}", content_type="text/plain", headers=auth_header, ) ids.add(json.loads(create.data)["id"]) response = client.get("/pastes", headers=auth_header) data = json.loads(response.data) # All created pastes should be present returned_ids = {p["id"] for p in data["pastes"]} assert returned_ids == ids assert data["count"] == 3 class TestPastesPrivacy: """Privacy-focused tests for paste listing.""" def test_cannot_see_other_user_pastes( self, client, sample_text, auth_header, other_auth_header ): """Users cannot see pastes owned by others.""" # User A creates paste create = client.post( "/", data=sample_text, content_type="text/plain", headers=auth_header, ) paste_id = json.loads(create.data)["id"] # User B lists pastes - should not see A's paste response = client.get("/pastes", headers=other_auth_header) data = json.loads(response.data) paste_ids = [p["id"] for p in data["pastes"]] assert paste_id not in paste_ids def test_no_admin_bypass(self, client, sample_text, auth_header): """No special admin access to list all pastes.""" # Create paste as regular user client.post( "/", data=sample_text, content_type="text/plain", headers=auth_header, ) # Different user cannot see it, regardless of auth header format admin_header = {"X-SSL-Client-SHA1": "0" * 40} response = client.get("/pastes", headers=admin_header) data = json.loads(response.data) assert data["count"] == 0 def test_content_never_exposed(self, client, auth_header): """Paste content is never exposed in listing.""" secret = "super secret content that should never be exposed" client.post( "/", data=secret, content_type="text/plain", headers=auth_header, ) response = client.get("/pastes", headers=auth_header) # Content should not appear anywhere in response assert secret.encode() not in response.data class TestPastesSearch: """Tests for paste search parameters.""" def test_search_by_type_exact(self, client, auth_header, png_bytes): """Search pastes by exact MIME type.""" # Create text paste client.post("/", data="text content", content_type="text/plain", headers=auth_header) # Create image paste create = client.post("/", data=png_bytes, content_type="image/png", headers=auth_header) png_id = json.loads(create.data)["id"] # Search for image/png response = client.get("/pastes?type=image/png", headers=auth_header) data = json.loads(response.data) assert data["count"] == 1 assert data["pastes"][0]["id"] == png_id def test_search_by_type_glob(self, client, auth_header, png_bytes, jpeg_bytes): """Search pastes by MIME type glob pattern.""" # Create text paste client.post("/", data="text content", content_type="text/plain", headers=auth_header) # Create image pastes client.post("/", data=png_bytes, content_type="image/png", headers=auth_header) client.post("/", data=jpeg_bytes, content_type="image/jpeg", headers=auth_header) # Search for all images response = client.get("/pastes?type=image/*", headers=auth_header) data = json.loads(response.data) assert data["count"] == 2 for paste in data["pastes"]: assert paste["mime_type"].startswith("image/") def test_search_by_after_timestamp(self, client, auth_header): """Search pastes created after timestamp.""" # Create paste create = client.post("/", data="test", content_type="text/plain", headers=auth_header) paste_data = json.loads(create.data) created_at = paste_data["created_at"] # Search for pastes after creation time (should find it) response = client.get(f"/pastes?after={created_at - 1}", headers=auth_header) data = json.loads(response.data) assert data["count"] == 1 # Search for pastes after creation time + 1 (should not find it) response = client.get(f"/pastes?after={created_at + 1}", headers=auth_header) data = json.loads(response.data) assert data["count"] == 0 def test_search_by_before_timestamp(self, client, auth_header): """Search pastes created before timestamp.""" # Create paste create = client.post("/", data="test", content_type="text/plain", headers=auth_header) paste_data = json.loads(create.data) created_at = paste_data["created_at"] # Search for pastes before creation time + 1 (should find it) response = client.get(f"/pastes?before={created_at + 1}", headers=auth_header) data = json.loads(response.data) assert data["count"] == 1 # Search for pastes before creation time (should not find it) response = client.get(f"/pastes?before={created_at - 1}", headers=auth_header) data = json.loads(response.data) assert data["count"] == 0 def test_search_combined_filters(self, client, auth_header, png_bytes): """Search with multiple filters combined.""" # Create text paste client.post("/", data="text", content_type="text/plain", headers=auth_header) # Create image paste create = client.post("/", data=png_bytes, content_type="image/png", headers=auth_header) png_data = json.loads(create.data) created_at = png_data["created_at"] # Search for images after a certain time response = client.get( f"/pastes?type=image/*&after={created_at - 1}", headers=auth_header, ) data = json.loads(response.data) assert data["count"] == 1 assert data["pastes"][0]["mime_type"] == "image/png" def test_search_no_matches(self, client, auth_header): """Search with no matching results.""" # Create text paste client.post("/", data="text", content_type="text/plain", headers=auth_header) # Search for video (no matches) response = client.get("/pastes?type=video/*", headers=auth_header) data = json.loads(response.data) assert data["count"] == 0 assert data["pastes"] == [] def test_search_invalid_timestamp(self, client, auth_header): """Search with invalid timestamp uses default.""" client.post("/", data="test", content_type="text/plain", headers=auth_header) # Invalid timestamp should be ignored response = client.get("/pastes?after=invalid", headers=auth_header) assert response.status_code == 200 data = json.loads(response.data) assert data["count"] == 1 class TestPastesDisplayName: """Tests for display_name field in paste listing.""" def test_create_with_display_name(self, client, auth_header): """Create paste with display_name returns it in response.""" response = client.post( "/", data="test content", content_type="text/plain", headers={**auth_header, "X-Display-Name": "my notes"}, ) assert response.status_code == 201 data = json.loads(response.data) assert data["display_name"] == "my notes" def test_display_name_in_listing(self, client, auth_header): """Display name appears in paste listing.""" client.post( "/", data="test content", content_type="text/plain", headers={**auth_header, "X-Display-Name": "project docs"}, ) response = client.get("/pastes", headers=auth_header) data = json.loads(response.data) assert data["count"] == 1 assert data["pastes"][0]["display_name"] == "project docs" def test_display_name_in_metadata(self, client, auth_header): """Display name appears in single-paste GET.""" create = client.post( "/", data="test content", content_type="text/plain", headers={**auth_header, "X-Display-Name": "readme"}, ) paste_id = json.loads(create.data)["id"] response = client.get(f"/{paste_id}") data = json.loads(response.data) assert data["display_name"] == "readme" def test_display_name_absent_when_unset(self, client, auth_header): """Display name is omitted from response when not set.""" create = client.post( "/", data="test content", content_type="text/plain", headers=auth_header, ) paste_id = json.loads(create.data)["id"] response = client.get(f"/{paste_id}") data = json.loads(response.data) assert "display_name" not in data def test_anonymous_display_name_ignored(self, client): """Anonymous user's display_name header is silently ignored.""" create = client.post( "/", data="test content", content_type="text/plain", headers={"X-Display-Name": "sneaky"}, ) assert create.status_code == 201 data = json.loads(create.data) assert "display_name" not in data def test_display_name_too_long(self, client, auth_header): """Display name exceeding 128 chars is rejected.""" response = client.post( "/", data="test content", content_type="text/plain", headers={**auth_header, "X-Display-Name": "a" * 129}, ) assert response.status_code == 400 data = json.loads(response.data) assert "too long" in data["error"].lower() def test_display_name_control_chars_rejected(self, client, auth_header): """Display name with control characters is rejected.""" response = client.post( "/", data="test content", content_type="text/plain", headers={**auth_header, "X-Display-Name": "bad\x00name"}, ) assert response.status_code == 400 data = json.loads(response.data) assert "invalid" in data["error"].lower()