Skip to content

Commit 2b8c50a

Browse files
committed
added e2e test for oauth1
1 parent 3d17d1c commit 2b8c50a

File tree

8 files changed

+303
-37
lines changed

8 files changed

+303
-37
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Generated by Django 4.2.7 on 2024-07-02 13:13
2+
3+
import django.db.models.deletion
4+
from django.db import (
5+
migrations,
6+
models,
7+
)
8+
9+
import addon_service.common.str_uuid_field
10+
11+
12+
class Migration(migrations.Migration):
13+
14+
dependencies = [
15+
("addon_service", "0001_initial"),
16+
]
17+
18+
operations = [
19+
migrations.CreateModel(
20+
name="OAuth1ClientConfig",
21+
fields=[
22+
(
23+
"id",
24+
addon_service.common.str_uuid_field.StrUUIDField(
25+
default=addon_service.common.str_uuid_field.str_uuid4,
26+
editable=False,
27+
primary_key=True,
28+
serialize=False,
29+
),
30+
),
31+
("created", models.DateTimeField(editable=False)),
32+
("modified", models.DateTimeField()),
33+
("request_token_url", models.URLField()),
34+
("auth_url", models.URLField()),
35+
("access_token_url", models.URLField()),
36+
("client_key", models.CharField(null=True)),
37+
("client_secret", models.CharField(null=True)),
38+
],
39+
options={
40+
"verbose_name": "OAuth1 Client Config",
41+
"verbose_name_plural": "OAuth1 Client Configs",
42+
},
43+
),
44+
migrations.AddField(
45+
model_name="externalstorageservice",
46+
name="oauth1_client_config",
47+
field=models.ForeignKey(
48+
blank=True,
49+
null=True,
50+
on_delete=django.db.models.deletion.SET_NULL,
51+
related_name="external_storage_services",
52+
to="addon_service.oauth1clientconfig",
53+
),
54+
),
55+
]
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 4.2.7 on 2024-07-03 11:01
2+
3+
from django.db import (
4+
migrations,
5+
models,
6+
)
7+
8+
9+
class Migration(migrations.Migration):
10+
11+
dependencies = [
12+
("addon_service", "0002_oauth1clientconfig_and_more"),
13+
]
14+
15+
operations = [
16+
migrations.AddField(
17+
model_name="authorizedstorageaccount",
18+
name="is_oauth1_ready",
19+
field=models.BooleanField(
20+
blank=True, null=True, verbose_name="addon_service.OAuth2TokenMetadata"
21+
),
22+
),
23+
]

addon_service/tests/_factories.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,17 @@ class Meta:
3636
client_secret = factory.Faker("word")
3737

3838

39+
class OAuth1ClientConfigFactory(DjangoModelFactory):
40+
class Meta:
41+
model = db.OAuth1ClientConfig
42+
43+
auth_url = "https://api.example/auth/"
44+
access_token_url = "https://osf.example/oauth/access"
45+
request_token_url = "https://api.example.com/oauth/request"
46+
client_key = factory.Faker("word")
47+
client_secret = factory.Faker("word")
48+
49+
3950
class AddonOperationInvocationFactory(DjangoModelFactory):
4051
class Meta:
4152
model = db.AddonOperationInvocation
@@ -64,7 +75,6 @@ class Meta:
6475
max_concurrent_downloads = factory.Faker("pyint")
6576
max_upload_mb = factory.Faker("pyint")
6677
int_addon_imp = known_imps.get_imp_number(known_imps.get_imp_by_name("BLARG"))
67-
oauth2_client_config = factory.SubFactory(OAuth2ClientConfigFactory)
6878
supported_scopes = ["service.url/grant_all"]
6979

7080
@classmethod
@@ -89,6 +99,27 @@ def _create(
8999
)
90100

91101

102+
class ExternalStorageOAuth2ServiceFactory(ExternalStorageServiceFactory):
103+
oauth2_client_config = factory.SubFactory(OAuth2ClientConfigFactory)
104+
105+
106+
class ExternalStorageOAuth1ServiceFactory(ExternalStorageServiceFactory):
107+
oauth1_client_config = factory.SubFactory(OAuth1ClientConfigFactory)
108+
109+
@classmethod
110+
def _create(
111+
cls,
112+
model_class,
113+
credentials_format=CredentialsFormats.OAUTH1A,
114+
service_type=ServiceTypes.PUBLIC,
115+
*args,
116+
**kwargs,
117+
):
118+
return super()._create(
119+
model_class, credentials_format, service_type, *args, **kwargs
120+
)
121+
122+
92123
class AuthorizedStorageAccountFactory(DjangoModelFactory):
93124
class Meta:
94125
model = db.AuthorizedStorageAccount
@@ -112,7 +143,9 @@ def _create(
112143
account = super()._create(
113144
model_class=model_class,
114145
external_storage_service=external_storage_service
115-
or ExternalStorageServiceFactory(credentials_format=credentials_format),
146+
or ExternalStorageOAuth2ServiceFactory(
147+
credentials_format=credentials_format
148+
),
116149
account_owner=account_owner or UserReferenceFactory(),
117150
*args,
118151
**kwargs,

addon_service/tests/_helpers.py

Lines changed: 96 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,14 @@
33
import secrets
44
from collections import defaultdict
55
from http import HTTPStatus
6-
from typing import Any
7-
from unittest.mock import patch
6+
from typing import (
7+
TYPE_CHECKING,
8+
Any,
9+
)
10+
from unittest.mock import (
11+
AsyncMock,
12+
patch,
13+
)
814
from urllib.parse import (
915
parse_qs,
1016
urlparse,
@@ -20,6 +26,10 @@
2026
from addon_service.common.aiohttp_session import get_singleton_client_session
2127

2228

29+
if TYPE_CHECKING:
30+
from addon_service.tests._factories import ExternalStorageOAuth1ServiceFactory
31+
32+
2333
class MockOSF:
2434
_configured_caller_uri: str | None = None
2535
_permissions: dict[str, dict[str, str | bool]]
@@ -45,13 +55,17 @@ def __init__(self, permissions=None):
4555

4656
@contextlib.contextmanager
4757
def mocking(self):
48-
with patch(
49-
"addon_service.authentication.GVCombinedAuthentication.authenticate",
50-
side_effect=self._mock_user_check,
51-
), patch(
52-
"addon_service.common.osf.has_osf_permission_on_resource",
53-
side_effect=self._mock_resource_check,
54-
), patch_encryption_key_derivation():
58+
with (
59+
patch(
60+
"addon_service.authentication.GVCombinedAuthentication.authenticate",
61+
side_effect=self._mock_user_check,
62+
),
63+
patch(
64+
"addon_service.common.osf.has_osf_permission_on_resource",
65+
side_effect=self._mock_resource_check,
66+
),
67+
patch_encryption_key_derivation(),
68+
):
5569
yield self
5670

5771
def configure_assumed_caller(self, caller_uri):
@@ -159,6 +173,79 @@ async def _route_post(self, url, *args, **kwargs):
159173
raise RuntimeError(f"Received unrecognized endpoint {url}")
160174

161175

176+
@dataclasses.dataclass
177+
class MockServiceProvider:
178+
_external_service: "ExternalStorageOAuth1ServiceFactory"
179+
_static_request_token: str
180+
_static_request_secret: str
181+
_static_verifier: str
182+
_static_oauth_token: str
183+
_static_oauth_secret: str
184+
185+
def __post_init__(self):
186+
if self._external_service.oauth1_client_config is not None:
187+
self._access_token_url = (
188+
self._external_service.oauth1_client_config.access_token_url
189+
)
190+
self._request_token_url = (
191+
self._external_service.oauth1_client_config.request_token_url
192+
)
193+
194+
@property
195+
def auth_url(self):
196+
return self._external_service.auth_url
197+
198+
def set_internal_client(self, client):
199+
"""Attach a DRF APIClient for making requests internally"""
200+
self._internal_client = client
201+
202+
@contextlib.asynccontextmanager
203+
async def amocking(self):
204+
client_session = await get_singleton_client_session()
205+
with (
206+
patch.object(client_session, "get", new=self._route_get),
207+
patch.object(client_session, "post", new=self._route_post),
208+
):
209+
yield self
210+
211+
@contextlib.contextmanager
212+
def mocking(self):
213+
with patch(
214+
"addon_service.oauth1.utils.get_singleton_client_session",
215+
AsyncMock(return_value=AsyncMock(post=self._route_post)),
216+
):
217+
yield self
218+
219+
def initiate_oauth_exchange(self):
220+
self._internal_client.get(
221+
reverse("oauth1-callback"),
222+
{"oauth_token": "oauth_token", "oauth_verifier": "oauth_verifier"},
223+
)
224+
return _FakeAiohttpResponse()
225+
226+
@contextlib.asynccontextmanager
227+
async def _route_post(self, url, *args, **kwargs):
228+
if url.startswith(self._access_token_url):
229+
yield _FakeAiohttpResponse(
230+
status=HTTPStatus.CREATED,
231+
data={
232+
"oauth_token": self._static_oauth_token,
233+
"oauth_token_secret": self._static_oauth_secret,
234+
},
235+
)
236+
elif url.startswith(self._request_token_url):
237+
yield _FakeAiohttpResponse(
238+
status=HTTPStatus.CREATED,
239+
data={
240+
"oauth_token": self._static_request_token,
241+
"oauth_token_secret": self._static_request_secret,
242+
"oauth_verifier": self._static_verifier,
243+
},
244+
)
245+
else:
246+
raise RuntimeError(f"Received unrecognized endpoint {url}")
247+
248+
162249
@dataclasses.dataclass
163250
class _FakeAiohttpResponse:
164251
status: HTTPStatus = HTTPStatus.OK

0 commit comments

Comments
 (0)