diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cd1169c0d9..cafda3ef61 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,6 +42,7 @@ repos: hooks: - id: mypy additional_dependencies: + - strawberry-graphql-django - types-jsonschema - types-lxml - types-python-dateutil diff --git a/backend/apps/common/graphql/__init__.py b/backend/apps/common/graphql/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/backend/apps/common/graphql/nodes.py b/backend/apps/common/graphql/nodes.py deleted file mode 100644 index b6fa1d4265..0000000000 --- a/backend/apps/common/graphql/nodes.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Common GraphQL nodes.""" - -from graphene_django import DjangoObjectType - - -class BaseNode(DjangoObjectType): - """Base node.""" - - class Meta: - abstract = True diff --git a/backend/apps/common/graphql/queries.py b/backend/apps/common/graphql/queries.py deleted file mode 100644 index b3b6bdfc48..0000000000 --- a/backend/apps/common/graphql/queries.py +++ /dev/null @@ -1,7 +0,0 @@ -"""Common GraphQL queries.""" - -import graphene - - -class BaseQuery(graphene.ObjectType): - """Base query.""" diff --git a/backend/apps/github/graphql/nodes/issue.py b/backend/apps/github/graphql/nodes/issue.py index 429049b31d..54994e38bb 100644 --- a/backend/apps/github/graphql/nodes/issue.py +++ b/backend/apps/github/graphql/nodes/issue.py @@ -1,31 +1,39 @@ """GitHub issue GraphQL node.""" -import graphene +import strawberry +import strawberry_django -from apps.common.graphql.nodes import BaseNode +from apps.github.graphql.nodes.user import UserNode from apps.github.models.issue import Issue -class IssueNode(BaseNode): +@strawberry_django.type( + Issue, + fields=[ + "created_at", + "state", + "title", + "url", + ], +) +class IssueNode: """GitHub issue node.""" - organization_name = graphene.String() - repository_name = graphene.String() - - class Meta: - model = Issue - fields = ( - "author", - "created_at", - "state", - "title", - "url", + @strawberry.field + def author(self) -> UserNode | None: + """Resolve author.""" + return self.author + + @strawberry.field + def organization_name(self) -> str | None: + """Resolve organization name.""" + return ( + self.repository.organization.login + if self.repository and self.repository.organization + else None ) - def resolve_organization_name(self, info) -> str | None: - """Return organization name.""" - return self.repository.organization.login if self.repository.organization else None - - def resolve_repository_name(self, info): + @strawberry.field + def repository_name(self) -> str | None: """Resolve the repository name.""" - return self.repository.name + return self.repository.name if self.repository else None diff --git a/backend/apps/github/graphql/nodes/milestone.py b/backend/apps/github/graphql/nodes/milestone.py index cc1eb67ae8..e263fe5328 100644 --- a/backend/apps/github/graphql/nodes/milestone.py +++ b/backend/apps/github/graphql/nodes/milestone.py @@ -1,42 +1,49 @@ """Github Milestone Node.""" -import graphene +import strawberry +import strawberry_django -from apps.common.graphql.nodes import BaseNode +from apps.github.graphql.nodes.user import UserNode from apps.github.models.milestone import Milestone -class MilestoneNode(BaseNode): +@strawberry_django.type( + Milestone, + fields=[ + "closed_issues_count", + "created_at", + "body", + "open_issues_count", + "title", + "url", + ], +) +class MilestoneNode: """Github Milestone Node.""" - organization_name = graphene.String() - progress = graphene.Float() - repository_name = graphene.String() - - class Meta: - model = Milestone - - fields = ( - "author", - "body", - "created_at", - "title", - "open_issues_count", - "closed_issues_count", - "url", + @strawberry.field + def author(self) -> UserNode | None: + """Resolve author.""" + return self.author + + @strawberry.field + def organization_name(self) -> str | None: + """Resolve organization name.""" + return ( + self.repository.organization.login + if self.repository and self.repository.organization + else None ) - def resolve_organization_name(self, info): - """Return organization name.""" - return self.repository.organization.login if self.repository.organization else None - - def resolve_progress(self, info): - """Return milestone progress.""" + @strawberry.field + def progress(self) -> float: + """Resolve milestone progress.""" total_issues_count = self.closed_issues_count + self.open_issues_count if not total_issues_count: return 0.0 return round((self.closed_issues_count / total_issues_count) * 100, 2) - def resolve_repository_name(self, info): + @strawberry.field + def repository_name(self) -> str | None: """Resolve repository name.""" - return self.repository.name + return self.repository.name if self.repository else None diff --git a/backend/apps/github/graphql/nodes/organization.py b/backend/apps/github/graphql/nodes/organization.py index cabdce8e31..8656ef10ee 100644 --- a/backend/apps/github/graphql/nodes/organization.py +++ b/backend/apps/github/graphql/nodes/organization.py @@ -1,47 +1,46 @@ """GitHub organization GraphQL node.""" -import graphene +import strawberry +import strawberry_django from django.db import models -from apps.common.graphql.nodes import BaseNode from apps.github.models.organization import Organization from apps.github.models.repository import Repository from apps.github.models.repository_contributor import RepositoryContributor -class OrganizationStatsNode(graphene.ObjectType): +@strawberry.type +class OrganizationStatsNode: """Organization stats node.""" - total_repositories = graphene.Int() - total_contributors = graphene.Int() - total_stars = graphene.Int() - total_forks = graphene.Int() - total_issues = graphene.Int() + total_contributors: int + total_forks: int + total_issues: int + total_repositories: int + total_stars: int -class OrganizationNode(BaseNode): +@strawberry_django.type( + Organization, + fields=[ + "avatar_url", + "collaborators_count", + "company", + "created_at", + "description", + "email", + "followers_count", + "location", + "login", + "name", + "updated_at", + ], +) +class OrganizationNode: """GitHub organization node.""" - stats = graphene.Field(OrganizationStatsNode) - url = graphene.String() - - class Meta: - model = Organization - fields = ( - "avatar_url", - "collaborators_count", - "company", - "created_at", - "description", - "email", - "followers_count", - "location", - "login", - "name", - "updated_at", - ) - - def resolve_stats(self, info): + @strawberry.field + def stats(self) -> OrganizationStatsNode: """Resolve organization stats.""" repositories = Repository.objects.filter(organization=self) @@ -51,9 +50,10 @@ def resolve_stats(self, info): total_forks=models.Sum("forks_count"), total_issues=models.Sum("open_issues_count"), ) - total_stars = aggregated_stats["total_stars"] or 0 - total_forks = aggregated_stats["total_forks"] or 0 - total_issues = aggregated_stats["total_issues"] or 0 + + total_stars = aggregated_stats.get("total_stars") or 0 + total_forks = aggregated_stats.get("total_forks") or 0 + total_issues = aggregated_stats.get("total_issues") or 0 unique_contributors = ( RepositoryContributor.objects.filter(repository__in=repositories) @@ -70,6 +70,7 @@ def resolve_stats(self, info): total_issues=total_issues, ) - def resolve_url(self, info): + @strawberry.field + def url(self) -> str: """Resolve organization URL.""" return self.url diff --git a/backend/apps/github/graphql/nodes/pull_request.py b/backend/apps/github/graphql/nodes/pull_request.py index 6cbdd5e4c1..e785d4df49 100644 --- a/backend/apps/github/graphql/nodes/pull_request.py +++ b/backend/apps/github/graphql/nodes/pull_request.py @@ -1,34 +1,42 @@ """GitHub Pull Request Node.""" -import graphene +import strawberry +import strawberry_django -from apps.common.graphql.nodes import BaseNode +from apps.github.graphql.nodes.user import UserNode from apps.github.models.pull_request import PullRequest -class PullRequestNode(BaseNode): +@strawberry_django.type( + PullRequest, + fields=[ + "created_at", + "title", + ], +) +class PullRequestNode: """GitHub pull request node.""" - organization_name = graphene.String() - repository_name = graphene.String() - url = graphene.String() - - class Meta: - model = PullRequest - fields = ( - "author", - "created_at", - "title", + @strawberry.field + def author(self) -> UserNode | None: + """Resolve author.""" + return self.author + + @strawberry.field + def organization_name(self) -> str | None: + """Resolve organization name.""" + return ( + self.repository.organization.login + if self.repository and self.repository.organization + else None ) - def resolve_organization_name(self, info): - """Return organization name.""" - return self.repository.organization.login if self.repository.organization else None - - def resolve_repository_name(self, info): + @strawberry.field + def repository_name(self) -> str | None: """Resolve repository name.""" - return self.repository.name + return self.repository.name if self.repository else None - def resolve_url(self, info): + @strawberry.field + def url(self) -> str: """Resolve URL.""" - return self.url + return str(self.url) if self.url else "" diff --git a/backend/apps/github/graphql/nodes/release.py b/backend/apps/github/graphql/nodes/release.py index b6b7c302f7..5014b75e80 100644 --- a/backend/apps/github/graphql/nodes/release.py +++ b/backend/apps/github/graphql/nodes/release.py @@ -1,46 +1,54 @@ """GitHub release GraphQL node.""" -from __future__ import annotations +import strawberry +import strawberry_django -import graphene - -from apps.common.graphql.nodes import BaseNode from apps.github.graphql.nodes.user import UserNode from apps.github.models.release import Release from apps.owasp.constants import OWASP_ORGANIZATION_NAME -class ReleaseNode(BaseNode): +@strawberry_django.type( + Release, + fields=[ + "is_pre_release", + "name", + "published_at", + "tag_name", + ], +) +class ReleaseNode: """GitHub release node.""" - author = graphene.Field(UserNode) - organization_name = graphene.String() - project_name = graphene.String() - repository_name = graphene.String() - url = graphene.String() - - class Meta: - model = Release - fields = ( - "author", - "is_pre_release", - "name", - "published_at", - "tag_name", + @strawberry.field + def author(self) -> UserNode | None: + """Resolve author.""" + return self.author + + @strawberry.field + def organization_name(self) -> str | None: + """Resolve organization name.""" + return ( + self.repository.organization.login + if self.repository and self.repository.organization + else None ) - def resolve_organization_name(self, info) -> str | None: - """Return organization name.""" - return self.repository.organization.login if self.repository.organization else None - - def resolve_project_name(self, info) -> str: - """Return project name.""" - return self.repository.project.name.lstrip(OWASP_ORGANIZATION_NAME) + @strawberry.field + def project_name(self) -> str: + """Resolve project name.""" + return ( + self.repository.project.name.lstrip(OWASP_ORGANIZATION_NAME) + if self.repository and self.repository.project + else None + ) - def resolve_repository_name(self, info) -> str: - """Return repository name.""" - return self.repository.name + @strawberry.field + def repository_name(self) -> str: + """Resolve repository name.""" + return self.repository.name if self.repository else None - def resolve_url(self, info) -> str: - """Return release URL.""" + @strawberry.field + def url(self) -> str: + """Resolve URL.""" return self.url diff --git a/backend/apps/github/graphql/nodes/repository.py b/backend/apps/github/graphql/nodes/repository.py index e9a80fa630..62a588a5de 100644 --- a/backend/apps/github/graphql/nodes/repository.py +++ b/backend/apps/github/graphql/nodes/repository.py @@ -1,10 +1,11 @@ """GitHub repository GraphQL node.""" -import graphene +import strawberry +import strawberry_django -from apps.common.graphql.nodes import BaseNode from apps.github.graphql.nodes.issue import IssueNode from apps.github.graphql.nodes.milestone import MilestoneNode +from apps.github.graphql.nodes.organization import OrganizationNode from apps.github.graphql.nodes.release import ReleaseNode from apps.github.graphql.nodes.repository_contributor import RepositoryContributorNode from apps.github.models.repository import Repository @@ -13,83 +14,75 @@ RECENT_RELEASES_LIMIT = 5 -class RepositoryNode(BaseNode): +@strawberry_django.type( + Repository, + fields=[ + "commits_count", + "contributors_count", + "created_at", + "description", + "forks_count", + "key", + "license", + "name", + "open_issues_count", + "size", + "stars_count", + "subscribers_count", + "updated_at", + ], +) +class RepositoryNode: """Repository node.""" - issues = graphene.List(IssueNode) - recent_milestones = graphene.List( - MilestoneNode, - limit=graphene.Int(default_value=5), - ) - languages = graphene.List(graphene.String) - latest_release = graphene.String() - owner_key = graphene.String() - releases = graphene.List(ReleaseNode) - top_contributors = graphene.List(RepositoryContributorNode) - topics = graphene.List(graphene.String) - url = graphene.String() - - class Meta: - model = Repository - fields = ( - "commits_count", - "contributors_count", - "created_at", - "description", - "forks_count", - "key", - "license", - "name", - "open_issues_count", - "organization", - "size", - "stars_count", - "subscribers_count", - "updated_at", - ) - - def resolve_issues(self, info): + @strawberry.field + def issues(self) -> list[IssueNode]: """Resolve recent issues.""" - return self.issues.select_related( - "author", - ).order_by( - "-created_at", - )[:RECENT_ISSUES_LIMIT] + # TODO(arkid15r): rename this to recent_issues. + return self.issues.select_related("author").order_by("-created_at")[:RECENT_ISSUES_LIMIT] - def resolve_languages(self, info): + @strawberry.field + def languages(self) -> list[str]: """Resolve languages.""" - return self.languages.keys() + return list(self.languages.keys()) - def resolve_latest_release(self, info): + @strawberry.field + def latest_release(self) -> str: """Resolve latest release.""" return self.latest_release - def resolve_owner_key(self, info): + @strawberry.field + def organization(self) -> OrganizationNode: + """Resolve organization.""" + return self.organization + + @strawberry.field + def owner_key(self) -> str: """Resolve owner key.""" return self.owner_key - def resolve_releases(self, info): - """Resolve recent releases.""" - return self.published_releases.order_by( - "-published_at", - )[:RECENT_RELEASES_LIMIT] - - def resolve_recent_milestones(self, info, limit=5): + @strawberry.field + def recent_milestones(self, limit: int = 5) -> list[MilestoneNode]: """Resolve recent milestones.""" - return self.recent_milestones.select_related( - "repository", - ).order_by( - "-created_at", - )[:limit] + return self.recent_milestones.select_related("repository").order_by("-created_at")[:limit] + + @strawberry.field + def releases(self) -> list[ReleaseNode]: + """Resolve recent releases.""" + # TODO(arkid15r): rename this to recent_releases. + return self.published_releases.order_by("-published_at")[:RECENT_RELEASES_LIMIT] - def resolve_top_contributors(self, info): + @strawberry.field + def top_contributors(self) -> list[RepositoryContributorNode]: """Resolve top contributors.""" return self.idx_top_contributors - def resolve_topics(self, info): + @strawberry.field + def topics(self) -> list[str]: """Resolve topics.""" return self.topics - def resolve_url(self, info): + @strawberry.field + def url(self) -> str: """Resolve URL.""" return self.url diff --git a/backend/apps/github/graphql/nodes/repository_contributor.py b/backend/apps/github/graphql/nodes/repository_contributor.py index bbe99c0f7a..4f788d02eb 100644 --- a/backend/apps/github/graphql/nodes/repository_contributor.py +++ b/backend/apps/github/graphql/nodes/repository_contributor.py @@ -1,14 +1,15 @@ """GitHub repository GraphQL node.""" -import graphene +import strawberry -class RepositoryContributorNode(graphene.ObjectType): +@strawberry.type +class RepositoryContributorNode: """Repository contributor node.""" - avatar_url = graphene.String() - contributions_count = graphene.Int() - login = graphene.String() - name = graphene.String() - project_key = graphene.String() - project_name = graphene.String() + avatar_url: str + contributions_count: int + login: str + name: str + project_key: str + project_name: str diff --git a/backend/apps/github/graphql/nodes/user.py b/backend/apps/github/graphql/nodes/user.py index 9b1ba4fc4b..f966769f78 100644 --- a/backend/apps/github/graphql/nodes/user.py +++ b/backend/apps/github/graphql/nodes/user.py @@ -1,60 +1,52 @@ """GitHub user GraphQL node.""" -import graphene +import strawberry +import strawberry_django -from apps.common.graphql.nodes import BaseNode from apps.github.models.user import User -class RepositoryType(graphene.ObjectType): - """Repository type for nested objects.""" - - key = graphene.String() - owner_key = graphene.String() - - -class UserNode(BaseNode): +@strawberry_django.type( + User, + fields=[ + "avatar_url", + "bio", + "company", + "contributions_count", + "email", + "followers_count", + "following_count", + "id", + "location", + "login", + "name", + "public_repositories_count", + ], +) +class UserNode: """GitHub user node.""" - created_at = graphene.Float() - issues_count = graphene.Int() - releases_count = graphene.Int() - updated_at = graphene.Float() - url = graphene.String() - - class Meta: - model = User - fields = ( - "avatar_url", - "bio", - "company", - "contributions_count", - "email", - "followers_count", - "following_count", - "id", - "location", - "login", - "name", - "public_repositories_count", - ) - - def resolve_created_at(self, info): + @strawberry.field + def created_at(self) -> float: """Resolve created at.""" return self.idx_created_at - def resolve_issues_count(self, info) -> int: + @strawberry.field + def issues_count(self) -> int: """Resolve issues count.""" return self.idx_issues_count - def resolve_releases_count(self, info) -> int: + @strawberry.field + def releases_count(self) -> int: """Resolve releases count.""" return self.idx_releases_count - def resolve_updated_at(self, info): + @strawberry.field + def updated_at(self) -> float: """Resolve updated at.""" return self.idx_updated_at - def resolve_url(self, info) -> str: + @strawberry.field + def url(self) -> str: """Resolve URL.""" return self.url diff --git a/backend/apps/github/graphql/queries/issue.py b/backend/apps/github/graphql/queries/issue.py index 5777e8891d..8f3fd8c846 100644 --- a/backend/apps/github/graphql/queries/issue.py +++ b/backend/apps/github/graphql/queries/issue.py @@ -1,47 +1,35 @@ """GraphQL queries for handling GitHub issues.""" -from __future__ import annotations +import strawberry +from django.db.models import OuterRef, Subquery -import graphene -from django.db.models import OuterRef, QuerySet, Subquery - -from apps.common.graphql.queries import BaseQuery from apps.github.graphql.nodes.issue import IssueNode from apps.github.models.issue import Issue -class IssueQuery(BaseQuery): +@strawberry.type +class IssueQuery: """GraphQL query class for retrieving GitHub issues.""" - recent_issues = graphene.List( - IssueNode, - distinct=graphene.Boolean(default_value=False), - limit=graphene.Int(default_value=5), - login=graphene.String(required=False), - organization=graphene.String(required=False), - ) - - def resolve_recent_issues( - root, - info, + @strawberry.field + def recent_issues( + self, *, distinct: bool = False, limit: int = 5, login: str | None = None, organization: str | None = None, - ) -> QuerySet: + ) -> list[IssueNode]: """Resolve recent issues with optional filtering. Args: - root (Any): The root query object. - info (ResolveInfo): The GraphQL execution context. distinct (bool): Whether to return unique issues per author and repository. limit (int): Maximum number of issues to return. login (str, optional): Filter issues by a specific author's login. organization (str, optional): Filter issues by a specific organization's login. Returns: - QuerySet: Queryset containing the filtered list of issues. + list[IssueNode]: List of issue nodes. """ queryset = Issue.objects.select_related( diff --git a/backend/apps/github/graphql/queries/milestone.py b/backend/apps/github/graphql/queries/milestone.py index a6cb159c1f..4f241f8e0a 100644 --- a/backend/apps/github/graphql/queries/milestone.py +++ b/backend/apps/github/graphql/queries/milestone.py @@ -1,41 +1,30 @@ """Github Milestone Queries.""" -import graphene +import strawberry from django.core.exceptions import ValidationError from django.db.models import OuterRef, Subquery -from apps.common.graphql.queries import BaseQuery from apps.github.graphql.nodes.milestone import MilestoneNode from apps.github.models.milestone import Milestone -class MilestoneQuery(BaseQuery): +@strawberry.type +class MilestoneQuery: """Github Milestone Queries.""" - recent_milestones = graphene.List( - MilestoneNode, - distinct=graphene.Boolean(default_value=False), - limit=graphene.Int(default_value=5), - login=graphene.String(required=False), - organization=graphene.String(required=False), - state=graphene.String(default_value="open"), - ) - - def resolve_recent_milestones( - root, - info, + @strawberry.field + def recent_milestones( + self, *, distinct: bool = False, limit: int = 5, login: str | None = None, organization: str | None = None, state: str = "open", - ): + ) -> list[MilestoneNode]: """Resolve milestones. Args: - root (object): The root object. - info (ResolveInfo): The GraphQL execution context. distinct (bool): Whether to return distinct milestones. limit (int): The maximum number of milestones to return. login (str, optional): The GitHub username to filter milestones. @@ -43,7 +32,7 @@ def resolve_recent_milestones( state (str, optional): The state of the milestones to return. Returns: - list: A list of milestones. + list[MilestoneNode]: A list of milestones. """ match state.lower(): diff --git a/backend/apps/github/graphql/queries/organization.py b/backend/apps/github/graphql/queries/organization.py index d69239369c..456cd2fab9 100644 --- a/backend/apps/github/graphql/queries/organization.py +++ b/backend/apps/github/graphql/queries/organization.py @@ -1,30 +1,28 @@ """GitHub organization GraphQL queries.""" -import graphene +import strawberry -from apps.common.graphql.queries import BaseQuery from apps.github.graphql.nodes.organization import OrganizationNode from apps.github.models.organization import Organization -class OrganizationQuery(BaseQuery): +@strawberry.type +class OrganizationQuery: """Organization queries.""" - organization = graphene.Field( - OrganizationNode, - login=graphene.String(required=True), - ) - - def resolve_organization(root, info, login): + @strawberry.field + def organization( + self, + *, + login: str, + ) -> OrganizationNode | None: """Resolve organization by login. Args: - root: The root object. - info: GraphQL execution info. login (str): The login of the organization. Returns: - Organization: The organization object if found, otherwise None. + OrganizationNode: The organization node if found, otherwise None. """ try: diff --git a/backend/apps/github/graphql/queries/pull_request.py b/backend/apps/github/graphql/queries/pull_request.py index 4ab5bc0e0e..18dd6b7397 100644 --- a/backend/apps/github/graphql/queries/pull_request.py +++ b/backend/apps/github/graphql/queries/pull_request.py @@ -1,32 +1,20 @@ """Github pull requests GraphQL queries.""" -from __future__ import annotations +import strawberry +from django.db.models import OuterRef, Subquery -import graphene -from django.db.models import OuterRef, QuerySet, Subquery - -from apps.common.graphql.queries import BaseQuery from apps.github.graphql.nodes.pull_request import PullRequestNode from apps.github.models.pull_request import PullRequest from apps.owasp.models.project import Project -class PullRequestQuery(BaseQuery): +@strawberry.type +class PullRequestQuery: """Pull request queries.""" - recent_pull_requests = graphene.List( - PullRequestNode, - distinct=graphene.Boolean(default_value=False), - limit=graphene.Int(default_value=5), - login=graphene.String(required=False), - organization=graphene.String(required=False), - project=graphene.String(required=False), - repository=graphene.String(required=False), - ) - - def resolve_recent_pull_requests( - root, - info, + @strawberry.field + def recent_pull_requests( + self, *, distinct: bool = False, limit: int = 5, @@ -34,12 +22,10 @@ def resolve_recent_pull_requests( organization: str | None = None, project: str | None = None, repository: str | None = None, - ) -> QuerySet: + ) -> list[PullRequestNode]: """Resolve recent pull requests. Args: - root (Any): The root query object. - info (ResolveInfo): The GraphQL execution context. distinct (bool): Whether to return unique pull requests per author and repository. limit (int): Maximum number of pull requests to return. login (str, optional): Filter pull requests by a specific author's login. @@ -48,7 +34,8 @@ def resolve_recent_pull_requests( repository (str, optional): Filter pull requests by a specific repository's login. Returns: - QuerySet: Queryset containing the filtered list of pull requests. + list[PullRequestNode]: List of pull request nodes containing the + filtered list of pull requests. """ queryset = ( diff --git a/backend/apps/github/graphql/queries/release.py b/backend/apps/github/graphql/queries/release.py index f3e3e670e2..4b05769da4 100644 --- a/backend/apps/github/graphql/queries/release.py +++ b/backend/apps/github/graphql/queries/release.py @@ -1,47 +1,35 @@ """GraphQL queries for handling OWASP releases.""" -from __future__ import annotations +import strawberry +from django.db.models import OuterRef, Subquery -import graphene -from django.db.models import OuterRef, QuerySet, Subquery - -from apps.common.graphql.queries import BaseQuery from apps.github.graphql.nodes.release import ReleaseNode from apps.github.models.release import Release -class ReleaseQuery(BaseQuery): +@strawberry.type +class ReleaseQuery: """GraphQL query class for retrieving recent GitHub releases.""" - recent_releases = graphene.List( - ReleaseNode, - distinct=graphene.Boolean(default_value=False), - limit=graphene.Int(default_value=6), - login=graphene.String(required=False), - organization=graphene.String(required=False), - ) - - def resolve_recent_releases( - root, - info, + @strawberry.field + def recent_releases( + self, *, distinct: bool = False, limit: int = 6, - login=None, + login: str | None = None, organization: str | None = None, - ) -> QuerySet: + ) -> list[ReleaseNode]: """Resolve recent releases with optional distinct filtering. Args: - root (Any): The root query object. - info (ResolveInfo): The GraphQL execution context. distinct (bool): Whether to return unique releases per author and repository. limit (int): Maximum number of releases to return. - login (str): Optional GitHub username for filtering releases. - organization (str): Optional GitHub organization for filtering releases. + login (str, optional): Filter releases by a specific author's login. + organization (str, optional): Filter releases by a specific organization's login. Returns: - QuerySet: Queryset containing the filtered list of releases. + list[ReleaseNode]: List of release nodes containing the filtered list of releases. """ queryset = Release.objects.filter( diff --git a/backend/apps/github/graphql/queries/repository.py b/backend/apps/github/graphql/queries/repository.py index 34b15a2cd0..6ed371288f 100644 --- a/backend/apps/github/graphql/queries/repository.py +++ b/backend/apps/github/graphql/queries/repository.py @@ -1,45 +1,29 @@ """OWASP repository GraphQL queries.""" -from __future__ import annotations +import strawberry -import graphene - -from apps.common.graphql.queries import BaseQuery from apps.github.graphql.nodes.repository import RepositoryNode from apps.github.models.repository import Repository -class RepositoryQuery(BaseQuery): +@strawberry.type +class RepositoryQuery: """Repository queries.""" - repository = graphene.Field( - RepositoryNode, - organization_key=graphene.String(required=True), - repository_key=graphene.String(required=True), - ) - - repositories = graphene.List( - RepositoryNode, - limit=graphene.Int(default_value=12), - organization=graphene.String(required=True), - ) - - def resolve_repository( - root, - info, + @strawberry.field + def repository( + self, organization_key: str, repository_key: str, - ) -> Repository | None: + ) -> RepositoryNode | None: """Resolve repository by key. Args: - root (Any): The root query object. - info (ResolveInfo): The GraphQL execution context. organization_key (str): The login of the organization. repository_key (str): The unique key of the repository. Returns: - Repository or None: The repository object if found, otherwise None. + RepositoryNode | None: The repository node if found, otherwise None. """ try: @@ -50,23 +34,21 @@ def resolve_repository( except Repository.DoesNotExist: return None - def resolve_repositories( - root, - info, + @strawberry.field + def repositories( + self, organization: str, *, limit: int = 12, - ) -> list[Repository]: + ) -> list[RepositoryNode]: """Resolve repositories. Args: - root (Any): The root query object. - info (ResolveInfo): The GraphQL execution context. - limit (int): Maximum number of repositories to return. organization (str): The login of the organization. + limit (int): Maximum number of repositories to return. Returns: - QuerySet: Queryset containing the repositories for the organization. + list[RepositoryNode]: A list of repositories. """ return ( diff --git a/backend/apps/github/graphql/queries/repository_contributor.py b/backend/apps/github/graphql/queries/repository_contributor.py index 6904c34556..0aaee648fd 100644 --- a/backend/apps/github/graphql/queries/repository_contributor.py +++ b/backend/apps/github/graphql/queries/repository_contributor.py @@ -1,32 +1,18 @@ """OWASP repository contributor GraphQL queries.""" -from __future__ import annotations +import strawberry -import graphene - -from apps.common.graphql.queries import BaseQuery from apps.github.graphql.nodes.repository_contributor import RepositoryContributorNode from apps.github.models.repository_contributor import RepositoryContributor -class RepositoryContributorQuery(BaseQuery): +@strawberry.type +class RepositoryContributorQuery: """Repository contributor queries.""" - top_contributors = graphene.List( - RepositoryContributorNode, - limit=graphene.Int(default_value=15), - chapter=graphene.String(required=False), - committee=graphene.String(required=False), - excluded_usernames=graphene.List(graphene.String, required=False), - organization=graphene.String(required=False), - project=graphene.String(required=False), - repository=graphene.String(required=False), - ) - - def resolve_top_contributors( - root, - info, - *, + @strawberry.field + def top_contributors( + self, limit: int = 15, chapter: str | None = None, committee: str | None = None, @@ -38,8 +24,6 @@ def resolve_top_contributors( """Resolve top contributors. Args: - root (Any): The root query object. - info (ResolveInfo): The GraphQL execution context. limit (int): Maximum number of contributors to return. chapter (str, optional): Chapter key to filter by. committee (str, optional): Committee key to filter by. diff --git a/backend/apps/github/graphql/queries/user.py b/backend/apps/github/graphql/queries/user.py index 51d198ae7a..07f8606752 100644 --- a/backend/apps/github/graphql/queries/user.py +++ b/backend/apps/github/graphql/queries/user.py @@ -1,32 +1,25 @@ """OWASP user GraphQL queries.""" -import graphene +import strawberry -from apps.common.graphql.queries import BaseQuery from apps.github.graphql.nodes.repository import RepositoryNode from apps.github.graphql.nodes.user import UserNode from apps.github.models.repository_contributor import RepositoryContributor from apps.github.models.user import User -class UserQuery(BaseQuery): +@strawberry.type +class UserQuery: """User queries.""" - top_contributed_repositories = graphene.List( - RepositoryNode, - login=graphene.String(required=True), - ) - user = graphene.Field( - UserNode, - login=graphene.String(required=True), - ) - - def resolve_top_contributed_repositories(root, info, login): + @strawberry.field + def top_contributed_repositories( + self, + login: str, + ) -> list[RepositoryNode]: """Resolve user top repositories. Args: - root (Any): The root query object. - info (ResolveInfo): The GraphQL execution context. login (str): The login of the user. Returns: @@ -43,12 +36,14 @@ def resolve_top_contributed_repositories(root, info, login): .order_by("-contributions_count") ] - def resolve_user(root, info, login): + @strawberry.field + def user( + self, + login: str, + ) -> UserNode | None: """Resolve user by login. Args: - root (Any): The root query object. - info (ResolveInfo): The GraphQL execution context. login (str): The login of the user. Returns: diff --git a/backend/apps/owasp/graphql/nodes/chapter.py b/backend/apps/owasp/graphql/nodes/chapter.py index 06df9b64d2..920378e9d1 100644 --- a/backend/apps/owasp/graphql/nodes/chapter.py +++ b/backend/apps/owasp/graphql/nodes/chapter.py @@ -1,47 +1,56 @@ """OWASP chapter GraphQL node.""" -import graphene +import strawberry +import strawberry_django from apps.owasp.graphql.nodes.common import GenericEntityNode from apps.owasp.models.chapter import Chapter -class GeoLocationType(graphene.ObjectType): +@strawberry.type +class GeoLocationType: """Geographic location type.""" - lat = graphene.Float() - lng = graphene.Float() - - + lat: float + lng: float + + +@strawberry_django.type( + Chapter, + fields=[ + "country", + "is_active", + "meetup_group", + "name", + "postal_code", + "region", + "summary", + "tags", + ], +) class ChapterNode(GenericEntityNode): """Chapter node.""" - key = graphene.String() - suggested_location = graphene.String() - geo_location = graphene.Field(GeoLocationType) - - class Meta: - model = Chapter - fields = ( - "created_at", - "country", - "is_active", - "meetup_group", - "name", - "postal_code", - "region", - "summary", - "tags", - ) + @strawberry.field + def created_at(self) -> float: + """Resolve created at.""" + return self.idx_created_at - def resolve_geo_location(self, info) -> GeoLocationType: + @strawberry.field + def geo_location(self) -> GeoLocationType | None: """Resolve geographic location.""" - return GeoLocationType(lat=self.latitude, lng=self.longitude) + return ( + GeoLocationType(lat=self.latitude, lng=self.longitude) + if self.latitude is not None and self.longitude is not None + else None + ) - def resolve_key(self, info) -> str: + @strawberry.field + def key(self) -> str: """Resolve key.""" return self.idx_key - def resolve_suggested_location(self, info) -> str: + @strawberry.field + def suggested_location(self) -> str | None: """Resolve suggested location.""" return self.idx_suggested_location diff --git a/backend/apps/owasp/graphql/nodes/committee.py b/backend/apps/owasp/graphql/nodes/committee.py index 50cc908b2f..2b319be78c 100644 --- a/backend/apps/owasp/graphql/nodes/committee.py +++ b/backend/apps/owasp/graphql/nodes/committee.py @@ -1,48 +1,42 @@ """OWASP committee GraphQL node.""" -import graphene +import strawberry +import strawberry_django from apps.owasp.graphql.nodes.common import GenericEntityNode from apps.owasp.models.committee import Committee +@strawberry_django.type(Committee, fields=["name", "summary"]) class CommitteeNode(GenericEntityNode): """Committee node.""" - contributors_count = graphene.Int() - created_at = graphene.Float() - forks_count = graphene.Int() - issues_count = graphene.Int() - repositories_count = graphene.Int() - stars_count = graphene.Int() - - class Meta: - model = Committee - fields = ( - "name", - "summary", - ) - - def resolve_created_at(self, info) -> str: - """Resolve created at.""" - return self.idx_created_at - - def resolve_contributors_count(self, info) -> int: + @strawberry.field + def contributors_count(self) -> int: """Resolve contributors count.""" return self.owasp_repository.contributors_count - def resolve_forks_count(self, info) -> int: + @strawberry.field + def created_at(self) -> float: + """Resolve created at.""" + return self.idx_created_at + + @strawberry.field + def forks_count(self) -> int: """Resolve forks count.""" return self.owasp_repository.forks_count - def resolve_issues_count(self, info) -> int: + @strawberry.field + def issues_count(self) -> int: """Resolve issues count.""" return self.owasp_repository.open_issues_count - def resolve_repositories_count(self, info) -> int: + @strawberry.field + def repositories_count(self) -> int: """Resolve repositories count.""" return 1 - def resolve_stars_count(self, info) -> int: + @strawberry.field + def stars_count(self) -> int: """Resolve stars count.""" return self.owasp_repository.stars_count diff --git a/backend/apps/owasp/graphql/nodes/common.py b/backend/apps/owasp/graphql/nodes/common.py index 244c4458e4..cf479349df 100644 --- a/backend/apps/owasp/graphql/nodes/common.py +++ b/backend/apps/owasp/graphql/nodes/common.py @@ -1,41 +1,35 @@ """OWASP common GraphQL node.""" -from __future__ import annotations +import strawberry -import graphene - -from apps.common.graphql.nodes import BaseNode from apps.github.graphql.nodes.repository_contributor import RepositoryContributorNode -class GenericEntityNode(BaseNode): +@strawberry.type +class GenericEntityNode: """Base node class for OWASP entities with common fields and resolvers.""" - leaders = graphene.List(graphene.String) - related_urls = graphene.List(graphene.String) - top_contributors = graphene.List(RepositoryContributorNode) - updated_at = graphene.Float() - url = graphene.String() - - class Meta: - abstract = True - - def resolve_url(self, info) -> str: - """Resolve URL.""" - return self.idx_url - - def resolve_updated_at(self, info) -> str: - """Resolve updated at.""" - return self.idx_updated_at + @strawberry.field + def leaders(self) -> list[str]: + """Resolve leaders.""" + return self.idx_leaders - def resolve_related_urls(self, info) -> list[str]: + @strawberry.field + def related_urls(self) -> list[str]: """Resolve related URLs.""" return self.idx_related_urls - def resolve_leaders(self, info) -> list[str]: - """Resolve leaders.""" - return self.idx_leaders - - def resolve_top_contributors(self, info) -> list[RepositoryContributorNode]: + @strawberry.field + def top_contributors(self) -> list[RepositoryContributorNode]: """Resolve top contributors.""" return [RepositoryContributorNode(**tc) for tc in self.idx_top_contributors] + + @strawberry.field + def updated_at(self) -> float: + """Resolve updated at.""" + return self.idx_updated_at + + @strawberry.field + def url(self) -> str: + """Resolve URL.""" + return self.idx_url diff --git a/backend/apps/owasp/graphql/nodes/event.py b/backend/apps/owasp/graphql/nodes/event.py index db6b9e6c94..999a1b6fe2 100644 --- a/backend/apps/owasp/graphql/nodes/event.py +++ b/backend/apps/owasp/graphql/nodes/event.py @@ -1,22 +1,23 @@ """OWASP event GraphQL node.""" -from apps.common.graphql.nodes import BaseNode +import strawberry_django + from apps.owasp.models.event import Event -class EventNode(BaseNode): +@strawberry_django.type( + Event, + fields=[ + "category", + "description", + "end_date", + "key", + "name", + "start_date", + "suggested_location", + "summary", + "url", + ], +) +class EventNode: """Event node.""" - - class Meta: - model = Event - fields = ( - "category", - "end_date", - "description", - "key", - "name", - "start_date", - "suggested_location", - "summary", - "url", - ) diff --git a/backend/apps/owasp/graphql/nodes/post.py b/backend/apps/owasp/graphql/nodes/post.py index 17df74d54c..952434c101 100644 --- a/backend/apps/owasp/graphql/nodes/post.py +++ b/backend/apps/owasp/graphql/nodes/post.py @@ -1,18 +1,19 @@ """OWASP blog posts GraphQL nodes.""" -from apps.common.graphql.nodes import BaseNode +import strawberry_django + from apps.owasp.models.post import Post -class PostNode(BaseNode): +@strawberry_django.type( + Post, + fields=[ + "author_image_url", + "author_name", + "published_at", + "title", + "url", + ], +) +class PostNode: """Post node.""" - - class Meta: - model = Post - fields = ( - "author_image_url", - "author_name", - "published_at", - "title", - "url", - ) diff --git a/backend/apps/owasp/graphql/nodes/project.py b/backend/apps/owasp/graphql/nodes/project.py index 45410c1b0a..3c1a34c16c 100644 --- a/backend/apps/owasp/graphql/nodes/project.py +++ b/backend/apps/owasp/graphql/nodes/project.py @@ -1,6 +1,7 @@ """OWASP project GraphQL node.""" -import graphene +import strawberry +import strawberry_django from apps.github.graphql.nodes.issue import IssueNode from apps.github.graphql.nodes.milestone import MilestoneNode @@ -15,78 +16,72 @@ RECENT_PULL_REQUESTS_LIMIT = 5 +@strawberry_django.type( + Project, + fields=[ + "contributors_count", + "created_at", + "forks_count", + "is_active", + "level", + "name", + "open_issues_count", + "stars_count", + "summary", + "type", + ], +) class ProjectNode(GenericEntityNode): """Project node.""" - issues_count = graphene.Int() - key = graphene.String() - languages = graphene.List(graphene.String) - level = graphene.String() - recent_issues = graphene.List(IssueNode) - recent_milestones = graphene.List(MilestoneNode, limit=graphene.Int(default_value=5)) - recent_releases = graphene.List(ReleaseNode) - recent_pull_requests = graphene.List(PullRequestNode) - repositories = graphene.List(RepositoryNode) - repositories_count = graphene.Int() - topics = graphene.List(graphene.String) - type = graphene.String() - - class Meta: - model = Project - fields = ( - "contributors_count", - "created_at", - "forks_count", - "is_active", - "level", - "name", - "open_issues_count", - "stars_count", - "summary", - "type", - ) - - def resolve_issues_count(self, info): + @strawberry.field + def issues_count(self) -> int: """Resolve issues count.""" return self.idx_issues_count - def resolve_key(self, info): + @strawberry.field + def key(self) -> str: """Resolve key.""" return self.idx_key - def resolve_languages(self, info): + @strawberry.field + def languages(self) -> list[str]: """Resolve languages.""" return self.idx_languages - def resolve_recent_issues(self, info): + @strawberry.field + def recent_issues(self) -> list[IssueNode]: """Resolve recent issues.""" return self.issues.select_related("author").order_by("-created_at")[:RECENT_ISSUES_LIMIT] - def resolve_recent_milestones(self, info, limit=5): + @strawberry.field + def recent_milestones(self, limit: int = 5) -> list[MilestoneNode]: """Resolve recent milestones.""" return self.recent_milestones.select_related("author").order_by("-created_at")[:limit] - def resolve_recent_pull_requests(self, info): + @strawberry.field + def recent_pull_requests(self) -> list[PullRequestNode]: """Resolve recent pull requests.""" - pull_requests = self.pull_requests.select_related( - "author", - ).order_by( - "-created_at", - ) - return pull_requests[:RECENT_PULL_REQUESTS_LIMIT] + return self.pull_requests.select_related("author").order_by("-created_at")[ + :RECENT_PULL_REQUESTS_LIMIT + ] - def resolve_recent_releases(self, info): + @strawberry.field + def recent_releases(self) -> list[ReleaseNode]: """Resolve recent releases.""" return self.published_releases.order_by("-published_at")[:RECENT_RELEASES_LIMIT] - def resolve_repositories(self, info): + @strawberry.field + def repositories(self) -> list[RepositoryNode]: """Resolve repositories.""" return self.repositories.order_by("-pushed_at", "-updated_at") - def resolve_repositories_count(self, info): + @strawberry.field + def repositories_count(self) -> int: """Resolve repositories count.""" return self.idx_repositories_count - def resolve_topics(self, info): + @strawberry.field + def topics(self) -> list[str]: """Resolve topics.""" return self.idx_topics diff --git a/backend/apps/owasp/graphql/nodes/snapshot.py b/backend/apps/owasp/graphql/nodes/snapshot.py index 53c15fd783..2a83146078 100644 --- a/backend/apps/owasp/graphql/nodes/snapshot.py +++ b/backend/apps/owasp/graphql/nodes/snapshot.py @@ -1,6 +1,7 @@ """OWASP snapshot GraphQL node.""" -import graphene +import strawberry +import strawberry_django from apps.github.graphql.nodes.issue import IssueNode from apps.github.graphql.nodes.release import ReleaseNode @@ -13,45 +14,44 @@ RECENT_ISSUES_LIMIT = 100 +@strawberry_django.type( + Snapshot, + fields=[ + "created_at", + "end_at", + "start_at", + "title", + ], +) class SnapshotNode(GenericEntityNode): """Snapshot node.""" - key = graphene.String() - new_chapters = graphene.List(ChapterNode) - new_issues = graphene.List(IssueNode) - new_projects = graphene.List(ProjectNode) - new_releases = graphene.List(ReleaseNode) - new_users = graphene.List(UserNode) - - class Meta: - model = Snapshot - fields = ( - "created_at", - "end_at", - "start_at", - "title", - ) - - def resolve_key(self, info): + @strawberry.field + def key(self) -> str: """Resolve key.""" return self.key - def resolve_new_chapters(self, info): + @strawberry.field + def new_chapters(self) -> list[ChapterNode]: """Resolve new chapters.""" return self.new_chapters.all() - def resolve_new_issues(self, info): - """Resolve recent new issues.""" + @strawberry.field + def new_issues(self) -> list[IssueNode]: + """Resolve new issues.""" return self.new_issues.order_by("-created_at")[:RECENT_ISSUES_LIMIT] - def resolve_new_projects(self, info): - """Resolve recent new projects.""" + @strawberry.field + def new_projects(self) -> list[ProjectNode]: + """Resolve new projects.""" return self.new_projects.order_by("-created_at") - def resolve_new_releases(self, info): - """Resolve recent new releases.""" + @strawberry.field + def new_releases(self) -> list[ReleaseNode]: + """Resolve new releases.""" return self.new_releases.order_by("-published_at") - def resolve_new_users(self, info): - """Resolve recent new users.""" + @strawberry.field + def new_users(self) -> list[UserNode]: + """Resolve new users.""" return self.new_users.order_by("-created_at") diff --git a/backend/apps/owasp/graphql/nodes/sponsor.py b/backend/apps/owasp/graphql/nodes/sponsor.py index 7e44826bc5..e4bce1bad4 100644 --- a/backend/apps/owasp/graphql/nodes/sponsor.py +++ b/backend/apps/owasp/graphql/nodes/sponsor.py @@ -1,17 +1,18 @@ """OWASP sponsors GraphQL node.""" -from apps.common.graphql.nodes import BaseNode +import strawberry_django + from apps.owasp.models.sponsor import Sponsor -class SponsorNode(BaseNode): +@strawberry_django.type( + Sponsor, + fields=[ + "image_url", + "name", + "sponsor_type", + "url", + ], +) +class SponsorNode: """Sponsor node.""" - - class Meta: - model = Sponsor - fields = ( - "image_url", - "name", - "sponsor_type", - "url", - ) diff --git a/backend/apps/owasp/graphql/nodes/stats.py b/backend/apps/owasp/graphql/nodes/stats.py index fcb835f7b9..ead6dc62c3 100644 --- a/backend/apps/owasp/graphql/nodes/stats.py +++ b/backend/apps/owasp/graphql/nodes/stats.py @@ -1,13 +1,14 @@ """OWASP stats GraphQL node.""" -import graphene +import strawberry -class StatsNode(graphene.ObjectType): +@strawberry.type +class StatsNode: """Stats node.""" - active_chapters_stats = graphene.Int() - active_projects_stats = graphene.Int() - contributors_stats = graphene.Int() - countries_stats = graphene.Int() - slack_workspace_stats = graphene.Int() + active_chapters_stats: int + active_projects_stats: int + contributors_stats: int + countries_stats: int + slack_workspace_stats: int diff --git a/backend/apps/owasp/graphql/queries/chapter.py b/backend/apps/owasp/graphql/queries/chapter.py index 7577b20e33..84ba77cbbd 100644 --- a/backend/apps/owasp/graphql/queries/chapter.py +++ b/backend/apps/owasp/graphql/queries/chapter.py @@ -1,45 +1,24 @@ """OWASP chapter GraphQL queries.""" -import graphene +import strawberry -from apps.common.graphql.queries import BaseQuery from apps.owasp.graphql.nodes.chapter import ChapterNode from apps.owasp.models.chapter import Chapter -class ChapterQuery(BaseQuery): +@strawberry.type +class ChapterQuery: """Chapter queries.""" - chapter = graphene.Field(ChapterNode, key=graphene.String(required=True)) - recent_chapters = graphene.List(ChapterNode, limit=graphene.Int(default_value=8)) - - def resolve_chapter(root, info, key): - """Resolve chapter by key. - - Args: - root: The root object. - info: GraphQL execution info. - key (str): The key of the chapter. - - Returns: - Chapter: The chapter object if found, otherwise None. - - """ + @strawberry.field + def chapter(self, key: str) -> ChapterNode | None: + """Resolve chapter.""" try: return Chapter.objects.get(key=f"www-chapter-{key}") except Chapter.DoesNotExist: return None - def resolve_recent_chapters(root, info, limit): - """Resolve recent chapters. - - Args: - root: The root object. - info: GraphQL execution info. - limit (int): The maximum number of recent chapters to return. - - Returns: - QuerySet: A queryset of recent active chapters. - - """ + @strawberry.field + def recent_chapters(self, limit: int = 8) -> list[ChapterNode]: + """Resolve recent chapters.""" return Chapter.objects.filter(is_active=True).order_by("-created_at")[:limit] diff --git a/backend/apps/owasp/graphql/queries/committee.py b/backend/apps/owasp/graphql/queries/committee.py index c92d2ad738..d5306ff68f 100644 --- a/backend/apps/owasp/graphql/queries/committee.py +++ b/backend/apps/owasp/graphql/queries/committee.py @@ -1,27 +1,25 @@ """OWASP committee GraphQL queries.""" -import graphene +import strawberry -from apps.common.graphql.queries import BaseQuery from apps.owasp.graphql.nodes.committee import CommitteeNode from apps.owasp.models.committee import Committee -class CommitteeQuery(BaseQuery): +@strawberry.type +class CommitteeQuery: """Committee queries.""" - committee = graphene.Field(CommitteeNode, key=graphene.String(required=True)) - - def resolve_committee(root, info, key): + @strawberry.field + def committee(self, key: str) -> CommitteeNode | None: """Resolve committee by key. Args: - root: The root object. info: GraphQL execution info. key (str): The key of the committee. Returns: - Committee: The committee object if found, otherwise None. + CommitteeNode: The committee object if found, otherwise None. """ try: diff --git a/backend/apps/owasp/graphql/queries/event.py b/backend/apps/owasp/graphql/queries/event.py index 64b318d108..c80e8f4df6 100644 --- a/backend/apps/owasp/graphql/queries/event.py +++ b/backend/apps/owasp/graphql/queries/event.py @@ -1,17 +1,16 @@ """OWASP event GraphQL queries.""" -import graphene +import strawberry -from apps.common.graphql.queries import BaseQuery from apps.owasp.graphql.nodes.event import EventNode from apps.owasp.models.event import Event -class EventQuery(BaseQuery): +@strawberry.type +class EventQuery: """Event queries.""" - upcoming_events = graphene.List(EventNode, limit=graphene.Int(default_value=6)) - - def resolve_upcoming_events(root, info, limit): - """Resolve all events.""" + @strawberry.field + def upcoming_events(self, limit: int = 6) -> list[EventNode]: + """Resolve upcoming events.""" return Event.upcoming_events()[:limit] diff --git a/backend/apps/owasp/graphql/queries/post.py b/backend/apps/owasp/graphql/queries/post.py index d7c5eb91b6..725c0a708d 100644 --- a/backend/apps/owasp/graphql/queries/post.py +++ b/backend/apps/owasp/graphql/queries/post.py @@ -1,19 +1,16 @@ """OWASP event GraphQL queries.""" -from __future__ import annotations +import strawberry -import graphene - -from apps.common.graphql.queries import BaseQuery from apps.owasp.graphql.nodes.post import PostNode from apps.owasp.models.post import Post -class PostQuery(BaseQuery): +@strawberry.type +class PostQuery: """GraphQL queries for Post model.""" - recent_posts = graphene.List(PostNode, limit=graphene.Int(default_value=5)) - - def resolve_recent_posts(root, info, limit: int = 6) -> list[PostNode]: + @strawberry.field + def recent_posts(self, limit: int = 5) -> list[PostNode]: """Return the 5 most recent posts.""" return Post.recent_posts()[:limit] diff --git a/backend/apps/owasp/graphql/queries/project.py b/backend/apps/owasp/graphql/queries/project.py index 631a30c52f..6285ccd774 100644 --- a/backend/apps/owasp/graphql/queries/project.py +++ b/backend/apps/owasp/graphql/queries/project.py @@ -1,35 +1,24 @@ """OWASP project GraphQL queries.""" -import graphene +import strawberry -from apps.common.graphql.queries import BaseQuery from apps.owasp.graphql.nodes.project import ProjectNode from apps.owasp.models.project import Project -class ProjectQuery(BaseQuery): +@strawberry.type +class ProjectQuery: """Project queries.""" - project = graphene.Field( - ProjectNode, - key=graphene.String(required=True), - ) - - recent_projects = graphene.List( - ProjectNode, - limit=graphene.Int(default_value=8), - ) - - def resolve_project(root, info, key): + @strawberry.field + def project(self, key: str) -> ProjectNode | None: """Resolve project. Args: - root: The root object. - info: GraphQL execution info. key (str): The key of the project. Returns: - Project: The project object if found, otherwise None. + ProjectNode | None: The project node if found, otherwise None. """ try: @@ -37,16 +26,15 @@ def resolve_project(root, info, key): except Project.DoesNotExist: return None - def resolve_recent_projects(root, info, limit): + @strawberry.field + def recent_projects(self, limit: int = 8) -> list[ProjectNode]: """Resolve recent projects. Args: - root: The root object. - info: GraphQL execution info. limit (int): The maximum number of recent projects to return. Returns: - QuerySet: A queryset of recent active projects. + list[ProjectNode]: A list of recent active projects. """ return Project.objects.filter(is_active=True).order_by("-created_at")[:limit] diff --git a/backend/apps/owasp/graphql/queries/snapshot.py b/backend/apps/owasp/graphql/queries/snapshot.py index 22b018f573..f2096307c4 100644 --- a/backend/apps/owasp/graphql/queries/snapshot.py +++ b/backend/apps/owasp/graphql/queries/snapshot.py @@ -1,26 +1,17 @@ """OWASP snapshot GraphQL queries.""" -import graphene +import strawberry -from apps.common.graphql.queries import BaseQuery from apps.owasp.graphql.nodes.snapshot import SnapshotNode from apps.owasp.models.snapshot import Snapshot -class SnapshotQuery(BaseQuery): +@strawberry.type +class SnapshotQuery: """Snapshot queries.""" - snapshot = graphene.Field( - SnapshotNode, - key=graphene.String(required=True), - ) - - snapshots = graphene.List( - SnapshotNode, - limit=graphene.Int(default_value=12), - ) - - def resolve_snapshot(root, info, key): + @strawberry.field + def snapshot(self, key: str) -> SnapshotNode | None: """Resolve snapshot by key.""" try: return Snapshot.objects.get( @@ -30,7 +21,8 @@ def resolve_snapshot(root, info, key): except Snapshot.DoesNotExist: return None - def resolve_snapshots(root, info, limit): + @strawberry.field + def snapshots(self, limit: int = 12) -> list[SnapshotNode]: """Resolve snapshots.""" return Snapshot.objects.filter( status=Snapshot.Status.COMPLETED, diff --git a/backend/apps/owasp/graphql/queries/sponsor.py b/backend/apps/owasp/graphql/queries/sponsor.py index 92fd32d229..fb8967e57d 100644 --- a/backend/apps/owasp/graphql/queries/sponsor.py +++ b/backend/apps/owasp/graphql/queries/sponsor.py @@ -1,18 +1,17 @@ """OWASP sponsors GraphQL queries.""" -import graphene +import strawberry -from apps.common.graphql.queries import BaseQuery from apps.owasp.graphql.nodes.sponsor import SponsorNode from apps.owasp.models.sponsor import Sponsor -class SponsorQuery(BaseQuery): +@strawberry.type +class SponsorQuery: """Sponsor queries.""" - sponsors = graphene.List(SponsorNode) - - def resolve_sponsors(root, info): + @strawberry.field + def sponsors(self) -> list[SponsorNode]: """Resolve sponsors.""" return sorted( Sponsor.objects.all(), diff --git a/backend/apps/owasp/graphql/queries/stats.py b/backend/apps/owasp/graphql/queries/stats.py index b461343d4b..5c21e98c70 100644 --- a/backend/apps/owasp/graphql/queries/stats.py +++ b/backend/apps/owasp/graphql/queries/stats.py @@ -1,6 +1,6 @@ """OWASP stats GraphQL queries.""" -import graphene +import strawberry from apps.common.utils import round_down from apps.github.models.user import User @@ -10,22 +10,13 @@ from apps.slack.models.workspace import Workspace +@strawberry.type class StatsQuery: """Stats queries.""" - stats_overview = graphene.Field(StatsNode) - - def resolve_stats_overview(self, info) -> StatsNode: - """Resolve stats overview. - - Args: - self: The StatsQuery instance. - info: GraphQL execution info. - - Returns: - StatsNode: A node containing aggregated statistics. - - """ + @strawberry.field + def stats_overview(self) -> StatsNode: + """Resolve stats overview.""" active_projects_stats = Project.active_projects_count() active_chapters_stats = Chapter.active_chapters_count() contributors_stats = User.objects.count() diff --git a/backend/poetry.lock b/backend/poetry.lock index 06c1b93ebc..e881004d8a 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -1088,53 +1088,6 @@ dev-test = ["coverage", "pytest (>=3.10)", "pytest-asyncio (>=0.17)", "sphinx (< requests = ["requests (>=2.16.2)", "urllib3 (>=1.24.2)"] timezone = ["pytz"] -[[package]] -name = "graphene" -version = "3.4.3" -description = "GraphQL Framework for Python" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "graphene-3.4.3-py2.py3-none-any.whl", hash = "sha256:820db6289754c181007a150db1f7fff544b94142b556d12e3ebc777a7bf36c71"}, - {file = "graphene-3.4.3.tar.gz", hash = "sha256:2a3786948ce75fe7e078443d37f609cbe5bb36ad8d6b828740ad3b95ed1a0aaa"}, -] - -[package.dependencies] -graphql-core = ">=3.1,<3.3" -graphql-relay = ">=3.1,<3.3" -python-dateutil = ">=2.7.0,<3" -typing-extensions = ">=4.7.1,<5" - -[package.extras] -dev = ["coveralls (>=3.3,<5)", "mypy (>=1.10,<2)", "pytest (>=8,<9)", "pytest-asyncio (>=0.16,<2)", "pytest-benchmark (>=4,<5)", "pytest-cov (>=5,<6)", "pytest-mock (>=3,<4)", "ruff (==0.5.0)", "types-python-dateutil (>=2.8.1,<3)"] -test = ["coveralls (>=3.3,<5)", "pytest (>=8,<9)", "pytest-asyncio (>=0.16,<2)", "pytest-benchmark (>=4,<5)", "pytest-cov (>=5,<6)", "pytest-mock (>=3,<4)"] - -[[package]] -name = "graphene-django" -version = "3.2.3" -description = "Graphene Django integration" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "graphene-django-3.2.3.tar.gz", hash = "sha256:d831bfe8e9a6e77e477b7854faef4addb318f386119a69ee4c57b74560f3e07d"}, - {file = "graphene_django-3.2.3-py2.py3-none-any.whl", hash = "sha256:0c673a4dad315b26b4d18eb379ad0c7027fd6a36d23a1848b7c7c09a14a9271e"}, -] - -[package.dependencies] -Django = ">=3.2" -graphene = ">=3.0,<4" -graphql-core = ">=3.1.0,<4" -graphql-relay = ">=3.1.1,<4" -promise = ">=2.1" -text-unidecode = "*" - -[package.extras] -dev = ["coveralls", "django-filter (>=22.1)", "djangorestframework (>=3.6.3)", "mock", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-django (>=4.5.2)", "pytest-random-order", "pytz", "ruff (==0.1.2)"] -rest-framework = ["djangorestframework (>=3.6.3)"] -test = ["coveralls", "django-filter (>=22.1)", "djangorestframework (>=3.6.3)", "mock", "pytest (>=7.3.1)", "pytest-cov", "pytest-django (>=4.5.2)", "pytest-random-order", "pytz"] - [[package]] name = "graphql-core" version = "3.2.6" @@ -1147,21 +1100,6 @@ files = [ {file = "graphql_core-3.2.6.tar.gz", hash = "sha256:c08eec22f9e40f0bd61d805907e3b3b1b9a320bc606e23dc145eebca07c8fbab"}, ] -[[package]] -name = "graphql-relay" -version = "3.2.0" -description = "Relay library for graphql-core" -optional = false -python-versions = ">=3.6,<4" -groups = ["main"] -files = [ - {file = "graphql-relay-3.2.0.tar.gz", hash = "sha256:1ff1c51298356e481a0be009ccdff249832ce53f30559c1338f22a0e0d17250c"}, - {file = "graphql_relay-3.2.0-py3-none-any.whl", hash = "sha256:c9b22bd28b170ba1fe674c74384a8ff30a76c8e26f88ac3aa1584dd3179953e5"}, -] - -[package.dependencies] -graphql-core = ">=3.2,<3.3" - [[package]] name = "gunicorn" version = "23.0.0" @@ -1913,23 +1851,6 @@ nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" -[[package]] -name = "promise" -version = "2.3" -description = "Promises/A+ implementation for Python" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "promise-2.3.tar.gz", hash = "sha256:dfd18337c523ba4b6a58801c164c1904a9d4d1b1747c7d5dbf45b693a49d93d0"}, -] - -[package.dependencies] -six = "*" - -[package.extras] -test = ["coveralls", "futures", "mock", "pytest (>=2.7.3)", "pytest-benchmark", "pytest-cov"] - [[package]] name = "propcache" version = "0.3.1" @@ -2946,17 +2867,64 @@ dev = ["build", "hatch"] doc = ["sphinx"] [[package]] -name = "text-unidecode" -version = "1.3" -description = "The most basic Text::Unidecode port" +name = "strawberry-graphql" +version = "0.270.1" +description = "A library for creating GraphQL APIs" optional = false -python-versions = "*" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, - {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, + {file = "strawberry_graphql-0.270.1-py3-none-any.whl", hash = "sha256:3593086dc08614ae241cb88f7691e90f90b01cab6ee6351cb3838fc5ba8bfab0"}, + {file = "strawberry_graphql-0.270.1.tar.gz", hash = "sha256:d64524a943851d83252e39b82ef768ee494259edecf9510b37bad9a46b745db8"}, ] +[package.dependencies] +asgiref = {version = ">=3.2,<4.0", optional = true, markers = "extra == \"django\" or extra == \"channels\""} +Django = {version = ">=3.2", optional = true, markers = "extra == \"django\""} +graphql-core = {version = ">=3.2.0,<3.4.0", markers = "python_version >= \"3.9\" and python_version < \"4.0\""} +packaging = ">=23" +python-dateutil = ">=2.7.0,<3.0.0" +typing-extensions = ">=4.5.0" + +[package.extras] +aiohttp = ["aiohttp (>=3.7.4.post0,<4.0.0)"] +asgi = ["python-multipart (>=0.0.7)", "starlette (>=0.18.0)"] +chalice = ["chalice (>=1.22,<2.0)"] +channels = ["asgiref (>=3.2,<4.0)", "channels (>=3.0.5)"] +cli = ["libcst (>=0.4.7)", "pygments (>=2.3,<3.0)", "rich (>=12.0.0)", "typer (>=0.7.0)"] +debug = ["libcst (>=0.4.7)", "rich (>=12.0.0)"] +debug-server = ["libcst (>=0.4.7)", "pygments (>=2.3,<3.0)", "python-multipart (>=0.0.7)", "rich (>=12.0.0)", "starlette (>=0.18.0)", "typer (>=0.7.0)", "uvicorn (>=0.11.6)"] +django = ["Django (>=3.2)", "asgiref (>=3.2,<4.0)"] +fastapi = ["fastapi (>=0.65.2)", "python-multipart (>=0.0.7)"] +flask = ["flask (>=1.1)"] +litestar = ["litestar (>=2) ; python_version >= \"3.10\" and python_version < \"4.0\""] +opentelemetry = ["opentelemetry-api (<2)", "opentelemetry-sdk (<2)"] +pydantic = ["pydantic (>1.6.1)"] +pyinstrument = ["pyinstrument (>=4.0.0)"] +quart = ["quart (>=0.19.3)"] +sanic = ["sanic (>=20.12.2)"] + +[[package]] +name = "strawberry-graphql-django" +version = "0.59.1" +description = "Strawberry GraphQL Django extension" +optional = false +python-versions = "<4.0,>=3.9" +groups = ["main"] +files = [ + {file = "strawberry_graphql_django-0.59.1-py3-none-any.whl", hash = "sha256:c0d74f6f2f140d8683fd3ac7405731ed63342d3aa9294443efe2412e51379227"}, + {file = "strawberry_graphql_django-0.59.1.tar.gz", hash = "sha256:b886f1371539962f286b43d273fd91505f943e5e6688f191d6113576b40070ad"}, +] + +[package.dependencies] +asgiref = ">=3.8" +django = ">=4.2" +strawberry-graphql = ">=0.264.0" + +[package.extras] +debug-toolbar = ["django-debug-toolbar (>=3.4)"] +enum = ["django-choices-field (>=2.2.2)"] + [[package]] name = "thefuzz" version = "0.22.1" @@ -3284,4 +3252,4 @@ propcache = ">=0.2.1" [metadata] lock-version = "2.1" python-versions = "^3.13" -content-hash = "9a54d98d8e2f8566c86399925a59b16e8dfa1953e4f7799e05aeaae6e3b6ccc7" +content-hash = "74b08acb4ffd0b2eb81f79aa7f03a92a4ecd113a770de88d4f64e2f14e81948c" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index ab3343c1a5..4f823446fa 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -31,7 +31,6 @@ django-redis = "^5.4.0" django-storages = { extras = ["s3"], version = "^1.14.4" } djangorestframework = "^3.15.2" geopy = "^2.4.1" -graphene-django = "^3.2.2" gunicorn = "^23.0.0" humanize = "^4.11.0" jinja2 = "^3.1.6" @@ -49,7 +48,9 @@ requests = "^2.32.3" sentry-sdk = { extras = ["django"], version = "^2.20.0" } slack-bolt = "^1.22.0" slack-sdk = "^3.35.0" +strawberry-graphql = {extras = ["django"], version = "^0.270.1"} thefuzz = "^0.22.1" +strawberry-graphql-django = "^0.59.1" [tool.poetry.group.dev.dependencies] djlint = "^1.36.4" @@ -95,6 +96,7 @@ log_level = "INFO" explicit_package_bases = true ignore_missing_imports = true mypy_path = "backend" +plugins = ["strawberry.ext.mypy_plugin"] [[tool.mypy.overrides]] disable_error_code = ["attr-defined"] @@ -104,6 +106,10 @@ module = ["apps.*.models.mixins.*", "apps.*.admin", "schema.tests.*"] disable_error_code = ["var-annotated"] module = ["apps.*.migrations.*"] +[[tool.mypy.overrides]] +disable_error_code = ["return-value", "attr-defined", "misc"] +module = ["apps.*.graphql.queries.*", "apps.*.graphql.nodes.*"] + [tool.ruff] line-length = 99 target-version = "py313" diff --git a/backend/settings/base.py b/backend/settings/base.py index 160252686b..a1f4cb5627 100644 --- a/backend/settings/base.py +++ b/backend/settings/base.py @@ -37,7 +37,6 @@ class Base(Configuration): THIRD_PARTY_APPS = ( "algoliasearch_django", "corsheaders", - "graphene_django", "rest_framework", "storages", ) @@ -148,10 +147,6 @@ class Base(Configuration): }, } - GRAPHENE = { - "SCHEMA": "settings.graphql.schema", - } - # Password validation # https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ diff --git a/backend/settings/graphql.py b/backend/settings/graphql.py index f16d5c0eb8..afcf0b5d74 100644 --- a/backend/settings/graphql.py +++ b/backend/settings/graphql.py @@ -1,13 +1,14 @@ """GraphQL schema.""" -import graphene +import strawberry from apps.github.graphql.queries import GithubQuery from apps.owasp.graphql.queries import OwaspQuery +@strawberry.type class Query(GithubQuery, OwaspQuery): """Schema queries.""" -schema = graphene.Schema(query=Query) +schema = strawberry.Schema(query=Query) diff --git a/backend/settings/urls.py b/backend/settings/urls.py index 4ec63d60ee..c19f50a804 100644 --- a/backend/settings/urls.py +++ b/backend/settings/urls.py @@ -9,14 +9,15 @@ from django.contrib import admin from django.urls import include, path from django.views.decorators.csrf import csrf_protect -from graphene_django.views import GraphQLView 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.graphql import schema router = routers.DefaultRouter() router.registry.extend(github_router.registry) @@ -25,7 +26,7 @@ urlpatterns = [ path("csrf/", get_csrf_token), path("idx/", csrf_protect(algolia_search)), - path("graphql/", csrf_protect(GraphQLView.as_view(graphiql=settings.DEBUG))), + path("graphql/", csrf_protect(GraphQLView.as_view(schema=schema, graphiql=settings.DEBUG))), path("api/v1/", include(router.urls)), path("a/", admin.site.urls), ] diff --git a/backend/tests/apps/common/graphql/__init__.py b/backend/tests/apps/common/graphql/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/backend/tests/apps/common/graphql/nodes_test.py b/backend/tests/apps/common/graphql/nodes_test.py deleted file mode 100644 index d20dfd5794..0000000000 --- a/backend/tests/apps/common/graphql/nodes_test.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Test cases for BaseNode.""" - -from graphene_django import DjangoObjectType - -from apps.common.graphql.nodes import BaseNode - - -class TestBaseNode: - """Test cases for BaseNode class.""" - - def test_base_node_inheritance(self): - """Test if BaseNode inherits from DjangoObjectType.""" - assert issubclass(BaseNode, DjangoObjectType) - - def test_base_node_meta(self): - """Test if BaseNode Meta is properly configured.""" - assert hasattr(BaseNode, "_meta") - assert isinstance(BaseNode, DjangoObjectType.__class__) diff --git a/backend/tests/apps/common/graphql/queries_test.py b/backend/tests/apps/common/graphql/queries_test.py deleted file mode 100644 index fc06fcc60f..0000000000 --- a/backend/tests/apps/common/graphql/queries_test.py +++ /dev/null @@ -1,13 +0,0 @@ -"""Test cases for BaseQuery.""" - -from graphene import ObjectType - -from apps.common.graphql.queries import BaseQuery - - -class TestBaseQuery: - """Test cases for BaseQuery class.""" - - def test_base_query_inheritance(self): - """Test if BaseQuery inherits from ObjectType.""" - assert issubclass(BaseQuery, ObjectType) diff --git a/backend/tests/apps/github/graphql/nodes/issue_test.py b/backend/tests/apps/github/graphql/nodes/issue_test.py index f15a1fc85d..d316ce5068 100644 --- a/backend/tests/apps/github/graphql/nodes/issue_test.py +++ b/backend/tests/apps/github/graphql/nodes/issue_test.py @@ -1,27 +1,23 @@ """Test cases for IssueNode.""" -from apps.common.graphql.nodes import BaseNode from apps.github.graphql.nodes.issue import IssueNode -from apps.github.models.issue import Issue class TestIssueNode: """Test cases for IssueNode class.""" - def test_issue_node_inheritance(self): - """Test if IssueNode inherits from BaseNode.""" - assert issubclass(IssueNode, BaseNode) + def test_issue_node_type(self): + assert hasattr(IssueNode, "__strawberry_definition__") - def test_meta_configuration(self): - """Test if Meta is properly configured.""" - assert IssueNode._meta.model == Issue - expected_fields = { - "author", + def test_issue_node_fields(self): + field_names = {field.name for field in IssueNode.__strawberry_definition__.fields} + expected_field_names = { "created_at", - "organization_name", - "repository_name", "state", "title", "url", + "author", + "organization_name", + "repository_name", } - assert set(IssueNode._meta.fields) == expected_fields + assert field_names == expected_field_names diff --git a/backend/tests/apps/github/graphql/nodes/milestone_test.py b/backend/tests/apps/github/graphql/nodes/milestone_test.py index 45f629c14c..d4de5b1324 100644 --- a/backend/tests/apps/github/graphql/nodes/milestone_test.py +++ b/backend/tests/apps/github/graphql/nodes/milestone_test.py @@ -1,21 +1,17 @@ """Test cases for MilestoneNode.""" -from apps.common.graphql.nodes import BaseNode from apps.github.graphql.nodes.milestone import MilestoneNode -from apps.github.models.milestone import Milestone class TestMilestoneNode: """Test cases for MilestoneNode class.""" - def test_milestone_node_inheritance(self): - """Test if IssueNode inherits from BaseNode.""" - assert issubclass(MilestoneNode, BaseNode) + def test_milestone_node_type(self): + assert hasattr(MilestoneNode, "__strawberry_definition__") - def test_meta_configuration(self): - """Test if Meta is properly configured.""" - assert MilestoneNode._meta.model == Milestone - expected_fields = { + def test_milestone_node_fields(self): + field_names = {field.name for field in MilestoneNode.__strawberry_definition__.fields} + expected_field_names = { "author", "body", "closed_issues_count", @@ -27,4 +23,4 @@ def test_meta_configuration(self): "title", "url", } - assert set(MilestoneNode._meta.fields) == expected_fields + assert field_names == expected_field_names diff --git a/backend/tests/apps/github/graphql/nodes/organization_test.py b/backend/tests/apps/github/graphql/nodes/organization_test.py index 13de33eeb4..ccd5891ba0 100644 --- a/backend/tests/apps/github/graphql/nodes/organization_test.py +++ b/backend/tests/apps/github/graphql/nodes/organization_test.py @@ -1,24 +1,18 @@ """Test cases for OrganizationNode.""" -from apps.common.graphql.nodes import BaseNode from apps.github.graphql.nodes.organization import ( OrganizationNode, OrganizationStatsNode, ) -from apps.github.models.organization import Organization class TestOrganizationNode: - """Test cases for OrganizationNode class.""" - def test_organization_node_inheritance(self): - """Test if OrganizationNode inherits from BaseNode.""" - assert issubclass(OrganizationNode, BaseNode) + assert hasattr(OrganizationNode, "__strawberry_definition__") def test_meta_configuration(self): - """Test if Meta is properly configured.""" - assert OrganizationNode._meta.model == Organization - expected_fields = { + field_names = {field.name for field in OrganizationNode.__strawberry_definition__.fields} + expected_field_names = { "avatar_url", "collaborators_count", "company", @@ -33,26 +27,26 @@ def test_meta_configuration(self): "updated_at", "url", } - assert set(OrganizationNode._meta.fields) == expected_fields + assert field_names == expected_field_names def test_resolve_stats(self): - """Test if stats field is properly configured.""" - stats_field = OrganizationNode._meta.fields.get("stats") + stats_field = next( + (f for f in OrganizationNode.__strawberry_definition__.fields if f.name == "stats"), + None, + ) assert stats_field is not None - assert stats_field.type == OrganizationStatsNode + assert stats_field.type is OrganizationStatsNode def test_resolve_url(self): - """Test if url field is properly configured.""" - url_field = OrganizationNode._meta.fields.get("url") + url_field = next( + (f for f in OrganizationNode.__strawberry_definition__.fields if f.name == "url"), None + ) assert url_field is not None - assert str(url_field.type) == "String" + assert url_field.type is str class TestOrganizationStatsNode: - """Test cases for OrganizationStatsNode class.""" - def test_organization_stats_node(self): - """Test if OrganizationStatsNode has the expected fields.""" expected_fields = { "total_contributors", "total_forks", @@ -60,4 +54,7 @@ def test_organization_stats_node(self): "total_repositories", "total_stars", } - assert set(OrganizationStatsNode._meta.fields) == expected_fields + field_names = { + field.name for field in OrganizationStatsNode.__strawberry_definition__.fields + } + assert field_names == expected_fields diff --git a/backend/tests/apps/github/graphql/nodes/release_test.py b/backend/tests/apps/github/graphql/nodes/release_test.py index dba43fb07f..58bc3b9bb0 100644 --- a/backend/tests/apps/github/graphql/nodes/release_test.py +++ b/backend/tests/apps/github/graphql/nodes/release_test.py @@ -1,24 +1,18 @@ """Test cases for ReleaseNode.""" -from graphene import Field - -from apps.common.graphql.nodes import BaseNode from apps.github.graphql.nodes.release import ReleaseNode from apps.github.graphql.nodes.user import UserNode -from apps.github.models.release import Release class TestReleaseNode: """Test cases for ReleaseNode class.""" def test_release_node_inheritance(self): - """Test if ReleaseNode inherits from BaseNode.""" - assert issubclass(ReleaseNode, BaseNode) + assert hasattr(ReleaseNode, "__strawberry_definition__") def test_meta_configuration(self): - """Test if Meta is properly configured.""" - assert ReleaseNode._meta.model == Release - expected_fields = { + field_names = {field.name for field in ReleaseNode.__strawberry_definition__.fields} + expected_field_names = { "author", "is_pre_release", "name", @@ -29,10 +23,10 @@ def test_meta_configuration(self): "tag_name", "url", } - assert set(ReleaseNode._meta.fields) == expected_fields + assert field_names == expected_field_names def test_author_field(self): - """Test if author field is properly configured.""" - author_field = ReleaseNode._meta.fields.get("author") - assert isinstance(author_field, Field) - assert author_field.type == UserNode + fields = ReleaseNode.__strawberry_definition__.fields + author_field = next((field for field in fields if field.name == "author"), None) + assert author_field is not None + assert author_field.type.of_type is UserNode diff --git a/backend/tests/apps/github/graphql/nodes/repository_test.py b/backend/tests/apps/github/graphql/nodes/repository_test.py index c25264471d..90e7303189 100644 --- a/backend/tests/apps/github/graphql/nodes/repository_test.py +++ b/backend/tests/apps/github/graphql/nodes/repository_test.py @@ -1,73 +1,94 @@ -"""Test cases for RepositoryNode.""" +"""Test cases for ProjectNode.""" -from graphene import List - -from apps.common.graphql.nodes import BaseNode from apps.github.graphql.nodes.issue import IssueNode +from apps.github.graphql.nodes.milestone import MilestoneNode +from apps.github.graphql.nodes.pull_request import PullRequestNode from apps.github.graphql.nodes.release import ReleaseNode from apps.github.graphql.nodes.repository import RepositoryNode -from apps.github.models.repository import Repository +from apps.owasp.graphql.nodes.project import ProjectNode -class TestRepositoryNode: - def test_repository_node_inheritance(self): - assert issubclass(RepositoryNode, BaseNode) +class TestProjectNode: + def test_project_node_inheritance(self): + assert hasattr(ProjectNode, "__strawberry_definition__") def test_meta_configuration(self): - assert RepositoryNode._meta.model == Repository - expected_fields = { - "commits_count", + field_names = {field.name for field in ProjectNode.__strawberry_definition__.fields} + expected_field_names = { "contributors_count", "created_at", - "description", "forks_count", - "issues", - "key", - "languages", - "latest_release", - "license", + "is_active", + "level", "name", "open_issues_count", - "organization", - "owner_key", - "recent_milestones", - "releases", - "size", "stars_count", - "subscribers_count", - "top_contributors", + "summary", + "type", + "issues_count", + "key", + "languages", + "recent_issues", + "recent_milestones", + "recent_pull_requests", + "recent_releases", + "repositories", + "repositories_count", "topics", - "updated_at", - "url", } - assert set(RepositoryNode._meta.fields) == expected_fields + assert expected_field_names.issubset(field_names) - def test_resolve_languages(self, mocker): - field = RepositoryNode._meta.fields.get("languages") + def _get_field_by_name(self, name): + return next( + (f for f in ProjectNode.__strawberry_definition__.fields if f.name == name), None + ) + + def test_resolve_issues_count(self): + field = self._get_field_by_name("issues_count") assert field is not None - assert str(field.type) == "[String]" + assert field.type is int - def test_resolve_topics(self, mocker): - field = RepositoryNode._meta.fields.get("topics") + def test_resolve_key(self): + field = self._get_field_by_name("key") assert field is not None - assert str(field.type) == "[String]" + assert field.type is str - def test_resolve_issues(self, mocker): - field = RepositoryNode._meta.fields.get("issues") + def test_resolve_languages(self): + field = self._get_field_by_name("languages") assert field is not None - assert field.type == List(IssueNode) + assert field.type == list[str] - def test_resolve_releases(self, mocker): - field = RepositoryNode._meta.fields.get("releases") + def test_resolve_recent_issues(self): + field = self._get_field_by_name("recent_issues") assert field is not None - assert field.type == List(ReleaseNode) + assert field.type.of_type is IssueNode - def test_resolve_top_contributors(self, mocker): - field = RepositoryNode._meta.fields.get("top_contributors") + def test_resolve_recent_milestones(self): + field = self._get_field_by_name("recent_milestones") assert field is not None - assert str(field.type) == "[RepositoryContributorNode]" + assert field.type.of_type is MilestoneNode - def test_resolve_url(self, mocker): - url = RepositoryNode._meta.fields.get("url") - assert url is not None - assert str(url.type) == "String" + def test_resolve_recent_pull_requests(self): + field = self._get_field_by_name("recent_pull_requests") + assert field is not None + assert field.type.of_type is PullRequestNode + + def test_resolve_recent_releases(self): + field = self._get_field_by_name("recent_releases") + assert field is not None + assert field.type.of_type is ReleaseNode + + def test_resolve_repositories(self): + field = self._get_field_by_name("repositories") + assert field is not None + assert field.type.of_type is RepositoryNode + + def test_resolve_repositories_count(self): + field = self._get_field_by_name("repositories_count") + assert field is not None + assert field.type is int + + def test_resolve_topics(self): + field = self._get_field_by_name("topics") + assert field is not None + assert field.type == list[str] diff --git a/backend/tests/apps/github/graphql/nodes/user_test.py b/backend/tests/apps/github/graphql/nodes/user_test.py index 7ea6df4739..85f3817814 100644 --- a/backend/tests/apps/github/graphql/nodes/user_test.py +++ b/backend/tests/apps/github/graphql/nodes/user_test.py @@ -1,8 +1,6 @@ """Test cases for UserNode.""" -from apps.common.graphql.nodes import BaseNode from apps.github.graphql.nodes.user import UserNode -from apps.github.models.user import User class TestUserNode: @@ -10,12 +8,12 @@ class TestUserNode: def test_user_node_inheritance(self): """Test if UserNode inherits from BaseNode.""" - assert issubclass(UserNode, BaseNode) + assert hasattr(UserNode, "__strawberry_definition__") def test_meta_configuration(self): """Test if Meta is properly configured.""" - assert UserNode._meta.model == User - expected_fields = { + field_names = {field.name for field in UserNode.__strawberry_definition__.fields} + expected_field_names = { "avatar_url", "bio", "company", @@ -34,4 +32,4 @@ def test_meta_configuration(self): "updated_at", "url", } - assert set(UserNode._meta.fields) == expected_fields + assert field_names == expected_field_names diff --git a/backend/tests/apps/github/graphql/queries/issue_test.py b/backend/tests/apps/github/graphql/queries/issue_test.py new file mode 100644 index 0000000000..8c503c9a81 --- /dev/null +++ b/backend/tests/apps/github/graphql/queries/issue_test.py @@ -0,0 +1,77 @@ +"""Test cases for IssueQuery.""" + +from unittest.mock import MagicMock, Mock, patch + +import pytest + +from apps.github.graphql.queries.issue import IssueQuery +from apps.github.models.issue import Issue + + +class TestIssueQuery: + """Test cases for IssueQuery class.""" + + @pytest.fixture + def mock_issue(self): + """Mock Issue instance.""" + issue = Mock(spec=Issue) + issue.id = 1 + issue.author_id = 42 + return issue + + @patch("apps.github.models.issue.Issue.objects.select_related") + def test_recent_issues_basic(self, mock_select_related, mock_issue): + """Test fetching recent issues with default parameters.""" + mock_queryset = MagicMock() + mock_queryset.order_by.return_value = [mock_issue] + mock_select_related.return_value = mock_queryset + + result = IssueQuery().recent_issues() + + assert result == [mock_issue] + mock_select_related.assert_called_once_with( + "author", "repository", "repository__organization" + ) + mock_queryset.order_by.assert_called_once_with("-created_at") + + @patch("apps.github.models.issue.Issue.objects.select_related") + def test_recent_issues_with_login(self, mock_select_related, mock_issue): + """Test filtering issues by login.""" + mock_queryset = MagicMock() + mock_queryset.order_by.return_value.filter.return_value = [mock_issue] + mock_select_related.return_value = mock_queryset + + result = IssueQuery().recent_issues(login="alice") + + mock_select_related.assert_called_once() + mock_queryset.order_by.assert_called_once() + mock_queryset.order_by.return_value.filter.assert_called_once_with(author__login="alice") + assert result == [mock_issue] + + @patch("apps.github.models.issue.Issue.objects.select_related") + def test_recent_issues_with_organization(self, mock_select_related, mock_issue): + """Test filtering issues by organization.""" + mock_queryset = MagicMock() + mock_queryset.order_by.return_value.filter.return_value = [mock_issue] + mock_select_related.return_value = mock_queryset + + result = IssueQuery().recent_issues(organization="orgX") + + mock_queryset.order_by.assert_called_once() + mock_queryset.order_by.return_value.filter.assert_called_once_with( + repository__organization__login="orgX" + ) + assert result == [mock_issue] + + @patch("apps.github.models.issue.Issue.objects.select_related") + def test_recent_issues_limit(self, mock_select_related, mock_issue): + """Test limiting the number of issues returned.""" + mock_queryset = MagicMock() + mock_queryset.order_by.return_value.__getitem__.return_value = [mock_issue] + mock_select_related.return_value = mock_queryset + + result = IssueQuery().recent_issues(limit=1) + + mock_select_related.assert_called_once() + mock_queryset.order_by.assert_called_once_with("-created_at") + assert result == [mock_issue] diff --git a/backend/tests/apps/github/graphql/queries/milestone_test.py b/backend/tests/apps/github/graphql/queries/milestone_test.py new file mode 100644 index 0000000000..501404d661 --- /dev/null +++ b/backend/tests/apps/github/graphql/queries/milestone_test.py @@ -0,0 +1,96 @@ +"""Test cases for MilestoneQuery.""" + +from unittest.mock import MagicMock, Mock, patch + +import pytest + +from apps.github.graphql.queries.milestone import MilestoneQuery +from apps.github.models.milestone import Milestone + + +class TestMilestoneQuery: + """Unit tests for MilestoneQuery.""" + + @pytest.fixture + def mock_milestone(self): + milestone = Mock(spec=Milestone) + milestone.id = 1 + return milestone + + @pytest.fixture + def get_queryset(self): + """Return a mocked queryset.""" + queryset = MagicMock() + queryset.select_related.return_value.prefetch_related.return_value = queryset + queryset.filter.return_value = queryset + queryset.order_by.return_value = queryset + queryset.__getitem__.return_value = [Mock()] + return queryset + + @pytest.mark.parametrize( + ("state", "manager"), + [ + ("open", "open_milestones"), + ("closed", "closed_milestones"), + ("all", "objects"), + ], + ) + def test_recent_milestones_by_state(self, get_queryset, state, manager): + """Test fetching milestones with different valid states.""" + with patch.object(Milestone, manager, new_callable=Mock) as mock_manager: + mock_manager.all.return_value = get_queryset + + get_queryset.select_related.return_value = get_queryset + get_queryset.prefetch_related.return_value = get_queryset + + result = MilestoneQuery().recent_milestones( + distinct=False, + limit=5, + login=None, + organization=None, + state=state, + ) + + assert isinstance(result, list) + assert get_queryset.select_related.called + assert get_queryset.prefetch_related.called + + def test_recent_milestones_with_filters(self, get_queryset): + """Test recent milestones with login and organization filters.""" + with patch.object(Milestone, "open_milestones", new_callable=Mock) as mock_manager: + mock_manager.all.return_value = get_queryset + + result = MilestoneQuery().recent_milestones( + distinct=False, + limit=3, + login="user", + organization="github", + state="open", + ) + + get_queryset.filter.assert_any_call(author__login="user") + get_queryset.filter.assert_any_call(repository__organization__login="github") + assert isinstance(result, list) + + def test_recent_milestones_distinct(self): + """Test distinct filtering with Subquery for milestones.""" + with patch.object(Milestone, "open_milestones", new_callable=Mock) as mock_manager: + base_queryset = MagicMock() + filtered_queryset = MagicMock() + base_queryset.select_related.return_value.prefetch_related.return_value = base_queryset + base_queryset.filter.return_value = filtered_queryset + filtered_queryset.filter.return_value = filtered_queryset + filtered_queryset.order_by.return_value = filtered_queryset + filtered_queryset.__getitem__.return_value = [Mock()] + mock_manager.all.return_value = base_queryset + + result = MilestoneQuery().recent_milestones( + distinct=True, + limit=1, + login=None, + organization=None, + state="open", + ) + + assert isinstance(result, list) + assert filtered_queryset.__getitem__.called diff --git a/backend/tests/apps/github/graphql/queries/repository_test.py b/backend/tests/apps/github/graphql/queries/repository_test.py index 437620e89f..9d4aa1673e 100644 --- a/backend/tests/apps/github/graphql/queries/repository_test.py +++ b/backend/tests/apps/github/graphql/queries/repository_test.py @@ -11,17 +11,14 @@ class TestRepositoryQuery: """Test cases for RepositoryQuery class.""" - @pytest.fixture - def mock_info(self): - """GraphQL info mock fixture.""" - return Mock() - @pytest.fixture def mock_repository(self): """Repository mock fixture.""" - return Mock(spec=Repository) + repo = Mock(spec=Repository) + repo.key = "test-repo" + return repo - def test_resolve_repository_existing(self, mock_repository, mock_info): + def test_resolve_repository_existing(self, mock_repository): """Test resolving an existing repository.""" mock_queryset = MagicMock() mock_queryset.get.return_value = mock_repository @@ -30,9 +27,7 @@ def test_resolve_repository_existing(self, mock_repository, mock_info): "apps.github.models.repository.Repository.objects.select_related", return_value=mock_queryset, ) as mock_select_related: - result = RepositoryQuery.resolve_repository( - None, - mock_info, + result = RepositoryQuery().repository( organization_key="test-org", repository_key="test-repo", ) @@ -44,7 +39,7 @@ def test_resolve_repository_existing(self, mock_repository, mock_info): key__iexact="test-repo", ) - def test_resolve_repository_not_found(self, mock_info): + def test_resolve_repository_not_found(self): """Test resolving a non-existent repository.""" mock_queryset = MagicMock() mock_queryset.get.side_effect = Repository.DoesNotExist @@ -53,9 +48,7 @@ def test_resolve_repository_not_found(self, mock_info): "apps.github.models.repository.Repository.objects.select_related", return_value=mock_queryset, ) as mock_select_related: - result = RepositoryQuery.resolve_repository( - None, - mock_info, + result = RepositoryQuery().repository( organization_key="non-existent-org", repository_key="non-existent-repo", ) @@ -66,3 +59,25 @@ def test_resolve_repository_not_found(self, mock_info): organization__login__iexact="non-existent-org", key__iexact="non-existent-repo", ) + + def test_resolve_repositories(self, mock_repository): + """Test resolving repositories list.""" + mock_queryset = MagicMock() + mock_queryset.filter.return_value.order_by.return_value.__getitem__.return_value = [ + mock_repository + ] + + with patch( + "apps.github.models.repository.Repository.objects.select_related", + return_value=mock_queryset, + ) as mock_select_related: + result = RepositoryQuery().repositories( + organization="test-org", + limit=1, + ) + + assert isinstance(result, list) + assert result[0] == mock_repository + mock_select_related.assert_called_once_with("organization") + mock_queryset.filter.assert_called_once_with(organization__login__iexact="test-org") + mock_queryset.filter.return_value.order_by.assert_called_once_with("-stars_count") diff --git a/backend/tests/apps/github/graphql/queries/user_test.py b/backend/tests/apps/github/graphql/queries/user_test.py index bfe8a5dc3a..bda98be0bc 100644 --- a/backend/tests/apps/github/graphql/queries/user_test.py +++ b/backend/tests/apps/github/graphql/queries/user_test.py @@ -1,12 +1,7 @@ -"""Test cases for UserQuery.""" - from unittest.mock import Mock, patch import pytest -from graphene import Field, NonNull, String -from apps.common.graphql.queries import BaseQuery -from apps.github.graphql.nodes.user import UserNode from apps.github.graphql.queries.user import UserQuery from apps.github.models.user import User @@ -14,73 +9,47 @@ class TestUserQuery: """Test cases for UserQuery class.""" - @pytest.fixture - def mock_info(self): - """GraphQL info mock fixture.""" - return Mock() - @pytest.fixture def mock_user(self): """User mock fixture.""" return Mock(spec=User) - def test_user_query_inheritance(self): - """Test if UserQuery inherits from BaseQuery.""" - assert issubclass(UserQuery, BaseQuery) - - def test_user_field_configuration(self): - """Test if user field is properly configured.""" - user_field = UserQuery._meta.fields.get("user") - assert isinstance(user_field, Field) - assert user_field.type == UserNode - - assert "login" in user_field.args - login_arg = user_field.args["login"] - assert isinstance(login_arg.type, NonNull) - assert login_arg.type.of_type == String - - -class TestUserResolution: - """Test cases for user resolution.""" - - @pytest.fixture - def mock_user(self): - """User mock fixture.""" - return Mock(spec=User) - - @pytest.fixture - def mock_info(self): - """GraphQL info mock fixture.""" - return Mock() - - def test_resolve_user_existing(self, mock_user, mock_info): + def test_resolve_user_existing(self, mock_user): """Test resolving an existing user.""" with patch("apps.github.models.user.User.objects.get") as mock_get: mock_get.return_value = mock_user - result = UserQuery.resolve_user(None, mock_info, login="test-user") + result = UserQuery().user(login="test-user") assert result == mock_user mock_get.assert_called_once_with(login="test-user") - def test_resolve_user_not_found(self, mock_info): + def test_resolve_user_not_found(self): """Test resolving a non-existent user.""" with patch("apps.github.models.user.User.objects.get") as mock_get: mock_get.side_effect = User.DoesNotExist - result = UserQuery.resolve_user(None, mock_info, login="non-existent") + result = UserQuery().user(login="non-existent") assert result is None mock_get.assert_called_once_with(login="non-existent") - def test_user_field_configuration(self): - """Test if user field is properly configured.""" - user_field = UserQuery._meta.fields.get("user") - - assert isinstance(user_field, Field) - assert user_field.type == UserNode - assert "login" in user_field.args - - login_arg = user_field.args["login"] - assert isinstance(login_arg.type, NonNull) - assert login_arg.type.of_type == String + def test_top_contributed_repositories(self): + """Test resolving top contributed repositories.""" + mock_repository = Mock() + mock_contributor = Mock(repository=mock_repository) + + with patch( + "apps.github.models.repository_contributor.RepositoryContributor.objects.select_related" + ) as mock_select_related: + mock_queryset = mock_select_related.return_value + mock_queryset.filter.return_value.order_by.return_value = [mock_contributor] + + result = UserQuery().top_contributed_repositories(login="test-user") + + assert result == [mock_repository] + mock_select_related.assert_called_once_with("repository", "repository__organization") + mock_queryset.filter.assert_called_once_with(user__login="test-user") + mock_queryset.filter.return_value.order_by.assert_called_once_with( + "-contributions_count" + ) diff --git a/backend/tests/apps/owasp/graphql/nodes/project_test.py b/backend/tests/apps/owasp/graphql/nodes/project_test.py index cbe4c619f9..90e7303189 100644 --- a/backend/tests/apps/owasp/graphql/nodes/project_test.py +++ b/backend/tests/apps/owasp/graphql/nodes/project_test.py @@ -1,115 +1,94 @@ """Test cases for ProjectNode.""" -from unittest.mock import MagicMock, Mock - -import pytest -from graphene import Field, List - -from apps.common.graphql.nodes import BaseNode from apps.github.graphql.nodes.issue import IssueNode +from apps.github.graphql.nodes.milestone import MilestoneNode +from apps.github.graphql.nodes.pull_request import PullRequestNode from apps.github.graphql.nodes.release import ReleaseNode -from apps.owasp.graphql.nodes.project import ( - RECENT_ISSUES_LIMIT, - RECENT_RELEASES_LIMIT, - ProjectNode, -) -from apps.owasp.models.project import Project +from apps.github.graphql.nodes.repository import RepositoryNode +from apps.owasp.graphql.nodes.project import ProjectNode class TestProjectNode: - """Test cases for ProjectNode class.""" - - @pytest.fixture - def mock_project(self): - """Create a mock project with issues and releases.""" - project = Mock(spec=Project) - mock_issues = MagicMock() - mock_releases = MagicMock() - - mock_ordered_issues = MagicMock() - mock_ordered_issues.__getitem__.return_value = [] - mock_issues.select_related.return_value.order_by.return_value = mock_ordered_issues - - mock_ordered_releases = MagicMock() - mock_ordered_releases.__getitem__.return_value = [] - mock_releases.order_by.return_value = mock_ordered_releases - - project.issues = mock_issues - project.published_releases = mock_releases - project.nest_url = "https://example.com/project" - return project - def test_project_node_inheritance(self): - """Test if ProjectNode inherits from BaseNode.""" - assert issubclass(ProjectNode, BaseNode) - - def test_model_meta_configuration(self): - """Test if model Meta is properly configured.""" - assert ProjectNode._meta.model == Project - assert len(ProjectNode._meta.fields) > 0 - - def test_recent_issues_field(self): - """Test if recent_issues field is properly configured.""" - recent_issues_field = ProjectNode._meta.fields.get("recent_issues") - assert isinstance(recent_issues_field, Field) - assert recent_issues_field.type == List(IssueNode) - - def test_recent_releases_field(self): - """Test if recent_releases field is properly configured.""" - recent_releases_field = ProjectNode._meta.fields.get("recent_releases") - assert isinstance(recent_releases_field, Field) - assert recent_releases_field.type == List(ReleaseNode) - - def test_resolve_recent_issues(self, mock_project): - """Test resolution of recent issues.""" - node = ProjectNode() - node.issues = mock_project.issues - - result = node.resolve_recent_issues(None) - - mock_project.issues.select_related.assert_called_once_with("author") - mock_project.issues.select_related.return_value.order_by.assert_called_once_with( - "-created_at" - ) - mock_project.issues.select_related.return_value.order_by.return_value.__getitem__.assert_called_once_with( - slice(None, RECENT_ISSUES_LIMIT) - ) - assert result == [] - - def test_resolve_recent_releases(self, mock_project): - """Test resolution of recent releases.""" - node = ProjectNode() - node.published_releases = mock_project.published_releases - - result = node.resolve_recent_releases(None) - - mock_project.published_releases.order_by.assert_called_once_with("-published_at") - mock_project.published_releases.order_by.return_value.__getitem__.assert_called_once_with( - slice(None, RECENT_RELEASES_LIMIT) - ) - assert result == [] - - def test_all_fields_exist_in_model(self): - """Test that all fields in Meta.fields exist in the Project model.""" - model_fields = {f.name for f in Project._meta.get_fields()} - node_fields = set(ProjectNode._meta.fields.keys()) - - custom_fields = { + assert hasattr(ProjectNode, "__strawberry_definition__") + + def test_meta_configuration(self): + field_names = {field.name for field in ProjectNode.__strawberry_definition__.fields} + expected_field_names = { + "contributors_count", + "created_at", + "forks_count", + "is_active", + "level", + "name", + "open_issues_count", + "stars_count", + "summary", + "type", "issues_count", "key", "languages", - "leaders", "recent_issues", "recent_milestones", "recent_pull_requests", "recent_releases", - "repositories_count", "repositories", - "top_contributors", + "repositories_count", "topics", - "updated_at", - "url", } - node_fields = node_fields - custom_fields + assert expected_field_names.issubset(field_names) + + def _get_field_by_name(self, name): + return next( + (f for f in ProjectNode.__strawberry_definition__.fields if f.name == name), None + ) - assert all(field in model_fields for field in node_fields) + def test_resolve_issues_count(self): + field = self._get_field_by_name("issues_count") + assert field is not None + assert field.type is int + + def test_resolve_key(self): + field = self._get_field_by_name("key") + assert field is not None + assert field.type is str + + def test_resolve_languages(self): + field = self._get_field_by_name("languages") + assert field is not None + assert field.type == list[str] + + def test_resolve_recent_issues(self): + field = self._get_field_by_name("recent_issues") + assert field is not None + assert field.type.of_type is IssueNode + + def test_resolve_recent_milestones(self): + field = self._get_field_by_name("recent_milestones") + assert field is not None + assert field.type.of_type is MilestoneNode + + def test_resolve_recent_pull_requests(self): + field = self._get_field_by_name("recent_pull_requests") + assert field is not None + assert field.type.of_type is PullRequestNode + + def test_resolve_recent_releases(self): + field = self._get_field_by_name("recent_releases") + assert field is not None + assert field.type.of_type is ReleaseNode + + def test_resolve_repositories(self): + field = self._get_field_by_name("repositories") + assert field is not None + assert field.type.of_type is RepositoryNode + + def test_resolve_repositories_count(self): + field = self._get_field_by_name("repositories_count") + assert field is not None + assert field.type is int + + def test_resolve_topics(self): + field = self._get_field_by_name("topics") + assert field is not None + assert field.type == list[str] diff --git a/backend/tests/apps/owasp/graphql/queries/chapter_test.py b/backend/tests/apps/owasp/graphql/queries/chapter_test.py index a74803a133..a71ae525f6 100644 --- a/backend/tests/apps/owasp/graphql/queries/chapter_test.py +++ b/backend/tests/apps/owasp/graphql/queries/chapter_test.py @@ -1,10 +1,7 @@ from unittest.mock import Mock, patch import pytest -from graphene import Field, NonNull, String -from apps.common.graphql.queries import BaseQuery -from apps.owasp.graphql.nodes.chapter import ChapterNode from apps.owasp.graphql.queries.chapter import ChapterQuery from apps.owasp.models.chapter import Chapter @@ -12,51 +9,59 @@ class TestChapterQuery: """Test cases for ChapterQuery class.""" - def test_chapter_query_inheritance(self): - """Test if ChapterQuery inherits from BaseQuery.""" - assert issubclass(ChapterQuery, BaseQuery) + def test_has_strawberry_definition(self): + """Test if ChapterQuery is a valid Strawberry type and has expected fields.""" + assert hasattr(ChapterQuery, "__strawberry_definition__") - def test_chapter_field_configuration(self): - """Test if chapter field is properly configured.""" - chapter_field = ChapterQuery._meta.fields.get("chapter") - assert isinstance(chapter_field, Field) - assert chapter_field.type == ChapterNode - - assert "key" in chapter_field.args - key_arg = chapter_field.args["key"] - assert isinstance(key_arg.type, NonNull) - assert key_arg.type.of_type == String + field_names = [field.name for field in ChapterQuery.__strawberry_definition__.fields] + assert "chapter" in field_names + assert "recent_chapters" in field_names class TestChapterResolution: - """Test cases for chapter resolution.""" - - @pytest.fixture - def mock_chapter(self): - """Chapter mock fixture.""" - return Mock(spec=Chapter) + """Test cases for chapter resolution methods.""" @pytest.fixture def mock_info(self): - """GraphQL info mock fixture.""" + """Mock GraphQL ResolveInfo object.""" return Mock() - def test_resolve_chapter_existing(self, mock_chapter, mock_info): - """Test resolving an existing chapter.""" + @pytest.fixture + def mock_chapter(self): + """Mock Chapter instance.""" + return Mock(spec=Chapter) + + def test_chapter_found(self, mock_info, mock_chapter): + """Test if a chapter is returned when found.""" with patch("apps.owasp.models.chapter.Chapter.objects.get") as mock_get: mock_get.return_value = mock_chapter - result = ChapterQuery.resolve_chapter(None, mock_info, key="test-chapter") + result = ChapterQuery().chapter(key="test-chapter") assert result == mock_chapter mock_get.assert_called_once_with(key="www-chapter-test-chapter") - def test_resolve_chapter_not_found(self, mock_info): - """Test resolving a non-existent chapter.""" + def test_chapter_not_found(self, mock_info): + """Test if None is returned when the chapter is not found.""" with patch("apps.owasp.models.chapter.Chapter.objects.get") as mock_get: mock_get.side_effect = Chapter.DoesNotExist - result = ChapterQuery.resolve_chapter(None, mock_info, key="non-existent") + result = ChapterQuery().chapter(key="non-existent") assert result is None mock_get.assert_called_once_with(key="www-chapter-non-existent") + + def test_recent_chapters_query(self, mock_info): + """Test if recent chapters are returned correctly.""" + mock_chapters = [Mock(), Mock()] + + with patch("apps.owasp.models.chapter.Chapter.objects.filter") as mock_filter: + mock_qs = mock_filter.return_value + mock_qs.order_by.return_value.__getitem__.return_value = mock_chapters + + result = ChapterQuery().recent_chapters(limit=2) + + assert result == mock_chapters + mock_filter.assert_called_once_with(is_active=True) + mock_qs.order_by.assert_called_once_with("-created_at") + mock_qs.order_by.return_value.__getitem__.assert_called_once_with(slice(None, 2)) diff --git a/backend/tests/apps/owasp/graphql/queries/committee_test.py b/backend/tests/apps/owasp/graphql/queries/committee_test.py index 2537a15fd9..1bfc1cb40b 100644 --- a/backend/tests/apps/owasp/graphql/queries/committee_test.py +++ b/backend/tests/apps/owasp/graphql/queries/committee_test.py @@ -1,62 +1,29 @@ -from unittest.mock import Mock, patch - -import pytest -from graphene import Field, NonNull, String - -from apps.common.graphql.queries import BaseQuery from apps.owasp.graphql.nodes.committee import CommitteeNode from apps.owasp.graphql.queries.committee import CommitteeQuery -from apps.owasp.models.committee import Committee class TestCommitteeQuery: """Test cases for CommitteeQuery class.""" - def test_committee_query_inheritance(self): - """Test if CommitteeQuery inherits from BaseQuery.""" - assert issubclass(CommitteeQuery, BaseQuery) - - def test_committee_field_configuration(self): - """Test if committee field is properly configured.""" - committee_field = CommitteeQuery._meta.fields.get("committee") - assert isinstance(committee_field, Field) - assert committee_field.type == CommitteeNode - - assert "key" in committee_field.args - key_arg = committee_field.args["key"] - assert isinstance(key_arg.type, NonNull) - assert key_arg.type.of_type == String - + def test_committee_query_has_strawberry_definition(self): + """Test if CommitteeQuery is a valid Strawberry type.""" + assert hasattr(CommitteeQuery, "__strawberry_definition__") -class TestCommitteeResolution: - """Test cases for committee resolution.""" + field_names = [field.name for field in CommitteeQuery.__strawberry_definition__.fields] + assert "committee" in field_names - @pytest.fixture - def mock_committee(self): - """Committee mock fixture.""" - return Mock(spec=Committee) - - @pytest.fixture - def mock_info(self): - """GraphQL info mock fixture.""" - return Mock() - - def test_resolve_committee_existing(self, mock_committee, mock_info): - """Test resolving an existing committee.""" - with patch("apps.owasp.models.committee.Committee.objects.get") as mock_get: - mock_get.return_value = mock_committee - - result = CommitteeQuery.resolve_committee(None, mock_info, key="test-committee") - - assert result == mock_committee - mock_get.assert_called_once_with(key="www-committee-test-committee") + def test_committee_field_configuration(self): + """Test if 'committee' field is configured properly.""" + committee_field = next( + field + for field in CommitteeQuery.__strawberry_definition__.fields + if field.name == "committee" + ) - def test_resolve_committee_not_found(self, mock_info): - """Test resolving a non-existent committee.""" - with patch("apps.owasp.models.committee.Committee.objects.get") as mock_get: - mock_get.side_effect = Committee.DoesNotExist + assert committee_field.type.of_type is CommitteeNode - result = CommitteeQuery.resolve_committee(None, mock_info, key="non-existent") + arg_names = [arg.python_name for arg in committee_field.arguments] + assert "key" in arg_names - assert result is None - mock_get.assert_called_once_with(key="www-committee-non-existent") + key_arg = next(arg for arg in committee_field.arguments if arg.python_name == "key") + assert key_arg.type_annotation.annotation is str diff --git a/backend/tests/apps/owasp/graphql/queries/project_test.py b/backend/tests/apps/owasp/graphql/queries/project_test.py index b14c0c13dc..291f534eb9 100644 --- a/backend/tests/apps/owasp/graphql/queries/project_test.py +++ b/backend/tests/apps/owasp/graphql/queries/project_test.py @@ -1,11 +1,7 @@ -"""Test cases for ProjectQuery.""" - from unittest.mock import Mock, patch import pytest -from graphene import Field, NonNull, String -from apps.common.graphql.queries import BaseQuery from apps.owasp.graphql.nodes.project import ProjectNode from apps.owasp.graphql.queries.project import ProjectQuery from apps.owasp.models.project import Project @@ -14,50 +10,59 @@ class TestProjectQuery: """Test cases for ProjectQuery class.""" - def test_project_query_inheritance(self): - """Test if ProjectQuery inherits from BaseQuery.""" - assert issubclass(ProjectQuery, BaseQuery) + def test_project_query_has_strawberry_definition(self): + """Check if ProjectQuery has valid Strawberry definition.""" + assert hasattr(ProjectQuery, "__strawberry_definition__") + + field_names = [field.name for field in ProjectQuery.__strawberry_definition__.fields] + assert "project" in field_names def test_project_field_configuration(self): - """Test if project field is properly configured.""" - project_field = ProjectQuery._meta.fields.get("project") - assert isinstance(project_field, Field) - assert project_field.type == ProjectNode + """Test if 'project' field is configured properly.""" + project_field = next( + field + for field in ProjectQuery.__strawberry_definition__.fields + if field.name == "project" + ) + + assert project_field.type.of_type is ProjectNode + + arg_names = [arg.python_name for arg in project_field.arguments] + assert "key" in arg_names + + key_arg = next(arg for arg in project_field.arguments if arg.python_name == "key") + assert key_arg.type_annotation.annotation is str - assert "key" in project_field.args - key_arg = project_field.args["key"] - assert isinstance(key_arg.type, NonNull) - assert key_arg.type.of_type == String - class TestProjectResolution: - """Test cases for project resolution.""" +class TestProjectResolution: + """Test cases for resolving the project field.""" - @pytest.fixture - def mock_project(self): - """Project mock fixture.""" - return Mock(spec=Project) + @pytest.fixture + def mock_info(self): + return Mock() - @pytest.fixture - def mock_info(self): - """GraphQL info mock fixture.""" - return Mock() + @pytest.fixture + def mock_project(self): + return Mock(spec=Project) - def test_resolve_project_existing(self, mock_project, mock_info): - """Test resolving an existing project.""" - with patch("apps.owasp.models.project.Project.objects.get") as mock_get: - mock_get.return_value = mock_project + def test_resolve_project_existing(self, mock_project, mock_info): + """Test resolving an existing project.""" + with patch("apps.owasp.models.project.Project.objects.get") as mock_get: + mock_get.return_value = mock_project - result = ProjectQuery.resolve_project(None, mock_info, key="test-project") + query = ProjectQuery() + result = query.__class__.__dict__["project"](query, key="test-project") - assert result == mock_project - mock_get.assert_called_once_with(key="www-project-test-project") + assert result == mock_project + mock_get.assert_called_once_with(key="www-project-test-project") - def test_resolve_project_not_found(self, mock_info): - """Test resolving a non-existent project.""" - with patch("apps.owasp.models.project.Project.objects.get") as mock_get: - mock_get.side_effect = Project.DoesNotExist + def test_resolve_project_not_found(self, mock_info): + """Test resolving a non-existent project.""" + with patch("apps.owasp.models.project.Project.objects.get") as mock_get: + mock_get.side_effect = Project.DoesNotExist - result = ProjectQuery.resolve_project(None, mock_info, key="non-existent") + query = ProjectQuery() + result = query.__class__.__dict__["project"](query, key="non-existent") - assert result is None - mock_get.assert_called_once_with(key="www-project-non-existent") + assert result is None + mock_get.assert_called_once_with(key="www-project-non-existent")