Skip to content

Commit a826684

Browse files
committed
refactor: Replace OAuth fixtures with factories and add test helper methods
1 parent 0a63655 commit a826684

File tree

2 files changed

+109
-116
lines changed

2 files changed

+109
-116
lines changed

testbed/core/factories.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import factory
22
from factory.django import DjangoModelFactory
33
from django.contrib.auth.models import User
4+
from datetime import datetime, timezone, timedelta
5+
from oauth2_provider.models import Application, AccessToken
46
from testbed.core.models import (
57
Actor,
68
Note,
@@ -128,4 +130,32 @@ def activities(self, create, extracted, **kwargs):
128130
note=None,
129131
visibility="public"
130132
)
131-
self.add_activity(activity)
133+
self.add_activity(activity)
134+
135+
# OAuth-related factories for testing authentication
136+
class ApplicationFactory(DjangoModelFactory):
137+
class Meta:
138+
model = Application
139+
140+
name = factory.Sequence(lambda n: f"Test Application {n}")
141+
client_type = Application.CLIENT_CONFIDENTIAL
142+
authorization_grant_type = Application.GRANT_AUTHORIZATION_CODE
143+
redirect_uris = "http://localhost:8000/callback/"
144+
145+
class AccessTokenFactory(DjangoModelFactory):
146+
class Meta:
147+
model = AccessToken
148+
149+
user = factory.SubFactory(UserOnlyFactory)
150+
application = factory.SubFactory(ApplicationFactory)
151+
token = factory.Sequence(lambda n: f"test-token-{n}")
152+
scope = "read write"
153+
expires = factory.LazyFunction(lambda: datetime.now(timezone.utc) + timedelta(hours=1))
154+
155+
class Params:
156+
lola_scope = factory.Trait(
157+
scope='activitypub_account_portability read write'
158+
)
159+
expired = factory.Trait(
160+
expires=factory.LazyFunction(lambda: datetime.now(timezone.utc) - timedelta(hours=1))
161+
)

testbed/core/tests/test_api.py

Lines changed: 78 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from django.contrib.auth import get_user_model
66
from oauth2_provider.models import Application, AccessToken
77
from testbed.core.models import Actor
8-
from testbed.core.factories import ActorFactory
8+
from testbed.core.factories import ActorFactory, ApplicationFactory, AccessTokenFactory
99
from testbed.core.tests.conftest import create_isolated_actor
1010
from testbed.core.json_ld_utils import (
1111
build_basic_context,
@@ -78,50 +78,44 @@ def test_outbox_not_found():
7878
Tests the complete request-response cycle with different authentication states
7979
to 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

Comments
 (0)