Skip to content

Commit

Permalink
feat: add Observers and Observations models (#14834)
Browse files Browse the repository at this point in the history
  • Loading branch information
miketheman authored Nov 20, 2023
1 parent d5ee54d commit c20a241
Show file tree
Hide file tree
Showing 18 changed files with 1,318 additions and 4 deletions.
20 changes: 20 additions & 0 deletions tests/common/db/observations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# 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.

from warehouse.observations.models import Observer

from .base import WarehouseFactory


class ObserverFactory(WarehouseFactory):
class Meta:
model = Observer
13 changes: 13 additions & 0 deletions tests/common/db/packaging.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import faker
import packaging.utils

from warehouse.observations.models import ObservationKind
from warehouse.packaging.models import (
Dependency,
DependencyKind,
Expand Down Expand Up @@ -56,6 +57,18 @@ class Meta:
source = factory.SubFactory(ProjectFactory)


class ProjectObservationFactory(WarehouseFactory):
class Meta:
model = Project.Observation

kind = factory.Faker(
"random_element", elements=[kind.value[1] for kind in ObservationKind]
)
payload = factory.Faker("json")
# TODO: add `observer` field
summary = factory.Faker("paragraph")


class DescriptionFactory(WarehouseFactory):
class Meta:
model = Description
Expand Down
28 changes: 28 additions & 0 deletions tests/unit/admin/test_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,34 @@ def test_includeme():
traverse="/{project_name}/{version}",
domain=warehouse,
),
pretend.call(
"admin.project.observations",
"/admin/projects/{project_name}/observations/",
factory="warehouse.packaging.models:ProjectFactory",
traverse="/{project_name}",
domain=warehouse,
),
pretend.call(
"admin.project.add_project_observation",
"/admin/projects/{project_name}/add_project_observation/",
factory="warehouse.packaging.models:ProjectFactory",
traverse="/{project_name}",
domain=warehouse,
),
pretend.call(
"admin.project.release.observations",
"/admin/projects/{project_name}/release/{version}/observations/",
factory="warehouse.packaging.models:ProjectFactory",
traverse="/{project_name}/{version}",
domain=warehouse,
),
pretend.call(
"admin.project.release.add_release_observation",
"/admin/projects/{project_name}/release/{version}/add_release_observation/",
factory="warehouse.packaging.models:ProjectFactory",
traverse="/{project_name}/{version}",
domain=warehouse,
),
pretend.call(
"admin.project.journals",
"/admin/projects/{project_name}/journals/",
Expand Down
192 changes: 192 additions & 0 deletions tests/unit/admin/views/test_projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,17 @@

from tests.common.db.oidc import GitHubPublisherFactory
from warehouse.admin.views import projects as views
from warehouse.observations.models import ObservationKind
from warehouse.packaging.models import Project, Role
from warehouse.packaging.tasks import update_release_description
from warehouse.search.tasks import reindex_project

from ....common.db.accounts import UserFactory
from ....common.db.observations import ObserverFactory
from ....common.db.packaging import (
JournalEntryFactory,
ProjectFactory,
ProjectObservationFactory,
ReleaseFactory,
RoleFactory,
)
Expand Down Expand Up @@ -101,6 +104,8 @@ def test_gets_project(self, db_request):
"MAX_PROJECT_SIZE": views.MAX_PROJECT_SIZE,
"ONE_GB": views.ONE_GB,
"UPLOAD_LIMIT_CAP": views.UPLOAD_LIMIT_CAP,
"observation_kinds": ObservationKind,
"observations": [],
}

def test_non_normalized_name(self, db_request):
Expand Down Expand Up @@ -128,6 +133,8 @@ def test_gets_release(self, db_request):
assert views.release_detail(release, db_request) == {
"release": release,
"journals": journals,
"observation_kinds": ObservationKind,
"observations": [],
}

def test_release_render(self, db_request):
Expand Down Expand Up @@ -157,6 +164,80 @@ def test_release_render(self, db_request):
]


class TestReleaseAddObservation:
def test_add_observation(self, db_request):
release = ReleaseFactory.create()
user = UserFactory.create()
db_request.route_path = pretend.call_recorder(
lambda *a, **kw: "/admin/projects/"
)
db_request.matchdict["project_name"] = release.project.normalized_name
db_request.POST["kind"] = ObservationKind.IsSpam.value[0]
db_request.POST["summary"] = "This is a summary"
db_request.user = user

views.add_release_observation(release, db_request)

assert len(release.observations) == 1

def test_no_kind_errors(self):
release = pretend.stub(
project=pretend.stub(name="foo", normalized_name="foo"), version="1.0"
)
request = pretend.stub(
POST={},
session=pretend.stub(flash=pretend.call_recorder(lambda *a, **kw: None)),
route_path=lambda *a, **kw: "/foo/bar/",
)

with pytest.raises(HTTPSeeOther) as exc:
views.add_release_observation(release, request)
assert exc.value.status_code == 303
assert exc.value.headers["Location"] == "/foo/bar/"

assert request.session.flash.calls == [
pretend.call("Provide a kind", queue="error")
]

def test_invalid_kind_errors(self):
release = pretend.stub(
project=pretend.stub(name="foo", normalized_name="foo"), version="1.0"
)
request = pretend.stub(
POST={"kind": "not a valid kind"},
session=pretend.stub(flash=pretend.call_recorder(lambda *a, **kw: None)),
route_path=lambda *a, **kw: "/foo/bar/",
)

with pytest.raises(HTTPSeeOther) as exc:
views.add_release_observation(release, request)
assert exc.value.status_code == 303
assert exc.value.headers["Location"] == "/foo/bar/"

assert request.session.flash.calls == [
pretend.call("Invalid kind", queue="error")
]

def test_no_summary_errors(self):
release = pretend.stub(
project=pretend.stub(name="foo", normalized_name="foo"), version="1.0"
)
request = pretend.stub(
POST={"kind": ObservationKind.IsSpam.value[0]},
session=pretend.stub(flash=pretend.call_recorder(lambda *a, **kw: None)),
route_path=lambda *a, **kw: "/foo/bar/",
)

with pytest.raises(HTTPSeeOther) as exc:
views.add_release_observation(release, request)
assert exc.value.status_code == 303
assert exc.value.headers["Location"] == "/foo/bar/"

assert request.session.flash.calls == [
pretend.call("Provide a summary", queue="error")
]


class TestProjectReleasesList:
def test_no_query(self, db_request):
project = ProjectFactory.create()
Expand Down Expand Up @@ -347,6 +428,117 @@ def test_non_normalized_name(self, db_request):
views.journals_list(project, db_request)


class TestProjectObservationsList:
def test_with_page(self, db_request):
observer = ObserverFactory.create()
UserFactory.create(observer=observer)
project = ProjectFactory.create()
observations = ProjectObservationFactory.create_batch(
size=30, related=project, observer=observer
)

db_request.matchdict["project_name"] = project.normalized_name
db_request.GET["page"] = "2"
result = views.project_observations_list(project, db_request)

assert result == {
"observations": observations[25:],
"project": project,
}

def test_with_invalid_page(self, db_request):
project = ProjectFactory.create()
db_request.matchdict["project_name"] = project.normalized_name
db_request.GET["page"] = "not an integer"

with pytest.raises(HTTPBadRequest):
views.project_observations_list(project, db_request)


class TestProjectAddObservation:
def test_add_observation(self, db_request):
project = ProjectFactory.create()
observer = ObserverFactory.create()
user = UserFactory.create(observer=observer)
db_request.route_path = pretend.call_recorder(
lambda *a, **kw: "/admin/projects/"
)
db_request.matchdict["project_name"] = project.normalized_name
db_request.POST["kind"] = ObservationKind.IsSpam.value[0]
db_request.POST["summary"] = "This is a summary"
db_request.user = user

views.add_project_observation(project, db_request)

assert len(project.observations) == 1

def test_no_user_observer(self, db_request):
project = ProjectFactory.create()
user = UserFactory.create()
db_request.route_path = pretend.call_recorder(
lambda *a, **kw: "/admin/projects/"
)
db_request.matchdict["project_name"] = project.normalized_name
db_request.POST["kind"] = ObservationKind.IsSpam.value[0]
db_request.POST["summary"] = "This is a summary"
db_request.user = user

views.add_project_observation(project, db_request)

assert len(project.observations) == 1

def test_no_kind_errors(self):
project = pretend.stub(name="foo", normalized_name="foo")
request = pretend.stub(
POST={},
session=pretend.stub(flash=pretend.call_recorder(lambda *a, **kw: None)),
route_path=lambda *a, **kw: "/foo/bar/",
)

with pytest.raises(HTTPSeeOther) as exc:
views.add_project_observation(project, request)
assert exc.value.status_code == 303
assert exc.value.headers["Location"] == "/foo/bar/"

assert request.session.flash.calls == [
pretend.call("Provide a kind", queue="error")
]

def test_invalid_kind_errors(self):
project = pretend.stub(name="foo", normalized_name="foo")
request = pretend.stub(
POST={"kind": "not a valid kind"},
session=pretend.stub(flash=pretend.call_recorder(lambda *a, **kw: None)),
route_path=lambda *a, **kw: "/foo/bar/",
)

with pytest.raises(HTTPSeeOther) as exc:
views.add_project_observation(project, request)
assert exc.value.status_code == 303
assert exc.value.headers["Location"] == "/foo/bar/"

assert request.session.flash.calls == [
pretend.call("Invalid kind", queue="error")
]

def test_no_summary_errors(self):
project = pretend.stub(name="foo", normalized_name="foo")
request = pretend.stub(
POST={"kind": ObservationKind.IsSpam.value[0]},
session=pretend.stub(flash=pretend.call_recorder(lambda *a, **kw: None)),
route_path=lambda *a, **kw: "/foo/bar/",
)

with pytest.raises(HTTPSeeOther) as exc:
views.add_project_observation(project, request)
assert exc.value.status_code == 303
assert exc.value.headers["Location"] == "/foo/bar/"

assert request.session.flash.calls == [
pretend.call("Provide a summary", queue="error")
]


class TestProjectSetTotalSizeLimit:
def test_sets_total_size_limitwith_integer(self, db_request):
project = ProjectFactory.create(name="foo")
Expand Down
11 changes: 11 additions & 0 deletions tests/unit/observations/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# 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.
Loading

0 comments on commit c20a241

Please sign in to comment.