diff --git a/backend/src/apps/common/index_types.py b/backend/src/apps/common/index_types.py new file mode 100644 index 0000000000..af3b3c4089 --- /dev/null +++ b/backend/src/apps/common/index_types.py @@ -0,0 +1,99 @@ +"""Type definitions for search index results.""" + +from __future__ import annotations + +from typing import TypedDict + + +class UserSearchHit(TypedDict, total=False): + """GitHub user search hit.""" + + idx_bio: str | None + idx_company: str | None + idx_followers_count: int + idx_following_count: int + idx_location: str | None + idx_login: str + idx_name: str | None + idx_public_repositories_count: int + idx_url: str + + +class ChapterSearchHit(TypedDict, total=False): + """OWASP chapter search hit.""" + + idx_country: str + idx_key: str + idx_leaders: list[str] + idx_name: str + idx_suggested_location: str | None + idx_summary: str + idx_url: str + + +class CommitteeSearchHit(TypedDict, total=False): + """OWASP committee search hit.""" + + idx_leaders: list[str] + idx_name: str + idx_summary: str + idx_url: str + + +class IssueSearchHit(TypedDict, total=False): + """GitHub issue search hit.""" + + idx_project_name: str + idx_project_url: str + idx_summary: str + idx_title: str + idx_url: str + + +class ProjectSearchHit(TypedDict, total=False): + """OWASP project search hit.""" + + idx_contributors_count: int + idx_forks_count: int + idx_key: str + idx_leaders: list[str] + idx_name: str + idx_stars_count: int + idx_summary: str + idx_updated_at: int + idx_url: str + + +class UserSearchResult(TypedDict): + """Result returned by a GitHub user search.""" + + hits: list[UserSearchHit] + nbPages: int + + +class ChapterSearchResult(TypedDict): + """Result returned by an OWASP chapter search.""" + + hits: list[ChapterSearchHit] + nbPages: int + + +class CommitteeSearchResult(TypedDict): + """Result returned by an OWASP committee search.""" + + hits: list[CommitteeSearchHit] + nbPages: int + + +class IssueSearchResult(TypedDict): + """Result returned by a GitHub issue search.""" + + hits: list[IssueSearchHit] + nbPages: int + + +class ProjectSearchResult(TypedDict): + """Result returned by an OWASP project search.""" + + hits: list[ProjectSearchHit] + nbPages: int diff --git a/backend/src/apps/github/index/search/user.py b/backend/src/apps/github/index/search/user.py index 4cc99ec11e..ed8303d201 100644 --- a/backend/src/apps/github/index/search/user.py +++ b/backend/src/apps/github/index/search/user.py @@ -2,6 +2,11 @@ from __future__ import annotations +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from apps.common.index_types import UserSearchResult + from algoliasearch_django import raw_search from apps.github.models.user import User @@ -9,11 +14,11 @@ def get_users( query: str, - attributes: list | None = None, + attributes: list[str] | None = None, limit: int = 25, page: int = 1, - searchable_attributes: list | None = None, -) -> dict: + searchable_attributes: list[str] | None = None, +) -> UserSearchResult: """Return users relevant to a search query. Args: diff --git a/backend/src/apps/owasp/index/search/chapter.py b/backend/src/apps/owasp/index/search/chapter.py index 036ca0074c..47a7e27616 100644 --- a/backend/src/apps/owasp/index/search/chapter.py +++ b/backend/src/apps/owasp/index/search/chapter.py @@ -2,6 +2,11 @@ from __future__ import annotations +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from apps.common.index_types import ChapterSearchResult + from algoliasearch_django import raw_search from apps.owasp.models.chapter import Chapter @@ -10,11 +15,11 @@ def get_chapters( query: str, *, - attributes: list | None = None, + attributes: list[str] | None = None, limit: int = 25, page: int = 1, - searchable_attributes: list | None = None, -) -> dict: + searchable_attributes: list[str] | None = None, +) -> ChapterSearchResult: """Return chapters relevant to a search query. Args: diff --git a/backend/src/apps/owasp/index/search/committee.py b/backend/src/apps/owasp/index/search/committee.py index 3b951f0e76..5de30f2238 100644 --- a/backend/src/apps/owasp/index/search/committee.py +++ b/backend/src/apps/owasp/index/search/committee.py @@ -2,6 +2,11 @@ from __future__ import annotations +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from apps.common.index_types import CommitteeSearchResult + from algoliasearch_django import raw_search from apps.owasp.models.committee import Committee @@ -10,10 +15,10 @@ def get_committees( query: str, *, - attributes: list | None = None, + attributes: list[str] | None = None, limit: int = 25, page: int = 1, -) -> dict: +) -> CommitteeSearchResult: """Return committees relevant to a search query. Args: diff --git a/backend/src/apps/owasp/index/search/issue.py b/backend/src/apps/owasp/index/search/issue.py index a1e7d07916..2346d953d4 100644 --- a/backend/src/apps/owasp/index/search/issue.py +++ b/backend/src/apps/owasp/index/search/issue.py @@ -2,6 +2,11 @@ from __future__ import annotations +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from apps.common.index_types import IssueSearchResult + from algoliasearch_django import raw_search from apps.github.models.issue import Issue @@ -12,11 +17,11 @@ def get_issues( query: str, *, - attributes: list | None = None, + attributes: list[str] | None = None, distinct: bool = False, limit: int = 25, page: int = 1, -) -> dict: +) -> IssueSearchResult: """Return issues relevant to a search query. Args: diff --git a/backend/src/apps/owasp/index/search/project.py b/backend/src/apps/owasp/index/search/project.py index e0bb2950e0..e3c919b9ba 100644 --- a/backend/src/apps/owasp/index/search/project.py +++ b/backend/src/apps/owasp/index/search/project.py @@ -2,6 +2,11 @@ from __future__ import annotations +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from apps.common.index_types import ProjectSearchResult + from algoliasearch_django import raw_search from apps.owasp.models.project import Project @@ -10,11 +15,11 @@ def get_projects( query: str, *, - attributes: list | None = None, + attributes: list[str] | None = None, limit: int = 25, page: int = 1, - searchable_attributes: list | None = None, -) -> dict: + searchable_attributes: list[str] | None = None, +) -> ProjectSearchResult: """Return projects relevant to a search query. Args: diff --git a/backend/src/apps/slack/common/handlers/users.py b/backend/src/apps/slack/common/handlers/users.py index 492a0719cc..088e3a4e0e 100644 --- a/backend/src/apps/slack/common/handlers/users.py +++ b/backend/src/apps/slack/common/handlers/users.py @@ -78,8 +78,8 @@ def get_blocks( bio = truncate(escape(user.get("idx_bio", "") or ""), presentation.summary_truncation) - location = escape(user.get("idx_location", "")) - company = escape(user.get("idx_company", "")) + location = escape(user.get("idx_location", "") or "") + company = escape(user.get("idx_company", "") or "") followers_count = user.get("idx_followers_count", 0) following_count = user.get("idx_following_count", 0) public_repositories = user.get("idx_public_repositories_count", 0) diff --git a/backend/tests/unit/apps/slack/common/handlers/users_test.py b/backend/tests/unit/apps/slack/common/handlers/users_test.py index da2bff4cd3..8efe02e86e 100644 --- a/backend/tests/unit/apps/slack/common/handlers/users_test.py +++ b/backend/tests/unit/apps/slack/common/handlers/users_test.py @@ -123,6 +123,32 @@ def test_get_blocks_with_empty_metadata_fields(self, mocker): assert "Location:" not in user_block_text assert "Followers:" not in user_block_text + def test_get_blocks_with_none_metadata_fields(self, mocker): + """Test users with nullable metadata fields.""" + mock_data = { + "hits": [ + { + "idx_name": "User NoMeta", + "idx_login": "user_nometa", + "idx_url": "https://github.com/user_nometa", + "idx_bio": None, + "idx_location": None, + "idx_company": None, + "idx_followers_count": 0, + "idx_following_count": 0, + "idx_public_repositories_count": 0, + } + ], + "nbPages": 1, + } + mocker.patch("apps.github.index.search.user.get_users", return_value=mock_data) + + blocks = get_blocks() + + user_block_text = blocks[1]["text"]["text"] + assert "Company:" not in user_block_text + assert "Location:" not in user_block_text + def test_get_blocks_with_no_name_uses_login(self, mocker): """Test users with no name field uses login instead.""" mock_data = {