# HTTP Test Server Fixture The HTTP test server fixture provides a way to test conda functionality that requires serving files over HTTP, such as: - Mock conda channels with packages - Remote environment files (`environment.yml`) - Remote configuration files - Any scenario where conda needs to fetch files from a URL ## Overview The `http_test_server` fixture starts a local HTTP server that serves files from a directory. The server runs on a random port and supports both IPv4 and IPv6. The fixture can be used in two ways: 1. **Without `@pytest.mark.parametrize`** - Use a temporary directory that you populate dynamically 2. **With `@pytest.mark.parametrize`** - Serve files from a pre-existing directory ```{tip} For proper type hints, import `HttpTestServerFixture` from `conda.testing.fixtures` under `TYPE_CHECKING`. See the [complete example](#complete-example-testing-a-mock-channel) for the full import pattern. ``` ## Basic Usage ### Dynamic Content (No Marker) The simplest usage - no marker needed. The server automatically uses a temporary directory that you can populate: ```python def test_dynamic_repodata(http_test_server: HttpTestServerFixture): """Create content on the fly - no setup needed.""" # Populate files directly in the server's directory (http_test_server.directory / "repodata.json").write_text('{"packages": {}}') # Make request response = requests.get(http_test_server.get_url("repodata.json")) assert response.status_code == 200 assert response.json() == {"packages": {}} ``` This pattern is ideal for: - Creating mock repodata files - Testing with minimal setup - Extending and creating your own fixtures programmatically ### Pre-existing Directory (With Parametrize) Use `@pytest.mark.parametrize()` with `indirect=True` when you have test data already prepared: ```python @pytest.mark.parametrize( "http_test_server", ["tests/data/mock-channel"], indirect=True, ) def test_fetch_from_channel(http_test_server: HttpTestServerFixture): # Server serves files from tests/data/mock-channel/ repodata_url = http_test_server.get_url("linux-64/repodata.json") response = requests.get(repodata_url) assert response.status_code == 200 ``` The `indirect=True` parameter tells pytest to pass the directory path to the fixture rather than directly to the test function. This pattern is ideal for: - Complex directory structures - Sharing test data across multiple tests - Binary files (packages, archives) - Large test datasets ## Testing Multiple Directories One of the benefits of using `@pytest.mark.parametrize` is that you can easily test the same logic against multiple directories: ```python @pytest.mark.parametrize( "http_test_server", [ "tests/data/channel1", "tests/data/channel2", "tests/data/channel3", ], indirect=True, ) def test_multiple_channels(http_test_server: HttpTestServerFixture): # This test runs three times, once for each channel directory response = requests.get(http_test_server.get_url("repodata.json")) assert response.status_code == 200 assert "packages" in response.json() ``` Each test run will use a different directory, making it easy to verify behavior across multiple datasets. You can also mix pre-existing directories with dynamic content by using `None`: ```python @pytest.mark.parametrize( "http_test_server", [ "tests/data/channel1", None, "tests/data/channel2", ], indirect=True, ) def test_mixed_sources(http_test_server: HttpTestServerFixture): # Runs 3 times: channel1, dynamic tmp dir, channel2 # When None, http_test_server.directory is a fresh temporary directory ... ``` ## Complete Example: Testing a Mock Channel Here's a full example with all imports showing dynamic content generation: ```python from __future__ import annotations import json from pathlib import Path from typing import TYPE_CHECKING import pytest import requests if TYPE_CHECKING: from conda.testing.fixtures import CondaCLIFixture, HttpTestServerFixture def test_install_from_mock_channel( http_test_server: HttpTestServerFixture, conda_cli: CondaCLIFixture, tmp_path: Path, ): """Test installing from a dynamically created mock channel.""" # Create channel structure on the fly noarch = http_test_server.directory / "noarch" noarch.mkdir() # Create minimal repodata repodata = {"packages": {}, "packages.conda": {}, "repodata_version": 1} (noarch / "repodata.json").write_text(json.dumps(repodata)) # Use the channel channel_url = http_test_server.url stdout, stderr, code = conda_cli( "search", f"--channel={channel_url}", "--override-channels", "*", ) # Verify it worked (no packages found but channel was accessible) assert code == 0 @pytest.mark.parametrize( "http_test_server", ["tests/data/mock-channel"], # Assume the following structure: # tests/data/mock-channel/ # ├── noarch/ # │ └── repodata.json # └── linux-64/ # ├── repodata.json # └── example-pkg-1.0.0-0.tar.bz2 indirect=True, ) def test_install_from_preexisting_channel( http_test_server: HttpTestServerFixture, conda_cli: CondaCLIFixture, tmp_path: Path, ): """Test installing from pre-existing mock channel.""" channel_url = http_test_server.url stdout, stderr, code = conda_cli( "create", f"--prefix={tmp_path}", f"--channel={channel_url}", "example-pkg", "--yes", ) assert code == 0 assert (tmp_path / "conda-meta" / "example-pkg-1.0.0-0.json").exists() ``` ## Fixture API Reference ### HttpTestServerFixture The fixture returns an instance with these attributes and methods: #### Attributes: - `server: http.server.ThreadingHTTPServer` - The underlying server instance - `host: str` - Server host (usually `127.0.0.1`) - `port: int` - Server port (random) - `url: str` - Base URL (e.g., `http://127.0.0.1:54321`) - `directory: Path` - The directory being served (writable, use to populate content) #### Methods: - `get_url(path: str = "") -> str` - Get full URL for a path - Example: `get_url("linux-64/repodata.json")` → `"http://127.0.0.1:54321/linux-64/repodata.json"` #### Using the `directory` attribute: ```python def test_dynamic_files(http_test_server: HttpTestServerFixture): # Write files directly to the served directory (http_test_server.directory / "file.txt").write_text("content") # Create subdirectories subdir = http_test_server.directory / "subdir" subdir.mkdir() (subdir / "nested.json").write_text('{"key": "value"}') # Files are immediately accessible via HTTP response = requests.get(http_test_server.get_url("subdir/nested.json")) assert response.json() == {"key": "value"} ``` ## Use in Downstream Projects The HTTP test server fixture is part of the `conda.testing` module and can be used by downstream projects: ```python # In your project's conftest.py pytest_plugins = "conda.testing.fixtures" ``` Then use it in your tests: ```python @pytest.mark.parametrize("http_test_server", ["tests/my-mock-channel"], indirect=True) def test_with_mock_channel(http_test_server: HttpTestServerFixture): channel_url = http_test_server.url # ... your test code ... ``` ## Troubleshooting ### "ValueError: Directory does not exist" - This error occurs when using `@pytest.mark.parametrize()` with an invalid path - Check that the directory path provided in parametrize exists - Use absolute paths or paths relative to the repository root - Use `Path(__file__).parent / "data"` if needed - Or omit the parametrize decorator entirely to use a temporary directory ### "ValueError: Path is not a directory" - This error occurs when the parametrize value points to a file instead of a directory - Ensure the path in `@pytest.mark.parametrize(..., indirect=True)` points to a directory - Or use the fixture without parametrize for dynamic content ### Address already in use - The fixture uses random ports, so this is rare - If it happens, the test will likely fail and retry automatically ### Server not shutting down cleanly - This is handled automatically by the fixture - The server runs on a daemon thread and will be cleaned up when tests finish ### Files not appearing in HTTP responses - Make sure files are written before making the HTTP request - Check that file paths don't have leading slashes when using `get_url()` - Verify the directory structure with `list(http_test_server.directory.iterdir())` ## Tips and Best Practices 1. **Prefer dynamic content**: Use the fixture without parametrize (dynamic content) for simple use cases. It's simpler and doesn't require maintaining test data files. 2. **Use parametrize for complex data**: Use `@pytest.mark.parametrize(..., indirect=True)` when you have complex directory structures, binary files, or data shared across many tests. 3. **Function scope for isolation**: Each test gets its own temporary directory with the `http_test_server` fixture (function scope), providing complete isolation. 4. **Organize test data**: When using parametrize, keep mock channel data in dedicated directories like `tests/data/mock-channels/` with README files explaining the structure. 5. **Test error scenarios**: Use dynamic content to easily test edge cases like malformed repodata, missing packages, or network timeouts. 6. **Cleanup is automatic**: The fixture handles cleanup automatically - no need to manually shut down servers or delete temporary files. ## Examples from conda Test Suite See these files for real-world usage examples: - `tests/testing/test_http_test_server.py` - Tests for the fixture itself - `tests/env/test_create.py::test_create_update_remote_env_file` - Using remote environment files - `tests/gateways/test_connection.py` - Connection and download testing