From 8ad4d240d8e13589a27accd2e8939ff746a89b21 Mon Sep 17 00:00:00 2001 From: Rakshak05 Date: Mon, 6 Jul 2026 22:04:50 +0530 Subject: [PATCH 1/3] Issue-#1411 --- .../unit/components/ApiKeySetupModal.test.tsx | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 frontend/testing/unit/components/ApiKeySetupModal.test.tsx diff --git a/frontend/testing/unit/components/ApiKeySetupModal.test.tsx b/frontend/testing/unit/components/ApiKeySetupModal.test.tsx new file mode 100644 index 000000000..2e66dfe99 --- /dev/null +++ b/frontend/testing/unit/components/ApiKeySetupModal.test.tsx @@ -0,0 +1,110 @@ +import React from 'react' +import { render, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { describe, it, expect, beforeEach, vi, type MockInstance } from 'vitest' +import ApiKeySetupModal from '../../../src/components/ApiKeySetupModal' + +vi.mock('../../../src/api', () => ({ + authenticateWithApiKey: vi.fn(), +})) + +import { authenticateWithApiKey } from '../../../src/api' + +describe('ApiKeySetupModal Error Handling', () => { + const defaultProps = { + onSaved: vi.fn(), + } + + let consoleLogSpy: MockInstance + let consoleErrorSpy: MockInstance + let consoleWarnSpy: MockInstance + + beforeEach(() => { + vi.clearAllMocks() + consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) + consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + }) + + it('renders the dialog with required elements', () => { + render() + + expect(screen.getByRole('dialog')).toBeInTheDocument() + expect(screen.getByText('API Key Required')).toBeInTheDocument() + expect(screen.getByLabelText('Backend API Key')).toBeInTheDocument() + expect(screen.getByRole('button', { name: /save and connect/i })).toBeInTheDocument() + }) + + it('handles successful API key validation', async () => { + const user = userEvent.setup() + vi.mocked(authenticateWithApiKey).mockResolvedValue(undefined) + + render() + + const input = screen.getByLabelText('Backend API Key') + await user.type(input, 'valid-key-123') + await user.click(screen.getByRole('button', { name: /save and connect/i })) + + expect(authenticateWithApiKey).toHaveBeenCalledWith('valid-key-123') + await waitFor(() => { + expect(defaultProps.onSaved).toHaveBeenCalled() + }) + }) + + it('shows standard validation error on empty submission', async () => { + const user = userEvent.setup() + render() + + await user.click(screen.getByRole('button', { name: /save and connect/i })) + + const alert = await screen.findByRole('alert') + expect(alert).toHaveTextContent('Please enter the API key.') + expect(authenticateWithApiKey).not.toHaveBeenCalled() + }) + + it('shows backend error message when api key is rejected', async () => { + const user = userEvent.setup() + const backendErrorMsg = 'Invalid/Expired API token' + vi.mocked(authenticateWithApiKey).mockRejectedValue(new Error(backendErrorMsg)) + + render() + + const input = screen.getByLabelText('Backend API Key') + await user.type(input, 'bad-key') + await user.click(screen.getByRole('button', { name: /save and connect/i })) + + const alert = await screen.findByRole('alert') + expect(alert).toHaveTextContent(backendErrorMsg) + expect(defaultProps.onSaved).not.toHaveBeenCalled() + }) + + it('ensures sensitive key text is not logged or echoed in failure UI', async () => { + const user = userEvent.setup() + const sensitiveKey = 'SUPER_SECRET_TOKEN_999' + vi.mocked(authenticateWithApiKey).mockRejectedValue(new Error('Authentication failed. Check the API key.')) + + render() + + const input = screen.getByLabelText('Backend API Key') + await user.type(input, sensitiveKey) + await user.click(screen.getByRole('button', { name: /save and connect/i })) + + // Assert useful error message is visible + const alert = await screen.findByRole('alert') + expect(alert).toBeInTheDocument() + + // Assert sensitive key text is NOT in the error message / failure UI text content + expect(alert.textContent).not.toContain(sensitiveKey) + + // Assert sensitive key text is NOT logged in console + const allLogCalls = [ + ...consoleLogSpy.mock.calls, + ...consoleErrorSpy.mock.calls, + ...consoleWarnSpy.mock.calls, + ].flatMap((args) => args.map((arg) => (typeof arg === 'string' ? arg : JSON.stringify(arg)))) + + for (const logText of allLogCalls) { + expect(logText).not.toContain(sensitiveKey) + } + }) +}) From 646b801f86519288a0743086e203eebb2490c040 Mon Sep 17 00:00:00 2001 From: Rakshak05 Date: Tue, 7 Jul 2026 01:32:38 +0530 Subject: [PATCH 2/3] test(backend): fix windows compatibility issues in parser sandbox tests --- testing/backend/unit/test_parser_sandbox_timeout_cleanup.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/testing/backend/unit/test_parser_sandbox_timeout_cleanup.py b/testing/backend/unit/test_parser_sandbox_timeout_cleanup.py index 9ae679d0e..fad1bf4f6 100644 --- a/testing/backend/unit/test_parser_sandbox_timeout_cleanup.py +++ b/testing/backend/unit/test_parser_sandbox_timeout_cleanup.py @@ -13,6 +13,7 @@ from __future__ import annotations import subprocess +import sys import pytest import time from pathlib import Path @@ -34,7 +35,7 @@ def test_timeout_expired_raises_parser_sandbox_error(self, tmp_path): with pytest.raises(subprocess.TimeoutExpired): proc = subprocess.Popen( - ["python3", "-c", _BOOTSTRAP_TEMPLATE.format( + [sys.executable, "-c", _BOOTSTRAP_TEMPLATE.format( parser_path=str(parser), max_input_bytes=1024, )], @@ -96,6 +97,7 @@ def test_timeout_error_includes_stderr(self, tmp_path): exc_message = str(exc_info.value) assert "early error" in exc_message + @pytest.mark.skipif(sys.platform == "win32", reason="resource module not available on Windows") def test_multiple_consecutive_timeouts_do_not_leak_resources(self, tmp_path): """Running multiple timeouts in sequence must not accumulate open file handles.""" from backend.secuscan.parser_sandbox import run_parser_in_sandbox From 50d0d4402ef25b7ddd609104785df9e81eb09cfc Mon Sep 17 00:00:00 2001 From: Rakshak05 Date: Tue, 7 Jul 2026 15:04:44 +0530 Subject: [PATCH 3/3] fixed some bugs. --- backend/secuscan/parser_sandbox.py | 14 +++++++++++++- .../unit/test_parser_sandbox_timeout_cleanup.py | 10 +++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/backend/secuscan/parser_sandbox.py b/backend/secuscan/parser_sandbox.py index bfb560887..9878d271f 100644 --- a/backend/secuscan/parser_sandbox.py +++ b/backend/secuscan/parser_sandbox.py @@ -82,6 +82,10 @@ def __init__(self, plugin_id: str, reason: str, stderr: str = "") -> None: # User-facing message: reason only — no stderr content. super().__init__(f"Parser sandbox failed for '{plugin_id}' ({reason})") + @property + def stderr_excerpt(self) -> str: + return self._stderr_diagnostic[:2000] + # --------------------------------------------------------------------------- # Bootstrap script injected into the child process via -c @@ -248,6 +252,7 @@ def _read_stderr() -> None: raise ParserSandboxError( plugin_id, f"output exceeded {max_output_bytes // (1024 * 1024)} MB limit", + stderr=stderr_text, ) if timed_out: @@ -262,7 +267,11 @@ def _read_stderr() -> None: plugin_id, _sanitize_stderr(stderr_text), ) - raise ParserSandboxError(plugin_id, f"timed out after {timeout_seconds}s") + raise ParserSandboxError( + plugin_id, + f"timed out after {timeout_seconds}s", + stderr=stderr_text, + ) if proc.returncode != 0: logger.error( @@ -280,6 +289,7 @@ def _read_stderr() -> None: raise ParserSandboxError( plugin_id, f"subprocess exited with code {proc.returncode}", + stderr=stderr_text, ) stdout_bytes = b"".join(stdout_chunks) @@ -302,12 +312,14 @@ def _read_stderr() -> None: raise ParserSandboxError( plugin_id, f"parser returned non-JSON output: {exc}", + stderr=stderr_text, ) if not isinstance(parsed, (dict, list)): raise ParserSandboxError( plugin_id, f"parser returned unexpected type {type(parsed).__name__}; expected dict or list", + stderr=stderr_text, ) logger.info("Parser sandbox completed successfully for plugin '%s'", plugin_id) return parsed if isinstance(parsed, dict) else {"findings": parsed} diff --git a/testing/backend/unit/test_parser_sandbox_timeout_cleanup.py b/testing/backend/unit/test_parser_sandbox_timeout_cleanup.py index fad1bf4f6..c17685a33 100644 --- a/testing/backend/unit/test_parser_sandbox_timeout_cleanup.py +++ b/testing/backend/unit/test_parser_sandbox_timeout_cleanup.py @@ -35,8 +35,8 @@ def test_timeout_expired_raises_parser_sandbox_error(self, tmp_path): with pytest.raises(subprocess.TimeoutExpired): proc = subprocess.Popen( - [sys.executable, "-c", _BOOTSTRAP_TEMPLATE.format( - parser_path=str(parser), + [sys.executable, "-c", _BOOTSTRAP_TEMPLATE.safe_substitute( + parser_path_repr=repr(str(parser)), max_input_bytes=1024, )], stdin=subprocess.PIPE, @@ -91,11 +91,11 @@ def test_timeout_error_includes_stderr(self, tmp_path): encoding="utf-8", ) - with pytest.raises(Exception) as exc_info: + from backend.secuscan.parser_sandbox import ParserSandboxError + with pytest.raises(ParserSandboxError) as exc_info: run_parser_in_sandbox(parser, "stderr_plugin", "data", timeout_seconds=1) - exc_message = str(exc_info.value) - assert "early error" in exc_message + assert "early error" in exc_info.value.stderr_excerpt @pytest.mark.skipif(sys.platform == "win32", reason="resource module not available on Windows") def test_multiple_consecutive_timeouts_do_not_leak_resources(self, tmp_path):