diff --git a/.github/workflows/update-nest-test-images.yaml b/.github/workflows/update-nest-test-images.yaml index 1c09ad4b92..8c0bb4a0c8 100644 --- a/.github/workflows/update-nest-test-images.yaml +++ b/.github/workflows/update-nest-test-images.yaml @@ -82,8 +82,8 @@ jobs: cache-to: | type=gha,compression=zstd type=registry,ref=owasp/nest:test-fuzz-backend-cache - context: backend/docker - file: Dockerfile.fuzz + context: backend + file: docker/backend/Dockerfile.fuzz platforms: linux/amd64 push: true tags: owasp/nest:test-fuzz-backend-latest diff --git a/backend/apps/common/management/commands/dump_data.py b/backend/apps/common/management/commands/dump_data.py index 6bba1f6e3c..8c9631338c 100644 --- a/backend/apps/common/management/commands/dump_data.py +++ b/backend/apps/common/management/commands/dump_data.py @@ -7,7 +7,7 @@ from django.conf import settings from django.core.management.base import BaseCommand, CommandError -from psycopg2 import ProgrammingError, connect, sql +from psycopg2 import OperationalError, ProgrammingError, connect, sql DEFAULT_DATABASE = settings.DATABASES["default"] DB_HOST = DEFAULT_DATABASE.get("HOST", "localhost") @@ -52,7 +52,14 @@ def handle(self, *args, **options): temp_db = f"temp_{DB_NAME}" try: - self._execute_sql("postgres", [f"CREATE DATABASE {temp_db} TEMPLATE {DB_NAME};"]) + self._execute_sql( + "postgres", + [ + sql.SQL("CREATE DATABASE {temp_db} TEMPLATE {DB_NAME};").format( + temp_db=sql.Identifier(temp_db), DB_NAME=sql.Identifier(DB_NAME) + ) + ], + ) self.stdout.write(self.style.SUCCESS(f"Created temporary DB: {temp_db}")) @@ -86,20 +93,27 @@ def handle(self, *args, **options): raise CommandError(message) from e finally: try: - self._execute_sql("postgres", [f"DROP DATABASE IF EXISTS {temp_db};"]) - except CalledProcessError: + self._execute_sql( + "postgres", + [ + sql.SQL("DROP DATABASE IF EXISTS {temp_db};").format( + temp_db=sql.Identifier(temp_db) + ) + ], + ) + except (ProgrammingError, OperationalError): self.stderr.write( self.style.WARNING(f"Failed to drop temp DB {temp_db} (ignored).") ) - def _table_list_query(self) -> str: - return """ + def _table_list_query(self) -> sql.Composable: + return sql.SQL(""" SELECT table_name FROM information_schema.columns WHERE table_schema = 'public' AND column_name = 'email'; - """ + """) - def _remove_emails(self, tables: list[str]) -> list[str]: + def _remove_emails(self, tables: list[str]) -> list[sql.Composable]: return [ sql.SQL("UPDATE {table} SET email = '';").format(table=sql.Identifier(table)) for table in tables @@ -108,7 +122,7 @@ def _remove_emails(self, tables: list[str]) -> list[str]: def _execute_sql( self, dbname: str, - sql_queries: list[str], + sql_queries: list[sql.Composable], ): connection = connect( dbname=dbname, diff --git a/backend/apps/github/api/internal/nodes/pull_request.py b/backend/apps/github/api/internal/nodes/pull_request.py index 3fabca50ba..4e42bea9af 100644 --- a/backend/apps/github/api/internal/nodes/pull_request.py +++ b/backend/apps/github/api/internal/nodes/pull_request.py @@ -38,4 +38,4 @@ def repository_name(self, root: PullRequest) -> str | None: @strawberry_django.field def url(self, root: PullRequest) -> str: """Resolve URL.""" - return root.url or "" + return root.url diff --git a/backend/apps/github/api/internal/queries/issue.py b/backend/apps/github/api/internal/queries/issue.py index 0e54e8ce60..99441cada6 100644 --- a/backend/apps/github/api/internal/queries/issue.py +++ b/backend/apps/github/api/internal/queries/issue.py @@ -48,7 +48,7 @@ def recent_issues( """Resolve recent issues with optional filtering. Args: - distinct (bool): Whether to return unique issues per author and repository. + distinct (bool): Whether to return unique issues per author. 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. diff --git a/backend/apps/github/api/internal/queries/pull_request.py b/backend/apps/github/api/internal/queries/pull_request.py index a966eaae99..d81e98dd3c 100644 --- a/backend/apps/github/api/internal/queries/pull_request.py +++ b/backend/apps/github/api/internal/queries/pull_request.py @@ -49,7 +49,7 @@ def recent_pull_requests( """Resolve recent pull requests. Args: - distinct (bool): Whether to return unique pull requests per author and repository. + distinct (bool): Whether to return unique pull requests per author. limit (int): Maximum number of pull requests to return. login (str, optional): Filter pull requests by a specific author's login. organization (str, optional): Filter pull requests by a specific organization's login. diff --git a/backend/apps/github/api/internal/queries/release.py b/backend/apps/github/api/internal/queries/release.py index 6960f3f450..9cc5f8ead4 100644 --- a/backend/apps/github/api/internal/queries/release.py +++ b/backend/apps/github/api/internal/queries/release.py @@ -33,7 +33,7 @@ def recent_releases( """Resolve recent releases with optional distinct filtering. Args: - distinct (bool): Whether to return unique releases per author and repository. + distinct (bool): Whether to return unique releases per author. limit (int): Maximum number of releases to return. login (str, optional): Filter releases by a specific author's login. organization (str, optional): Filter releases by a specific organization's login. diff --git a/backend/apps/github/api/internal/queries/user.py b/backend/apps/github/api/internal/queries/user.py index cc39fe9ff0..41b4fa0c02 100644 --- a/backend/apps/github/api/internal/queries/user.py +++ b/backend/apps/github/api/internal/queries/user.py @@ -37,7 +37,9 @@ def top_contributed_repositories( .order_by("-contributions_count") ] - @strawberry_django.field(select_related=["owasp_profile", "user_badges__badge"]) + @strawberry_django.field( + select_related=["owasp_profile"], prefetch_related=["user_badges__badge"] + ) def user( self, login: str, diff --git a/backend/apps/owasp/api/internal/nodes/committee.py b/backend/apps/owasp/api/internal/nodes/committee.py index 8d9d51cb26..51165cf09e 100644 --- a/backend/apps/owasp/api/internal/nodes/committee.py +++ b/backend/apps/owasp/api/internal/nodes/committee.py @@ -16,7 +16,7 @@ def contributors_count(self, root: Committee) -> int: return root.owasp_repository.contributors_count if root.owasp_repository else 0 @strawberry_django.field - def created_at(self, root: Committee) -> float: + def created_at(self, root: Committee) -> float | None: """Resolve created at.""" return root.idx_created_at diff --git a/backend/apps/owasp/api/internal/queries/post.py b/backend/apps/owasp/api/internal/queries/post.py index 143e1932a4..a7f4ca3d23 100644 --- a/backend/apps/owasp/api/internal/queries/post.py +++ b/backend/apps/owasp/api/internal/queries/post.py @@ -15,5 +15,5 @@ class PostQuery: @strawberry_django.field def recent_posts(self, limit: int = 5) -> list[PostNode]: - """Return the 5 most recent posts.""" + """Return the most recent posts.""" return Post.recent_posts()[:limit] if (limit := min(limit, MAX_LIMIT)) > 0 else [] diff --git a/backend/apps/owasp/models/project_health_metrics.py b/backend/apps/owasp/models/project_health_metrics.py index 6ff2456f0e..8e055cf04a 100644 --- a/backend/apps/owasp/models/project_health_metrics.py +++ b/backend/apps/owasp/models/project_health_metrics.py @@ -149,7 +149,7 @@ def owasp_page_last_update_days_requirement(self) -> int: @cached_property def project_requirements(self) -> ProjectHealthRequirements | None: """Get the project health requirements for the project's level.""" - return ProjectHealthRequirements.objects.get(level=self.project.level) + return ProjectHealthRequirements.objects.filter(level=self.project.level).first() @staticmethod def bulk_save(metrics: list, fields: list | None = None) -> None: # type: ignore[override] diff --git a/backend/tests/apps/common/management/commands/dump_data_test.py b/backend/tests/apps/common/management/commands/dump_data_test.py index c98d7d4924..06bba46cf5 100644 --- a/backend/tests/apps/common/management/commands/dump_data_test.py +++ b/backend/tests/apps/common/management/commands/dump_data_test.py @@ -2,6 +2,7 @@ from django.core.management import call_command from django.test import override_settings +from psycopg2 import sql DATABASES = { "default": { @@ -20,8 +21,7 @@ class TestDumpDataCommand: @patch("apps.common.management.commands.dump_data.run") @patch("apps.common.management.commands.dump_data.connect") @patch("apps.common.management.commands.dump_data.Path") - @patch("apps.common.management.commands.dump_data.sql") - def test_dump_data(self, mock_sql, mock_path, mock_connect, mock_run): + def test_dump_data(self, mock_path, mock_connect, mock_run): # Mock psycopg2 connection/cursor mock_conn = MagicMock() mock_cursor = MagicMock() @@ -30,7 +30,6 @@ def test_dump_data(self, mock_sql, mock_path, mock_connect, mock_run): mock_cursor.fetchall.return_value = [("public.users",), ("public.members",)] mock_resolve = MagicMock() mock_path.return_value.resolve.return_value = mock_resolve - mock_sql.SQL.return_value.format.return_value = "UPDATE public.users SET email = '';" call_command( "dump_data", "--output", @@ -48,18 +47,23 @@ def test_dump_data(self, mock_sql, mock_path, mock_connect, mock_run): port="5432", ) mock_cursor.execute.assert_any_call( - f"CREATE DATABASE {expected_temp_db} TEMPLATE db-name;" + sql.SQL("CREATE DATABASE {temp_db} TEMPLATE {DB_NAME};").format( + temp_db=sql.Identifier(expected_temp_db), + DB_NAME=sql.Identifier("db-name"), + ) ) executed_sql = [str(c.args[0]) for c in mock_cursor.execute.call_args_list] - assert "UPDATE public.users SET email = '';" in executed_sql - assert any( - """ + assert ( + str( + sql.SQL( + """ SELECT table_name FROM information_schema.columns WHERE table_schema = 'public' AND column_name = 'email'; - """.strip() - in str(query).strip() - for query in executed_sql + """ + ) + ) + in executed_sql ) assert [ @@ -87,6 +91,10 @@ def test_dump_data(self, mock_sql, mock_path, mock_connect, mock_run): str(mock_resolve), ] == mock_run.call_args[0][0] # Ensure DROP DATABASE executed at the end - mock_cursor.execute.assert_any_call(f"DROP DATABASE IF EXISTS {expected_temp_db};") + mock_cursor.execute.assert_any_call( + sql.SQL("DROP DATABASE IF EXISTS {temp_db};").format( + temp_db=sql.Identifier(expected_temp_db) + ) + ) mock_path.return_value.resolve.assert_called_once() mock_resolve.parent.mkdir.assert_called_once_with(parents=True, exist_ok=True) diff --git a/backend/tests/apps/github/api/internal/nodes/pull_request_test.py b/backend/tests/apps/github/api/internal/nodes/pull_request_test.py index e025bc96b3..d0515d7e5e 100644 --- a/backend/tests/apps/github/api/internal/nodes/pull_request_test.py +++ b/backend/tests/apps/github/api/internal/nodes/pull_request_test.py @@ -88,12 +88,3 @@ def test_url_with_url(self): field = self._get_field_by_name("url", PullRequestNode) result = field.base_resolver.wrapped_func(None, mock_pr) assert result == "https://github.com/test-org/test-repo/pull/123" - - def test_url_without_url(self): - """Test url field when URL doesn't exist.""" - mock_pr = Mock() - mock_pr.url = None - - field = self._get_field_by_name("url", PullRequestNode) - result = field.base_resolver.wrapped_func(None, mock_pr) - assert result == "" diff --git a/frontend/.env.e2e.example b/frontend/.env.e2e.example index 970daa7387..aec00ea3a7 100644 --- a/frontend/.env.e2e.example +++ b/frontend/.env.e2e.example @@ -1,3 +1,5 @@ +NEXTAUTH_SECRET= +NEXTAUTH_URL=http://localhost:3000/ NEXT_PUBLIC_API_URL=http://localhost:9000/ NEXT_PUBLIC_CSRF_URL=http://localhost:9000/csrf/ NEXT_PUBLIC_ENVIRONMENT=local @@ -12,5 +14,3 @@ NEXT_SERVER_DISABLE_SSR=false NEXT_SERVER_GITHUB_CLIENT_ID=your-github-client-id NEXT_SERVER_GITHUB_CLIENT_SECRET=your-github-client-secret NEXT_SERVER_GRAPHQL_URL=http://localhost:9000/graphql/ -NEXTAUTH_SECRET= -NEXTAUTH_URL=http://localhost:3000/ diff --git a/frontend/.env.example b/frontend/.env.example index 61379d4ba6..d399643ce0 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1,3 +1,5 @@ +NEXTAUTH_SECRET= +NEXTAUTH_URL=http://localhost:3000/ NEXT_PUBLIC_API_URL=http://localhost:8000/ NEXT_PUBLIC_CSRF_URL=http://localhost:8000/csrf/ NEXT_PUBLIC_ENVIRONMENT=local @@ -12,5 +14,3 @@ NEXT_SERVER_DISABLE_SSR=false NEXT_SERVER_GITHUB_CLIENT_ID=your-github-client-id NEXT_SERVER_GITHUB_CLIENT_SECRET=your-github-client-secret NEXT_SERVER_GRAPHQL_URL=http://backend:8000/graphql/ -NEXTAUTH_SECRET= -NEXTAUTH_URL=http://localhost:3000/