diff --git a/README.md b/README.md index 7dec83b..b5bbb54 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,11 @@ title = response.get("title") - [Why Use OpenAPI Validation?](#why-use-openapi-validation) - [Setup](#setup) - [Use Cases](#use-cases-1) +- [Request/Response Tracking](#requestresponse-tracking) + - [Why Use Tracking?](#why-use-tracking) + - [Quick Setup](#quick-setup) + - [pytest-html Integration](#pytest-html-integration) + - [Manual Tracking](#manual-tracking) - [Error Handling](#error-handling) - [Migration from v1](#migration-from-v1) @@ -920,6 +925,165 @@ def test_validation_error_matches_spec(api): ) ``` +## Request/Response Tracking + +BerAPI includes built-in request/response tracking for debugging and integration with pytest-html reports. When tests fail, you can see exactly what API calls were made and what responses were received. + +### Why Use Tracking? + +- **Debug Failed Tests** - See exact request/response details when tests fail +- **pytest-html Integration** - Automatic HTML reports with API call details +- **Sensitive Data Masking** - Hide authorization tokens in reports +- **Multiple Requests** - Track all API calls in a single test + +### Quick Setup + +The easiest way to enable tracking is with the pytest plugin: + +```python +# conftest.py +pytest_plugins = ["berapi.contrib.pytest_plugin"] + +from berapi.contrib.pytest_plugin import create_tracking_client +import pytest + +@pytest.fixture +def api(): + return create_tracking_client( + base_url="https://api.example.com", + mask_headers=["Authorization", "X-Api-Key"], # Hide sensitive headers + ) +``` + +```python +# test_api.py +def test_user_api(api): + # All requests are automatically tracked + response = api.get("/users/1").assert_2xx() + + # If this fails, the HTML report will show: + # - Request URL, method, headers, body + # - Response status, headers, body + # - Response time + assert response.get("name") == "Expected Name" +``` + +Run with pytest-html: +```bash +pytest --html=report.html +``` + +### pytest-html Integration + +The tracking plugin automatically adds request/response details to pytest-html reports: + +1. **Passed tests** - No extra information shown +2. **Failed tests** - Click to expand and see: + - Full request URL and method + - Request headers (with sensitive values masked) + - Request body (JSON formatted) + - Response status code (color-coded: green/yellow/red) + - Response headers + - Full response body (JSON formatted) + - Response time + +#### Configuration Options + +```python +from berapi.contrib.pytest_plugin import create_tracking_client, configure_tracking + +# Configure global tracking behavior +configure_tracking( + track_only_failures=True, # Only show tracking on failed tests (default) + max_requests=10, # Max requests to track per test + mask_headers=["Authorization", "X-Api-Key"], +) + +@pytest.fixture +def api(): + return create_tracking_client( + base_url="https://api.example.com", + headers={"Content-Type": "application/json"}, + timeout=30.0, + mask_headers=["Authorization"], # Override global setting + max_requests=20, # Override global setting + ) +``` + +#### Track All Tests (Not Just Failures) + +```python +# conftest.py +from berapi.contrib.pytest_plugin import configure_tracking + +configure_tracking(track_only_failures=False) +``` + +### Manual Tracking + +You can also use tracking middleware manually without the pytest plugin: + +```python +from berapi import BerAPI, Settings +from berapi.middleware import TrackingMiddleware, RequestTracker + +# Create a tracker +tracker = RequestTracker( + max_requests=10, + mask_headers=["Authorization"], +) + +# Create middleware with tracker +middleware = TrackingMiddleware(tracker) + +# Create client with tracking +api = BerAPI( + Settings(base_url="https://api.example.com"), + middlewares=[middleware], +) + +# Make requests - they're automatically tracked +api.get("/users/1").assert_2xx() +api.post("/users", json={"name": "John"}).assert_2xx() + +# Access tracked data +print(f"Tracked {len(tracker)} requests") + +# Generate HTML report +html = tracker.to_html() +print(html) + +# Clear tracking +tracker.clear() +``` + +### Combining with Other Middleware + +Tracking middleware works alongside other middleware: + +```python +from berapi import BerAPI, Settings +from berapi.middleware import ( + LoggingMiddleware, + BearerAuthMiddleware, + TrackingMiddleware, + RequestTracker, +) + +tracker = RequestTracker() + +api = BerAPI( + Settings(base_url="https://api.example.com"), + middlewares=[ + LoggingMiddleware(), # Log to console + BearerAuthMiddleware(token="secret"), # Add auth header + TrackingMiddleware(tracker), # Track for reports + ], +) +``` + +--- + ## Error Handling ```python diff --git a/pyproject.toml b/pyproject.toml index f183684..8ca5c6f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "berapi" -version = "2.0.0" +version = "2.1.0" description = "A modern, scalable API testing library for Python with middleware support, structured logging, and fluent assertions" authors = ["fachrulch "] license = "MIT" diff --git a/src/berapi/__init__.py b/src/berapi/__init__.py index 58f9c59..1966853 100644 --- a/src/berapi/__init__.py +++ b/src/berapi/__init__.py @@ -33,6 +33,8 @@ LoggingMiddleware, BearerAuthMiddleware, ApiKeyMiddleware, + TrackingMiddleware, + RequestTracker, ) # Re-export exceptions @@ -54,7 +56,7 @@ RetryExhaustedError, ) -__version__ = "2.0.0" +__version__ = "2.1.0" __all__ = [ # Main client @@ -73,6 +75,8 @@ "LoggingMiddleware", "BearerAuthMiddleware", "ApiKeyMiddleware", + "TrackingMiddleware", + "RequestTracker", # Exceptions "BerAPIError", "HTTPError", diff --git a/src/berapi/contrib/__init__.py b/src/berapi/contrib/__init__.py new file mode 100644 index 0000000..5712afc --- /dev/null +++ b/src/berapi/contrib/__init__.py @@ -0,0 +1,17 @@ +"""Contrib modules for berapi integrations. + +This package contains optional integrations with external tools: +- pytest_plugin: pytest-html report integration +""" + +from berapi.contrib.pytest_plugin import ( + create_tracking_client, + get_tracker, + pytest_configure, +) + +__all__ = [ + "create_tracking_client", + "get_tracker", + "pytest_configure", +] diff --git a/src/berapi/contrib/pytest_plugin.py b/src/berapi/contrib/pytest_plugin.py new file mode 100644 index 0000000..875b752 --- /dev/null +++ b/src/berapi/contrib/pytest_plugin.py @@ -0,0 +1,242 @@ +"""pytest-html integration for berapi request/response tracking. + +This module provides pytest hooks and fixtures for automatically capturing +API requests and responses, then displaying them in pytest-html reports +when tests fail. + +Usage: + # In your conftest.py: + pytest_plugins = ["berapi.contrib.pytest_plugin"] + + @pytest.fixture + def api(): + from berapi.contrib.pytest_plugin import create_tracking_client + return create_tracking_client(base_url="https://api.example.com") + + # Or for more control: + from berapi.contrib.pytest_plugin import get_tracker, create_tracking_client + + @pytest.fixture + def api(): + client = create_tracking_client( + base_url="https://api.example.com", + mask_headers=["Authorization"], + ) + return client +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +import pytest + +from berapi import BerAPI, Settings +from berapi.middleware.tracking import RequestTracker, TrackingMiddleware + +if TYPE_CHECKING: + from berapi.middleware.base import Middleware + +# Global tracker instance - shared across all tests in a session +_global_tracker: RequestTracker | None = None +_track_only_failures: bool = True + + +def get_tracker() -> RequestTracker: + """Get the global request tracker instance. + + Returns: + The global RequestTracker instance. + """ + global _global_tracker + if _global_tracker is None: + _global_tracker = RequestTracker() + return _global_tracker + + +def create_tracking_client( + base_url: str | None = None, + headers: dict[str, str] | None = None, + timeout: float = 30.0, + mask_headers: list[str] | None = None, + max_requests: int = 10, + middlewares: list[Middleware] | None = None, + **settings_kwargs: Any, +) -> BerAPI: + """Create a BerAPI client with request/response tracking enabled. + + This client automatically tracks all requests and responses, which + will be displayed in pytest-html reports when tests fail. + + Args: + base_url: Base URL for API requests. + headers: Default headers to include in all requests. + timeout: Request timeout in seconds. + mask_headers: Header names to mask in reports (e.g., ["Authorization"]). + max_requests: Maximum number of requests to track per test. + middlewares: Additional middleware to add (tracking is added automatically). + **settings_kwargs: Additional settings passed to Settings. + + Returns: + BerAPI client with tracking enabled. + + Example: + >>> @pytest.fixture + ... def api(): + ... return create_tracking_client( + ... base_url="https://api.example.com", + ... mask_headers=["Authorization", "X-Api-Key"], + ... ) + ... + >>> def test_users(api): + ... api.get("/users").assert_2xx() + """ + global _global_tracker + + # Configure the global tracker with provided settings + if _global_tracker is None: + _global_tracker = RequestTracker( + max_requests=max_requests, + mask_headers=mask_headers, + ) + else: + # Update settings on existing tracker + _global_tracker.max_requests = max_requests + if mask_headers: + _global_tracker.mask_headers = [h.lower() for h in mask_headers] + + # Create settings + settings = Settings( + base_url=base_url, + headers=headers or {}, + timeout=timeout, + **settings_kwargs, + ) + + # Build middleware list + all_middlewares: list[Middleware] = [] + if middlewares: + all_middlewares.extend(middlewares) + all_middlewares.append(TrackingMiddleware(_global_tracker)) + + return BerAPI(settings=settings, middlewares=all_middlewares) + + +def configure_tracking( + track_only_failures: bool = True, + max_requests: int = 10, + mask_headers: list[str] | None = None, +) -> None: + """Configure global tracking settings. + + Call this in conftest.py to customize tracking behavior. + + Args: + track_only_failures: If True, only add tracking info to failed tests. + max_requests: Maximum requests to track per test. + mask_headers: Headers to mask in reports. + + Example: + >>> # In conftest.py + >>> from berapi.contrib.pytest_plugin import configure_tracking + >>> configure_tracking(track_only_failures=False, max_requests=20) + """ + global _track_only_failures, _global_tracker + _track_only_failures = track_only_failures + + if _global_tracker is None: + _global_tracker = RequestTracker( + max_requests=max_requests, + mask_headers=mask_headers, + ) + else: + _global_tracker.max_requests = max_requests + if mask_headers: + _global_tracker.mask_headers = [h.lower() for h in mask_headers] + + +# ============================================================================= +# Pytest Hooks +# ============================================================================= + + +def pytest_configure(config: pytest.Config) -> None: + """Register the plugin marker.""" + config.addinivalue_line( + "markers", + "berapi_tracking: Enable request/response tracking for this test", + ) + + +@pytest.hookimpl(tryfirst=True) +def pytest_runtest_setup(item: pytest.Item) -> None: + """Clear request tracker before each test.""" + tracker = get_tracker() + tracker.clear() + + +@pytest.hookimpl(hookwrapper=True) +def pytest_runtest_makereport(item: pytest.Item, call: pytest.CallInfo) -> Any: + """Add request/response data to HTML report.""" + outcome = yield + report = outcome.get_result() + + # Only process after test execution (not setup/teardown) + if report.when != "call": + return + + # Check if we should add tracking info + should_add = ( + (not _track_only_failures) or + (report.failed) + ) + + if not should_add: + return + + tracker = get_tracker() + if not tracker.requests: + return + + # Get or create extras list + extras = getattr(report, "extras", None) + if extras is None: + extras = getattr(report, "extra", []) + + # Generate and add HTML content + html_content = f''' +
+

+ API Requests/Responses ({len(tracker.requests)} call{'s' if len(tracker.requests) != 1 else ''}) +

+ {tracker.to_html()} +
+ ''' + extras.append(_create_html_extra(html_content)) + + # Set extras back on report + if hasattr(report, "extras"): + report.extras = extras + else: + report.extra = extras + + +def _create_html_extra(content: str) -> Any: + """Create pytest-html extra HTML content. + + Args: + content: HTML content string. + + Returns: + pytest-html extra object. + """ + try: + from pytest_html import extras + return extras.html(content) + except ImportError: + # Fallback for older pytest-html or when not installed + class HtmlExtra: + def __init__(self, content: str) -> None: + self.content = content + self.name = "html" + return HtmlExtra(content) diff --git a/src/berapi/middleware/__init__.py b/src/berapi/middleware/__init__.py index 7dc274a..c79a25c 100644 --- a/src/berapi/middleware/__init__.py +++ b/src/berapi/middleware/__init__.py @@ -11,6 +11,10 @@ BearerAuthMiddleware, ApiKeyMiddleware, ) +from berapi.middleware.tracking import ( + TrackingMiddleware, + RequestTracker, +) __all__ = [ "Middleware", @@ -20,4 +24,6 @@ "LoggingMiddleware", "BearerAuthMiddleware", "ApiKeyMiddleware", + "TrackingMiddleware", + "RequestTracker", ] diff --git a/src/berapi/middleware/tracking.py b/src/berapi/middleware/tracking.py new file mode 100644 index 0000000..ca389e3 --- /dev/null +++ b/src/berapi/middleware/tracking.py @@ -0,0 +1,289 @@ +"""Request/Response tracking middleware for debugging and reporting.""" + +from __future__ import annotations + +import json +from dataclasses import dataclass, field +from datetime import timedelta +from html import escape +from typing import Any + +from berapi.middleware.base import RequestContext, ResponseContext + + +@dataclass +class TrackedRequest: + """A tracked API request with its response.""" + + method: str + url: str + request_headers: dict[str, str] = field(default_factory=dict) + request_body: str | None = None + status_code: int | None = None + response_headers: dict[str, str] = field(default_factory=dict) + response_body: str | None = None + elapsed: timedelta | None = None + + +class RequestTracker: + """Stores and renders tracked API requests for debugging. + + This class collects request/response data and can generate HTML + reports suitable for pytest-html integration. + + Example: + >>> tracker = RequestTracker(max_requests=10) + >>> tracker.track_request("GET", "https://api.example.com/users") + >>> tracker.track_response(200, {"Content-Type": "application/json"}, '{"id": 1}') + >>> html = tracker.to_html() + """ + + def __init__( + self, + max_requests: int = 10, + mask_headers: list[str] | None = None, + ) -> None: + """Initialize the request tracker. + + Args: + max_requests: Maximum number of requests to store (oldest removed first). + mask_headers: Header names to mask (e.g., ["Authorization", "X-Api-Key"]). + """ + self.requests: list[TrackedRequest] = [] + self.max_requests = max_requests + self.mask_headers = [h.lower() for h in (mask_headers or [])] + + def track_request( + self, + method: str, + url: str, + headers: dict[str, str] | None = None, + body: Any | None = None, + ) -> None: + """Track an outgoing request. + + Args: + method: HTTP method (GET, POST, etc.). + url: Request URL. + headers: Request headers. + body: Request body (will be converted to string). + """ + masked_headers = self._mask_headers(headers or {}) + body_str = self._safe_stringify(body) + + self.requests.append(TrackedRequest( + method=method, + url=url, + request_headers=masked_headers, + request_body=body_str, + )) + + # Remove oldest if over limit + if len(self.requests) > self.max_requests: + self.requests.pop(0) + + def track_response( + self, + status_code: int, + headers: dict[str, str] | None = None, + body: Any | None = None, + elapsed: timedelta | None = None, + ) -> None: + """Track an incoming response for the most recent request. + + Args: + status_code: HTTP status code. + headers: Response headers. + body: Response body. + elapsed: Response time. + """ + if not self.requests: + return + + current = self.requests[-1] + if current.status_code is not None: + return # Already has response + + current.status_code = status_code + current.response_headers = dict(headers) if headers else {} + current.response_body = self._safe_stringify(body) + current.elapsed = elapsed + + def clear(self) -> None: + """Clear all tracked requests.""" + self.requests.clear() + + def _mask_headers(self, headers: dict[str, str]) -> dict[str, str]: + """Mask sensitive header values.""" + return { + k: "***MASKED***" if k.lower() in self.mask_headers else v + for k, v in headers.items() + } + + def _safe_stringify(self, value: Any) -> str | None: + """Safely convert value to string.""" + if value is None: + return None + if isinstance(value, bytes): + try: + return value.decode("utf-8") + except UnicodeDecodeError: + return "" + if isinstance(value, (dict, list)): + try: + return json.dumps(value, indent=2, default=str) + except (TypeError, ValueError): + return str(value) + return str(value) + + def _format_json(self, text: str | None) -> str: + """Try to format as pretty JSON.""" + if not text: + return "" + try: + parsed = json.loads(text) + return json.dumps(parsed, indent=2) + except (json.JSONDecodeError, TypeError): + return text + + def _get_status_color(self, status: int | None) -> str: + """Get color for status code.""" + if status is None: + return "#6c757d" # gray + if 200 <= status < 300: + return "#28a745" # green + if 300 <= status < 400: + return "#17a2b8" # blue + if 400 <= status < 500: + return "#ffc107" # yellow + return "#dc3545" # red + + def to_html(self) -> str: + """Generate HTML representation of tracked requests. + + Returns: + HTML string suitable for pytest-html reports. + """ + if not self.requests: + return "

No API requests tracked

" + + html_parts = [] + for i, req in enumerate(self.requests, 1): + req_body = self._format_json(req.request_body) + resp_body = self._format_json(req.response_body) + status_color = self._get_status_color(req.status_code) + elapsed_str = f"{req.elapsed.total_seconds():.3f}s" if req.elapsed else "" + + html_parts.append(f''' +
+
+ Request #{i}: + {escape(req.method)} + {escape(req.url)} + + {req.status_code or 'N/A'} + + {f'{elapsed_str}' if elapsed_str else ''} +
+
+
+ Request Headers: +
{escape(json.dumps(req.request_headers, indent=2))}
+ {f'Request Body:
{escape(req_body)}
' if req_body else ''} +
+
+ Response Headers: +
{escape(json.dumps(req.response_headers, indent=2))}
+ Response Body: +
{escape(resp_body) if resp_body else 'No body'}
+
+
+
+ ''') + + return ''.join(html_parts) + + def __len__(self) -> int: + """Return number of tracked requests.""" + return len(self.requests) + + +class TrackingMiddleware: + """Middleware that tracks requests and responses for debugging. + + Use this middleware to capture API calls for debugging, logging, + or integration with test reporting tools like pytest-html. + + Example: + >>> from berapi import BerAPI + >>> from berapi.middleware import TrackingMiddleware + >>> + >>> tracker = RequestTracker() + >>> middleware = TrackingMiddleware(tracker) + >>> api = BerAPI(middlewares=[middleware]) + >>> + >>> api.get("https://api.example.com/users").assert_2xx() + >>> print(tracker.to_html()) # Get HTML report + """ + + def __init__(self, tracker: RequestTracker | None = None) -> None: + """Initialize tracking middleware. + + Args: + tracker: RequestTracker instance to store data. + If None, creates a new tracker. + """ + self.tracker = tracker if tracker is not None else RequestTracker() + + def process_request(self, context: RequestContext) -> RequestContext: + """Track the outgoing request. + + Args: + context: Request context. + + Returns: + Unchanged request context. + """ + body = context.json_body or context.data + self.tracker.track_request( + method=context.method, + url=context.url, + headers=context.headers, + body=body, + ) + return context + + def process_response(self, context: ResponseContext) -> ResponseContext: + """Track the incoming response. + + Args: + context: Response context. + + Returns: + Unchanged response context. + """ + response = context.response + try: + body = response.json() + except Exception: + try: + body = response.text[:5000] if response.text else None + except Exception: + body = None + + self.tracker.track_response( + status_code=response.status_code, + headers=dict(response.headers), + body=body, + elapsed=response.elapsed, + ) + return context + + def on_error(self, error: Exception, context: RequestContext) -> None: + """Handle errors (no-op for tracking). + + Args: + error: The exception that occurred. + context: Request context. + """ + pass diff --git a/tests/test_tracking.py b/tests/test_tracking.py new file mode 100644 index 0000000..5a79e34 --- /dev/null +++ b/tests/test_tracking.py @@ -0,0 +1,224 @@ +"""Tests for request/response tracking middleware. + +These tests demonstrate the TrackingMiddleware and pytest-html integration. +Run with: pytest tests/test_tracking.py --html=reports/tracking_report.html +""" + +from berapi import BerAPI, Settings +from berapi.middleware import TrackingMiddleware, RequestTracker +from berapi.contrib.pytest_plugin import create_tracking_client + + +class TestRequestTracker: + """Test RequestTracker functionality.""" + + def test_track_request(self): + """Test tracking a request.""" + tracker = RequestTracker() + tracker.track_request( + method="GET", + url="https://api.example.com/users", + headers={"Accept": "application/json"}, + ) + + assert len(tracker) == 1 + assert tracker.requests[0].method == "GET" + assert tracker.requests[0].url == "https://api.example.com/users" + + def test_track_response(self): + """Test tracking a response.""" + tracker = RequestTracker() + tracker.track_request("GET", "https://api.example.com/users") + tracker.track_response( + status_code=200, + headers={"Content-Type": "application/json"}, + body={"id": 1, "name": "John"}, + ) + + assert tracker.requests[0].status_code == 200 + assert "id" in tracker.requests[0].response_body + + def test_max_requests_limit(self): + """Test that oldest requests are removed when limit is exceeded.""" + tracker = RequestTracker(max_requests=3) + + for i in range(5): + tracker.track_request("GET", f"https://api.example.com/users/{i}") + + assert len(tracker) == 3 + # Should have requests 2, 3, 4 (oldest removed) + assert "/users/2" in tracker.requests[0].url + assert "/users/4" in tracker.requests[2].url + + def test_mask_headers(self): + """Test that sensitive headers are masked.""" + tracker = RequestTracker(mask_headers=["Authorization", "X-Api-Key"]) + tracker.track_request( + method="GET", + url="https://api.example.com/users", + headers={ + "Authorization": "Bearer secret-token", + "X-Api-Key": "my-secret-key", + "Accept": "application/json", + }, + ) + + headers = tracker.requests[0].request_headers + assert headers["Authorization"] == "***MASKED***" + assert headers["X-Api-Key"] == "***MASKED***" + assert headers["Accept"] == "application/json" + + def test_clear(self): + """Test clearing tracked requests.""" + tracker = RequestTracker() + tracker.track_request("GET", "https://api.example.com/users") + tracker.track_request("POST", "https://api.example.com/users") + + assert len(tracker) == 2 + tracker.clear() + assert len(tracker) == 0 + + def test_to_html(self): + """Test HTML generation.""" + tracker = RequestTracker() + tracker.track_request("GET", "https://api.example.com/users") + tracker.track_response(200, {}, {"id": 1}) + + html = tracker.to_html() + assert "GET" in html + assert "api.example.com" in html + assert "200" in html + + def test_empty_tracker_html(self): + """Test HTML generation with no requests.""" + tracker = RequestTracker() + html = tracker.to_html() + assert "No API requests tracked" in html + + +class TestTrackingMiddleware: + """Test TrackingMiddleware functionality.""" + + def test_middleware_tracks_requests(self): + """Test that middleware tracks requests and responses.""" + tracker = RequestTracker() + middleware = TrackingMiddleware(tracker) + + api = BerAPI( + Settings(base_url="https://jsonplaceholder.typicode.com"), + middlewares=[middleware], + ) + + api.get("/users/1").assert_2xx() + + assert len(tracker) == 1 + assert tracker.requests[0].method == "GET" + assert tracker.requests[0].status_code == 200 + + def test_middleware_tracks_post_body(self): + """Test that middleware tracks POST request body.""" + tracker = RequestTracker() + middleware = TrackingMiddleware(tracker) + + api = BerAPI( + Settings(base_url="https://jsonplaceholder.typicode.com"), + middlewares=[middleware], + ) + + api.post("/posts", json={"title": "Test", "body": "Content"}).assert_2xx() + + assert len(tracker) == 1 + assert "Test" in tracker.requests[0].request_body + + def test_middleware_with_existing_middlewares(self): + """Test that tracking middleware works alongside other middlewares.""" + from berapi.middleware import LoggingMiddleware + + tracker = RequestTracker() + + api = BerAPI( + Settings(base_url="https://jsonplaceholder.typicode.com"), + middlewares=[ + LoggingMiddleware(), + TrackingMiddleware(tracker), + ], + ) + + api.get("/users/1").assert_2xx() + assert len(tracker) == 1 + + +class TestPytestPluginHelpers: + """Test pytest plugin helper functions.""" + + def test_create_tracking_client(self): + """Test create_tracking_client helper.""" + api = create_tracking_client( + base_url="https://jsonplaceholder.typicode.com", + mask_headers=["Authorization"], + ) + + response = api.get("/users/1").assert_2xx() + assert response.status_code == 200 + + def test_create_tracking_client_with_headers(self): + """Test create_tracking_client with custom headers.""" + api = create_tracking_client( + base_url="https://httpbin.org", + headers={"X-Custom": "test-value"}, + ) + + response = api.get("/headers").assert_2xx() + assert response.to_dict()["headers"]["X-Custom"] == "test-value" + + +class TestTrackingIntegration: + """Integration tests demonstrating tracking in action. + + Run these tests with pytest-html to see the tracking output: + pytest tests/test_tracking.py::TestTrackingIntegration --html=reports/tracking_report.html + + Then open the HTML report and click on any failed test to see + the full request/response details. + """ + + def test_successful_tracked_request(self): + """Successful test - tracking info available but not shown in report.""" + api = create_tracking_client( + base_url="https://jsonplaceholder.typicode.com" + ) + + (api.get("/users/1") + .assert_2xx() + .assert_json_path("name", "Leanne Graham")) + + def test_multiple_requests_tracked(self): + """Multiple requests in one test - all are tracked.""" + api = create_tracking_client( + base_url="https://jsonplaceholder.typicode.com" + ) + + # Make multiple requests + api.get("/users/1").assert_2xx() + api.get("/posts/1").assert_2xx() + api.post("/posts", json={"title": "Test"}).assert_status(201) + + # All requests are tracked + from berapi.contrib.pytest_plugin import get_tracker + tracker = get_tracker() + assert len(tracker) >= 3 + + def test_tracked_request_with_masked_headers(self): + """Test that sensitive headers are masked in reports.""" + api = create_tracking_client( + base_url="https://httpbin.org", + headers={"Authorization": "Bearer secret-token"}, + mask_headers=["Authorization"], + ) + + api.get("/headers").assert_2xx() + + from berapi.contrib.pytest_plugin import get_tracker + tracker = get_tracker() + # Authorization should be masked + assert "***MASKED***" in tracker.requests[-1].request_headers.get("Authorization", "")