11"""API tests for paste endpoints."""
2+ from datetime import datetime , timedelta , timezone
3+
24import pytest
35from httpx import AsyncClient
46
57
68@pytest .mark .asyncio
7- class TestPasteCreationAPI :
9+ class TestPasteCreateAPI :
810 """API tests for paste creation endpoint."""
911
1012 async def test_create_paste_returns_plaintext_tokens (
11- self , test_client : AsyncClient , sample_paste_data
13+ self , test_client : AsyncClient , sample_paste_data , bypass_headers
1214 ):
1315 """POST /pastes should return plaintext tokens to user."""
14- response = await test_client .post ("/pastes" , json = sample_paste_data )
16+ response = await test_client .post ("/pastes" , json = sample_paste_data , headers = bypass_headers )
1517
1618 assert response .status_code == 200
1719 data = response .json ()
@@ -24,17 +26,326 @@ async def test_create_paste_returns_plaintext_tokens(
2426 assert len (data ["edit_token" ]) == 32 # UUID hex
2527 assert len (data ["delete_token" ]) == 32
2628
29+ async def test_create_paste_with_expiration (
30+ self , test_client : AsyncClient , bypass_headers
31+ ):
32+ """POST /pastes should handle expiration time."""
33+ expires_at = (datetime .now (tz = timezone .utc ) + timedelta (days = 1 )).isoformat ()
34+ paste_data = {
35+ "title" : "Expiring Paste" ,
36+ "content" : "This will expire" ,
37+ "content_language" : "plain_text" ,
38+ "expires_at" : expires_at ,
39+ }
40+
41+ response = await test_client .post ("/pastes" , json = paste_data , headers = bypass_headers )
42+
43+ assert response .status_code == 200
44+ data = response .json ()
45+ assert data ["expires_at" ] is not None
46+
47+ async def test_create_paste_without_expiration (
48+ self , test_client : AsyncClient , bypass_headers
49+ ):
50+ """POST /pastes should create permanent paste without expiration."""
51+ paste_data = {
52+ "title" : "Permanent Paste" ,
53+ "content" : "No expiration" ,
54+ "content_language" : "plain_text" ,
55+ }
56+
57+ response = await test_client .post ("/pastes" , json = paste_data , headers = bypass_headers )
58+
59+ assert response .status_code == 200
60+ data = response .json ()
61+ assert data ["expires_at" ] is None
62+
63+
64+ @pytest .mark .asyncio
65+ class TestPasteGetAPI :
66+ """API tests for paste retrieval endpoints."""
67+
2768 async def test_get_paste_by_id_returns_content (
28- self , test_client : AsyncClient , authenticated_paste
69+ self , test_client : AsyncClient , authenticated_paste , bypass_headers
2970 ):
3071 """GET /pastes/{id} should return paste content."""
3172 paste_id = authenticated_paste ["id" ]
3273
33- response = await test_client .get (f"/pastes/{ paste_id } " )
74+ response = await test_client .get (f"/pastes/{ paste_id } " , headers = bypass_headers )
3475
3576 assert response .status_code == 200
3677 data = response .json ()
3778
3879 assert data ["id" ] == paste_id
3980 assert data ["title" ] == "Test Paste"
4081 assert data ["content" ] == "This is test content"
82+
83+ async def test_get_paste_returns_404_for_nonexistent (
84+ self , test_client : AsyncClient , bypass_headers
85+ ):
86+ """GET /pastes/{id} should return 404 for non-existent paste."""
87+ import uuid
88+ nonexistent_id = str (uuid .uuid4 ())
89+
90+ response = await test_client .get (f"/pastes/{ nonexistent_id } " , headers = bypass_headers )
91+
92+ assert response .status_code == 404
93+ data = response .json ()
94+ assert data ["error" ] == "paste_not_found"
95+
96+ async def test_get_paste_caches_response (
97+ self , test_client : AsyncClient , authenticated_paste , bypass_headers
98+ ):
99+ """GET /pastes/{id} should cache successful responses."""
100+ paste_id = authenticated_paste ["id" ]
101+
102+ # First request
103+ response1 = await test_client .get (f"/pastes/{ paste_id } " , headers = bypass_headers )
104+ assert response1 .status_code == 200
105+ assert "Cache-Control" in response1 .headers
106+ assert "public" in response1 .headers ["Cache-Control" ]
107+
108+ # Second request should hit cache
109+ response2 = await test_client .get (f"/pastes/{ paste_id } " , headers = bypass_headers )
110+ assert response2 .status_code == 200
111+ assert response2 .json () == response1 .json ()
112+
113+ async def test_get_paste_cache_control_headers (
114+ self , test_client : AsyncClient , authenticated_paste , bypass_headers
115+ ):
116+ """GET /pastes/{id} should include proper cache control headers."""
117+ paste_id = authenticated_paste ["id" ]
118+
119+ response = await test_client .get (f"/pastes/{ paste_id } " , headers = bypass_headers )
120+
121+ assert response .status_code == 200
122+ assert "Cache-Control" in response .headers
123+ cache_control = response .headers ["Cache-Control" ]
124+ assert "public" in cache_control
125+ assert "max-age" in cache_control
126+
127+
128+ @pytest .mark .asyncio
129+ class TestPasteEditAPI :
130+ """API tests for paste editing endpoint."""
131+
132+ async def test_edit_paste_with_valid_token_updates_title (
133+ self , test_client : AsyncClient , authenticated_paste
134+ ):
135+ """PUT /pastes/{id} should update title with valid token."""
136+ paste_id = authenticated_paste ["id" ]
137+ edit_token = authenticated_paste ["edit_token" ]
138+
139+ edit_data = {"title" : "Updated Title" }
140+ response = await test_client .put (
141+ f"/pastes/{ paste_id } " ,
142+ json = edit_data ,
143+ headers = {"Authorization" : edit_token }
144+ )
145+
146+ assert response .status_code == 200
147+ data = response .json ()
148+ assert data ["title" ] == "Updated Title"
149+ assert data ["content" ] == "This is test content" # Unchanged
150+
151+ async def test_edit_paste_with_valid_token_updates_content (
152+ self , test_client : AsyncClient , authenticated_paste
153+ ):
154+ """PUT /pastes/{id} should update content with valid token."""
155+ paste_id = authenticated_paste ["id" ]
156+ edit_token = authenticated_paste ["edit_token" ]
157+
158+ edit_data = {"content" : "Updated content" }
159+ response = await test_client .put (
160+ f"/pastes/{ paste_id } " ,
161+ json = edit_data ,
162+ headers = {"Authorization" : edit_token }
163+ )
164+
165+ assert response .status_code == 200
166+ data = response .json ()
167+ assert data ["content" ] == "Updated content"
168+ assert data ["title" ] == "Test Paste" # Unchanged
169+
170+ async def test_edit_paste_with_valid_token_updates_language (
171+ self , test_client : AsyncClient , authenticated_paste
172+ ):
173+ """PUT /pastes/{id} should update content language with valid token."""
174+ paste_id = authenticated_paste ["id" ]
175+ edit_token = authenticated_paste ["edit_token" ]
176+
177+ edit_data = {"content_language" : "plain_text" }
178+ response = await test_client .put (
179+ f"/pastes/{ paste_id } " ,
180+ json = edit_data ,
181+ headers = {"Authorization" : edit_token }
182+ )
183+
184+ assert response .status_code == 200
185+ data = response .json ()
186+ assert data ["content_language" ] == "plain_text"
187+
188+ async def test_edit_paste_with_valid_token_updates_expiration (
189+ self , test_client : AsyncClient , authenticated_paste
190+ ):
191+ """PUT /pastes/{id} should update expiration with valid token."""
192+ paste_id = authenticated_paste ["id" ]
193+ edit_token = authenticated_paste ["edit_token" ]
194+
195+ new_expiration = (datetime .now (tz = timezone .utc ) + timedelta (days = 7 )).isoformat ()
196+ edit_data = {"expires_at" : new_expiration }
197+ response = await test_client .put (
198+ f"/pastes/{ paste_id } " ,
199+ json = edit_data ,
200+ headers = {"Authorization" : edit_token }
201+ )
202+
203+ assert response .status_code == 200
204+ data = response .json ()
205+ assert data ["expires_at" ] is not None
206+
207+ async def test_edit_paste_partial_update (
208+ self , test_client : AsyncClient , authenticated_paste
209+ ):
210+ """PUT /pastes/{id} should support partial updates."""
211+ paste_id = authenticated_paste ["id" ]
212+ edit_token = authenticated_paste ["edit_token" ]
213+
214+ # Only update title, leaving other fields unchanged
215+ edit_data = {
216+ "title" : "New Title"
217+ }
218+ response = await test_client .put (
219+ f"/pastes/{ paste_id } " ,
220+ json = edit_data ,
221+ headers = {"Authorization" : edit_token }
222+ )
223+
224+ assert response .status_code == 200
225+ data = response .json ()
226+ assert data ["title" ] == "New Title"
227+ assert data ["content_language" ] == "plain_text" # Unchanged
228+ assert data ["content" ] == "This is test content" # Unchanged
229+
230+ async def test_edit_paste_returns_404_with_invalid_token (
231+ self , test_client : AsyncClient , authenticated_paste
232+ ):
233+ """PUT /pastes/{id} should return 404 with invalid token."""
234+ paste_id = authenticated_paste ["id" ]
235+ invalid_token = "invalid_token_12345678901234567890"
236+
237+ edit_data = {"title" : "Should Fail" }
238+ response = await test_client .put (
239+ f"/pastes/{ paste_id } " ,
240+ json = edit_data ,
241+ headers = {"Authorization" : invalid_token }
242+ )
243+
244+ assert response .status_code == 404
245+ data = response .json ()
246+ assert "detail" in data
247+ assert data ["detail" ]["error" ] == "paste_not_found"
248+
249+ async def test_edit_paste_returns_404_for_nonexistent_paste (
250+ self , test_client : AsyncClient
251+ ):
252+ """PUT /pastes/{id} should return 404 for non-existent paste."""
253+ import uuid
254+ nonexistent_id = str (uuid .uuid4 ())
255+ fake_token = "a" * 32
256+
257+ edit_data = {"title" : "Should Fail" }
258+ response = await test_client .put (
259+ f"/pastes/{ nonexistent_id } " ,
260+ json = edit_data ,
261+ headers = {"Authorization" : fake_token }
262+ )
263+
264+ assert response .status_code == 404
265+
266+ async def test_edit_paste_requires_authorization_header (
267+ self , test_client : AsyncClient , authenticated_paste
268+ ):
269+ """PUT /pastes/{id} should require Authorization header."""
270+ paste_id = authenticated_paste ["id" ]
271+
272+ edit_data = {"title" : "Should Fail" }
273+ response = await test_client .put (
274+ f"/pastes/{ paste_id } " ,
275+ json = edit_data
276+ )
277+
278+ # Should fail with 401 Unauthorized (missing auth header)
279+ assert response .status_code == 401
280+
281+
282+ @pytest .mark .asyncio
283+ class TestPasteDeleteAPI :
284+ """API tests for paste deletion endpoint."""
285+
286+ async def test_delete_paste_with_valid_token (
287+ self , test_client : AsyncClient , authenticated_paste
288+ ):
289+ """DELETE /pastes/{id} should succeed with valid token."""
290+ paste_id = authenticated_paste ["id" ]
291+ delete_token = authenticated_paste ["delete_token" ]
292+
293+ response = await test_client .delete (
294+ f"/pastes/{ paste_id } " ,
295+ headers = {"Authorization" : delete_token }
296+ )
297+
298+ assert response .status_code == 200
299+ data = response .json ()
300+ assert data ["message" ] == "Paste deleted successfully"
301+
302+ # Verify paste is gone
303+ get_response = await test_client .get (f"/pastes/{ paste_id } " )
304+ assert get_response .status_code == 404
305+
306+ async def test_delete_paste_returns_404_with_invalid_token (
307+ self , test_client : AsyncClient , authenticated_paste
308+ ):
309+ """DELETE /pastes/{id} should return 404 with invalid token."""
310+ paste_id = authenticated_paste ["id" ]
311+ invalid_token = "invalid_token_12345678901234567890"
312+
313+ response = await test_client .delete (
314+ f"/pastes/{ paste_id } " ,
315+ headers = {"Authorization" : invalid_token }
316+ )
317+
318+ assert response .status_code == 404
319+ data = response .json ()
320+ assert "detail" in data
321+ assert data ["detail" ]["error" ] == "paste_not_found"
322+
323+ # Verify paste still exists
324+ get_response = await test_client .get (f"/pastes/{ paste_id } " )
325+ assert get_response .status_code == 200
326+
327+ async def test_delete_paste_returns_404_for_nonexistent_paste (
328+ self , test_client : AsyncClient
329+ ):
330+ """DELETE /pastes/{id} should return 404 for non-existent paste."""
331+ import uuid
332+ nonexistent_id = str (uuid .uuid4 ())
333+ fake_token = "a" * 32
334+
335+ response = await test_client .delete (
336+ f"/pastes/{ nonexistent_id } " ,
337+ headers = {"Authorization" : fake_token }
338+ )
339+
340+ assert response .status_code == 404
341+
342+ async def test_delete_paste_requires_authorization_header (
343+ self , test_client : AsyncClient , authenticated_paste
344+ ):
345+ """DELETE /pastes/{id} should require Authorization header."""
346+ paste_id = authenticated_paste ["id" ]
347+
348+ response = await test_client .delete (f"/pastes/{ paste_id } " )
349+
350+ # Should fail with 401 Unauthorized (missing auth header)
351+ assert response .status_code == 401
0 commit comments