diff --git a/backend/apps/github/api/issue.py b/backend/apps/github/api/issue.py deleted file mode 100644 index b957c9e027..0000000000 --- a/backend/apps/github/api/issue.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Issue API.""" - -from rest_framework import serializers, viewsets - -from apps.github.models.issue import Issue - - -# Serializers define the API representation. -class IssueSerializer(serializers.HyperlinkedModelSerializer): - """Issue serializer.""" - - class Meta: - model = Issue - fields = ( - "title", - "body", - "state", - "url", - "created_at", - "updated_at", - ) - - -# ViewSets define the view behavior. -class IssueViewSet(viewsets.ReadOnlyModelViewSet): - """Issue view set.""" - - queryset = Issue.objects.all() - serializer_class = IssueSerializer diff --git a/backend/apps/github/api/label.py b/backend/apps/github/api/label.py deleted file mode 100644 index 288dec142a..0000000000 --- a/backend/apps/github/api/label.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Label API.""" - -from rest_framework import serializers, viewsets - -from apps.github.models.label import Label - - -# Serializers define the API representation. -class LabelSerializer(serializers.HyperlinkedModelSerializer): - """Label serializer.""" - - class Meta: - model = Label - fields = ( - "name", - "description", - "color", - ) - - -# ViewSets define the view behavior. -class LabelViewSet(viewsets.ReadOnlyModelViewSet): - """Label view set.""" - - queryset = Label.objects.all() - serializer_class = LabelSerializer diff --git a/backend/apps/github/api/organization.py b/backend/apps/github/api/organization.py deleted file mode 100644 index d34f88c614..0000000000 --- a/backend/apps/github/api/organization.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Organization API.""" - -from rest_framework import serializers, viewsets - -from apps.github.models.organization import Organization - - -# Serializers define the API representation. -class OrganizationSerializer(serializers.HyperlinkedModelSerializer): - """Organization serializer.""" - - class Meta: - model = Organization - fields = ( - "name", - "login", - "company", - "location", - "created_at", - "updated_at", - ) - - -# ViewSets define the view behavior. -class OrganizationViewSet(viewsets.ReadOnlyModelViewSet): - """Organization view set.""" - - queryset = Organization.objects.filter(is_owasp_related_organization=True) - serializer_class = OrganizationSerializer diff --git a/backend/apps/github/api/release.py b/backend/apps/github/api/release.py deleted file mode 100644 index 0e99c51831..0000000000 --- a/backend/apps/github/api/release.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Release API.""" - -from rest_framework import serializers, viewsets - -from apps.github.models.release import Release - - -# Serializers define the API representation. -class ReleaseSerializer(serializers.HyperlinkedModelSerializer): - """Release serializer.""" - - class Meta: - model = Release - fields = ( - "name", - "tag_name", - "description", - "created_at", - "published_at", - ) - - -# ViewSets define the view behavior. -class ReleaseViewSet(viewsets.ReadOnlyModelViewSet): - """Release view set.""" - - queryset = Release.objects.all() - serializer_class = ReleaseSerializer diff --git a/backend/apps/github/api/repository.py b/backend/apps/github/api/repository.py deleted file mode 100644 index 56a8e421b0..0000000000 --- a/backend/apps/github/api/repository.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Repository API.""" - -from rest_framework import serializers, viewsets - -from apps.github.models.repository import Repository - - -# Serializers define the API representation. -class RepositorySerializer(serializers.HyperlinkedModelSerializer): - """Repository serializer.""" - - class Meta: - model = Repository - fields = ( - "name", - "description", - "created_at", - "updated_at", - ) - - -# ViewSets define the view behavior. -class RepositoryViewSet(viewsets.ReadOnlyModelViewSet): - """Repository view set.""" - - queryset = Repository.objects.all() - serializer_class = RepositorySerializer diff --git a/backend/apps/github/api/urls.py b/backend/apps/github/api/urls.py deleted file mode 100644 index 60673d1ae3..0000000000 --- a/backend/apps/github/api/urls.py +++ /dev/null @@ -1,19 +0,0 @@ -"""GitHub API URLs.""" - -from rest_framework import routers - -from apps.github.api.issue import IssueViewSet -from apps.github.api.label import LabelViewSet -from apps.github.api.organization import OrganizationViewSet -from apps.github.api.release import ReleaseViewSet -from apps.github.api.repository import RepositoryViewSet -from apps.github.api.user import UserViewSet - -router = routers.SimpleRouter() - -router.register(r"github/issues", IssueViewSet) -router.register(r"github/labels", LabelViewSet) -router.register(r"github/organizations", OrganizationViewSet) -router.register(r"github/releases", ReleaseViewSet) -router.register(r"github/repositories", RepositoryViewSet) -router.register(r"github/users", UserViewSet) diff --git a/backend/apps/github/api/user.py b/backend/apps/github/api/user.py deleted file mode 100644 index aa3cbe3ec3..0000000000 --- a/backend/apps/github/api/user.py +++ /dev/null @@ -1,60 +0,0 @@ -"""User API.""" - -from rest_framework import serializers, viewsets -from rest_framework.decorators import action -from rest_framework.response import Response - -from apps.github.models.user import User - - -# Serializers define the API representation. -class UserSerializer(serializers.HyperlinkedModelSerializer): - """User serializer.""" - - class Meta: - model = User - fields = ( - "avatar_url", - "bio", - "company", - "email", - "followers_count", - "following_count", - "location", - "login", - "name", - "public_repositories_count", - "title", - "twitter_username", - "url", - "created_at", - "updated_at", - ) - - -# ViewSets define the view behavior. -class UserViewSet(viewsets.ReadOnlyModelViewSet): - """User view set.""" - - queryset = User.objects.all() - serializer_class = UserSerializer - - @action(detail=False, methods=["get"], url_path="login/(?P<login>[^/.]+)") - def get_user_by_login(self, request, login=None): - """Retrieve a user by their login. - - Args: - request (Request): The HTTP request object. - login (str, optional): The login of the user to retrieve. - - Returns: - Response: The serialized user data or a 404 error if the user is not found. - - """ - try: - user = User.objects.get(login=login) - serializer = self.get_serializer(user) - data = serializer.data - return Response(data) - except User.DoesNotExist: - return Response({"detail": "User not found."}, status=404) diff --git a/backend/apps/github/api/v1/__init__.py b/backend/apps/github/api/v1/__init__.py new file mode 100644 index 0000000000..a9466986f2 --- /dev/null +++ b/backend/apps/github/api/v1/__init__.py @@ -0,0 +1 @@ +"""Version 1 API.""" diff --git a/backend/apps/github/api/v1/issue.py b/backend/apps/github/api/v1/issue.py new file mode 100644 index 0000000000..a58085a64e --- /dev/null +++ b/backend/apps/github/api/v1/issue.py @@ -0,0 +1,33 @@ +"""Issue API.""" + +from datetime import datetime + +from django.http import HttpRequest +from ninja import Router, Schema +from ninja.errors import HttpError +from ninja.pagination import PageNumberPagination, paginate + +from apps.github.models.issue import Issue + +router = Router() + + +class IssueSchema(Schema): + """Schema for Issue.""" + + body: str + created_at: datetime + title: str + state: str + updated_at: datetime + url: str + + +@router.get("/", response={200: list[IssueSchema], 404: dict}) +@paginate(PageNumberPagination, page_size=100) +def list_issues(request: HttpRequest) -> list[IssueSchema] | dict: + """Get all issues.""" + issues = Issue.objects.all() + if not issues.exists(): + raise HttpError(404, "Issues not found") + return issues diff --git a/backend/apps/github/api/v1/label.py b/backend/apps/github/api/v1/label.py new file mode 100644 index 0000000000..be8af689ee --- /dev/null +++ b/backend/apps/github/api/v1/label.py @@ -0,0 +1,28 @@ +"""Label API.""" + +from django.http import HttpRequest +from ninja import Router, Schema +from ninja.errors import HttpError +from ninja.pagination import PageNumberPagination, paginate + +from apps.github.models.label import Label + +router = Router() + + +class LabelSchema(Schema): + """Schema for Label.""" + + color: str + description: str + name: str + + +@router.get("/", response={200: list[LabelSchema], 404: dict}) +@paginate(PageNumberPagination, page_size=100) +def list_label(request: HttpRequest) -> list[LabelSchema] | dict: + """Get all labels.""" + labels = Label.objects.all() + if not labels.exists(): + raise HttpError(404, "Labels not found") + return labels diff --git a/backend/apps/github/api/v1/organization.py b/backend/apps/github/api/v1/organization.py new file mode 100644 index 0000000000..a4ce989f3b --- /dev/null +++ b/backend/apps/github/api/v1/organization.py @@ -0,0 +1,33 @@ +"""Organization API.""" + +from datetime import datetime + +from django.http import HttpRequest +from ninja import Router, Schema +from ninja.errors import HttpError +from ninja.pagination import PageNumberPagination, paginate + +from apps.github.models.organization import Organization + +router = Router() + + +class OrganizationSchema(Schema): + """Schema for Organization.""" + + company: str + created_at: datetime + location: str + login: str + name: str + updated_at: datetime + + +@router.get("/", response={200: list[OrganizationSchema], 404: dict}) +@paginate(PageNumberPagination, page_size=100) +def list_organization(request: HttpRequest) -> list[OrganizationSchema] | dict: + """Get all organizations.""" + organizations = Organization.objects.filter(is_owasp_related_organization=True) + if not organizations.exists(): + raise HttpError(404, "Organizations not found") + return organizations diff --git a/backend/apps/github/api/v1/release.py b/backend/apps/github/api/v1/release.py new file mode 100644 index 0000000000..d9549aa906 --- /dev/null +++ b/backend/apps/github/api/v1/release.py @@ -0,0 +1,32 @@ +"""Release API.""" + +from datetime import datetime + +from django.http import HttpRequest +from ninja import Router, Schema +from ninja.errors import HttpError +from ninja.pagination import PageNumberPagination, paginate + +from apps.github.models.release import Release + +router = Router() + + +class ReleaseSchema(Schema): + """Schema for Release.""" + + created_at: datetime + description: str + name: str + published_at: datetime + tag_name: str + + +@router.get("/", response={200: list[ReleaseSchema], 404: dict}) +@paginate(PageNumberPagination, page_size=100) +def list_release(request: HttpRequest) -> list[ReleaseSchema]: + """Get all releases.""" + releases = Release.objects.all() + if not releases.exists(): + raise HttpError(404, "Releases not found") + return releases diff --git a/backend/apps/github/api/v1/repository.py b/backend/apps/github/api/v1/repository.py new file mode 100644 index 0000000000..020014f7c6 --- /dev/null +++ b/backend/apps/github/api/v1/repository.py @@ -0,0 +1,31 @@ +"""Repository API.""" + +from datetime import datetime + +from django.http import HttpRequest +from ninja import Router, Schema +from ninja.errors import HttpError +from ninja.pagination import PageNumberPagination, paginate + +from apps.github.models.repository import Repository + +router = Router() + + +class RepositorySchema(Schema): + """Schema for Repository.""" + + created_at: datetime + description: str + name: str + updated_at: datetime + + +@router.get("/", response={200: list[RepositorySchema], 404: dict}) +@paginate(PageNumberPagination, page_size=100) +def list_repository(request: HttpRequest) -> list[RepositorySchema]: + """Get all repositories.""" + repositories = Repository.objects.all() + if not repositories.exists(): + raise HttpError(404, "Repositories not found") + return repositories diff --git a/backend/apps/github/api/v1/urls.py b/backend/apps/github/api/v1/urls.py new file mode 100644 index 0000000000..c381f770c7 --- /dev/null +++ b/backend/apps/github/api/v1/urls.py @@ -0,0 +1,19 @@ +"""GitHub API URLs.""" + +from ninja import Router + +from apps.github.api.v1.issue import router as issue_router +from apps.github.api.v1.label import router as label_router +from apps.github.api.v1.organization import router as organization_router +from apps.github.api.v1.release import router as release_router +from apps.github.api.v1.repository import router as repository_router +from apps.github.api.v1.user import router as user_router + +router = Router() + +router.add_router(r"/issues", issue_router) +router.add_router(r"/labels", label_router) +router.add_router(r"/organizations", organization_router) +router.add_router(r"/releases", release_router) +router.add_router(r"/repositories", repository_router) +router.add_router(r"/users", user_router) diff --git a/backend/apps/github/api/v1/user.py b/backend/apps/github/api/v1/user.py new file mode 100644 index 0000000000..5222c21538 --- /dev/null +++ b/backend/apps/github/api/v1/user.py @@ -0,0 +1,51 @@ +"""User API.""" + +from datetime import datetime + +from django.http import HttpRequest +from ninja import Router, Schema +from ninja.errors import HttpError +from ninja.pagination import PageNumberPagination, paginate + +from apps.github.models.user import User + +router = Router() + + +class UserSchema(Schema): + """Schema for User.""" + + avatar_url: str + bio: str + company: str + created_at: datetime + email: str + followers_count: int + following_count: int + location: str + login: str + name: str + public_repositories_count: int + title: str + twitter_username: str + updated_at: datetime + url: str + + +@router.get("/", response={200: list[UserSchema], 404: dict}) +@paginate(PageNumberPagination, page_size=100) +def list_users(request: HttpRequest) -> list[UserSchema]: + """Get all users.""" + users = User.objects.all() + if not users.exists(): + raise HttpError(404, "Users not found") + return users + + +@router.get("/{login}", response={200: UserSchema, 404: dict}) +def get_user(request: HttpRequest, login: str) -> UserSchema: + """Get user by login.""" + user = User.objects.filter(login=login).first() + if not user: + raise HttpError(404, "User not found") + return user diff --git a/backend/apps/owasp/api/chapter.py b/backend/apps/owasp/api/chapter.py deleted file mode 100644 index bd91b5e7c4..0000000000 --- a/backend/apps/owasp/api/chapter.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Chapter API.""" - -from rest_framework import serializers, viewsets - -from apps.owasp.models.chapter import Chapter - - -# Serializers define the API representation. -class ChapterSerializer(serializers.HyperlinkedModelSerializer): - """Chapter serializer.""" - - class Meta: - model = Chapter - fields = ( - "name", - "country", - "region", - "created_at", - "updated_at", - ) - - -# ViewSets define the view behavior. -class ChapterViewSet(viewsets.ReadOnlyModelViewSet): - """Chapter view set.""" - - queryset = Chapter.objects.all() - serializer_class = ChapterSerializer diff --git a/backend/apps/owasp/api/committee.py b/backend/apps/owasp/api/committee.py deleted file mode 100644 index 7e30c720f8..0000000000 --- a/backend/apps/owasp/api/committee.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Committee API.""" - -from rest_framework import serializers, viewsets - -from apps.owasp.models.committee import Committee - - -# Serializers define the API representation. -class CommitteeSerializer(serializers.HyperlinkedModelSerializer): - """Committee serializer.""" - - class Meta: - model = Committee - fields = ( - "name", - "description", - "created_at", - "updated_at", - ) - - -# ViewSets define the view behavior. -class CommitteeViewSet(viewsets.ReadOnlyModelViewSet): - """Committee view set.""" - - queryset = Committee.objects.all() - serializer_class = CommitteeSerializer diff --git a/backend/apps/owasp/api/event.py b/backend/apps/owasp/api/event.py deleted file mode 100644 index 8683c0c195..0000000000 --- a/backend/apps/owasp/api/event.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Event API.""" - -from rest_framework import serializers, viewsets - -from apps.owasp.models.event import Event - - -# Serializers define the API representation. -class EventSerializer(serializers.HyperlinkedModelSerializer): - """Event serializer.""" - - class Meta: - model = Event - fields = ( - "name", - "description", - "url", - ) - - -# ViewSets define the view behavior. -class EventViewSet(viewsets.ReadOnlyModelViewSet): - """Event view set.""" - - queryset = Event.objects.all() - serializer_class = EventSerializer diff --git a/backend/apps/owasp/api/project.py b/backend/apps/owasp/api/project.py deleted file mode 100644 index 74d869a7bc..0000000000 --- a/backend/apps/owasp/api/project.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Project API.""" - -from rest_framework import serializers, viewsets - -from apps.owasp.models.project import Project - - -# Serializers define the API representation. -class ProjectSerializer(serializers.HyperlinkedModelSerializer): - """Project serializer.""" - - class Meta: - model = Project - fields = ( - "name", - "description", - "level", - "created_at", - "updated_at", - ) - - -# ViewSets define the view behavior. -class ProjectViewSet(viewsets.ReadOnlyModelViewSet): - """Project view set.""" - - queryset = Project.objects.all() - serializer_class = ProjectSerializer diff --git a/backend/apps/owasp/api/urls.py b/backend/apps/owasp/api/urls.py deleted file mode 100644 index 31fd1ff4c1..0000000000 --- a/backend/apps/owasp/api/urls.py +++ /dev/null @@ -1,15 +0,0 @@ -"""GitHub API URLs.""" - -from rest_framework import routers - -from apps.owasp.api.chapter import ChapterViewSet -from apps.owasp.api.committee import CommitteeViewSet -from apps.owasp.api.event import EventViewSet -from apps.owasp.api.project import ProjectViewSet - -router = routers.SimpleRouter() - -router.register(r"owasp/chapters", ChapterViewSet) -router.register(r"owasp/committees", CommitteeViewSet) -router.register(r"owasp/events", EventViewSet) -router.register(r"owasp/projects", ProjectViewSet) diff --git a/backend/apps/owasp/api/v1/__init__.py b/backend/apps/owasp/api/v1/__init__.py new file mode 100644 index 0000000000..a9466986f2 --- /dev/null +++ b/backend/apps/owasp/api/v1/__init__.py @@ -0,0 +1 @@ +"""Version 1 API.""" diff --git a/backend/apps/owasp/api/v1/chapter.py b/backend/apps/owasp/api/v1/chapter.py new file mode 100644 index 0000000000..7a28b1c41c --- /dev/null +++ b/backend/apps/owasp/api/v1/chapter.py @@ -0,0 +1,32 @@ +"""Chapter API.""" + +from datetime import datetime + +from django.http import HttpRequest +from ninja import Router, Schema +from ninja.errors import HttpError +from ninja.pagination import PageNumberPagination, paginate + +from apps.owasp.models.chapter import Chapter + +router = Router() + + +class ChapterSchema(Schema): + """Schema for Chapter.""" + + country: str + created_at: datetime + name: str + region: str + updated_at: datetime + + +@router.get("/", response={200: list[ChapterSchema], 404: dict}) +@paginate(PageNumberPagination, page_size=100) +def list_chapters(request: HttpRequest) -> list[ChapterSchema]: + """Get all chapters.""" + chapters = Chapter.objects.all() + if not chapters.exists(): + raise HttpError(404, "Chapters not found") + return chapters diff --git a/backend/apps/owasp/api/v1/committee.py b/backend/apps/owasp/api/v1/committee.py new file mode 100644 index 0000000000..615dc59db4 --- /dev/null +++ b/backend/apps/owasp/api/v1/committee.py @@ -0,0 +1,31 @@ +"""Committee API.""" + +from datetime import datetime + +from django.http import HttpRequest +from ninja import Router, Schema +from ninja.errors import HttpError +from ninja.pagination import PageNumberPagination, paginate + +from apps.owasp.models.committee import Committee + +router = Router() + + +class CommitteeSchema(Schema): + """Schema for Committee.""" + + name: str + description: str + created_at: datetime + updated_at: datetime + + +@router.get("/", response={200: list[CommitteeSchema], 404: dict}) +@paginate(PageNumberPagination, page_size=100) +def list_committees(request: HttpRequest) -> list[CommitteeSchema]: + """Get all committees.""" + committees = Committee.objects.all() + if not committees.exists(): + raise HttpError(404, "Committees not found") + return committees diff --git a/backend/apps/owasp/api/v1/event.py b/backend/apps/owasp/api/v1/event.py new file mode 100644 index 0000000000..f917a88a0f --- /dev/null +++ b/backend/apps/owasp/api/v1/event.py @@ -0,0 +1,28 @@ +"""Event API.""" + +from django.http import HttpRequest +from ninja import Router, Schema +from ninja.errors import HttpError +from ninja.pagination import PageNumberPagination, paginate + +from apps.owasp.models.event import Event + +router = Router() + + +class EventSchema(Schema): + """Schema for Event.""" + + description: str + name: str + url: str + + +@router.get("/", response={200: list[EventSchema], 404: dict}) +@paginate(PageNumberPagination, page_size=100) +def list_events(request: HttpRequest) -> list[EventSchema]: + """Get all events.""" + events = Event.objects.all() + if not events.exists(): + raise HttpError(404, "Events not found") + return events diff --git a/backend/apps/owasp/api/v1/project.py b/backend/apps/owasp/api/v1/project.py new file mode 100644 index 0000000000..5225e23a7c --- /dev/null +++ b/backend/apps/owasp/api/v1/project.py @@ -0,0 +1,32 @@ +"""Project API.""" + +from datetime import datetime + +from django.http import HttpRequest +from ninja import Router, Schema +from ninja.errors import HttpError +from ninja.pagination import PageNumberPagination, paginate + +from apps.owasp.models.project import Project + +router = Router() + + +class ProjectSchema(Schema): + """Schema for Project.""" + + created_at: datetime + description: str + level: str + name: str + updated_at: datetime + + +@router.get("/", response={200: list[ProjectSchema], 404: dict}) +@paginate(PageNumberPagination, page_size=100) +def list_projects(request: HttpRequest) -> list[ProjectSchema]: + """Get all projects.""" + projects = Project.objects.all() + if not projects.exists(): + raise HttpError(404, "Projects not found") + return projects diff --git a/backend/apps/owasp/api/v1/urls.py b/backend/apps/owasp/api/v1/urls.py new file mode 100644 index 0000000000..49c10310b4 --- /dev/null +++ b/backend/apps/owasp/api/v1/urls.py @@ -0,0 +1,15 @@ +"""GitHub API URLs.""" + +from ninja import Router + +from apps.owasp.api.v1.chapter import router as chapter_router +from apps.owasp.api.v1.committee import router as committee_router +from apps.owasp.api.v1.event import router as event_router +from apps.owasp.api.v1.project import router as project_router + +router = Router() + +router.add_router(r"/chapters", chapter_router) +router.add_router(r"/committees", committee_router) +router.add_router(r"/events", event_router) +router.add_router(r"/projects", project_router) diff --git a/backend/poetry.lock b/backend/poetry.lock index a06c589f02..9181269bb7 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -799,19 +799,25 @@ asgiref = ">=3.6" django = ">=4.2" [[package]] -name = "django-filter" -version = "25.1" -description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically." +name = "django-ninja" +version = "1.4.3" +description = "Django Ninja - Fast Django REST framework" optional = false -python-versions = ">=3.9" +python-versions = ">=3.7" groups = ["main"] files = [ - {file = "django_filter-25.1-py3-none-any.whl", hash = "sha256:4fa48677cf5857b9b1347fed23e355ea792464e0fe07244d1fdfb8a806215b80"}, - {file = "django_filter-25.1.tar.gz", hash = "sha256:1ec9eef48fa8da1c0ac9b411744b16c3f4c31176c867886e4c48da369c407153"}, + {file = "django_ninja-1.4.3-py3-none-any.whl", hash = "sha256:f3204137a059437b95677049474220611f1cf9efedba9213556474b75168fa01"}, + {file = "django_ninja-1.4.3.tar.gz", hash = "sha256:e46d477ca60c228d2a5eb3cc912094928ea830d364501f966661eeada67cb038"}, ] [package.dependencies] -Django = ">=4.2" +Django = ">=3.1" +pydantic = ">=2.0,<3.0.0" + +[package.extras] +dev = ["pre-commit"] +doc = ["markdown-include", "mkdocs", "mkdocs-material", "mkdocstrings"] +test = ["django-stubs", "mypy (==1.7.1)", "psycopg2-binary", "pytest", "pytest-asyncio", "pytest-cov", "pytest-django", "ruff (==0.5.7)"] [[package]] name = "django-redis" @@ -857,21 +863,6 @@ libcloud = ["apache-libcloud"] s3 = ["boto3 (>=1.4.4)"] sftp = ["paramiko (>=1.15)"] -[[package]] -name = "djangorestframework" -version = "3.16.0" -description = "Web APIs for Django, made easy." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "djangorestframework-3.16.0-py3-none-any.whl", hash = "sha256:bea7e9f6b96a8584c5224bfb2e4348dfb3f8b5e34edbecb98da258e892089361"}, - {file = "djangorestframework-3.16.0.tar.gz", hash = "sha256:f022ff46613584de994c0c6a4aebbace5fd700555fbe9d33b865ebf173eba6c9"}, -] - -[package.dependencies] -django = ">=4.2" - [[package]] name = "djlint" version = "1.36.4" @@ -1291,14 +1282,14 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "httpx-sse" -version = "0.4.0" +version = "0.4.1" description = "Consume Server-Sent Event (SSE) messages with HTTPX." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721"}, - {file = "httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f"}, + {file = "httpx_sse-0.4.1-py3-none-any.whl", hash = "sha256:cba42174344c3a5b06f255ce65b350880f962d99ead85e776f23c6618a377a37"}, + {file = "httpx_sse-0.4.1.tar.gz", hash = "sha256:8f44d34414bc7b21bf3602713005c5df4917884f76072479b21f68befa4ea26e"}, ] [[package]] @@ -1637,21 +1628,21 @@ langchain-core = ">=0.3.51,<1.0.0" [[package]] name = "langsmith" -version = "0.4.1" +version = "0.4.3" description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform." optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "langsmith-0.4.1-py3-none-any.whl", hash = "sha256:19c4c40bbb6735cb1136c453b2edcde265ca5ba1b108b7e0e3583ec4bda28625"}, - {file = "langsmith-0.4.1.tar.gz", hash = "sha256:ae8ec403fb2b9cabcfc3b0c54556d65555598c85879dac83b009576927f7eb1d"}, + {file = "langsmith-0.4.3-py3-none-any.whl", hash = "sha256:b8ed57fb21fb3370bc7e4e8c4a3003017040336df694a66a34afe6f9872e68da"}, + {file = "langsmith-0.4.3.tar.gz", hash = "sha256:151d8cbf3d26a49f67bd720462eae20d3282196958f86b59d1ac1aad484c52f1"}, ] [package.dependencies] httpx = ">=0.23.0,<1" orjson = {version = ">=3.9.14,<4.0.0", markers = "platform_python_implementation != \"PyPy\""} packaging = ">=23.2" -pydantic = {version = ">=2.7.4,<3.0.0", markers = "python_full_version >= \"3.12.4\""} +pydantic = ">=1,<3" requests = ">=2,<3" requests-toolbelt = ">=1.0.0,<2.0.0" zstandard = ">=0.23.0,<0.24.0" @@ -2647,14 +2638,14 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pydantic-settings" -version = "2.10.0" +version = "2.10.1" description = "Settings management using Pydantic" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pydantic_settings-2.10.0-py3-none-any.whl", hash = "sha256:33781dfa1c7405d5ed2b6f150830a93bb58462a847357bd8f162f8bacb77c027"}, - {file = "pydantic_settings-2.10.0.tar.gz", hash = "sha256:7a12e0767ba283954f3fd3fefdd0df3af21b28aa849c40c35811d52d682fa876"}, + {file = "pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796"}, + {file = "pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee"}, ] [package.dependencies] @@ -4011,4 +4002,4 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "2.1" python-versions = "^3.13" -content-hash = "50c9bfb1cba43fb146a8818efcb20b79636efd34c8e86b93cca0b9a7603598d1" +content-hash = "f9fb65bde2722d0de24cb1ac91d3d519efb2ec397ffd5e13ca7bd6a83239d724" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 8c2a68c138..bd9311b1b7 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -26,10 +26,9 @@ algoliasearch-django = "^4.0.0" django = "^5.1" django-configurations = "^2.5.1" django-cors-headers = "^4.7.0" -django-filter = "^25.1" +django-ninja = "^1.4.3" django-redis = "^5.4.0" django-storages = { extras = ["s3"], version = "^1.14.4" } -djangorestframework = "^3.15.2" emoji= "^2.14.1" geopy = "^2.4.1" gunicorn = "^23.0.0" @@ -147,7 +146,7 @@ select = ["ALL"] [tool.ruff.lint.per-file-ignores] "**/__init__.py" = ["D104", "F401"] "**/admin.py" = ["D100", "D101", "D104"] -"**/api/*.py" = ["D106"] +"**/api/*.py" = ["ARG001", "D106"] "**/apps.py" = ["D100", "D101", "D104"] "**/graphql/**/nodes.py" = ["D106"] "**/graphql/nodes/*.py" = ["D106"] diff --git a/backend/settings/api_v1.py b/backend/settings/api_v1.py new file mode 100644 index 0000000000..b3679048f2 --- /dev/null +++ b/backend/settings/api_v1.py @@ -0,0 +1,20 @@ +"""OWASP Nest API v1 configuration.""" + +from ninja import NinjaAPI +from ninja.throttling import AnonRateThrottle, AuthRateThrottle + +from apps.github.api.v1.urls import router as github_router +from apps.owasp.api.v1.urls import router as owasp_router + +api = NinjaAPI( + description="API for OWASP related entities", + title="OWASP Nest API", + version="1.0.0", + throttle=[ + AnonRateThrottle("1/s"), + AuthRateThrottle("10/s"), + ], +) + +api.add_router("github", github_router) +api.add_router("owasp", owasp_router) diff --git a/backend/settings/base.py b/backend/settings/base.py index 1caf388bfa..8075d60870 100644 --- a/backend/settings/base.py +++ b/backend/settings/base.py @@ -38,7 +38,6 @@ class Base(Configuration): THIRD_PARTY_APPS = ( "algoliasearch_django", "corsheaders", - "rest_framework", "storages", ) @@ -83,16 +82,6 @@ class Base(Configuration): "django.contrib.messages.middleware.MessageMiddleware", ] - REST_FRAMEWORK = { - # Use Django's standard `django.contrib.auth` permissions, - # or allow read-only access for unauthenticated users. - "DEFAULT_PERMISSION_CLASSES": [ - "rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly" - ], - "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", - "PAGE_SIZE": 100, - } - ROOT_URLCONF = "settings.urls" TEMPLATES = [ diff --git a/backend/settings/urls.py b/backend/settings/urls.py index c19f50a804..772e8ef046 100644 --- a/backend/settings/urls.py +++ b/backend/settings/urls.py @@ -7,27 +7,21 @@ from django.conf import settings from django.conf.urls.static import static from django.contrib import admin -from django.urls import include, path +from django.urls import path from django.views.decorators.csrf import csrf_protect -from rest_framework import routers from strawberry.django.views import GraphQLView from apps.core.api.algolia import algolia_search from apps.core.api.csrf import get_csrf_token -from apps.github.api.urls import router as github_router -from apps.owasp.api.urls import router as owasp_router from apps.slack.apps import SlackConfig +from settings.api_v1 import api as api_v1 from settings.graphql import schema -router = routers.DefaultRouter() -router.registry.extend(github_router.registry) -router.registry.extend(owasp_router.registry) - urlpatterns = [ path("csrf/", get_csrf_token), path("idx/", csrf_protect(algolia_search)), path("graphql/", csrf_protect(GraphQLView.as_view(schema=schema, graphiql=settings.DEBUG))), - path("api/v1/", include(router.urls)), + path("api/v1/", api_v1.urls), path("a/", admin.site.urls), ] diff --git a/backend/tests/apps/github/api/issue_test.py b/backend/tests/apps/github/api/issue_test.py index cf45f9199e..85b87951fc 100644 --- a/backend/tests/apps/github/api/issue_test.py +++ b/backend/tests/apps/github/api/issue_test.py @@ -1,9 +1,11 @@ +from datetime import datetime + import pytest -from apps.github.api.issue import IssueSerializer +from apps.github.api.v1.issue import IssueSchema -class TestIssueSerializer: +class TestIssueSchema: @pytest.mark.parametrize( "issue_data", [ @@ -25,14 +27,13 @@ class TestIssueSerializer: }, ], ) - def test_issue_serializer(self, issue_data): - serializer = IssueSerializer(data=issue_data) - assert serializer.is_valid() - validated_data = serializer.validated_data - validated_data["created_at"] = ( - validated_data["created_at"].isoformat().replace("+00:00", "Z") - ) - validated_data["updated_at"] = ( - validated_data["updated_at"].isoformat().replace("+00:00", "Z") - ) - assert validated_data == issue_data + def test_issue_schema(self, issue_data): + schema = IssueSchema(**issue_data) + + assert schema.title == issue_data["title"] + assert schema.body == issue_data["body"] + assert schema.state == issue_data["state"] + assert schema.url == issue_data["url"] + + assert schema.created_at == datetime.fromisoformat(issue_data["created_at"]) + assert schema.updated_at == datetime.fromisoformat(issue_data["updated_at"]) diff --git a/backend/tests/apps/github/api/label_test.py b/backend/tests/apps/github/api/label_test.py index 1ba6bd045d..8266834ab5 100644 --- a/backend/tests/apps/github/api/label_test.py +++ b/backend/tests/apps/github/api/label_test.py @@ -1,28 +1,26 @@ import pytest -from apps.github.api.label import LabelSerializer +from apps.github.api.v1.label import LabelSchema -class TestLabelSerializer: +class TestLabelSchema: @pytest.mark.parametrize( "label_data", [ { - "name": "bug", - "description": "Indicates a bug in the project", "color": "f29513", + "description": "Indicates a bug in the project", + "name": "bug", }, { - "name": "enhancement", - "description": "Indicates a new feature or enhancement", "color": "a2eeef", + "description": "Indicates a new feature or enhancement", + "name": "enhancement", }, ], ) - def test_label_serializer(self, label_data): - serializer = LabelSerializer(data=label_data) - assert serializer.is_valid(), serializer.errors - validated_data = serializer.validated_data - assert validated_data["name"] == label_data["name"] - assert validated_data["description"] == label_data["description"] - assert validated_data["color"] == label_data["color"] + def test_label_schema(self, label_data): + label = LabelSchema(**label_data) + assert label.name == label_data["name"] + assert label.description == label_data["description"] + assert label.color == label_data["color"] diff --git a/backend/tests/apps/github/api/organization_test.py b/backend/tests/apps/github/api/organization_test.py index 07775ad7a9..95f57edc32 100644 --- a/backend/tests/apps/github/api/organization_test.py +++ b/backend/tests/apps/github/api/organization_test.py @@ -1,12 +1,11 @@ -from unittest.mock import MagicMock, patch +from datetime import datetime import pytest -from apps.github.api.organization import OrganizationSerializer -from apps.github.models.organization import Organization +from apps.github.api.v1.organization import OrganizationSchema -class TestOrganizationSerializer: +class TestOrganizationSchema: @pytest.mark.parametrize( "organization_data", [ @@ -28,27 +27,12 @@ class TestOrganizationSerializer: }, ], ) - # Ensures that test runs without actual database access by simulating behavior of a queryset. - @patch("apps.github.models.organization.Organization.objects.filter") - def test_organization_serializer(self, mock_filter, organization_data): - mock_qs = MagicMock() - # To mimic a queryset where no matching objects are found. - mock_qs.exists.return_value = False - mock_filter.return_value = mock_qs + def test_organization_schema(self, organization_data): + schema = OrganizationSchema(**organization_data) + assert schema.created_at == datetime.fromisoformat(organization_data["created_at"]) + assert schema.updated_at == datetime.fromisoformat(organization_data["updated_at"]) - serializer = OrganizationSerializer(data=organization_data) - assert serializer.is_valid() - validated_data = serializer.validated_data - validated_data["created_at"] = ( - validated_data["created_at"].isoformat().replace("+00:00", "Z") - ) - validated_data["updated_at"] = ( - validated_data["updated_at"].isoformat().replace("+00:00", "Z") - ) - assert validated_data == organization_data - - @patch("apps.github.models.organization.Organization.objects.values_list") - def test_get_logins(self, mock_values_list): - mock_values_list.return_value = ["github", "microsoft"] - assert Organization.get_logins() == {"github", "microsoft"} - mock_values_list.assert_called_once_with("login", flat=True) + assert schema.name == organization_data["name"] + assert schema.login == organization_data["login"] + assert schema.company == organization_data["company"] + assert schema.location == organization_data["location"] diff --git a/backend/tests/apps/github/api/release_test.py b/backend/tests/apps/github/api/release_test.py index 30913dc26b..96ef265cf2 100644 --- a/backend/tests/apps/github/api/release_test.py +++ b/backend/tests/apps/github/api/release_test.py @@ -1,9 +1,11 @@ +from datetime import datetime + import pytest -from apps.github.api.release import ReleaseSerializer +from apps.github.api.v1.release import ReleaseSchema -class TestReleaseSerializer: +class TestReleaseSchema: @pytest.mark.parametrize( "release_data", [ @@ -23,15 +25,11 @@ class TestReleaseSerializer: }, ], ) - def test_release_serializer(self, release_data): - serializer = ReleaseSerializer(data=release_data) - assert serializer.is_valid() - validated_data = serializer.validated_data + def test_release_schema(self, release_data): + schema = ReleaseSchema(**release_data) + assert schema.created_at == datetime.fromisoformat(release_data["created_at"]) + assert schema.published_at == datetime.fromisoformat(release_data["published_at"]) - validated_data["created_at"] = ( - validated_data["created_at"].isoformat().replace("+00:00", "Z") - ) - validated_data["published_at"] = ( - validated_data["published_at"].isoformat().replace("+00:00", "Z") - ) - assert validated_data == release_data + assert schema.name == release_data["name"] + assert schema.tag_name == release_data["tag_name"] + assert schema.description == release_data["description"] diff --git a/backend/tests/apps/github/api/repository_test.py b/backend/tests/apps/github/api/repository_test.py index 0f4a34896f..3b384b8373 100644 --- a/backend/tests/apps/github/api/repository_test.py +++ b/backend/tests/apps/github/api/repository_test.py @@ -1,9 +1,11 @@ +from datetime import datetime + import pytest -from apps.github.api.repository import RepositorySerializer +from apps.github.api.v1.repository import RepositorySchema -class TestRepositorySerializer: +class TestRepositorySchema: @pytest.mark.parametrize( "repository_data", [ @@ -21,15 +23,10 @@ class TestRepositorySerializer: }, ], ) - def test_repository_serializer(self, repository_data): - serializer = RepositorySerializer(data=repository_data) - assert serializer.is_valid() - validated_data = serializer.validated_data + def test_repository_schema(self, repository_data): + repository = RepositorySchema(**repository_data) - validated_data["created_at"] = ( - validated_data["created_at"].isoformat().replace("+00:00", "Z") - ) - validated_data["updated_at"] = ( - validated_data["updated_at"].isoformat().replace("+00:00", "Z") - ) - assert validated_data == repository_data + assert repository.name == repository_data["name"] + assert repository.description == repository_data["description"] + assert repository.created_at == datetime.fromisoformat(repository_data["created_at"]) + assert repository.updated_at == datetime.fromisoformat(repository_data["updated_at"]) diff --git a/backend/tests/apps/github/api/urls_test.py b/backend/tests/apps/github/api/urls_test.py index ddf44b8b49..614227a1d0 100644 --- a/backend/tests/apps/github/api/urls_test.py +++ b/backend/tests/apps/github/api/urls_test.py @@ -1,36 +1,38 @@ import pytest -from apps.github.api.issue import IssueViewSet -from apps.github.api.label import LabelViewSet -from apps.github.api.organization import OrganizationViewSet -from apps.github.api.release import ReleaseViewSet -from apps.github.api.repository import RepositoryViewSet -from apps.github.api.urls import router -from apps.github.api.user import UserViewSet +from apps.github.api.v1.issue import router as issue_router +from apps.github.api.v1.label import router as label_router +from apps.github.api.v1.organization import router as organization_router +from apps.github.api.v1.release import router as release_router +from apps.github.api.v1.repository import router as repository_router +from apps.github.api.v1.urls import router as main_router +from apps.github.api.v1.user import router as user_router class TestRouterRegistration: + """Test the urls registration.""" + + EXPECTED_ROUTERS = { + "/issues": issue_router, + "/labels": label_router, + "/organizations": organization_router, + "/releases": release_router, + "/repositories": repository_router, + "/users": user_router, + } + + def test_all_routers_are_registered(self): + """Verifies that the main router has the correct number of registered sub-routers.""" + registered_sub_routers = main_router._routers + assert len(registered_sub_routers) == len(self.EXPECTED_ROUTERS) + @pytest.mark.parametrize( - ("url_name", "viewset_class", "expected_prefix"), - [ - ("issue-list", IssueViewSet, "github/issues"), - ("label-list", LabelViewSet, "github/labels"), - ("organization-list", OrganizationViewSet, "github/organizations"), - ("release-list", ReleaseViewSet, "github/releases"), - ("repository-list", RepositoryViewSet, "github/repositories"), - ("user-list", UserViewSet, "github/users"), - ], + ("prefix", "expected_router_instance"), list(EXPECTED_ROUTERS.items()) ) - def test_router_registration(self, url_name, expected_prefix, viewset_class): - matching_routes = [route for route in router.urls if route.name == url_name] - assert matching_routes, f"Route '{url_name}' not found in router." - - for route in matching_routes: - assert expected_prefix in route.pattern.describe(), ( - f"Prefix '{expected_prefix}' not found in route '{route.name}'." - ) + def test_sub_router_registration(self, prefix, expected_router_instance): + """Tests that each specific router is registered with the correct prefix.""" + registered_router_map = dict(main_router._routers) - viewset = route.callback.cls - assert issubclass(viewset, viewset_class), ( - f"Viewset for '{route.name}' does not match {viewset_class}." - ) + assert prefix in registered_router_map + actual_router = registered_router_map[prefix] + assert actual_router is expected_router_instance diff --git a/backend/tests/apps/github/api/user_test.py b/backend/tests/apps/github/api/user_test.py index dd0fb3efb9..bcdfc6a4c9 100644 --- a/backend/tests/apps/github/api/user_test.py +++ b/backend/tests/apps/github/api/user_test.py @@ -1,12 +1,11 @@ -from unittest.mock import MagicMock, patch +from datetime import datetime import pytest -from apps.github.api.user import UserSerializer -from apps.github.models.user import User +from apps.github.api.v1.user import UserSchema -class TestUserSerializer: +class TestUserSchema: @pytest.mark.parametrize( "user_data", [ @@ -15,49 +14,35 @@ class TestUserSerializer: "login": "johndoe", "company": "GitHub", "location": "San Francisco", + "avatar_url": "https://github.com/images/johndoe.png", + "bio": "Developer advocate", + "email": "john@example.com", + "followers_count": 10, + "following_count": 5, + "public_repositories_count": 3, + "title": "Senior Engineer", + "twitter_username": "johndoe", + "url": "https://github.com/johndoe", "created_at": "2024-12-30T00:00:00Z", "updated_at": "2024-12-30T00:00:00Z", }, - { - "name": "Jane Smith", - "login": "jane-smith", - "company": "Microsoft", - "location": "Redmond", - "created_at": "2024-12-29T00:00:00Z", - "updated_at": "2024-12-30T00:00:00Z", - }, ], ) - # Ensures that test runs without actual database access by simulating behavior of a queryset. - @patch("apps.github.models.user.User.objects.filter") - def test_user_serializer(self, mock_filter, user_data): - mock_qs = MagicMock() - # To mimic a queryset where no matching objects are found. - mock_qs.exists.return_value = False - mock_filter.return_value = mock_qs - - serializer = UserSerializer(data=user_data) - assert serializer.is_valid() - validated_data = serializer.validated_data - - validated_data["created_at"] = ( - validated_data["created_at"].isoformat().replace("+00:00", "Z") - ) - validated_data["updated_at"] = ( - validated_data["updated_at"].isoformat().replace("+00:00", "Z") - ) - assert validated_data == user_data + def test_user_schema(self, user_data): + user = UserSchema(**user_data) - @pytest.mark.parametrize( - ("login", "organization_logins", "expected_result"), - [ - ("johndoe", ["github", "microsoft"], True), # Normal user - ("github", ["github", "microsoft"], False), # Organization login - ("ghost", ["github", "microsoft"], False), # Special 'ghost' user - ], - ) - @patch("apps.github.models.organization.Organization.get_logins") - def test_is_indexable(self, mock_get_logins, login, organization_logins, expected_result): - mock_get_logins.return_value = organization_logins - user = User(login=login) - assert user.is_indexable == expected_result + assert user.name == user_data["name"] + assert user.login == user_data["login"] + assert user.company == user_data["company"] + assert user.location == user_data["location"] + assert user.avatar_url == user_data["avatar_url"] + assert user.bio == user_data["bio"] + assert user.email == user_data["email"] + assert user.followers_count == user_data["followers_count"] + assert user.following_count == user_data["following_count"] + assert user.public_repositories_count == user_data["public_repositories_count"] + assert user.title == user_data["title"] + assert user.twitter_username == user_data["twitter_username"] + assert user.url == user_data["url"] + assert user.created_at == datetime.fromisoformat(user_data["created_at"]) + assert user.updated_at == datetime.fromisoformat(user_data["updated_at"]) diff --git a/backend/tests/apps/owasp/api/chapter_test.py b/backend/tests/apps/owasp/api/chapter_test.py deleted file mode 100644 index 8a0d218ae3..0000000000 --- a/backend/tests/apps/owasp/api/chapter_test.py +++ /dev/null @@ -1,35 +0,0 @@ -import pytest - -from apps.owasp.api.chapter import ChapterSerializer - - -@pytest.mark.parametrize( - ("data", "expected"), - [ - ( - { - "name": "OWASP Nagoya", - "description": "A test chapter", - "level": "other", - "created_at": "2024-11-01T00:00:00Z", - "updated_at": "2024-07-02T00:00:00Z", - }, - True, - ), - ( - { - "name": "OWASP something", - "description": "it is description", - "level": "github", - "created_at": "2023-12-01T00:00:00Z", - "updated_at": "2023-09-02T00:00:00Z", - }, - True, - ), - ], -) -def test_chapter_serializer_validation(data, expected): - serializer = ChapterSerializer(data=data) - is_valid = serializer.is_valid() - - assert is_valid == expected diff --git a/backend/tests/apps/owasp/api/committee_test.py b/backend/tests/apps/owasp/api/committee_test.py deleted file mode 100644 index c6101da9c2..0000000000 --- a/backend/tests/apps/owasp/api/committee_test.py +++ /dev/null @@ -1,33 +0,0 @@ -import pytest - -from apps.owasp.api.committee import CommitteeSerializer - - -@pytest.mark.parametrize( - ("data", "expected"), - [ - ( - { - "name": "Test Project", - "description": "A test project", - "created_at": "2024-11-01T00:00:00Z", - "updated_at": "2024-07-02T00:00:00Z", - }, - True, - ), - ( - { - "name": "this is a project", - "description": "A project without a name", - "created_at": "2023-12-01T00:00:00Z", - "updated_at": "2023-09-02T00:00:00Z", - }, - True, - ), - ], -) -def test_committee_serializer_validation(data, expected): - serializer = CommitteeSerializer(data=data) - is_valid = serializer.is_valid() - - assert is_valid == expected diff --git a/backend/tests/apps/owasp/api/event_test.py b/backend/tests/apps/owasp/api/event_test.py deleted file mode 100644 index f2a6adb36c..0000000000 --- a/backend/tests/apps/owasp/api/event_test.py +++ /dev/null @@ -1,31 +0,0 @@ -import pytest - -from apps.owasp.api.event import EventSerializer - - -@pytest.mark.parametrize( - ("data", "expected"), - [ - ( - { - "name": "Test Event", - "description": "A test event", - "url": "https://github.com/owasp/Nest", - }, - True, - ), - ( - { - "name": "biggest event", - "description": "this is a biggest event", - "url": "https://github.com/owasp", - }, - True, - ), - ], -) -def test_event_serializer_validation(data, expected): - serializer = EventSerializer(data=data) - is_valid = serializer.is_valid() - - assert is_valid == expected diff --git a/backend/tests/apps/owasp/api/project_test.py b/backend/tests/apps/owasp/api/project_test.py deleted file mode 100644 index 6da788bc5b..0000000000 --- a/backend/tests/apps/owasp/api/project_test.py +++ /dev/null @@ -1,35 +0,0 @@ -import pytest - -from apps.owasp.api.project import ProjectSerializer - - -@pytest.mark.parametrize( - ("data", "expected"), - [ - ( - { - "name": "another project", - "description": "A test project by owasp", - "level": "other", - "created_at": "2023-01-01T00:00:00Z", - "updated_at": "2023-01-02T00:00:00Z", - }, - True, - ), - ( - { - "name": "this is a project", - "description": "this is not a project, this is just a file", - "level": "Hello", - "created_at": "2023-01-01T00:00:00Z", - "updated_at": "2023-01-02T00:00:00Z", - }, - False, - ), - ], -) -def test_project_serializer_validation(data, expected): - serializer = ProjectSerializer(data=data) - is_valid = serializer.is_valid() - - assert is_valid == expected diff --git a/backend/tests/apps/owasp/api/urls_test.py b/backend/tests/apps/owasp/api/urls_test.py deleted file mode 100644 index be8d9e7e83..0000000000 --- a/backend/tests/apps/owasp/api/urls_test.py +++ /dev/null @@ -1,31 +0,0 @@ -import pytest - -from apps.owasp.api.chapter import ChapterViewSet -from apps.owasp.api.committee import CommitteeViewSet -from apps.owasp.api.event import EventViewSet -from apps.owasp.api.project import ProjectViewSet -from apps.owasp.api.urls import router - - -@pytest.mark.parametrize( - ("url_name", "expected_prefix", "viewset_class"), - [ - ("chapter-list", "owasp/chapters", ChapterViewSet), - ("committee-list", "owasp/committees", CommitteeViewSet), - ("event-list", "owasp/events", EventViewSet), - ("project-list", "owasp/projects", ProjectViewSet), - ], -) -def test_router_registration(url_name, expected_prefix, viewset_class): - matching_routes = [route for route in router.urls if route.name == url_name] - assert matching_routes, f"Route '{url_name}' not found in router." - - for route in matching_routes: - assert expected_prefix in route.pattern.describe(), ( - f"Prefix '{expected_prefix}' not found in route '{route.name}'." - ) - - viewset = route.callback.cls - assert issubclass(viewset, viewset_class), ( - f"Viewset for '{route.name}' does not match {viewset_class}." - ) diff --git a/backend/tests/apps/owasp/api/v1/__init__.py b/backend/tests/apps/owasp/api/v1/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/backend/tests/apps/owasp/api/v1/chapter_test.py b/backend/tests/apps/owasp/api/v1/chapter_test.py new file mode 100644 index 0000000000..eb6a394d20 --- /dev/null +++ b/backend/tests/apps/owasp/api/v1/chapter_test.py @@ -0,0 +1,33 @@ +from datetime import datetime + +import pytest + +from apps.owasp.api.v1.chapter import ChapterSchema + + +@pytest.mark.parametrize( + "chapter_data", + [ + { + "name": "OWASP Nagoya", + "country": "America", + "region": "Europe", + "created_at": "2024-11-01T00:00:00Z", + "updated_at": "2024-07-02T00:00:00Z", + }, + { + "name": "OWASP something", + "country": "India", + "region": "Asia", + "created_at": "2023-12-01T00:00:00Z", + "updated_at": "2023-09-02T00:00:00Z", + }, + ], +) +def test_chapter_serializer_validation(chapter_data): + chapter = ChapterSchema(**chapter_data) + assert chapter.name == chapter_data["name"] + assert chapter.country == chapter_data["country"] + assert chapter.region == chapter_data["region"] + assert chapter.created_at == datetime.fromisoformat(chapter_data["created_at"]) + assert chapter.updated_at == datetime.fromisoformat(chapter_data["updated_at"]) diff --git a/backend/tests/apps/owasp/api/v1/committee_test.py b/backend/tests/apps/owasp/api/v1/committee_test.py new file mode 100644 index 0000000000..a6216931d8 --- /dev/null +++ b/backend/tests/apps/owasp/api/v1/committee_test.py @@ -0,0 +1,30 @@ +from datetime import datetime + +import pytest + +from apps.owasp.api.v1.committee import CommitteeSchema + + +@pytest.mark.parametrize( + "committee_data", + [ + { + "name": "Test Committee", + "description": "A test committee", + "created_at": "2024-11-01T00:00:00Z", + "updated_at": "2024-07-02T00:00:00Z", + }, + { + "name": "this is a committee", + "description": "A committee without a name", + "created_at": "2023-12-01T00:00:00Z", + "updated_at": "2023-09-02T00:00:00Z", + }, + ], +) +def test_committee_serializer_validation(committee_data): + committee = CommitteeSchema(**committee_data) + assert committee.name == committee_data["name"] + assert committee.description == committee_data["description"] + assert committee.created_at == datetime.fromisoformat(committee_data["created_at"]) + assert committee.updated_at == datetime.fromisoformat(committee_data["updated_at"]) diff --git a/backend/tests/apps/owasp/api/v1/event_test.py b/backend/tests/apps/owasp/api/v1/event_test.py new file mode 100644 index 0000000000..d12d29411a --- /dev/null +++ b/backend/tests/apps/owasp/api/v1/event_test.py @@ -0,0 +1,25 @@ +import pytest + +from apps.owasp.api.v1.event import EventSchema + + +@pytest.mark.parametrize( + "event_data", + [ + { + "name": "Test Event", + "description": "A test event", + "url": "https://github.com/owasp/Nest", + }, + { + "name": "biggest event", + "description": "this is a biggest event", + "url": "https://github.com/owasp", + }, + ], +) +def test_event_serializer_validation(event_data): + event = EventSchema(**event_data) + assert event.name == event_data["name"] + assert event.description == event_data["description"] + assert event.url == event_data["url"] diff --git a/backend/tests/apps/owasp/api/v1/project_test.py b/backend/tests/apps/owasp/api/v1/project_test.py new file mode 100644 index 0000000000..0bb8ed969e --- /dev/null +++ b/backend/tests/apps/owasp/api/v1/project_test.py @@ -0,0 +1,33 @@ +from datetime import datetime + +import pytest + +from apps.owasp.api.v1.project import ProjectSchema + + +@pytest.mark.parametrize( + "project_data", + [ + { + "name": "another project", + "description": "A test project by owasp", + "level": "other", + "created_at": "2023-01-01T00:00:00Z", + "updated_at": "2023-01-02T00:00:00Z", + }, + { + "name": "this is a project", + "description": "this is not a project, this is just a file", + "level": "Hello", + "created_at": "2023-01-01T00:00:00Z", + "updated_at": "2023-01-02T00:00:00Z", + }, + ], +) +def test_project_serializer_validation(project_data): + project = ProjectSchema(**project_data) + assert project.name == project_data["name"] + assert project.description == project_data["description"] + assert project.level == project_data["level"] + assert project.created_at == datetime.fromisoformat(project_data["created_at"]) + assert project.updated_at == datetime.fromisoformat(project_data["updated_at"]) diff --git a/backend/tests/apps/owasp/api/v1/urls_test.py b/backend/tests/apps/owasp/api/v1/urls_test.py new file mode 100644 index 0000000000..da2a258d5b --- /dev/null +++ b/backend/tests/apps/owasp/api/v1/urls_test.py @@ -0,0 +1,35 @@ +import pytest + +from apps.owasp.api.v1.chapter import router as chapter_router +from apps.owasp.api.v1.committee import router as committee_router +from apps.owasp.api.v1.event import router as event_router +from apps.owasp.api.v1.project import router as project_router +from apps.owasp.api.v1.urls import router as main_router + + +class TestRouterRegistration: + """Test the urls registration.""" + + EXPECTED_ROUTERS = { + "/chapters": chapter_router, + "/committees": committee_router, + "/events": event_router, + "/projects": project_router, + } + + def test_all_routers_are_registered(self): + """Verifies that the main router has the correct number of registered sub-routers.""" + registered_sub_routers = main_router._routers + assert len(registered_sub_routers) == len(self.EXPECTED_ROUTERS) + + @pytest.mark.parametrize( + ("prefix", "expected_router_instance"), list(EXPECTED_ROUTERS.items()) + ) + def test_sub_router_registration(self, prefix, expected_router_instance): + """Tests that each specific router is registered with the correct prefix.""" + registered_router_map = dict(main_router._routers) + + assert prefix in registered_router_map + actual_router = registered_router_map[prefix] + + assert actual_router is expected_router_instance diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index de9a905b04..8047940c4f 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -3537,8 +3537,8 @@ packages: dompurify@3.2.6: resolution: {integrity: sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==} - dotenv@16.6.0: - resolution: {integrity: sha512-Omf1L8paOy2VJhILjyhrhqwLIdstqm1BvcDPKg4NGAlkwEu9ODyrFbvk8UymUOMCT+HXo31jg1lArIrVAAhuGA==} + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} dunder-proto@1.0.1: @@ -9079,7 +9079,7 @@ snapshots: '@babel/core': 7.27.7 '@sentry/babel-plugin-component-annotate': 3.5.0 '@sentry/cli': 2.42.2 - dotenv: 16.6.0 + dotenv: 16.6.1 find-up: 5.0.0 glob: 9.3.5 magic-string: 0.30.8 @@ -10372,7 +10372,7 @@ snapshots: optionalDependencies: '@types/trusted-types': 2.0.7 - dotenv@16.6.0: {} + dotenv@16.6.1: {} dunder-proto@1.0.1: dependencies: