Skip to content

Commit 3b22026

Browse files
committed
Implement PEP 752
1 parent a1f73fd commit 3b22026

39 files changed

+1671
-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/admin/test_routes.py

+6
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ def test_includeme():
5454
"/admin/organization_applications/{organization_application_id}/decline/",
5555
domain=warehouse,
5656
),
57+
pretend.call("admin.namespace.list", "/admin/namespaces/", domain=warehouse),
58+
pretend.call(
59+
"admin.namespace.detail",
60+
"/admin/namespaces/{namespace_id}/",
61+
domain=warehouse,
62+
),
5763
pretend.call("admin.user.list", "/admin/users/", domain=warehouse),
5864
pretend.call(
5965
"admin.user.detail",
+150
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License");
2+
# you may not use this file except in compliance with the License.
3+
# You may obtain a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS,
9+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
# See the License for the specific language governing permissions and
11+
# limitations under the License.
12+
13+
import pretend
14+
import pytest
15+
16+
from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound
17+
18+
from warehouse.admin.views import namespaces as views
19+
20+
from ....common.db.organizations import NamespaceFactory
21+
22+
23+
class TestNamespaceList:
24+
25+
def test_no_query(self, db_request):
26+
namespaces = sorted(
27+
NamespaceFactory.create_batch(30), key=lambda n: n.normalized_name
28+
)
29+
result = views.namespace_list(db_request)
30+
31+
assert result == {"namespaces": namespaces[:25], "query": "", "terms": []}
32+
33+
def test_with_page(self, db_request):
34+
db_request.GET["page"] = "2"
35+
namespaces = sorted(
36+
NamespaceFactory.create_batch(30), key=lambda n: n.normalized_name
37+
)
38+
result = views.namespace_list(db_request)
39+
40+
assert result == {"namespaces": namespaces[25:], "query": "", "terms": []}
41+
42+
def test_with_invalid_page(self):
43+
request = pretend.stub(
44+
flags=pretend.stub(enabled=lambda *a: False),
45+
params={"page": "not an integer"},
46+
)
47+
48+
with pytest.raises(HTTPBadRequest):
49+
views.namespace_list(request)
50+
51+
def test_basic_query(self, db_request):
52+
namespaces = sorted(
53+
NamespaceFactory.create_batch(5), key=lambda n: n.normalized_name
54+
)
55+
db_request.GET["q"] = namespaces[0].name
56+
result = views.namespace_list(db_request)
57+
58+
assert namespaces[0] in result["namespaces"]
59+
assert result["query"] == namespaces[0].name
60+
assert result["terms"] == [namespaces[0].name]
61+
62+
def test_name_query(self, db_request):
63+
namespaces = sorted(
64+
NamespaceFactory.create_batch(5), key=lambda n: n.normalized_name
65+
)
66+
db_request.GET["q"] = f"name:{namespaces[0].name}"
67+
result = views.namespace_list(db_request)
68+
69+
assert namespaces[0] in result["namespaces"]
70+
assert result["query"] == f"name:{namespaces[0].name}"
71+
assert result["terms"] == [f"name:{namespaces[0].name}"]
72+
73+
def test_organization_query(self, db_request):
74+
namespaces = sorted(
75+
NamespaceFactory.create_batch(5), key=lambda n: n.normalized_name
76+
)
77+
db_request.GET["q"] = f"organization:{namespaces[0].owner.name}"
78+
result = views.namespace_list(db_request)
79+
80+
assert namespaces[0] in result["namespaces"]
81+
assert result["query"] == f"organization:{namespaces[0].owner.name}"
82+
assert result["terms"] == [f"organization:{namespaces[0].owner.name}"]
83+
84+
def test_is_approved_query(self, db_request):
85+
namespaces = sorted(
86+
NamespaceFactory.create_batch(5), key=lambda n: n.normalized_name
87+
)
88+
namespaces[0].is_approved = True
89+
namespaces[1].is_approved = True
90+
namespaces[2].is_approved = False
91+
namespaces[3].is_approved = False
92+
namespaces[4].is_approved = False
93+
db_request.GET["q"] = "is:approved"
94+
result = views.namespace_list(db_request)
95+
96+
assert result == {
97+
"namespaces": namespaces[:2],
98+
"query": "is:approved",
99+
"terms": ["is:approved"],
100+
}
101+
102+
def test_is_pending_query(self, db_request):
103+
namespaces = sorted(
104+
NamespaceFactory.create_batch(5), key=lambda n: n.normalized_name
105+
)
106+
namespaces[0].is_approved = True
107+
namespaces[1].is_approved = True
108+
namespaces[2].is_approved = False
109+
namespaces[3].is_approved = False
110+
namespaces[4].is_approved = False
111+
db_request.GET["q"] = "is:pending"
112+
result = views.namespace_list(db_request)
113+
114+
assert result == {
115+
"namespaces": namespaces[2:],
116+
"query": "is:pending",
117+
"terms": ["is:pending"],
118+
}
119+
120+
def test_is_invalid_query(self, db_request):
121+
namespaces = sorted(
122+
NamespaceFactory.create_batch(5), key=lambda n: n.normalized_name
123+
)
124+
db_request.GET["q"] = "is:not-actually-a-valid-query"
125+
result = views.namespace_list(db_request)
126+
127+
assert result == {
128+
"namespaces": namespaces[:25],
129+
"query": "is:not-actually-a-valid-query",
130+
"terms": ["is:not-actually-a-valid-query"],
131+
}
132+
133+
134+
class TestNamespaceDetail:
135+
def test_detail(self, db_request):
136+
namespaces = sorted(
137+
NamespaceFactory.create_batch(5), key=lambda n: n.normalized_name
138+
)
139+
db_request.matchdict["namespace_id"] = str(namespaces[1].id)
140+
141+
assert views.namespace_detail(db_request) == {
142+
"namespace": namespaces[1],
143+
}
144+
145+
def test_detail_not_found(self, db_request):
146+
NamespaceFactory.create_batch(5)
147+
db_request.matchdict["namespace_id"] = "c6a1a66b-d1af-45fc-ae9f-21b36662c2ac"
148+
149+
with pytest.raises(HTTPNotFound):
150+
views.namespace_detail(db_request)

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

0 commit comments

Comments
 (0)