# Error Path Testing Documentation **Phase:** 104-backend-error-path-testing **Date:** 2026-02-28 **Purpose:** Comprehensive guide for error path testing patterns and best practices --- ## Table of Contents 1. [Overview](#overview) 2. [VALIDATED_BUG Docstring Pattern](#validated_bug-docstring-pattern) 3. [Error Path Categories](#error-path-categories) 4. [Test Structure Patterns](#test-structure-patterns) 5. [Common Error Scenarios](#common-error-scenarios) 6. [Severity Classification](#severity-classification) 7. [Reusability for Frontend Phases](#reusability-for-frontend-phases) 8. [Complete Test Examples](#complete-test-examples) 9. [Best Practices](#best-practices) 10. [Tools and Frameworks](#tools-and-frameworks) --- ## Overview ### Purpose of Error Path Testing Error path testing focuses on validating how a system handles **invalid inputs, exceptional conditions, and edge cases**. Unlike unit tests (which verify happy paths) and property tests (which verify invariants), error path tests specifically target: - **Crash prevention** - Ensure invalid inputs don't cause unhandled exceptions - **Graceful degradation** - Verify error messages are helpful and actionable - **Security validation** - Prevent malicious inputs from exploiting vulnerabilities - **Data integrity** - Ensure error states don't corrupt data or create inconsistencies ### When to Use Error Path Testing **Use error path testing when:** - A function accepts user input (API endpoints, CLI commands, form fields) - A function performs critical operations (payments, authentication, database writes) - A function has complex validation logic (business rules, constraints, sanitization) - A function interfaces with external systems (APIs, databases, file systems) - Previous bugs indicate missing input validation **Don't use error path testing for:** - Trivial getters/setters (overkill for simple accessors) - Private helper methods (test via public interface) - Performance optimization (use profiling tools instead) - Happy path validation (use standard unit tests instead) ### Difference from Other Test Types | Test Type | Purpose | Example | Tools | |-----------|---------|---------|-------| | **Unit Tests** | Verify correct behavior with valid inputs | `login(user, pass)` returns token | pytest, unittest | | **Property Tests** | Verify invariants hold for all inputs | `sort(sort(x)) == sort(x)` for all lists | Hypothesis, pytest | | **Error Path Tests** | Verify graceful handling of invalid inputs | `login(None, None)` returns error, not crash | pytest, pytest.raises | | **Integration Tests** | Verify multi-component workflows | Login → Create agent → Execute skill | pytest, fixtures | --- ## VALIDATED_BUG Docstring Pattern The `VALIDATED_BUG` docstring pattern is the standard way to document bugs discovered during error path testing. This pattern ensures **reproducibility, severity assessment, and clear fix recommendations**. ### Template ```python def test_example_error_case(self): """ VALIDATED_BUG: Brief description of the bug Expected: - What should happen when the error condition occurs - Expected return value, exception type, or behavior Actual: - What actually happens (the bug) - Actual error type, return value, or crash behavior Severity: [CRITICAL, HIGH, MEDIUM, LOW] Impact: - Production impact (crashes, data loss, security issues) - User impact (confusion, incorrect results) - Business impact (lost revenue, compliance issues) Fix: - Recommended code change with line numbers - Example fix code snippet - Verification steps after fix Validated: ✅ Test confirms bug exists """ # Test implementation here ``` ### Example: Real Bug from Phase 104 ```python def test_verify_password_with_none_password(self): """ VALIDATED_BUG: verify_password() crashes with None password Expected: - verify_password() should return False for None password - Graceful degradation without crash Actual: - TypeError: 'NoneType' object is not subscriptable - Crash at line 48: plain_password[:71] Severity: HIGH Impact: - Login endpoint crashes if None password passed - Potential DoS vector if attacker sends None passwords - Inconsistent with expected graceful degradation Fix: Add None check at start of verify_password(): ```python def verify_password(plain_password: str, hashed_password: str) -> bool: if plain_password is None or hashed_password is None: return False # ... rest of function ``` Validated: ✅ Test confirms bug exists """ valid_hash = get_password_hash("test_password") with pytest.raises(TypeError): result = verify_password(None, valid_hash) ``` ### Field Descriptions #### Expected vs Actual - **Expected**: What the code SHOULD do (correct behavior) - **Actual**: What the code ACTUALLY does (bug behavior) - **Contrast**: The difference between expected and actual is the bug #### Severity Levels | Severity | Definition | Examples | |----------|------------|----------| | **CRITICAL** | Production crashes, data loss, security vulnerabilities | Zero-day exploits, database corruption, authentication bypass | | **HIGH** | Graceful degradation failures, incorrect results | Crashes on invalid input, TOCTOU race conditions, negative values accepted | | **MEDIUM** | Error messages not helpful, validation missing | Empty strings accepted, None inputs cause crashes, inconsistent error handling | | **LOW** | Cosmetic issues, logging improvements | Confusing debug logs, non-standard error codes, verbose error messages | #### Impact Section The **Impact** field answers: "Why does this bug matter?" - **Production impact**: Will this crash production? Cause data loss? Create security issues? - **User impact**: Will users see errors? Experience incorrect results? Be confused? - **Business impact**: Will this cause lost revenue? Compliance violations? Reputational damage? #### Fix Section The **Fix** field provides: 1. **Location**: Exact file and line number where fix should be applied 2. **Code change**: Minimal code snippet showing the fix 3. **Verification**: How to test that the fix works --- ## Error Path Categories Error paths fall into 4 main categories based on the type of error being tested. ### 1. Authentication Failures **Tests:** Invalid credentials, token issues, session management **Common Error Scenarios:** - None/empty username or password - Invalid token signatures - Expired tokens - Malformed tokens - Missing token headers - Concurrent login attempts **Example Bugs:** - Bug #10: `verify_password()` crashes with None password - Bug #12: `verify_mobile_token()` crashes with None token - Bug #13: `get_current_user_ws()` crashes with None token **File:** `backend/tests/error_paths/test_auth_error_paths.py` --- ### 2. Security Violations **Tests:** Rate limiting, authorization bypass, input sanitization **Common Error Scenarios:** - Negative or zero rate limits - Excessive requests (DoS attempts) - SQL injection attempts - XSS attempts - Path traversal attempts - CSRF token validation - Header manipulation **Example Bugs:** - Bug #10-12: `RateLimitMiddleware` accepts negative/zero limits, crashes on None client - Bug #13: Race condition in concurrent requests allows rate limit bypass **File:** `backend/tests/error_paths/test_security_error_paths.py` --- ### 3. Financial Errors **Tests:** Payment failures, calculation errors, budget violations **Common Error Scenarios:** - Negative payment amounts - Negative budget limits - Zero budget limits - Float to Decimal precision loss - Division by zero - Concurrent spend checks (TOCTOU races) - Negative invoice tolerance **Example Bugs:** - Bug #15: Negative payment amounts accepted - Bug #16: Negative monthly limit accepted - Bug #17: Zero monthly limit causes incorrect behavior - Bug #20: TOCTOU race in concurrent budget spend checks **File:** `backend/tests/error_paths/test_finance_error_paths.py` --- ### 4. Edge Cases **Tests:** Empty inputs, null values, boundary violations, type errors **Common Error Scenarios:** - None inputs - Empty strings/lists/dicts - Unicode/special characters - Numeric edge cases (zero, negative, infinity, NaN) - Datetime edge cases (leap years, DST transitions) - Concurrent access patterns **Example Bugs:** - Bug #15: `GovernanceCache` crashes on None action_type - Bug #16: Leap year date addition fails - Bug #17: Empty string agent_id accepted **File:** `backend/tests/error_paths/test_edge_case_error_paths.py` --- ## Test Structure Patterns ### Test Class Organization **Organize tests by service or error type:** ```python class TestAuthFailures: """Tests for authentication error scenarios""" class TestTokenValidation: """Tests for token validation error scenarios""" class TestPasswordHashingEdgeCases: """Tests for password hashing edge cases""" ``` **Recommended structure:** - **By service** (preferred for large test suites): `TestAuthServiceErrors`, `TestFinanceServiceErrors` - **By error type** (preferred for cross-service testing): `TestNoneInputs`, `TestEmptyInputs`, `TestBoundaryViolations` ### Test Method Naming **Use descriptive names that explain the error scenario:** ```python # Good: Descriptive def test_verify_password_with_none_password(self): """Test that verify_password() handles None password gracefully""" pass def test_budget_limit_with_negative_monthly_limit(self): """Test that negative budget limits are rejected""" pass # Bad: Vague def test_auth_error(self): """Test auth error handling""" # Which error? pass def test_budget_limit(self): """Test budget limit""" # What aspect? pass ``` **Naming pattern:** `test_{function_name}_with_{error_condition}` ### Fixture Usage **Use pytest fixtures for common test setup:** ```python import pytest from unittest.mock import Mock, MagicMock from core.auth import verify_password, get_password_hash @pytest.fixture def mock_db(): """Mock database session""" return Mock(spec=Session) @pytest.fixture def valid_password_hash(): """Valid bcrypt hash for 'test_password'""" return get_password_hash("test_password") class TestAuthFailures: def test_verify_password_with_none_password(self, valid_password_hash): """Test that verify_password() handles None password gracefully""" with pytest.raises(TypeError): verify_password(None, valid_password_hash) ``` **Common fixtures:** - `mock_db`: Mock database session - `mock_request`: Mock HTTP request object - `mock_app`: Mock FastAPI app - `valid_*`: Valid test data (passwords, tokens, hashes) --- ## Common Error Scenarios ### 1. None/Null Inputs **Test pattern:** Verify function handles None gracefully without crashing ```python def test_function_with_none_input(self): """Test that function() handles None input gracefully""" with pytest.raises((AttributeError, TypeError, ValueError)): result = function(None) # OR if function should return None/False result = function(None) assert result is None # or result is False ``` **Common None input bugs:** - Missing `if x is None:` checks before calling methods - Passing None to external libraries (jwt.encode, bcrypt.hashpw) - Uninitialized optional parameters **Fix pattern:** ```python def function(param: Optional[str]) -> Optional[ReturnType]: if param is None: return None # or raise ValueError # ... rest of function ``` --- ### 2. Empty Collections **Test pattern:** Verify function handles empty lists/dicts/strings correctly ```python def test_function_with_empty_list(self): """Test that function() handles empty list gracefully""" result = function([]) assert result is None # or result == [] or result == False def test_function_with_empty_dict(self): """Test that function() handles empty dict gracefully""" result = function({}) assert result is None # or result == {} ``` **Common empty collection bugs:** - Iterating over empty list without checking length - Accessing `list[0]` on empty list - Returning incorrect default values **Fix pattern:** ```python def function(items: List[Any]) -> Optional[ReturnType]: if not items: # Covers [], (), {}, '', None return None # ... rest of function ``` --- ### 3. Invalid Types **Test pattern:** Verify function rejects wrong types with clear errors ```python def test_function_with_wrong_type(self): """Test that function() handles wrong type gracefully""" # int instead of str with pytest.raises((TypeError, AttributeError)): result = function(123) # list instead of str with pytest.raises((TypeError, AttributeError)): result = function(["item"]) # dict instead of str with pytest.raises((TypeError, AttributeError)): result = function({"key": "value"}) ``` **Common type error bugs:** - Slicing on non-subscriptable types (`int[:71]` crashes) - Calling string methods on non-strings (`None.lower()` crashes) - Inconsistent error handling (some types crash, others return False) **Fix pattern:** ```python def function(param: str) -> ReturnType: if not isinstance(param, str): raise TypeError(f"Expected str, got {type(param).__name__}") # ... rest of function ``` --- ### 4. Boundary Values **Test pattern:** Test min/max values, zero, negative numbers ```python def test_function_with_boundary_values(self): """Test that function() handles boundary values correctly""" # Zero result = function(0) assert result == expected_zero_behavior # Negative result = function(-1) assert result == expected_negative_behavior # Should reject or handle # Very large result = function(2**63) assert result == expected_large_behavior # Infinity result = function(float('inf')) assert result == expected_inf_behavior # NaN result = function(float('nan')) assert math.isnan(result) # NaN != NaN, use math.isnan() ``` **Common boundary bugs:** - Negative values accepted when they should be rejected - Division by zero not handled - Integer overflow (rare in Python 3, but possible with float conversions) - NaN propagation (NaN + any = NaN) **Fix pattern:** ```python def function(amount: Union[int, float]) -> ReturnType: if amount < 0: raise ValueError(f"Amount must be non-negative, got {amount}") if math.isnan(amount) or math.isinf(amount): raise ValueError(f"Amount must be finite, got {amount}") # ... rest of function ``` --- ### 5. Concurrent Access **Test pattern:** Test thread safety with concurrent operations ```python def test_concurrent_function_calls(self): """Test that function() is thread-safe under concurrent access""" import threading shared_resource = SharedResource() results = [] errors = [] def worker(): try: result = shared_resource.method() results.append(result) except Exception as e: errors.append(e) # Launch 10 threads threads = [threading.Thread(target=worker) for _ in range(10)] for t in threads: t.start() for t in threads: t.join() # Verify no errors occurred assert len(errors) == 0, f"Concurrent access caused errors: {errors}" ``` **Common concurrency bugs:** - TOCTOU (time-of-check-time-of-use) race conditions - Non-atomic check-and-increment operations - Missing locks on shared state **Fix pattern:** ```python class ThreadSafeClass: def __init__(self): self._lock = threading.Lock() self._state = {} def method(self): with self._lock: # Atomic operation if key not in self._state: self._state[key] = compute_value() return self._state[key] ``` --- ### 6. Network Failures **Test pattern:** Test graceful handling of network errors ```python def test_function_with_network_failure(self): """Test that function() handles network failures gracefully""" import pytest from unittest.mock import patch # Mock network failure with patch('requests.get', side_effect=ConnectionError("Network error")): result = function() assert result is None # or raises specific exception # Mock timeout with patch('requests.get', side_effect=TimeoutError("Request timeout")): result = function() assert result is None # or raises specific exception ``` **Common network failure bugs:** - Crashes on ConnectionError instead of graceful degradation - No retry logic for transient failures - Missing timeout configuration (infinite hangs) **Fix pattern:** ```python def function() -> Optional[ReturnType]: try: response = requests.get(url, timeout=10) response.raise_for_status() return process(response) except (ConnectionError, TimeoutError) as e: logger.warning(f"Network error: {e}") return None ``` --- ### 7. Timeouts **Test pattern:** Test timeout handling for long-running operations ```python import pytest def test_function_with_timeout(self): """Test that function() handles timeouts correctly""" # Mock slow operation with patch('module.slow_operation', side_effect=lambda: time.sleep(100)): with pytest.raises(TimeoutError): result = function(timeout=1) # 1 second timeout ``` **Common timeout bugs:** - No timeout configured (infinite hangs) - Timeout doesn't cancel operation (resource leaks) - Timeout exception crashes instead of graceful degradation **Fix pattern:** ```python import signal from contextlib import contextmanager @contextmanager def timeout(seconds): def timeout_handler(signum, frame): raise TimeoutError(f"Operation timed out after {seconds} seconds") signal.signal(signal.SIGALRM, timeout_handler) signal.alarm(seconds) try: yield finally: signal.alarm(0) def function() -> Optional[ReturnType]: try: with timeout(10): result = slow_operation() return result except TimeoutError as e: logger.warning(f"Operation timed out: {e}") return None ``` --- ## Severity Classification ### CRITICAL Severity **Definition:** Production crashes, data loss, security vulnerabilities **Examples:** - Authentication bypass (login without password) - SQL injection vulnerabilities - Database corruption (writes to wrong tables) - Zero-day exploits (remote code execution) **Action Required:** Fix immediately, do not ship to production **Examples from Phase 104:** - Bug #1 (Phase 088): Zero vector cosine similarity returns NaN (episode boundary detection failure) - Bug #10 (Phase 104): RateLimitMiddleware accepts negative limit (configuration error causes outage) --- ### HIGH Severity **Definition:** Graceful degradation failures, incorrect results **Examples:** - Crashes on invalid input (None, wrong type) - TOCTOU race conditions (budget exceeded under concurrency) - Negative values accepted (could cause accounting errors) **Action Required:** Fix before next production deployment **Examples from Phase 104:** - Bug #10-14 (Phase 104): verify_password(), verify_mobile_token(), get_current_user_ws() crash with None - Bug #15-16 (Phase 104): Negative payment amounts/limits accepted - Bug #20 (Phase 104): TOCTOU race in concurrent budget spend checks --- ### MEDIUM Severity **Definition:** Error messages not helpful, validation missing **Examples:** - Empty strings accepted (creates confusing state) - None inputs cause crashes (but caught by exception handler) - Inconsistent error handling (some types crash, others return False) **Action Required:** Fix within next 2 sprints **Examples from Phase 104:** - Bug #11 (Phase 104): verify_password() crashes with int/float/dict types (inconsistent) - Bug #17 (Phase 104): Zero monthly limit causes incorrect behavior - Bug #18-19 (Phase 104): Negative tolerance/user count accepted --- ### LOW Severity **Definition:** Cosmetic issues, logging improvements **Examples:** - Confusing cache keys (empty agent_id creates ":action" key) - Non-standard error codes (500 instead of 400) - Verbose error messages (exposes internal details) **Action Required:** Fix when convenient, backlog item **Examples from Phase 104:** - Bug #17 (Phase 104): Empty string agent_id accepted - Bug #16 (Phase 104): Leap year date addition fails (Python datetime limitation) --- ## Reusability for Frontend Phases Error path testing patterns from Phase 104 can be **directly applied to frontend testing** in Phases 105-109 with minimal modifications. ### Frontend-Specific Error Scenarios **React error paths to test:** - Network failures (API calls return 500, 404, timeout) - User input edge cases (empty strings, None values, special characters) - Component state errors (missing props, undefined state) - Race conditions (concurrent API calls, rapid user interactions) ### Frontend Testing Tools **React Testing Library** (recommended): ```javascript import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' test('login form handles empty password gracefully', async () => { render() // Submit with empty password await userEvent.click(screen.getByRole('button', { name: 'Login' })) // Should show error message, not crash expect(screen.getByText('Password is required')).toBeInTheDocument() }) ``` **MSW (Mock Service Worker)** for API error simulation: ```javascript import { rest } from 'msw' import { setupServer } from 'msw/node' const server = setupServer( rest.post('/api/login', (req, res, ctx) => { return res(ctx.status(500), ctx.json({ error: 'Internal server error' })) }) ) test('login form handles API errors gracefully', async () => { render() await userEvent.click(screen.getByRole('button', { name: 'Login' })) // Should show error message, not crash await waitFor(() => { expect(screen.getByText('Login failed. Please try again.')).toBeInTheDocument() }) }) ``` ### Mapping Backend to Frontend Patterns | Backend Pattern | Frontend Equivalent | |----------------|---------------------| | `test_verify_password_with_none_password` | `test_login_form_handles_empty_password` | | `test_rate_limit_with_negative_limit` | `test_api_client_handles_429_responses` | | `test_budget_limit_with_negative_monthly_limit` | `test_budget_form_rejects_negative_values` | | `test_function_with_wrong_type` | `test_component_handles_invalid_props` | | `test_concurrent_function_calls` | `test_component_handles_rapid_user_interactions` | ### Frontend VALIDATED_BUG Pattern Adapt the VALIDATED_BUG pattern for frontend bugs: ```javascript test('select dropdown crashes with None options', () => { /** * VALIDATED_BUG: Select component crashes with None options * * Expected: * - Select component should handle None/undefined options gracefully * - Should show empty state or "No options available" message * * Actual: * - TypeError: Cannot read property 'map' of undefined * - Crash at line 42: options.map(...) * * Severity: HIGH * Impact: * - Select component crashes when options prop is None * - User cannot select value, form submission blocked * * Fix: * Add default empty array: * const options = props.options || [] * * Validated: ✅ Test confirms bug exists */ render(