http-snapshot 0.1.8
http-snapshot is a pytest plugin that snapshots requests made with popular Python HTTP clients.
Requires Python: >=3.9

# http-snapshot

`http-snapshot` is a pytest plugin that captures and snapshots HTTP requests/responses made with popular Python HTTP clients like `httpx` and `requests`. It uses [inline-snapshot](https://github.com/15r10nk/inline-snapshot) to store HTTP interactions as JSON files, enabling fast and reliable HTTP testing without making actual network calls.

## Features

- 🚀 **Support for multiple HTTP clients**: `httpx` (async, sync) and `requests` (sync)
- 📸 **Automatic HTTP interaction capture**: Records both requests and responses
- 🔒 **Security-aware**: Automatically excludes sensitive headers like authorization and cookies
- ⚙️ **Configurable**: Control what gets captured and what gets excluded
- 🧪 **pytest integration**: Works seamlessly with your existing pytest test suite
- 📁 **External snapshots**: Stores snapshots in organized JSON files

## Installation

```bash
pip install http-snapshot
```

For specific HTTP client support:

```bash
# For httpx support
pip install http-snapshot[httpx]

# For requests support
pip install http-snapshot[requests]

# For both
pip install http-snapshot[httpx,requests]
```

## Quick Start

### Using Context Managers (Recommended)

The context manager API provides proper resource cleanup and doesn't require any additional dependencies.

#### Using with httpx (async)

```python
import pytest
import inline_snapshot
from http_snapshot.httpx import HttpxAsyncSnapshotClient

@pytest.mark.anyio
@pytest.mark.parametrize(
    "http_snapshot",
    [inline_snapshot.external("uuid:my-test-snapshot.json")],
)
async def test_api_call(http_snapshot, is_recording: bool) -> None:
    async with HttpxAsyncSnapshotClient(
        snapshot=http_snapshot,
        is_recording=is_recording,
    ) as client:
        response = await client.get("https://api.example.com/users")
        assert response.status_code == 200
        assert "users" in response.json()
```

#### Using with httpx (sync)

```python
import pytest
import inline_snapshot
from http_snapshot.httpx import HttpxSyncSnapshotClient

@pytest.mark.parametrize(
    "http_snapshot",
    [inline_snapshot.external("uuid:my-test-snapshot.json")],
)
def test_api_call(http_snapshot, is_recording: bool) -> None:
    with HttpxSyncSnapshotClient(
        snapshot=http_snapshot,
        is_recording=is_recording,
    ) as client:
        response = client.get("https://api.example.com/users")
        assert response.status_code == 200
        assert "users" in response.json()
```

#### Using with requests (sync)

```python
import pytest
import inline_snapshot
from http_snapshot.requests import RequestsSnapshotSession

@pytest.mark.parametrize(
    "http_snapshot",
    [inline_snapshot.external("uuid:my-test-snapshot.json")],
)
def test_api_call(http_snapshot, is_recording: bool) -> None:
    with RequestsSnapshotSession(
        snapshot=http_snapshot,
        is_recording=is_recording,
    ) as session:
        response = session.get("https://api.example.com/users")
        assert response.status_code == 200
        assert "users" in response.json()
```

### Using with pytest fixtures (Deprecated)

> **Note**: The pytest fixture API is deprecated. Please use the context manager API shown above instead.

#### Using with httpx (async)

```python
import httpx
import pytest
import inline_snapshot

@pytest.mark.anyio
@pytest.mark.parametrize(
    "http_snapshot",
    [inline_snapshot.external("uuid:my-test-snapshot.json")],
)
async def test_api_call(snapshot_async_httpx_client: httpx.AsyncClient) -> None:
    # This will be captured on first run, replayed on subsequent runs
    response = await snapshot_async_httpx_client.get("https://api.example.com/users")
    assert response.status_code == 200
    assert "users" in response.json()
```

#### Using with httpx (sync)

```python
import httpx
import pytest
import inline_snapshot

@pytest.mark.parametrize(
    "http_snapshot",
    [inline_snapshot.external("uuid:my-test-snapshot.json")],
)
def test_api_call(snapshot_sync_httpx_client: httpx.Client) -> None:
    # This will be captured on first run, replayed on subsequent runs
    response = snapshot_sync_httpx_client.get("https://api.example.com/users")
    assert response.status_code == 200
    assert "users" in response.json()
```

#### Using with requests (sync)

```python
import requests
import pytest
import inline_snapshot

@pytest.mark.parametrize(
    "http_snapshot",
    [inline_snapshot.external("uuid:my-test-snapshot.json")],
)
def test_api_call(snapshot_requests_session: requests.Session) -> None:
    # This will be captured on first run, replayed on subsequent runs
    response = snapshot_requests_session.get("https://api.example.com/users")
    assert response.status_code == 200
    assert "users" in response.json()
```

## Migration Guide

If you're currently using the deprecated pytest fixtures, here's how to migrate to the context manager API:

### Before (using fixtures):

```python
@pytest.mark.anyio
@pytest.mark.parametrize(
    "http_snapshot",
    [inline_snapshot.external("uuid:test.json")],
)
async def test_api(snapshot_async_httpx_client: httpx.AsyncClient):
    await snapshot_async_httpx_client.get("https://example.com")
```

### After (using context managers):

```python
from http_snapshot.httpx import HttpxAsyncSnapshotClient

@pytest.mark.anyio
@pytest.mark.parametrize(
    "http_snapshot",
    [inline_snapshot.external("uuid:test.json")],
)
async def test_api(http_snapshot, is_recording: bool):
    async with HttpxAsyncSnapshotClient(
        snapshot=http_snapshot,
        is_recording=is_recording,
    ) as client:
        await client.get("https://example.com")
```

### Key differences:

1. **Import the context manager**: Instead of relying on pytest fixtures, import the context manager class
2. **Use context manager syntax**: Use `async with` (for async) or `with` (for sync)
3. **Pass parameters explicitly**: `snapshot` and `is_recording` are now constructor parameters
4. **Add `is_recording` fixture**: The `is_recording` pytest fixture is still available and works the same way
5. **No additional dependencies needed**: Unlike the fixtures which had async teardown issues, context managers work without pytest-asyncio

## How It Works

```bash
# Record new HTTP interactions (makes actual network calls and creates snapshots)
pytest tests/ --http-record --inline-snapshot=create

# Re-record and update existing snapshots (makes actual network calls and updates snapshots)
pytest tests/ --http-record --inline-snapshot=fix

# Replay existing snapshots (default - no network calls made)
pytest tests/
```

## Configuration Options

You can customize what gets captured using `SnapshotSerializerOptions`:

### Using with context managers:

```python
import pytest
import inline_snapshot
from http_snapshot.requests import RequestsSnapshotSession, SnapshotSerializerOptions

@pytest.mark.parametrize(
    "http_snapshot",
    [inline_snapshot.external("uuid:my-test-snapshot.json")],
)
def test_with_custom_options(http_snapshot, is_recording: bool) -> None:
    serializer_options = SnapshotSerializerOptions(
        exclude_request_headers=["X-API-Key"],
        include_request=True,
    )

    with RequestsSnapshotSession(
        snapshot=http_snapshot,
        is_recording=is_recording,
        serializer_options=serializer_options,
    ) as session:
        response = session.get(
            "https://api.example.com/protected",
            headers={"X-API-Key": "secret-key"}
        )
        assert response.status_code == 200
```

### Using with fixtures (deprecated):

```python
import pytest
import inline_snapshot
from http_snapshot import SnapshotSerializerOptions

@pytest.mark.parametrize(
    "http_snapshot, http_snapshot_serializer_options",
    [
        (
            inline_snapshot.external("uuid:my-test-snapshot.json"),
            SnapshotSerializerOptions(
                exclude_request_headers=["X-API-Key"],
                include_request=True,
            ),
        ),
    ],
)
def test_with_custom_options(
    snapshot_requests_session: requests.Session,
    http_snapshot_serializer_options: SnapshotSerializerOptions,
) -> None:
    response = snapshot_requests_session.get(
        "https://api.example.com/protected",
        headers={"X-API-Key": "secret-key"}
    )
    assert response.status_code == 200
```

### Available Options

- `include_request`: Whether to include request details in snapshots (default: `True`)
- `exclude_request_headers`: List of request headers to exclude from snapshots
- `exclude_response_headers`: List of response headers to exclude from snapshots

By default, the following sensitive headers are always excluded:

- **Request**: `authorization`, `cookie`
- **Response**: `set-cookie`, `www-authenticate`, `proxy-authenticate`, `authentication-info`, `proxy-authentication-info`, `transfer-encoding`, `content-encoding`

## Snapshot Format

Snapshots are stored as JSON files with the following structure:

```json
[
  {
    "request": {
      "method": "GET",
      "url": "https://api.example.com/users",
      "headers": {
        "host": "api.example.com",
        "accept": "*/*",
        "accept-encoding": "gzip, deflate",
        "connection": "keep-alive",
        "user-agent": "python-httpx/0.28.1"
      },
      "body": ""
    },
    "response": {
      "status_code": 200,
      "headers": {
        "date": "Thu, 21 Aug 2025 15:49:45 GMT",
        "content-type": "application/json; charset=utf-8",
        "connection": "keep-alive",
        "server": "nginx/1.18.0"
      },
      "body": {
        "users": [
          {
            "id": 1,
            "name": "John Doe",
            "email": "john@example.com"
          },
          {
            "id": 2,
            "name": "Jane Smith",
            "email": "jane@example.com"
          }
        ]
      }
    }
  }
]
```

### Content Encoding

The plugin intelligently handles different content types:

- **JSON**: Formatted with proper indentation for readability
- **Text**: Stored as UTF-8 strings
- **Binary**: Base64 encoded

## Advanced Examples

### Testing API with Multiple Requests

```python
from http_snapshot.httpx import HttpxAsyncSnapshotClient

@pytest.mark.anyio
@pytest.mark.parametrize(
    "http_snapshot",
    [inline_snapshot.external("uuid:multi-request-test.json")],
)
async def test_multiple_requests(http_snapshot, is_recording: bool) -> None:
    async with HttpxAsyncSnapshotClient(
        snapshot=http_snapshot,
        is_recording=is_recording,
    ) as client:
        create_response = await client.post(
            "https://api.example.com/users",
            json={"name": "Alice", "email": "alice@example.com"}
        )
        assert create_response.status_code == 201
        user_id = create_response.json()["id"]

        get_response = await client.get(
            f"https://api.example.com/users/{user_id}"
        )
        assert get_response.status_code == 200
        assert get_response.json()["name"] == "Alice"
```

### Testing with Authentication

```python
from http_snapshot.requests import RequestsSnapshotSession, SnapshotSerializerOptions

@pytest.mark.parametrize(
    "http_snapshot",
    [inline_snapshot.external("uuid:auth-test.json")],
)
def test_authenticated_request(http_snapshot, is_recording: bool) -> None:
    serializer_options = SnapshotSerializerOptions(
        exclude_request_headers=["Authorization"]
    )

    with RequestsSnapshotSession(
        snapshot=http_snapshot,
        is_recording=is_recording,
        serializer_options=serializer_options,
    ) as session:
        response = session.get(
            "https://api.example.com/profile",
            headers={"Authorization": "Bearer secret-token"}
        )
        assert response.status_code == 200
```

## Best Practices

1. **Exclude sensitive data**: Always exclude headers containing secrets, tokens, or personal data
2. **Review snapshots**: Check generated snapshot files into version control and review changes