55from django .contrib .auth import get_user_model
66from oauth2_provider .models import Application , AccessToken
77from testbed .core .models import Actor
8- from testbed .core .factories import ActorFactory
8+ from testbed .core .factories import ActorFactory , ApplicationFactory , AccessTokenFactory
99from testbed .core .tests .conftest import create_isolated_actor
1010from testbed .core .json_ld_utils import (
1111 build_basic_context ,
@@ -78,50 +78,44 @@ def test_outbox_not_found():
7878Tests the complete request-response cycle with different authentication states
7979to verify that endpoints properly serve enhanced data for LOLA-authenticated requests.
8080"""
81- class TestLOLAAuthenticationAPI :
82-
83- # Create OAuth application for testing
84- @pytest .fixture
85- def oauth_application (self , db ):
86- return Application .objects .create (
87- name = "LOLA Test Application" ,
88- client_type = Application .CLIENT_CONFIDENTIAL ,
89- authorization_grant_type = Application .GRANT_AUTHORIZATION_CODE ,
90- redirect_uris = "http://localhost:8000/callback/" ,
91- )
92-
93- # Create user for OAuth token testing
94- @pytest .fixture
95- def authenticated_user (self , db ):
96- return User .objects .create_user (
97- username = 'lola_test_user' ,
98- 99- password = 'test123password'
100- )
101-
102- # Create access token with LOLA portability scope
103- @pytest .fixture
104- def lola_token (self , oauth_application , authenticated_user ):
105- from datetime import datetime , timezone , timedelta
106- return AccessToken .objects .create (
107- user = authenticated_user ,
108- application = oauth_application ,
109- token = 'lola-portability-token-12345' ,
110- scope = 'activitypub_account_portability read write' ,
111- expires = datetime .now (timezone .utc ) + timedelta (hours = 1 ) # Expires in 1 hour
112- )
81+ class TestLOLAAuthenticationAPI :
82+ # Constants for OAuth scopes
83+ LOLA_SCOPE = 'activitypub_account_portability read write'
84+ BASIC_SCOPE = 'read write'
11385
114- # Create access token without LOLA portability scope
115- @pytest .fixture
116- def basic_token (self , oauth_application , authenticated_user ):
117- from datetime import datetime , timezone , timedelta
118- return AccessToken .objects .create (
119- user = authenticated_user ,
120- application = oauth_application ,
121- token = 'basic-oauth-token-67890' ,
122- scope = 'read write' ,
123- expires = datetime .now (timezone .utc ) + timedelta (hours = 1 ) # Expires in 1 hour
124- )
86+ # Helper methods for repeated assertions
87+
88+ # Helper to verify standard ActivityPub fields
89+ def assert_basic_activitypub_structure (self , data , actor ):
90+ assert data ["@context" ] == build_actor_context ()
91+ assert data ["type" ] == "Person"
92+ assert data ["id" ] == build_actor_id (actor .id )
93+ assert data ["preferredUsername" ] == actor .username
94+
95+ # Helper to verify LOLA fields are absent
96+ def assert_no_lola_fields (self , data ):
97+ lola_fields = ["accountPortabilityOauth" , "content" , "blocked" , "migration" ]
98+ for field in lola_fields :
99+ assert field not in data
100+
101+ # Helper to verify LOLA fields are present and properly formatted
102+ def assert_has_lola_fields (self , data , actor ):
103+ assert "accountPortabilityOauth" in data
104+ assert "content" in data
105+ assert "blocked" in data
106+ assert "migration" in data
107+
108+ # Verify LOLA URLs are properly formatted
109+ assert data ["accountPortabilityOauth" ].endswith ("/oauth/authorize/" )
110+ assert data ["content" ].endswith (f"/actors/{ actor .id } /content" )
111+ assert data ["blocked" ].endswith (f"/actors/{ actor .id } /blocked" )
112+ assert data ["migration" ].endswith (f"/actors/{ actor .id } /outbox" )
113+
114+ # Helper to create authenticated client
115+ def get_authenticated_client (self , token ):
116+ client = APIClient ()
117+ client .credentials (HTTP_AUTHORIZATION = f'Bearer { token .token } ' )
118+ return client
125119
126120 # Test that unauthenticated requests return basic ActivityPub data only
127121 @pytest .mark .django_db
@@ -135,76 +129,49 @@ def test_actor_detail_unauthenticated_returns_basic_activitypub(self):
135129 assert response .status_code == status .HTTP_200_OK
136130
137131 data = response .data
138- # Should have standard ActivityPub fields
139- assert data ["@context" ] == build_actor_context ()
140- assert data ["type" ] == "Person"
141- assert data ["id" ] == build_actor_id (actor .id )
142- assert data ["preferredUsername" ] == actor .username
143-
144- # Should NOT have LOLA-specific fields
145- assert "accountPortabilityOauth" not in data
146- assert "content" not in data
147- assert "blocked" not in data
148- assert "migration" not in data
132+ # Use helper methods for assertions
133+ self .assert_basic_activitypub_structure (data , actor )
134+ self .assert_no_lola_fields (data )
149135
150136 # Test that LOLA-authenticated requests return enhanced data with collection URLs
151137 @pytest .mark .django_db
152- def test_actor_detail_with_lola_scope_returns_enhanced_data (self , lola_token ):
138+ def test_actor_detail_with_lola_scope_returns_enhanced_data (self ):
153139 actor = create_isolated_actor ("lola_enhanced_test" )
154- client = APIClient ( )
155- client . credentials ( HTTP_AUTHORIZATION = f'Bearer { lola_token . token } ' )
140+ lola_token = AccessTokenFactory ( lola_scope = True )
141+ client = self . get_authenticated_client ( lola_token )
156142
157143 response = client .get (reverse ("actor-detail" , kwargs = {"pk" : actor .id }))
158144
159145 # Should succeed with enhanced response
160146 assert response .status_code == status .HTTP_200_OK
161147
162148 data = response .data
163- # Should have standard ActivityPub fields
164- assert data ["@context" ] == build_actor_context ()
165- assert data ["type" ] == "Person"
166- assert data ["id" ] == build_actor_id (actor .id )
167- assert data ["preferredUsername" ] == actor .username
168-
169- # Should HAVE LOLA-specific collection URLs
170- assert "accountPortabilityOauth" in data
171- assert "content" in data
172- assert "blocked" in data
173- assert "migration" in data
174-
175- # Verify LOLA URLs are properly formatted
176- assert data ["accountPortabilityOauth" ].endswith ("/oauth/authorize/" )
177- assert data ["content" ].endswith (f"/actors/{ actor .id } /content" )
178- assert data ["blocked" ].endswith (f"/actors/{ actor .id } /blocked" )
179- assert data ["migration" ].endswith (f"/actors/{ actor .id } /outbox" )
149+ # Use helper methods for assertions
150+ self .assert_basic_activitypub_structure (data , actor )
151+ self .assert_has_lola_fields (data , actor )
180152
181153 # Test that authenticated requests without LOLA scope return basic data
182154 @pytest .mark .django_db
183- def test_actor_detail_with_basic_token_returns_basic_data (self , basic_token ):
155+ def test_actor_detail_with_basic_token_returns_basic_data (self ):
184156 actor = create_isolated_actor ("basic_token_test" )
185- client = APIClient ( )
186- client . credentials ( HTTP_AUTHORIZATION = f'Bearer { basic_token . token } ' )
157+ basic_token = AccessTokenFactory ( scope = self . BASIC_SCOPE )
158+ client = self . get_authenticated_client ( basic_token )
187159
188160 response = client .get (reverse ("actor-detail" , kwargs = {"pk" : actor .id }))
189161
190162 # Should succeed but return basic data (no LOLA scope)
191163 assert response .status_code == status .HTTP_200_OK
192164
193165 data = response .data
194- # Should have standard fields
195- assert data ["type" ] == "Person"
196- assert data ["preferredUsername" ] == actor .username
197-
198- # Should NOT have LOLA fields (lacks portability scope)
199- assert "accountPortabilityOauth" not in data
200- assert "content" not in data
201- assert "blocked" not in data
202- assert "migration" not in data
166+ # Use helper methods for assertions
167+ self .assert_basic_activitypub_structure (data , actor )
168+ self .assert_no_lola_fields (data )
203169
204170 # Test that URL parameter authentication works for LOLA testing
205171 @pytest .mark .django_db
206- def test_actor_detail_url_parameter_authentication (self , lola_token ):
172+ def test_actor_detail_url_parameter_authentication (self ):
207173 actor = create_isolated_actor ("url_param_test" )
174+ lola_token = AccessTokenFactory (lola_scope = True )
208175 client = APIClient ()
209176
210177 # Use auth_token URL parameter instead of Authorization header
@@ -215,14 +182,13 @@ def test_actor_detail_url_parameter_authentication(self, lola_token):
215182
216183 data = response .data
217184 # Should have LOLA fields (proves URL parameter auth worked)
218- assert "accountPortabilityOauth" in data
219- assert "content" in data
220- assert "blocked" in data
185+ self .assert_has_lola_fields (data , actor )
221186
222187 # Test that outbox shows different content based on authentication
223188 @pytest .mark .django_db
224- def test_outbox_content_filtering_by_authentication (self , lola_token ):
189+ def test_outbox_content_filtering_by_authentication (self ):
225190 actor = create_isolated_actor ("outbox_filtering_test" )
191+ lola_token = AccessTokenFactory (lola_scope = True )
226192 client = APIClient ()
227193
228194 # Test unauthenticated outbox (public activities only)
@@ -256,17 +222,17 @@ def test_outbox_content_filtering_by_authentication(self, lola_token):
256222
257223 # Test that demonstrates clear differences between public and LOLA responses
258224 @pytest .mark .django_db
259- def test_side_by_side_authentication_comparison (self , lola_token ):
225+ def test_side_by_side_authentication_comparison (self ):
260226 actor = create_isolated_actor ("comparison_test" )
227+ lola_token = AccessTokenFactory (lola_scope = True )
261228
262229 # Public request
263230 public_client = APIClient ()
264231 public_response = public_client .get (reverse ("actor-detail" , kwargs = {"pk" : actor .id }))
265232 public_data = public_response .data
266233
267234 # LOLA request
268- lola_client = APIClient ()
269- lola_client .credentials (HTTP_AUTHORIZATION = f'Bearer { lola_token .token } ' )
235+ lola_client = self .get_authenticated_client (lola_token )
270236 lola_response = lola_client .get (reverse ("actor-detail" , kwargs = {"pk" : actor .id }))
271237 lola_data = lola_response .data
272238
@@ -307,35 +273,32 @@ def test_invalid_token_graceful_degradation(self):
307273 assert "blocked" not in data
308274
309275 # Test graceful handling of malformed authorization headers
276+ @pytest .mark .parametrize ("malformed_header" , [
277+ "Bearer" , # Missing token
278+ "Basic invalid-format" , # Wrong auth type
279+ "Bearer " , # Empty token
280+ "InvalidFormat token" , # Malformed header
281+ ])
310282 @pytest .mark .django_db
311- def test_malformed_authorization_header_handling (self ):
283+ def test_malformed_authorization_header_handling (self , malformed_header ):
312284 actor = create_isolated_actor ("malformed_header_test" )
313285 client = APIClient ()
286+ client .credentials (HTTP_AUTHORIZATION = malformed_header )
314287
315- test_cases = [
316- "Bearer" , # Missing token
317- "Basic invalid-format" , # Wrong auth type
318- "Bearer " , # Empty token
319- "InvalidFormat token" , # Malformed header
320- ]
288+ response = client .get (reverse ("actor-detail" , kwargs = {"pk" : actor .id }))
321289
322- for malformed_header in test_cases :
323- client .credentials (HTTP_AUTHORIZATION = malformed_header )
324- response = client .get (reverse ("actor-detail" , kwargs = {"pk" : actor .id }))
325-
326- # Should succeed with public data for all malformed cases
327- assert response .status_code == status .HTTP_200_OK
328- data = response .data
329- assert data ["type" ] == "Person"
330- # Should NOT have LOLA fields
331- assert "accountPortabilityOauth" not in data
290+ # Should succeed with public data for all malformed cases
291+ assert response .status_code == status .HTTP_200_OK
292+ data = response .data
293+ self .assert_basic_activitypub_structure (data , actor )
294+ self .assert_no_lola_fields (data )
332295
333296 # Test that content-type headers are set correctly for API responses
334297 @pytest .mark .django_db
335- def test_content_type_headers_set_correctly (self , lola_token ):
298+ def test_content_type_headers_set_correctly (self ):
336299 actor = create_isolated_actor ("content_type_test" )
337- client = APIClient ( )
338- client . credentials ( HTTP_AUTHORIZATION = f'Bearer { lola_token . token } ' )
300+ lola_token = AccessTokenFactory ( lola_scope = True )
301+ client = self . get_authenticated_client ( lola_token )
339302
340303 # Request with format=json
341304 response = client .get (reverse ("actor-detail" , kwargs = {"pk" : actor .id }), {"format" : "json" })
0 commit comments