Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
a506458
Refactor and update test utils
Roffenlund Aug 28, 2025
60999b6
Implement query tests and update existing tests
Roffenlund Aug 28, 2025
fa1a437
Fix styling with black
Roffenlund Aug 28, 2025
59bf19d
Refactor test utils
Roffenlund Sep 4, 2025
42339ba
Merge pull request #1173 from thunderstore-io/query-count-tests
Roffenlund Sep 22, 2025
9843753
Improve get_package_version
Roffenlund Sep 22, 2025
6731f28
Implement pagination.py file in cyberstorm API
Roffenlund Sep 22, 2025
adb6766
Add dependencies serializer
Roffenlund Sep 22, 2025
00cefee
Implement package dependencies list api view
Roffenlund Sep 22, 2025
562a3b4
Add URL for package dependencies api list view
Roffenlund Sep 22, 2025
631291b
Implement service for remove team member
Roffenlund Sep 17, 2025
d3fe9c3
Implement api view for removing team member
Roffenlund Sep 17, 2025
7848f5a
Update existing form and form view
Roffenlund Sep 17, 2025
88ddc7f
Add remove team member endpoint to tests
Roffenlund Sep 22, 2025
5f83436
Add query count test
Roffenlund Sep 22, 2025
46fa16e
Fix broken test
Roffenlund Sep 22, 2025
1e6fc6f
Merge pull request #1184 from thunderstore-io/cyberstorm-api-dependen…
Roffenlund Sep 26, 2025
f6c2187
Merge pull request #1182 from thunderstore-io/cyberstorm-api-leave-team
Roffenlund Sep 26, 2025
b893ad5
Implement listing status serializer
Roffenlund Sep 2, 2025
a85bf55
Implement view for getting listing status
Roffenlund Sep 2, 2025
87ada42
Add permission check for listing_admin_url
Roffenlund Sep 5, 2025
a0fb687
Add endpoint to endpoint tests
Roffenlund Sep 23, 2025
04e1017
Merge pull request #1179 from thunderstore-io/cyberstorm-api-listing-…
Roffenlund Sep 26, 2025
1ac0481
Implement team services
Roffenlund Sep 24, 2025
6821d01
Implement serializer for create service account
Roffenlund Sep 24, 2025
7c1270f
Implement create & delete service account views
Roffenlund Sep 24, 2025
e665e38
Implement create/delete service account endpoints
Roffenlund Sep 24, 2025
12defc4
Update service account form and views
Roffenlund Sep 24, 2025
38522a2
Add endpoints to endpoint tests
Roffenlund Sep 25, 2025
dd16ce9
Update test_team_services import
Roffenlund Sep 26, 2025
0d3753f
Merge pull request #1098 from thunderstore-io/cyberstorm-api-create-s…
Roffenlund Sep 26, 2025
f560775
Move few PackageListing serializers to shared files
Oksamies Sep 26, 2025
9fdd1cc
Add PackageVersionAPIView and related serializers
Oksamies Sep 26, 2025
7ae1cca
Implement remove user and social auth services
Roffenlund Sep 25, 2025
37a594e
Implement views for removing user & social account
Roffenlund Sep 25, 2025
8ad5d57
Update form views and forms
Roffenlund Sep 25, 2025
cfb09ba
Add endpoints to endpoint tests
Roffenlund Sep 26, 2025
ce84c0a
Merge pull request #1114 from thunderstore-io/cyberstorm-api-delete-user
Roffenlund Sep 29, 2025
9fb74ad
Merge pull request #1183 from thunderstore-io/09-18-add_packageversio…
anttimaki Sep 29, 2025
623b935
Use custom decorator to hide Cyberstorm endpoints from API docs
anttimaki Sep 30, 2025
64da51b
Merge pull request #1185 from thunderstore-io/hide-cyberstorm-api
anttimaki Oct 1, 2025
99eb8fc
Implement report package view in cyberstorm API
Roffenlund Oct 1, 2025
83864e7
Implement endpoint for reporting packages
Roffenlund Oct 1, 2025
e8f1e3a
Merge pull request #1186 from thunderstore-io/cyberstorm-api-report-p…
Roffenlund Oct 2, 2025
01349c9
Implement service for unlisting package listing
Roffenlund Sep 4, 2025
3b42da5
Implement unlist view
Roffenlund Sep 4, 2025
1974378
Implement URL for unlisting package listing
Roffenlund Sep 4, 2025
40f44ad
Add endpoint to test endpoints tests
Roffenlund Sep 23, 2025
09e29c4
Sort endpoints in alphabetical order
Roffenlund Oct 1, 2025
a84219c
Update parameter type in test
Roffenlund Oct 1, 2025
4f3b42d
Use conditional_swagger_auto_schema
Roffenlund Oct 1, 2025
d5eb376
Fix variable name in tests
Roffenlund Oct 2, 2025
a51c2c1
Optimize dependencies list queryset query
Roffenlund Oct 3, 2025
3cd0109
Remove redundant test function
Roffenlund Oct 6, 2025
2a4b8b8
Add the unlist action to tests
Roffenlund Oct 6, 2025
0f2eef0
Merge pull request #1178 from thunderstore-io/cyberstorm-api-unlist-p…
Roffenlund Oct 6, 2025
271aa1a
Implement service for updating team member
Roffenlund Sep 24, 2025
0891dd3
Implement serializer for updating team members
Roffenlund Sep 24, 2025
826b545
Implement view for updating team member
Roffenlund Sep 24, 2025
bae8455
Add URL endpoint for update team member
Roffenlund Sep 24, 2025
b47ddc6
Update form and form view for update team member
Roffenlund Sep 24, 2025
94f5809
Add endpoint to endpoint tests
Roffenlund Sep 24, 2025
003ffd5
python-packages update
Roffenlund Oct 6, 2025
f249529
Add package source endpoint
Roffenlund Oct 6, 2025
b6aa784
Skip tests if python-packages endpoint not available
Roffenlund Oct 6, 2025
3777922
Merge pull request #1187 from thunderstore-io/cyberstorm-api-improve-…
Roffenlund Oct 6, 2025
db03a91
Merge pull request #1117 from thunderstore-io/cyberstorm-api-update-t…
Roffenlund Oct 6, 2025
805dbf8
Implement basic test for plugin_registry
Roffenlund Oct 6, 2025
2e7df1c
Merge pull request #1094 from thunderstore-io/api-endpoint-for-packag…
Roffenlund Oct 7, 2025
17b201f
Remove validation from Team.create
Roffenlund Oct 10, 2025
c8ba27d
Update team services
Roffenlund Oct 10, 2025
62751e6
Update create team and disband team views
Roffenlund Oct 10, 2025
70cede3
Use services in create/disband form views
Roffenlund Oct 10, 2025
988b7e8
Update test_team_services error validation
Roffenlund Oct 14, 2025
1ba470a
Merge pull request #1139 from thunderstore-io/refactor-team-form-views
Roffenlund Oct 14, 2025
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
16 changes: 16 additions & 0 deletions django/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from django.core.files.base import File
from PIL import Image
from rest_framework.test import APIClient
from social_django.models import UserSocialAuth

from django_contracts.models import LegalContract, LegalContractVersion
from thunderstore.account.factories import UserFlagFactory
Expand Down Expand Up @@ -120,6 +121,21 @@ def user(django_user_model):
)


@pytest.fixture()
def user_with_social_auths(user):
UserSocialAuth.objects.create(
user=user,
provider="discord",
uid="1234567890",
)
UserSocialAuth.objects.create(
user=user,
provider="github",
uid="0987654321",
)
return user


@pytest.fixture()
def user_with_settings(django_user_model):
user = django_user_model.objects.create_user(
Expand Down
46 changes: 32 additions & 14 deletions django/thunderstore/account/forms.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
from django import forms
from django.core.exceptions import ValidationError
from django.db import transaction

from thunderstore.account.models import ServiceAccount
from thunderstore.api.cyberstorm.services.team import (
create_service_account,
delete_service_account,
)
from thunderstore.core.types import UserType
from thunderstore.repository.models import Team

Expand All @@ -16,19 +21,28 @@ def __init__(self, user: UserType, *args, **kwargs) -> None:
queryset=Team.objects.filter(members__user=user, is_active=True),
)

def clean_team(self) -> Team:
team = self.cleaned_data["team"]
team.ensure_can_create_service_account(self.user)
return team

@transaction.atomic
def save(self) -> ServiceAccount:
if self.errors:
raise ValueError("Cannot save form with errors")
Comment on lines +26 to +27
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this actually necessary/useful to guard against? Asking because I'd expect Django to have this built in if yes


self.api_token = ""

owner = self.cleaned_data["team"]
nickname = self.cleaned_data["nickname"]
(service_account, token) = ServiceAccount.create(
owner=owner, nickname=nickname, creator=self.user
)
self.api_token = token
service_account = None

try:
service_account, token = create_service_account(
agent=self.user,
team=owner,
nickname=nickname,
)
self.api_token = token
except ValidationError as e:
self.add_error(None, e)
raise ValidationError(self.errors)

return service_account


Expand All @@ -40,13 +54,17 @@ def __init__(self, user: UserType, *args, **kwargs) -> None:
queryset=ServiceAccount.objects.filter(owner__members__user=user),
)

def clean_service_account(self) -> ServiceAccount:
def save(self) -> None:
if self.errors:
raise ValueError("Cannot save form with errors")
Comment on lines +58 to +59
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this actually necessary/useful to guard against? Asking because I'd expect Django to have this built in if yes


service_account = self.cleaned_data["service_account"]
service_account.owner.ensure_can_delete_service_account(self.user)
return service_account

def save(self) -> None:
self.cleaned_data["service_account"].delete()
try:
delete_service_account(self.user, service_account)
except ValidationError as e:
self.add_error(None, e)
raise ValidationError(self.errors)


class EditServiceAccountForm(forms.Form):
Expand Down
48 changes: 41 additions & 7 deletions django/thunderstore/account/tests/test_service_account.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pytest
from django.core.exceptions import ValidationError
from rest_framework.authtoken.models import Token

from thunderstore.account.forms import (
Expand Down Expand Up @@ -46,6 +47,24 @@ def test_service_account_create(user, team):
)


@pytest.mark.django_db
def test_service_account_create_error_on_save(user, team):
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be a good idea to explain why the test should fail

TeamMember.objects.create(
user=user,
team=team,
role=TeamMemberRole.owner,
)

form = CreateServiceAccountForm(
user,
data={"team": team, "nickname": "x" * 1000},
)

assert form.is_valid() is False
with pytest.raises(ValueError):
form.save()


@pytest.mark.django_db
def test_service_account_create_nickname_too_long(user, team):
TeamMember.objects.create(
Expand Down Expand Up @@ -91,9 +110,11 @@ def test_service_account_create_not_owner(user, team):
user,
data={"team": team, "nickname": "Nickname"},
)
with pytest.raises(ValidationError):
form.save()
assert form.is_valid() is False
assert len(form.errors["team"]) == 1
assert form.errors["team"][0] == "Must be an owner to create a service account"
assert len(form.errors["__all__"]) == 1
assert form.errors["__all__"][0] == "Must be an owner to create a service account"


@pytest.mark.django_db
Expand Down Expand Up @@ -129,6 +150,20 @@ def test_service_account_delete_not_member(service_account):
)


@pytest.mark.django_db
def test_service_account_delete_error_on_save(service_account):
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be a good idea to explain why the test should fail

user = UserFactory.create()

form = DeleteServiceAccountForm(
user,
data={"service_account": service_account},
)

assert form.is_valid() is False
with pytest.raises(ValueError):
form.save()


@pytest.mark.django_db
def test_service_account_delete_not_owner(service_account):
user = UserFactory.create()
Expand All @@ -141,12 +176,11 @@ def test_service_account_delete_not_owner(service_account):
user,
data={"service_account": service_account},
)
with pytest.raises(ValidationError):
form.save()
assert form.is_valid() is False
assert len(form.errors["service_account"]) == 1
assert (
form.errors["service_account"][0]
== "Must be an owner to delete a service account"
)
assert len(form.errors["__all__"]) == 1
assert form.errors["__all__"][0] == "Must be an owner to delete a service account"


@pytest.mark.django_db
Expand Down
19 changes: 18 additions & 1 deletion django/thunderstore/api/cyberstorm/serializers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,45 @@
CyberstormPackageCategorySerializer,
CyberstormPackageListingSectionSerializer,
)
from .package import CyberstormPackagePreviewSerializer, PackagePermissionsSerializer
from .package import (
CyberstormPackageDependencySerializer,
CyberstormPackagePreviewSerializer,
CyberstormPackageTeamSerializer,
PackagePermissionsSerializer,
)
from .package_listing import PackageListingStatusResponseSerializer
from .package_version import PackageVersionResponseSerializer
from .team import (
CyberstormCreateServiceAccountSerializer,
CyberstormCreateTeamSerializer,
CyberstormServiceAccountSerializer,
CyberstormTeamAddMemberRequestSerializer,
CyberstormTeamAddMemberResponseSerializer,
CyberstormTeamMemberSerializer,
CyberstormTeamMemberUpdateSerializer,
CyberstormTeamSerializer,
CyberstormTeamUpdateSerializer,
)
from .utils import EmptyStringAsNoneField

__all__ = [
"EmptyStringAsNoneField",
"CyberstormTeamAddMemberRequestSerializer",
"CyberstormTeamAddMemberResponseSerializer",
"CyberstormCreateTeamSerializer",
"CyberstormCreateServiceAccountSerializer",
"CyberstormCommunitySerializer",
"CyberstormPackageCategorySerializer",
"CyberstormPackageListingSectionSerializer",
"CyberstormPackagePreviewSerializer",
"CyberstormPackageTeamSerializer",
"CyberstormServiceAccountSerializer",
"CyberstormTeamMemberSerializer",
"CyberstormTeamMemberUpdateSerializer",
"CyberstormTeamSerializer",
"PackagePermissionsSerializer",
"CyberstormTeamUpdateSerializer",
"CyberstormPackageDependencySerializer",
"PackageListingStatusResponseSerializer",
"PackageVersionResponseSerializer",
]
40 changes: 40 additions & 0 deletions django/thunderstore/api/cyberstorm/serializers/package.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from typing import Optional

from rest_framework import serializers

from thunderstore.api.cyberstorm.serializers.community import (
CyberstormPackageCategorySerializer,
)
from thunderstore.api.cyberstorm.serializers.team import CyberstormTeamMemberSerializer
from thunderstore.community.models import Community
from thunderstore.repository.models import Namespace, Package, PackageVersion

Expand Down Expand Up @@ -55,3 +58,40 @@ class CyberstormPackagePreviewSerializer(serializers.Serializer):
rating_count = serializers.IntegerField(min_value=0)
size = serializers.IntegerField(min_value=0)
datetime_created = serializers.DateTimeField()


class CyberstormPackageDependencySerializer(serializers.Serializer):
description = serializers.SerializerMethodField()
icon_url = serializers.SerializerMethodField()
is_active = serializers.BooleanField(source="is_effectively_active")
name = serializers.CharField()
namespace = serializers.CharField(source="package.namespace.name")
version_number = serializers.CharField()
is_removed = serializers.SerializerMethodField()

def get_is_removed(self, obj: PackageVersion) -> bool:
package_is_removed = not (
obj.package.is_active and obj.package_has_active_versions
)
if package_is_removed:
return True
return not obj.is_active

def get_description(self, obj: PackageVersion) -> str:
return (
obj.description
if obj.is_effectively_active
else "This package has been removed."
)

def get_icon_url(self, obj: PackageVersion) -> Optional[str]:
return obj.icon.url if obj.is_effectively_active else None
Comment on lines +72 to +88
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like it could generate lots of queries if this serializer is used in a list context, but I don't know if that's the case (and perhaps we had query count tests already protecting against it?)



class CyberstormPackageTeamSerializer(serializers.Serializer):
"""
Minimal information to present the team on package detail view.
"""

name = serializers.CharField()
members = CyberstormTeamMemberSerializer(many=True, source="public_members")
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,10 @@ class PackageListingApproveSerializer(serializers.Serializer):
internal_notes = serializers.CharField(
allow_blank=True, allow_null=True, required=False
)


class PackageListingStatusResponseSerializer(serializers.Serializer):
review_status = serializers.CharField(required=False, allow_null=True)
rejection_reason = serializers.CharField(required=False, allow_null=True)
internal_notes = serializers.CharField(required=False, allow_null=True)
listing_admin_url = serializers.CharField(required=False, allow_null=True)
29 changes: 29 additions & 0 deletions django/thunderstore/api/cyberstorm/serializers/package_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from rest_framework import serializers

from thunderstore.api.cyberstorm.serializers.package import (
CyberstormPackageTeamSerializer,
)
from thunderstore.api.cyberstorm.serializers.utils import EmptyStringAsNoneField


class PackageVersionResponseSerializer(serializers.Serializer):
"""
Data shown on package version detail view.

Expects an annotated and customized CustomListing object.
"""

datetime_created = serializers.DateTimeField(source="date_created")
dependency_count = serializers.IntegerField(min_value=0)
description = serializers.CharField()
download_count = serializers.IntegerField(source="downloads", min_value=0)
download_url = serializers.CharField(source="full_download_url")
full_version_name = serializers.CharField()
icon_url = serializers.CharField(source="icon.url")
install_url = serializers.CharField()
name = serializers.CharField()
version_number = serializers.CharField()
namespace = serializers.CharField(source="package.namespace.name")
size = serializers.IntegerField(min_value=0, source="file_size")
team = CyberstormPackageTeamSerializer(source="owner")
website_url = EmptyStringAsNoneField()
13 changes: 13 additions & 0 deletions django/thunderstore/api/cyberstorm/serializers/team.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from rest_framework import serializers

from thunderstore.repository.forms import AddTeamMemberForm
from thunderstore.repository.models.team import TeamMemberRole
from thunderstore.repository.validators import PackageReferenceComponentValidator
from thunderstore.social.utils import get_user_avatar_url

Expand Down Expand Up @@ -60,3 +61,15 @@ class CyberstormTeamUpdateSerializer(serializers.Serializer):
donation_link = serializers.CharField(
max_length=1024, validators=[URLValidator(["https"])]
)


class CyberstormCreateServiceAccountSerializer(serializers.Serializer):
nickname = serializers.CharField(max_length=32)
team_name = serializers.CharField(read_only=True)
api_token = serializers.CharField(read_only=True)


class CyberstormTeamMemberUpdateSerializer(serializers.Serializer):
role = serializers.ChoiceField(choices=TeamMemberRole.as_choices())
team_name = serializers.CharField(source="team.name", read_only=True)
username = serializers.CharField(source="user.username", read_only=True)
18 changes: 18 additions & 0 deletions django/thunderstore/api/cyberstorm/serializers/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from rest_framework import serializers


class EmptyStringAsNoneField(serializers.Field):
"""
Serialize empty string to None and deserialize vice versa.
"""

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.allow_null = True
self.allow_blank = True

def to_representation(self, value):
return None if value == "" else value

def to_internal_value(self, data):
return "" if data is None else data
11 changes: 11 additions & 0 deletions django/thunderstore/api/cyberstorm/services/package_listing.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django.db import transaction

from thunderstore.core.exceptions import PermissionValidationError
from thunderstore.core.types import UserType
from thunderstore.permissions.utils import validate_user
from thunderstore.repository.models import Package, PackageListing, PackageVersion
Expand Down Expand Up @@ -75,3 +76,13 @@ def report_package_listing(
package_version=package_version,
description=description,
)


@transaction.atomic
def unlist_package_listing(agent: UserType, listing: PackageListing) -> None:

user = validate_user(agent)
if not user.is_superuser:
raise PermissionValidationError("Only superusers can unlist packages")

listing.package.deactivate()
Loading
Loading