Skip to content

Implement PEP 752 #17691

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions tests/common/db/organizations.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@

import factory
import faker
import packaging.utils

from warehouse.organizations.models import (
Namespace,
Organization,
OrganizationApplication,
OrganizationInvitation,
Expand Down Expand Up @@ -186,3 +188,18 @@ class Meta:
role_name = TeamProjectRoleType.Owner
project = factory.SubFactory(ProjectFactory)
team = factory.SubFactory(TeamFactory)


class NamespaceFactory(WarehouseFactory):
class Meta:
model = Namespace

is_approved = True
created = factory.Faker(
"date_time_between_dates", datetime_start=datetime.datetime(2008, 1, 1)
)
name = factory.Faker("pystr", max_chars=12)
normalized_name = factory.LazyAttribute(
lambda o: packaging.utils.canonicalize_name(o.name)
)
owner = factory.SubFactory(OrganizationFactory)
9 changes: 8 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
from warehouse.oidc.interfaces import IOIDCPublisherService
from warehouse.oidc.utils import ACTIVESTATE_OIDC_ISSUER_URL, GITHUB_OIDC_ISSUER_URL
from warehouse.organizations import services as organization_services
from warehouse.organizations.interfaces import IOrganizationService
from warehouse.organizations.interfaces import INamespaceService, IOrganizationService
from warehouse.packaging import services as packaging_services
from warehouse.packaging.interfaces import IProjectService
from warehouse.subscriptions import services as subscription_services
Expand Down Expand Up @@ -153,6 +153,7 @@ def pyramid_services(
email_service,
metrics,
organization_service,
namespace_service,
subscription_service,
token_service,
user_service,
Expand All @@ -171,6 +172,7 @@ def pyramid_services(
services.register_service(email_service, IEmailSender, None, name="")
services.register_service(metrics, IMetricsService, None, name="")
services.register_service(organization_service, IOrganizationService, None, name="")
services.register_service(namespace_service, INamespaceService, None, name="")
services.register_service(subscription_service, ISubscriptionService, None, name="")
services.register_service(token_service, ITokenService, None, name="password")
services.register_service(token_service, ITokenService, None, name="email")
Expand Down Expand Up @@ -484,6 +486,11 @@ def organization_service(db_session):
return organization_services.DatabaseOrganizationService(db_session)


@pytest.fixture
def namespace_service(db_session):
return organization_services.DatabaseNamespaceService(db_session)


@pytest.fixture
def billing_service(app_config):
stripe.api_base = app_config.registry.settings["billing.api_base"]
Expand Down
6 changes: 6 additions & 0 deletions tests/unit/admin/test_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ def test_includeme():
"/admin/organization_applications/{organization_application_id}/decline/",
domain=warehouse,
),
pretend.call("admin.namespace.list", "/admin/namespaces/", domain=warehouse),
pretend.call(
"admin.namespace.detail",
"/admin/namespaces/{namespace_id}/",
domain=warehouse,
),
pretend.call("admin.user.list", "/admin/users/", domain=warehouse),
pretend.call(
"admin.user.detail",
Expand Down
150 changes: 150 additions & 0 deletions tests/unit/admin/views/test_namespaces.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import pretend
import pytest

from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound

from warehouse.admin.views import namespaces as views

from ....common.db.organizations import NamespaceFactory


class TestNamespaceList:

def test_no_query(self, db_request):
namespaces = sorted(
NamespaceFactory.create_batch(30), key=lambda n: n.normalized_name
)
result = views.namespace_list(db_request)

assert result == {"namespaces": namespaces[:25], "query": "", "terms": []}

def test_with_page(self, db_request):
db_request.GET["page"] = "2"
namespaces = sorted(
NamespaceFactory.create_batch(30), key=lambda n: n.normalized_name
)
result = views.namespace_list(db_request)

assert result == {"namespaces": namespaces[25:], "query": "", "terms": []}

def test_with_invalid_page(self):
request = pretend.stub(
flags=pretend.stub(enabled=lambda *a: False),
params={"page": "not an integer"},
)

with pytest.raises(HTTPBadRequest):
views.namespace_list(request)

def test_basic_query(self, db_request):
namespaces = sorted(
NamespaceFactory.create_batch(5), key=lambda n: n.normalized_name
)
db_request.GET["q"] = namespaces[0].name
result = views.namespace_list(db_request)

assert namespaces[0] in result["namespaces"]
assert result["query"] == namespaces[0].name
assert result["terms"] == [namespaces[0].name]

def test_name_query(self, db_request):
namespaces = sorted(
NamespaceFactory.create_batch(5), key=lambda n: n.normalized_name
)
db_request.GET["q"] = f"name:{namespaces[0].name}"
result = views.namespace_list(db_request)

assert namespaces[0] in result["namespaces"]
assert result["query"] == f"name:{namespaces[0].name}"
assert result["terms"] == [f"name:{namespaces[0].name}"]

def test_organization_query(self, db_request):
namespaces = sorted(
NamespaceFactory.create_batch(5), key=lambda n: n.normalized_name
)
db_request.GET["q"] = f"organization:{namespaces[0].owner.name}"
result = views.namespace_list(db_request)

assert namespaces[0] in result["namespaces"]
assert result["query"] == f"organization:{namespaces[0].owner.name}"
assert result["terms"] == [f"organization:{namespaces[0].owner.name}"]

def test_is_approved_query(self, db_request):
namespaces = sorted(
NamespaceFactory.create_batch(5), key=lambda n: n.normalized_name
)
namespaces[0].is_approved = True
namespaces[1].is_approved = True
namespaces[2].is_approved = False
namespaces[3].is_approved = False
namespaces[4].is_approved = False
db_request.GET["q"] = "is:approved"
result = views.namespace_list(db_request)

assert result == {
"namespaces": namespaces[:2],
"query": "is:approved",
"terms": ["is:approved"],
}

def test_is_pending_query(self, db_request):
namespaces = sorted(
NamespaceFactory.create_batch(5), key=lambda n: n.normalized_name
)
namespaces[0].is_approved = True
namespaces[1].is_approved = True
namespaces[2].is_approved = False
namespaces[3].is_approved = False
namespaces[4].is_approved = False
db_request.GET["q"] = "is:pending"
result = views.namespace_list(db_request)

assert result == {
"namespaces": namespaces[2:],
"query": "is:pending",
"terms": ["is:pending"],
}

def test_is_invalid_query(self, db_request):
namespaces = sorted(
NamespaceFactory.create_batch(5), key=lambda n: n.normalized_name
)
db_request.GET["q"] = "is:not-actually-a-valid-query"
result = views.namespace_list(db_request)

assert result == {
"namespaces": namespaces[:25],
"query": "is:not-actually-a-valid-query",
"terms": ["is:not-actually-a-valid-query"],
}


class TestNamespaceDetail:
def test_detail(self, db_request):
namespaces = sorted(
NamespaceFactory.create_batch(5), key=lambda n: n.normalized_name
)
db_request.matchdict["namespace_id"] = str(namespaces[1].id)

assert views.namespace_detail(db_request) == {
"namespace": namespaces[1],
}

def test_detail_not_found(self, db_request):
NamespaceFactory.create_batch(5)
db_request.matchdict["namespace_id"] = "c6a1a66b-d1af-45fc-ae9f-21b36662c2ac"

with pytest.raises(HTTPNotFound):
views.namespace_detail(db_request)
97 changes: 97 additions & 0 deletions tests/unit/api/test_simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@
from warehouse.packaging.utils import API_VERSION, _valid_simple_detail_context

from ...common.db.accounts import UserFactory
from ...common.db.organizations import (
NamespaceFactory,
OrganizationFactory,
OrganizationProjectFactory,
)
from ...common.db.packaging import (
AlternateRepositoryFactory,
FileFactory,
Expand Down Expand Up @@ -221,6 +226,7 @@ def test_no_files_no_serial(self, db_request, content_type, renderer_override):
"files": [],
"versions": [],
"alternate-locations": [],
"namespace": None,
}
context = _update_context(context, content_type, renderer_override)
assert simple.simple_detail(project, db_request) == context
Expand Down Expand Up @@ -253,6 +259,92 @@ def test_no_files_with_serial(self, db_request, content_type, renderer_override)
"files": [],
"versions": [],
"alternate-locations": sorted(al.url for al in als),
"namespace": None,
}
context = _update_context(context, content_type, renderer_override)
assert simple.simple_detail(project, db_request) == context

assert db_request.response.headers["X-PyPI-Last-Serial"] == str(je.id)
assert db_request.response.content_type == content_type
_assert_has_cors_headers(db_request.response.headers)

if renderer_override is not None:
assert db_request.override_renderer == renderer_override

@pytest.mark.parametrize(
("content_type", "renderer_override"),
CONTENT_TYPE_PARAMS,
)
def test_with_namespaces_authorized(
self, db_request, content_type, renderer_override
):
db_request.accept = content_type
org = OrganizationFactory.create()
namespace = NamespaceFactory.create(owner=org)
project = ProjectFactory.create(name=f"{namespace.name}-foo")
OrganizationProjectFactory.create(organization=org, project=project)
db_request.matchdict["name"] = project.normalized_name
user = UserFactory.create()
je = JournalEntryFactory.create(name=project.name, submitted_by=user)
als = [
AlternateRepositoryFactory.create(project=project),
AlternateRepositoryFactory.create(project=project),
]

context = {
"meta": {"_last-serial": je.id, "api-version": API_VERSION},
"name": project.normalized_name,
"files": [],
"versions": [],
"alternate-locations": sorted(al.url for al in als),
"namespace": {
"prefix": namespace.normalized_name,
"open": namespace.is_open,
"authorized": True,
},
}
context = _update_context(context, content_type, renderer_override)
assert simple.simple_detail(project, db_request) == context

assert db_request.response.headers["X-PyPI-Last-Serial"] == str(je.id)
assert db_request.response.content_type == content_type
_assert_has_cors_headers(db_request.response.headers)

if renderer_override is not None:
assert db_request.override_renderer == renderer_override

@pytest.mark.parametrize(
("content_type", "renderer_override"),
CONTENT_TYPE_PARAMS,
)
def test_with_namespaces_not_authorized(
self, db_request, content_type, renderer_override
):
db_request.accept = content_type
org = OrganizationFactory.create()
namespace = NamespaceFactory.create(owner=org)
project = ProjectFactory.create(name=f"{namespace.name}-foo")
project2 = ProjectFactory.create(name=f"{namespace.name}-foo2")
OrganizationProjectFactory.create(organization=org, project=project2)
db_request.matchdict["name"] = project.normalized_name
user = UserFactory.create()
je = JournalEntryFactory.create(name=project.name, submitted_by=user)
als = [
AlternateRepositoryFactory.create(project=project),
AlternateRepositoryFactory.create(project=project),
]

context = {
"meta": {"_last-serial": je.id, "api-version": API_VERSION},
"name": project.normalized_name,
"files": [],
"versions": [],
"alternate-locations": sorted(al.url for al in als),
"namespace": {
"prefix": namespace.normalized_name,
"open": namespace.is_open,
"authorized": False,
},
}
context = _update_context(context, content_type, renderer_override)
assert simple.simple_detail(project, db_request) == context
Expand Down Expand Up @@ -305,6 +397,7 @@ def test_with_files_no_serial(self, db_request, content_type, renderer_override)
for f in files
],
"alternate-locations": [],
"namespace": None,
}
context = _update_context(context, content_type, renderer_override)
assert simple.simple_detail(project, db_request) == context
Expand Down Expand Up @@ -357,6 +450,7 @@ def test_with_files_with_serial(self, db_request, content_type, renderer_overrid
for f in files
],
"alternate-locations": [],
"namespace": None,
}
context = _update_context(context, content_type, renderer_override)
assert simple.simple_detail(project, db_request) == context
Expand Down Expand Up @@ -454,6 +548,7 @@ def test_with_files_with_version_multi_digit(
for f in files
],
"alternate-locations": [],
"namespace": None,
}
context = _update_context(context, content_type, renderer_override)
assert simple.simple_detail(project, db_request) == context
Expand Down Expand Up @@ -486,6 +581,7 @@ def test_with_files_quarantined_omitted_from_index(
"files": [],
"versions": [],
"alternate-locations": [],
"namespace": None,
}
context = _update_context(context, content_type, renderer_override)

Expand Down Expand Up @@ -606,6 +702,7 @@ def route_url(route, **kw):
for f in files
],
"alternate-locations": [],
"namespace": None,
}
context = _update_context(context, content_type, renderer_override)

Expand Down
Loading