From ce1af8b922c87548e30cb5fd05a34776e479cef1 Mon Sep 17 00:00:00 2001 From: Oksamies Date: Tue, 26 Mar 2024 22:14:41 +0200 Subject: [PATCH] Add CS Package Deprecate form and view --- .../api/cyberstorm/tests/test_package.py | 98 +++++++++++++ .../api/cyberstorm/views/__init__.py | 2 + .../api/cyberstorm/views/package.py | 48 ++++++ django/thunderstore/api/urls.py | 6 + .../thunderstore/repository/forms/__init__.py | 1 + .../thunderstore/repository/forms/package.py | 34 +++++ .../repository/tests/test_package_forms.py | 137 ++++++++++++++++++ 7 files changed, 326 insertions(+) create mode 100644 django/thunderstore/api/cyberstorm/tests/test_package.py create mode 100644 django/thunderstore/api/cyberstorm/views/package.py create mode 100644 django/thunderstore/repository/forms/package.py create mode 100644 django/thunderstore/repository/tests/test_package_forms.py diff --git a/django/thunderstore/api/cyberstorm/tests/test_package.py b/django/thunderstore/api/cyberstorm/tests/test_package.py new file mode 100644 index 000000000..fa2878d2c --- /dev/null +++ b/django/thunderstore/api/cyberstorm/tests/test_package.py @@ -0,0 +1,98 @@ +import json + +import pytest +from rest_framework.test import APIClient + +from thunderstore.core.types import UserType +from thunderstore.repository.models import Package +from thunderstore.repository.models.team import TeamMember + + +@pytest.mark.django_db +def test_package_deprecate_api_view__succeeds( + api_client: APIClient, + package: Package, + team_member: TeamMember, +) -> None: + api_client.force_authenticate(team_member.user) + + assert Package.objects.get(pk=package.pk).is_deprecated == False + + response = api_client.post( + f"/api/cyberstorm/package/{package.namespace}/{package.name}/deprecate/", + json.dumps({"is_deprecated": True}), + content_type="application/json", + ) + actual = response.json() + + assert actual["is_deprecated"] == True + assert Package.objects.get(pk=package.pk).is_deprecated == True + + response = api_client.post( + f"/api/cyberstorm/package/{package.namespace}/{package.name}/deprecate/", + json.dumps({"is_deprecated": False}), + content_type="application/json", + ) + actual = response.json() + + assert actual["is_deprecated"] == False + assert Package.objects.get(pk=package.pk).is_deprecated == False + + +@pytest.mark.django_db +def test_package_deprecate_api_view__returns_error_for_non_existent_package( + api_client: APIClient, + user: UserType, +) -> None: + api_client.force_authenticate(user) + response = api_client.post( + f"/api/cyberstorm/package/BAD/BAD/deprecate/", + json.dumps({"is_deprecated": True}), + content_type="application/json", + ) + actual = response.json() + + assert actual["detail"] == "Not found." + + +@pytest.mark.django_db +def test_package_deprecate_api_view__returns_error_for_no_user( + api_client: APIClient, +) -> None: + response = api_client.post( + f"/api/cyberstorm/package/BAD/BAD/deprecate/", + json.dumps({"is_deprecated": True}), + content_type="application/json", + ) + actual = response.json() + + assert actual["detail"] == "Authentication credentials were not provided." + + +@pytest.mark.django_db +def test_package_deprecate_api_view__returns_error_for_bad_data( + api_client: APIClient, + package: Package, + user: UserType, +) -> None: + api_client.force_authenticate(user) + package.is_active = False + package.save() + + response = api_client.post( + f"/api/cyberstorm/package/{package.namespace}/{package.name}/deprecate/", + json.dumps({"bad_data": True}), + content_type="application/json", + ) + actual = response.json() + + assert actual["is_deprecated"] == ["This field is required."] + + response = api_client.post( + f"/api/cyberstorm/package/{package.namespace}/{package.name}/deprecate/", + json.dumps({"is_deprecated": "bad"}), + content_type="application/json", + ) + actual = response.json() + + assert actual["is_deprecated"] == ["Must be a valid boolean."] diff --git a/django/thunderstore/api/cyberstorm/views/__init__.py b/django/thunderstore/api/cyberstorm/views/__init__.py index 16a223c10..37d6b46ce 100644 --- a/django/thunderstore/api/cyberstorm/views/__init__.py +++ b/django/thunderstore/api/cyberstorm/views/__init__.py @@ -2,6 +2,7 @@ from .community_filters import CommunityFiltersAPIView from .community_list import CommunityListAPIView from .markdown import PackageVersionChangelogAPIView, PackageVersionReadmeAPIView +from .package import PackageDeprecateAPIView from .package_listing import PackageListingAPIView from .package_listing_list import ( PackageListingByCommunityListAPIView, @@ -33,4 +34,5 @@ "TeamMemberListAPIView", "TeamServiceAccountListAPIView", "PackageRatingRateAPIView", + "PackageDeprecateAPIView", ] diff --git a/django/thunderstore/api/cyberstorm/views/package.py b/django/thunderstore/api/cyberstorm/views/package.py new file mode 100644 index 000000000..72c1e161f --- /dev/null +++ b/django/thunderstore/api/cyberstorm/views/package.py @@ -0,0 +1,48 @@ +from django.http import HttpRequest +from rest_framework import serializers +from rest_framework.exceptions import ValidationError +from rest_framework.generics import get_object_or_404 +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView + +from thunderstore.api.utils import conditional_swagger_auto_schema +from thunderstore.repository.forms import DeprecateForm +from thunderstore.repository.models import Package + + +class CyberstormDeprecatePackageRequestSerialiazer(serializers.Serializer): + is_deprecated = serializers.BooleanField() + + +class CyberstormDeprecatePackageResponseSerialiazer(serializers.Serializer): + is_deprecated = serializers.BooleanField() + + +class PackageDeprecateAPIView(APIView): + permission_classes = [IsAuthenticated] + + @conditional_swagger_auto_schema( + request_body=CyberstormDeprecatePackageRequestSerialiazer, + responses={200: CyberstormDeprecatePackageResponseSerialiazer}, + operation_id="cyberstorm.package.deprecate", + tags=["cyberstorm"], + ) + def post(self, request: HttpRequest, namespace_id: str, package_name: str): + serializer = CyberstormDeprecatePackageRequestSerialiazer(data=request.data) + serializer.is_valid(raise_exception=True) + package = get_object_or_404( + Package, + namespace__name=namespace_id, + name__iexact=package_name, + ) + form = DeprecateForm( + user=request.user, + instance=package, + data=serializer.validated_data, + ) + if form.is_valid(): + package = form.execute() + return Response(CyberstormDeprecatePackageResponseSerialiazer(package).data) + else: + raise ValidationError(form.errors) diff --git a/django/thunderstore/api/urls.py b/django/thunderstore/api/urls.py index a6d7af51e..abf9c04ec 100644 --- a/django/thunderstore/api/urls.py +++ b/django/thunderstore/api/urls.py @@ -4,6 +4,7 @@ CommunityAPIView, CommunityFiltersAPIView, CommunityListAPIView, + PackageDeprecateAPIView, PackageListingAPIView, PackageListingByCommunityListAPIView, PackageListingByDependencyListAPIView, @@ -84,6 +85,11 @@ PackageRatingRateAPIView.as_view(), name="cyberstorm.package_rating.rate", ), + path( + "package///deprecate/", + PackageDeprecateAPIView.as_view(), + name="cyberstorm.package.deprecate", + ), path( "team//", TeamAPIView.as_view(), diff --git a/django/thunderstore/repository/forms/__init__.py b/django/thunderstore/repository/forms/__init__.py index ae0e16012..556a3d7d5 100644 --- a/django/thunderstore/repository/forms/__init__.py +++ b/django/thunderstore/repository/forms/__init__.py @@ -1,2 +1,3 @@ +from .package import * from .package_rating import * from .team import * diff --git a/django/thunderstore/repository/forms/package.py b/django/thunderstore/repository/forms/package.py new file mode 100644 index 000000000..822ccaab0 --- /dev/null +++ b/django/thunderstore/repository/forms/package.py @@ -0,0 +1,34 @@ +from typing import Optional + +from django import forms +from django.contrib.auth import get_user_model + +from thunderstore.core.types import UserType +from thunderstore.repository.models import Package + +User = get_user_model() + + +class DeprecateForm(forms.ModelForm): + class Meta: + model = Package + fields = ["is_deprecated"] + + def __init__(self, user: Optional[UserType], *args, **kwargs): + super().__init__(*args, **kwargs) + self.user = user + + def clean(self): + self.instance.ensure_user_can_manage_deprecation(self.user) + value = self.data.get("is_deprecated", None) + if not isinstance(value, bool): + raise forms.ValidationError("Given value for is_deprecated is invalid.") + return super().clean() + + def execute(self): + desired_state = self.cleaned_data.get("is_deprecated") + if desired_state: + self.instance.deprecate() + else: + self.instance.undeprecate() + return self.instance diff --git a/django/thunderstore/repository/tests/test_package_forms.py b/django/thunderstore/repository/tests/test_package_forms.py new file mode 100644 index 000000000..8b709a197 --- /dev/null +++ b/django/thunderstore/repository/tests/test_package_forms.py @@ -0,0 +1,137 @@ +import pytest +from django.forms import ValidationError + +from thunderstore.account.models.service_account import ServiceAccount +from thunderstore.repository.forms import DeprecateForm +from thunderstore.repository.models import Package +from thunderstore.repository.models.team import TeamMember + + +@pytest.mark.django_db +def test_package_deprecate_form__correct_values__succeeds( + team_member: TeamMember, package: Package +) -> None: + # Deprecate + p = Package.objects.get(pk=package.pk) + form = DeprecateForm( + user=team_member.user, + instance=p, + data={"is_deprecated": True}, + ) + assert form.is_valid() is True + pkg = form.execute() + assert pkg.is_deprecated == True + # Undeprecate + p = Package.objects.get(pk=package.pk) + form = DeprecateForm( + user=team_member.user, + instance=p, + data={"is_deprecated": False}, + ) + assert form.is_valid() is True + pkg = form.execute() + assert pkg.is_deprecated == False + + +@pytest.mark.django_db +def test_package_deprecate_form__already_on_state__succeeds( + team_member: TeamMember, package: Package +) -> None: + # Deprecate + p = Package.objects.get(pk=package.pk) + form = DeprecateForm( + user=team_member.user, + instance=p, + data={"is_deprecated": True}, + ) + assert form.is_valid() is True + pkg = form.execute() + assert pkg.is_deprecated == True + # Second time + p = Package.objects.get(pk=package.pk) + form = DeprecateForm( + user=team_member.user, + instance=p, + data={"is_deprecated": True}, + ) + assert form.is_valid() is True + pkg = form.execute() + assert pkg.is_deprecated == True + # Undeprecate + p = Package.objects.get(pk=package.pk) + form = DeprecateForm( + user=team_member.user, + instance=p, + data={"is_deprecated": False}, + ) + assert form.is_valid() is True + pkg = form.execute() + assert pkg.is_deprecated == False + # Second time + p = Package.objects.get(pk=package.pk) + form = DeprecateForm( + user=team_member.user, + instance=p, + data={"is_deprecated": False}, + ) + assert form.is_valid() is True + pkg = form.execute() + assert pkg.is_deprecated == False + + +@pytest.mark.django_db +def test_package_deprecate_form__bad_value__fails( + team_member: TeamMember, package: Package +) -> None: + error = "Given value for is_deprecated is invalid." + form = DeprecateForm( + user=team_member.user, + instance=package, + data={"is_deprecated": "bad"}, + ) + assert form.is_valid() is False + assert error in str(repr(form.errors)) + + +@pytest.mark.django_db +def test_package_deprecate_form__user_none__fails( + package: Package, +) -> None: + form = DeprecateForm( + user=None, + instance=package, + data={"is_deprecated": True}, + ) + with pytest.raises(ValidationError) as e: + form.clean() + assert "Must be authenticated" in str(e.value) + + +@pytest.mark.django_db +def test_package_deprecate_form__user_deactivated__fails( + team_member: TeamMember, package: Package +) -> None: + team_member.user.is_active = False + team_member.user.save() + form = DeprecateForm( + user=team_member.user, + instance=package, + data={"is_deprecated": True}, + ) + with pytest.raises(ValidationError) as e: + form.clean() + assert "User has been deactivated" in str(e.value) + + +@pytest.mark.django_db +def test_package_deprecate_form__user_is_service_account__fails( + service_account: ServiceAccount, package: Package +) -> None: + form = DeprecateForm( + user=service_account.user, + instance=package, + data={"is_deprecated": True}, + ) + with pytest.raises(ValidationError) as e: + form.clean() + assert "Service accounts are unable to perform this action" in str(e.value)