Skip to content

Commit 7ef731e

Browse files
committed
Implement PEP 752
1 parent bd56b0b commit 7ef731e

26 files changed

+1136
-63
lines changed

tests/common/db/organizations.py

+17
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@
1414

1515
import factory
1616
import faker
17+
import packaging.utils
1718

1819
from warehouse.organizations.models import (
20+
Namespace,
1921
Organization,
2022
OrganizationApplication,
2123
OrganizationInvitation,
@@ -186,3 +188,18 @@ class Meta:
186188
role_name = TeamProjectRoleType.Owner
187189
project = factory.SubFactory(ProjectFactory)
188190
team = factory.SubFactory(TeamFactory)
191+
192+
193+
class NamespaceFactory(WarehouseFactory):
194+
class Meta:
195+
model = Namespace
196+
197+
is_approved = True
198+
created = factory.Faker(
199+
"date_time_between_dates", datetime_start=datetime.datetime(2008, 1, 1)
200+
)
201+
name = factory.Faker("pystr", max_chars=12)
202+
normalized_name = factory.LazyAttribute(
203+
lambda o: packaging.utils.canonicalize_name(o.name)
204+
)
205+
owner = factory.SubFactory(OrganizationFactory)

tests/conftest.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
from warehouse.oidc.interfaces import IOIDCPublisherService
6060
from warehouse.oidc.utils import ACTIVESTATE_OIDC_ISSUER_URL, GITHUB_OIDC_ISSUER_URL
6161
from warehouse.organizations import services as organization_services
62-
from warehouse.organizations.interfaces import IOrganizationService
62+
from warehouse.organizations.interfaces import INamespaceService, IOrganizationService
6363
from warehouse.packaging import services as packaging_services
6464
from warehouse.packaging.interfaces import IProjectService
6565
from warehouse.subscriptions import services as subscription_services
@@ -153,6 +153,7 @@ def pyramid_services(
153153
email_service,
154154
metrics,
155155
organization_service,
156+
namespace_service,
156157
subscription_service,
157158
token_service,
158159
user_service,
@@ -171,6 +172,7 @@ def pyramid_services(
171172
services.register_service(email_service, IEmailSender, None, name="")
172173
services.register_service(metrics, IMetricsService, None, name="")
173174
services.register_service(organization_service, IOrganizationService, None, name="")
175+
services.register_service(namespace_service, INamespaceService, None, name="")
174176
services.register_service(subscription_service, ISubscriptionService, None, name="")
175177
services.register_service(token_service, ITokenService, None, name="password")
176178
services.register_service(token_service, ITokenService, None, name="email")
@@ -484,6 +486,11 @@ def organization_service(db_session):
484486
return organization_services.DatabaseOrganizationService(db_session)
485487

486488

489+
@pytest.fixture
490+
def namespace_service(db_session):
491+
return organization_services.DatabaseNamespaceService(db_session)
492+
493+
487494
@pytest.fixture
488495
def billing_service(app_config):
489496
stripe.api_base = app_config.registry.settings["billing.api_base"]

tests/unit/api/test_simple.py

+97
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@
2222
from warehouse.packaging.utils import API_VERSION, _valid_simple_detail_context
2323

2424
from ...common.db.accounts import UserFactory
25+
from ...common.db.organizations import (
26+
NamespaceFactory,
27+
OrganizationFactory,
28+
OrganizationProjectFactory,
29+
)
2530
from ...common.db.packaging import (
2631
AlternateRepositoryFactory,
2732
FileFactory,
@@ -221,6 +226,7 @@ def test_no_files_no_serial(self, db_request, content_type, renderer_override):
221226
"files": [],
222227
"versions": [],
223228
"alternate-locations": [],
229+
"namespace": None,
224230
}
225231
context = _update_context(context, content_type, renderer_override)
226232
assert simple.simple_detail(project, db_request) == context
@@ -253,6 +259,92 @@ def test_no_files_with_serial(self, db_request, content_type, renderer_override)
253259
"files": [],
254260
"versions": [],
255261
"alternate-locations": sorted(al.url for al in als),
262+
"namespace": None,
263+
}
264+
context = _update_context(context, content_type, renderer_override)
265+
assert simple.simple_detail(project, db_request) == context
266+
267+
assert db_request.response.headers["X-PyPI-Last-Serial"] == str(je.id)
268+
assert db_request.response.content_type == content_type
269+
_assert_has_cors_headers(db_request.response.headers)
270+
271+
if renderer_override is not None:
272+
assert db_request.override_renderer == renderer_override
273+
274+
@pytest.mark.parametrize(
275+
("content_type", "renderer_override"),
276+
CONTENT_TYPE_PARAMS,
277+
)
278+
def test_with_namespaces_authorized(
279+
self, db_request, content_type, renderer_override
280+
):
281+
db_request.accept = content_type
282+
org = OrganizationFactory.create()
283+
namespace = NamespaceFactory.create(owner=org)
284+
project = ProjectFactory.create(name=f"{namespace.name}-foo")
285+
OrganizationProjectFactory.create(organization=org, project=project)
286+
db_request.matchdict["name"] = project.normalized_name
287+
user = UserFactory.create()
288+
je = JournalEntryFactory.create(name=project.name, submitted_by=user)
289+
als = [
290+
AlternateRepositoryFactory.create(project=project),
291+
AlternateRepositoryFactory.create(project=project),
292+
]
293+
294+
context = {
295+
"meta": {"_last-serial": je.id, "api-version": API_VERSION},
296+
"name": project.normalized_name,
297+
"files": [],
298+
"versions": [],
299+
"alternate-locations": sorted(al.url for al in als),
300+
"namespace": {
301+
"prefix": namespace.normalized_name,
302+
"open": namespace.is_open,
303+
"authorized": True,
304+
},
305+
}
306+
context = _update_context(context, content_type, renderer_override)
307+
assert simple.simple_detail(project, db_request) == context
308+
309+
assert db_request.response.headers["X-PyPI-Last-Serial"] == str(je.id)
310+
assert db_request.response.content_type == content_type
311+
_assert_has_cors_headers(db_request.response.headers)
312+
313+
if renderer_override is not None:
314+
assert db_request.override_renderer == renderer_override
315+
316+
@pytest.mark.parametrize(
317+
("content_type", "renderer_override"),
318+
CONTENT_TYPE_PARAMS,
319+
)
320+
def test_with_namespaces_not_authorized(
321+
self, db_request, content_type, renderer_override
322+
):
323+
db_request.accept = content_type
324+
org = OrganizationFactory.create()
325+
namespace = NamespaceFactory.create(owner=org)
326+
project = ProjectFactory.create(name=f"{namespace.name}-foo")
327+
project2 = ProjectFactory.create(name=f"{namespace.name}-foo2")
328+
OrganizationProjectFactory.create(organization=org, project=project2)
329+
db_request.matchdict["name"] = project.normalized_name
330+
user = UserFactory.create()
331+
je = JournalEntryFactory.create(name=project.name, submitted_by=user)
332+
als = [
333+
AlternateRepositoryFactory.create(project=project),
334+
AlternateRepositoryFactory.create(project=project),
335+
]
336+
337+
context = {
338+
"meta": {"_last-serial": je.id, "api-version": API_VERSION},
339+
"name": project.normalized_name,
340+
"files": [],
341+
"versions": [],
342+
"alternate-locations": sorted(al.url for al in als),
343+
"namespace": {
344+
"prefix": namespace.normalized_name,
345+
"open": namespace.is_open,
346+
"authorized": False,
347+
},
256348
}
257349
context = _update_context(context, content_type, renderer_override)
258350
assert simple.simple_detail(project, db_request) == context
@@ -305,6 +397,7 @@ def test_with_files_no_serial(self, db_request, content_type, renderer_override)
305397
for f in files
306398
],
307399
"alternate-locations": [],
400+
"namespace": None,
308401
}
309402
context = _update_context(context, content_type, renderer_override)
310403
assert simple.simple_detail(project, db_request) == context
@@ -357,6 +450,7 @@ def test_with_files_with_serial(self, db_request, content_type, renderer_overrid
357450
for f in files
358451
],
359452
"alternate-locations": [],
453+
"namespace": None,
360454
}
361455
context = _update_context(context, content_type, renderer_override)
362456
assert simple.simple_detail(project, db_request) == context
@@ -454,6 +548,7 @@ def test_with_files_with_version_multi_digit(
454548
for f in files
455549
],
456550
"alternate-locations": [],
551+
"namespace": None,
457552
}
458553
context = _update_context(context, content_type, renderer_override)
459554
assert simple.simple_detail(project, db_request) == context
@@ -486,6 +581,7 @@ def test_with_files_quarantined_omitted_from_index(
486581
"files": [],
487582
"versions": [],
488583
"alternate-locations": [],
584+
"namespace": None,
489585
}
490586
context = _update_context(context, content_type, renderer_override)
491587

@@ -606,6 +702,7 @@ def route_url(route, **kw):
606702
for f in files
607703
],
608704
"alternate-locations": [],
705+
"namespace": None,
609706
}
610707
context = _update_context(context, content_type, renderer_override)
611708

tests/unit/manage/test_forms.py

+31
Original file line numberDiff line numberDiff line change
@@ -1108,3 +1108,34 @@ def test_validate(self, pyramid_request, name, errors):
11081108
# NOTE(jleightcap): testing with Regexp validators returns raw LazyString
11091109
# objects in the error dict's values. Just assert on keys.
11101110
assert list(form.errors.keys()) == errors
1111+
1112+
1113+
class TestRequestOrganizationNamespaceForm:
1114+
1115+
@pytest.mark.parametrize(
1116+
("name", "errors", "existing"),
1117+
[
1118+
("", ["name"], False),
1119+
(" namespace ", ["name"], False),
1120+
(".namespace", ["name"], False),
1121+
("namespace-", ["name"], False),
1122+
("namespace", ["name"], True),
1123+
("namespace", [], False),
1124+
],
1125+
)
1126+
def test_validate(self, pyramid_request, name, errors, existing):
1127+
pyramid_request.POST = MultiDict({"name": name})
1128+
namespace_service = pretend.stub(
1129+
get_namespace=lambda name: pretend.stub() if existing else None,
1130+
)
1131+
1132+
form = forms.RequestOrganizationNamespaceForm(
1133+
pyramid_request.POST,
1134+
namespace_service=namespace_service,
1135+
)
1136+
1137+
assert form.namespace_service is namespace_service
1138+
assert not form.validate() if errors else form.validate(), str(form.errors)
1139+
# NOTE(jleightcap): testing with Regexp validators returns raw LazyString
1140+
# objects in the error dict's values. Just assert on keys.
1141+
assert list(form.errors.keys()) == errors

tests/unit/manage/views/test_organizations.py

+91
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
from tests.common.db.accounts import EmailFactory, UserFactory
2424
from tests.common.db.organizations import (
25+
NamespaceFactory,
2526
OrganizationEventFactory,
2627
OrganizationFactory,
2728
OrganizationInvitationFactory,
@@ -3017,3 +3018,93 @@ def test_raises_404_with_out_of_range_page(self, db_request):
30173018

30183019
with pytest.raises(HTTPNotFound):
30193020
assert org_views.manage_organization_history(organization, db_request)
3021+
3022+
3023+
class TestManageOrganizationNamespaces:
3024+
@pytest.mark.usefixtures("_enable_organizations")
3025+
def test_manage_organization_namespaces(
3026+
self,
3027+
db_request,
3028+
pyramid_user,
3029+
organization_service,
3030+
monkeypatch,
3031+
):
3032+
organization = OrganizationFactory.create()
3033+
organization.namespaces = [NamespaceFactory.create()]
3034+
3035+
db_request.POST = MultiDict()
3036+
3037+
view = org_views.ManageOrganizationNamespacesViews(organization, db_request)
3038+
result = view.manage_organization_namespaces()
3039+
form = result["request_organization_namespace_form"]
3040+
3041+
assert view.request == db_request
3042+
assert view.organization_service == organization_service
3043+
assert result == {
3044+
"organization": organization,
3045+
"request_organization_namespace_form": form,
3046+
}
3047+
3048+
@pytest.mark.usefixtures("_enable_organizations")
3049+
def test_request_namespace(
3050+
self,
3051+
db_request,
3052+
pyramid_user,
3053+
namespace_service,
3054+
monkeypatch,
3055+
):
3056+
organization = OrganizationFactory.create()
3057+
organization.namespaces = [NamespaceFactory.create()]
3058+
3059+
db_request.POST = MultiDict({"name": "my-ns"})
3060+
3061+
OrganizationRoleFactory.create(
3062+
organization=organization, user=db_request.user, role_name="Owner"
3063+
)
3064+
3065+
def request_namespace(name, *args, **kwargs):
3066+
ns = NamespaceFactory.create(name=name)
3067+
organization.namespaces.append(ns)
3068+
return ns
3069+
3070+
monkeypatch.setattr(namespace_service, "request_namespace", request_namespace)
3071+
3072+
view = org_views.ManageOrganizationNamespacesViews(organization, db_request)
3073+
result = view.request_organization_namespace()
3074+
3075+
assert isinstance(result, HTTPSeeOther)
3076+
assert result.headers["Location"] == db_request.path
3077+
assert len(organization.namespaces) == 2
3078+
assert organization.namespaces[-1].name == "my-ns"
3079+
3080+
@pytest.mark.usefixtures("_enable_organizations")
3081+
def test_request_namespace_invalid(
3082+
self,
3083+
db_request,
3084+
pyramid_user,
3085+
namespace_service,
3086+
monkeypatch,
3087+
):
3088+
organization = OrganizationFactory.create()
3089+
organization.namespaces = [NamespaceFactory.create()]
3090+
3091+
OrganizationRoleFactory.create(
3092+
organization=organization, user=db_request.user, role_name="Owner"
3093+
)
3094+
3095+
db_request.POST = MultiDict({"name": organization.namespaces[0].name})
3096+
3097+
view = org_views.ManageOrganizationNamespacesViews(organization, db_request)
3098+
result = view.request_organization_namespace()
3099+
form = result["request_organization_namespace_form"]
3100+
3101+
assert view.request == db_request
3102+
assert view.namespace_service == namespace_service
3103+
assert result == {
3104+
"organization": organization,
3105+
"request_organization_namespace_form": form,
3106+
}
3107+
assert form.name.errors == [
3108+
"This namespace has already been requested. Choose a different namespace."
3109+
]
3110+
assert len(organization.namespaces) == 1

tests/unit/organizations/test_init.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@
1515
from celery.schedules import crontab
1616

1717
from warehouse import organizations
18-
from warehouse.organizations.interfaces import IOrganizationService
19-
from warehouse.organizations.services import database_organization_factory
18+
from warehouse.organizations.interfaces import INamespaceService, IOrganizationService
19+
from warehouse.organizations.services import (
20+
database_namespace_factory,
21+
database_organization_factory,
22+
)
2023
from warehouse.organizations.tasks import (
2124
delete_declined_organizations,
2225
update_organization_invitation_status,
@@ -36,6 +39,7 @@ def test_includeme():
3639

3740
assert config.register_service_factory.calls == [
3841
pretend.call(database_organization_factory, IOrganizationService),
42+
pretend.call(database_namespace_factory, INamespaceService),
3943
]
4044

4145
assert config.add_periodic_task.calls == [

0 commit comments

Comments
 (0)