From dfa5cde81562466db1738a38d3cf14da58edbee7 Mon Sep 17 00:00:00 2001 From: Bud Date: Wed, 1 Jul 2026 17:04:23 +0200 Subject: [PATCH 1/2] implement file server as a config flag --- optimade/server/config.py | 44 ++++ optimade/server/create_app.py | 17 ++ tests/server/routers/test_file_server.py | 280 +++++++++++++++++++++++ 3 files changed, 341 insertions(+) create mode 100644 tests/server/routers/test_file_server.py diff --git a/optimade/server/config.py b/optimade/server/config.py index 44502e272..e3ce56297 100644 --- a/optimade/server/config.py +++ b/optimade/server/config.py @@ -165,6 +165,45 @@ class GZipConfig(BaseModel): ] = 3 +class StaticFilesConfig(BaseModel): + """Configuration for serving static files.""" + + enabled: Annotated[ + bool, + Field(description="Enable serving static files from a directory."), + ] = False + + directory: Annotated[ + Path | None, + Field( + description="Absolute path to the directory containing static files to serve.", + examples=["/path/to/static"], + ), + ] = None + + route: Annotated[ + str, + Field( + description="URL route prefix for serving static files (e.g., '/static').", + examples=["/static", "/assets"], + ), + ] = "/static" + + @field_validator("directory") + @classmethod + def validate_directory(cls, value: Path | None) -> Path | None: + """Validate that the directory exists and is accessible.""" + if value is not None: + path = Path(value) + if not path.exists(): + raise ValueError(f"Static files directory does not exist: {path}") + if not path.is_dir(): + raise ValueError(f"Static files path is not a directory: {path}") + if not os.access(path, os.R_OK): + raise ValueError(f"Static files directory is not readable: {path}") + return value + + class ServerConfig(BaseSettings): """This class stores server config parameters in a way that can be easily extended for new config file types. @@ -225,6 +264,11 @@ class ServerConfig(BaseSettings): Field(description="Configuration options for GZip compression."), ] = GZipConfig() + static_files: Annotated[ + StaticFilesConfig, + Field(description="Configuration options for serving static files."), + ] = StaticFilesConfig() + use_real_mongo: Annotated[ bool | None, Field(description="DEPRECATED: force usage of MongoDB over any other backend."), diff --git a/optimade/server/create_app.py b/optimade/server/create_app.py index 756e9ced5..dc59b0cb9 100644 --- a/optimade/server/create_app.py +++ b/optimade/server/create_app.py @@ -288,6 +288,23 @@ async def set_context(request, call_next): compresslevel=config.gzip.compresslevel, ) + # Mount static files + if config.static_files.enabled and config.static_files.directory: + static_dir = config.static_files.directory + if ( + static_dir.exists() + and static_dir.is_dir() + and os.access(static_dir, os.R_OK) + ): + # Import here to avoid circular imports if needed + from fastapi.staticfiles import StaticFiles + + app.mount( + config.static_files.route, + StaticFiles(directory=str(static_dir)), + name="static", + ) + # Add exception handlers for exception, handler in OPTIMADE_EXCEPTIONS: app.add_exception_handler(exception, handler) diff --git a/tests/server/routers/test_file_server.py b/tests/server/routers/test_file_server.py new file mode 100644 index 000000000..4fbef7059 --- /dev/null +++ b/tests/server/routers/test_file_server.py @@ -0,0 +1,280 @@ +import os +import tempfile +from pathlib import Path + +import pytest +from fastapi.testclient import TestClient + +from optimade.server.config import ServerConfig, StaticFilesConfig +from optimade.server.main import create_app + + +def test_default_behavior_disabled(): + """Test that static files are disabled by default.""" + app = create_app() # No config + client = TestClient(app) + + # Static files should not be served + response = client.get("/static/test.txt") + assert response.status_code == 404 + + # API should still work + response = client.get("/info") + assert response.status_code == 200 + + +def test_static_files_served_correctly(): + """Test that static files are served correctly when enabled.""" + with tempfile.TemporaryDirectory() as tmp_dir: + static_path = Path(tmp_dir) + + # Create test files + (static_path / "test.txt").write_text("Hello, World!") + (static_path / "style.css").write_text("body { color: red; }") + + # Create subdirectory with file + subdir = static_path / "images" + subdir.mkdir() + (subdir / "logo.png").write_text("fake png content") + + # Configure and create app + config = ServerConfig( + static_files=StaticFilesConfig( + enabled=True, directory=static_path, route="/static" + ) + ) + app = create_app(config=config) + client = TestClient(app) + + # Test serving files + response = client.get("/static/test.txt") + assert response.status_code == 200 + assert response.text == "Hello, World!" + assert "text/plain" in response.headers["content-type"] + + response = client.get("/static/style.css") + assert response.status_code == 200 + assert response.text == "body { color: red; }" + assert "text/css" in response.headers["content-type"] + + # Test serving from subdirectory + response = client.get("/static/images/logo.png") + assert response.status_code == 200 + assert response.text == "fake png content" + + # Test non-existent file + response = client.get("/static/nonexistent.txt") + assert response.status_code == 404 + + # Test directory listing (should be disabled) + response = client.get("/static/") + assert response.status_code == 404 + + +def test_custom_route(): + """Test that static files work with a custom route.""" + with tempfile.TemporaryDirectory() as tmp_dir: + static_path = Path(tmp_dir) + (static_path / "test.txt").write_text("Custom route test") + + config = ServerConfig( + static_files=StaticFilesConfig( + enabled=True, directory=static_path, route="/assets" + ) + ) + app = create_app(config=config) + client = TestClient(app) + + # File should be served on custom route + response = client.get("/assets/test.txt") + assert response.status_code == 200 + assert response.text == "Custom route test" + + # File should NOT be served on default route + response = client.get("/static/test.txt") + assert response.status_code == 404 + + +def test_static_files_with_api_routes(): + """Test that static files don't interfere with API routes.""" + with tempfile.TemporaryDirectory() as tmp_dir: + static_path = Path(tmp_dir) + + # Create a file that might conflict with API routes + (static_path / "info").mkdir() + (static_path / "info" / "test.txt").write_text("Static info") + + config = ServerConfig( + static_files=StaticFilesConfig( + enabled=True, directory=static_path, route="/static" + ) + ) + app = create_app(config=config) + client = TestClient(app) + + # API endpoints should still work + response = client.get("/info") + assert response.status_code == 200 + assert "data" in response.json() + + # Static files should work too + response = client.get("/static/info/test.txt") + assert response.status_code == 200 + assert response.text == "Static info" + + +def test_static_files_with_gzip(): + """Test that static files work with gzip middleware.""" + with tempfile.TemporaryDirectory() as tmp_dir: + static_path = Path(tmp_dir) + + # Create a large file to test gzip + (static_path / "large.txt").write_text("x" * 1000) + (static_path / "small.txt").write_text("Small file") + + config = ServerConfig( + static_files=StaticFilesConfig( + enabled=True, directory=static_path, route="/static" + ), + gzip={"enabled": True, "minimum_size": 100}, + ) + app = create_app(config=config) + client = TestClient(app) + + # Large file should work (may or may not be gzipped) + response = client.get("/static/large.txt") + assert response.status_code == 200 + assert len(response.content) == 1000 + + # Small file should work + response = client.get("/static/small.txt") + assert response.status_code == 200 + assert response.text == "Small file" + + +def test_directory_must_exist(): + """Test that providing a non-existent directory raises an error.""" + with pytest.raises(ValueError, match="Static files directory does not exist"): + StaticFilesConfig( + enabled=True, + directory=Path("/tmp/definitely/does/not/exist/12345"), + route="/static", + ) + + +def test_config_from_environment(monkeypatch): + """Test that static files can be configured via environment variables.""" + with tempfile.TemporaryDirectory() as tmp_dir: + monkeypatch.delenv("OPTIMADE_CONFIG_FILE", raising=False) + + monkeypatch.setenv("OPTIMADE_STATIC_FILES__ENABLED", "true") + monkeypatch.setenv("OPTIMADE_STATIC_FILES__DIRECTORY", tmp_dir) + monkeypatch.setenv("OPTIMADE_STATIC_FILES__ROUTE", "/assets") + + # Load config from environment + config = ServerConfig() + + # Create a config file instead + config_file = Path(tmp_dir) / "config.json" + config_file.write_text(f""" + {{ + "static_files": {{ + "enabled": true, + "directory": "{tmp_dir}", + "route": "/assets" + }} + }} + """) + + # Set the config file env var + monkeypatch.setenv("OPTIMADE_CONFIG_FILE", str(config_file)) + + # Reload config from file + config = ServerConfig() + + assert config.static_files.enabled is True + assert config.static_files.directory == Path(tmp_dir) + assert config.static_files.route == "/assets" + + # Verify it actually works + (Path(tmp_dir) / "test.txt").write_text("Hello from env") + app = create_app(config=config) + client = TestClient(app) + response = client.get("/assets/test.txt") + assert response.status_code == 200 + assert response.text == "Hello from env" + + +def test_config_from_file(tmp_path): + """Test that static files can be configured via a config file.""" + with tempfile.TemporaryDirectory() as tmp_dir: + # Create config file + config_file = tmp_path / "config.json" + config_file.write_text(f""" + {{ + "static_files": {{ + "enabled": true, + "directory": "{tmp_dir}", + "route": "/assets" + }} + }} + """) + + # Set environment to use config file + os.environ["OPTIMADE_CONFIG_FILE"] = str(config_file) + + # Load config + config = ServerConfig() + + assert config.static_files.enabled is True + assert config.static_files.directory == Path(tmp_dir) + assert config.static_files.route == "/assets" + + # Verify it works + (Path(tmp_dir) / "test.txt").write_text("Hello from config file") + app = create_app(config=config) + client = TestClient(app) + response = client.get("/assets/test.txt") + assert response.status_code == 200 + assert response.text == "Hello from config file" + + +def test_no_reach_outside(): + """Test that files outside of the current route are not accessible.""" + # For some reason i need this - unclear why... + if "OPTIMADE_CONFIG_FILE" in os.environ: + del os.environ["OPTIMADE_CONFIG_FILE"] + + with tempfile.TemporaryDirectory() as tmp_dir: + static_path = Path(tmp_dir) / "static" + outside_path = Path(tmp_dir) / "outside" + + static_path.mkdir() + outside_path.mkdir() + + (static_path / "test.txt").write_text("Inside static dir") + (outside_path / "secret.txt").write_text("This should not be accessible") + + config = ServerConfig( + static_files=StaticFilesConfig( + enabled=True, directory=static_path, route="/assets" + ) + ) + app = create_app(config=config) + client = TestClient(app) + + # File inside static directory should work + response = client.get("/assets/test.txt") + assert response.status_code == 200 + assert response.text == "Inside static dir" + + # File outside static directory should NOT be accessible + response = client.get("/assets/../outside/secret.txt") + assert response.status_code in [404, 403] + + response = client.get("/assets/../../outside/secret.txt") + assert response.status_code in [404, 403] + + # URL encoded traversal + response = client.get("/assets/%2e%2e/outside/secret.txt") + assert response.status_code in [404, 403] From 01dba5ff998389de843e9cf5b46b83f511419d67 Mon Sep 17 00:00:00 2001 From: Bud Date: Thu, 2 Jul 2026 09:45:32 +0200 Subject: [PATCH 2/2] fix tests leaking --- optimade/server/create_app.py | 1 - tests/server/routers/test_file_server.py | 436 ++++++++++++----------- 2 files changed, 237 insertions(+), 200 deletions(-) diff --git a/optimade/server/create_app.py b/optimade/server/create_app.py index dc59b0cb9..bc14ca831 100644 --- a/optimade/server/create_app.py +++ b/optimade/server/create_app.py @@ -296,7 +296,6 @@ async def set_context(request, call_next): and static_dir.is_dir() and os.access(static_dir, os.R_OK) ): - # Import here to avoid circular imports if needed from fastapi.staticfiles import StaticFiles app.mount( diff --git a/tests/server/routers/test_file_server.py b/tests/server/routers/test_file_server.py index 4fbef7059..6fc3430ec 100644 --- a/tests/server/routers/test_file_server.py +++ b/tests/server/routers/test_file_server.py @@ -9,272 +9,310 @@ from optimade.server.main import create_app -def test_default_behavior_disabled(): - """Test that static files are disabled by default.""" - app = create_app() # No config - client = TestClient(app) - - # Static files should not be served - response = client.get("/static/test.txt") - assert response.status_code == 404 - - # API should still work - response = client.get("/info") - assert response.status_code == 200 - - -def test_static_files_served_correctly(): - """Test that static files are served correctly when enabled.""" - with tempfile.TemporaryDirectory() as tmp_dir: - static_path = Path(tmp_dir) - - # Create test files - (static_path / "test.txt").write_text("Hello, World!") - (static_path / "style.css").write_text("body { color: red; }") - - # Create subdirectory with file - subdir = static_path / "images" - subdir.mkdir() - (subdir / "logo.png").write_text("fake png content") - - # Configure and create app - config = ServerConfig( - static_files=StaticFilesConfig( - enabled=True, directory=static_path, route="/static" - ) - ) - app = create_app(config=config) +class TestStaticFilesDefault: + """Tests for default static files behavior (disabled).""" + + def test_default_behavior_disabled(self): + """Test that static files are disabled by default.""" + app = create_app() client = TestClient(app) - # Test serving files response = client.get("/static/test.txt") + assert response.status_code == 404 + + response = client.get("/info") + assert response.status_code == 200 + + +class TestStaticFilesServing: + """Tests for serving static files.""" + + @pytest.fixture + def static_app(self): + """Create an app with static files enabled.""" + with tempfile.TemporaryDirectory() as tmp_dir: + static_path = Path(tmp_dir) + + # Create test files + (static_path / "test.txt").write_text("Hello, World!") + (static_path / "style.css").write_text("body { color: red; }") + + subdir = static_path / "images" + subdir.mkdir() + (subdir / "logo.png").write_text("fake png content") + + config = ServerConfig( + static_files=StaticFilesConfig( + enabled=True, directory=static_path, route="/static" + ) + ) + app = create_app(config=config) + yield app, static_path + + @pytest.fixture + def static_client(self, static_app): + """Create a test client with static files enabled.""" + app, _ = static_app + return TestClient(app) + + def test_serve_text_file(self, static_client): + """Test serving a text file.""" + response = static_client.get("/static/test.txt") assert response.status_code == 200 assert response.text == "Hello, World!" assert "text/plain" in response.headers["content-type"] - response = client.get("/static/style.css") + def test_serve_css_file(self, static_client): + """Test serving a CSS file.""" + response = static_client.get("/static/style.css") assert response.status_code == 200 assert response.text == "body { color: red; }" assert "text/css" in response.headers["content-type"] - # Test serving from subdirectory - response = client.get("/static/images/logo.png") + def test_serve_file_in_subdirectory(self, static_client): + """Test serving a file from a subdirectory.""" + response = static_client.get("/static/images/logo.png") assert response.status_code == 200 assert response.text == "fake png content" - # Test non-existent file - response = client.get("/static/nonexistent.txt") + def test_nonexistent_file_returns_404(self, static_client): + """Test that non-existent file returns 404.""" + response = static_client.get("/static/nonexistent.txt") assert response.status_code == 404 - # Test directory listing (should be disabled) - response = client.get("/static/") + def test_directory_listing_disabled(self, static_client): + """Test that directory listing is disabled.""" + response = static_client.get("/static/") assert response.status_code == 404 -def test_custom_route(): - """Test that static files work with a custom route.""" - with tempfile.TemporaryDirectory() as tmp_dir: - static_path = Path(tmp_dir) - (static_path / "test.txt").write_text("Custom route test") +class TestStaticFilesCustomRoute: + """Tests for static files with custom routes.""" - config = ServerConfig( - static_files=StaticFilesConfig( - enabled=True, directory=static_path, route="/assets" - ) - ) - app = create_app(config=config) - client = TestClient(app) + @pytest.fixture + def custom_route_app(self): + """Create an app with static files on a custom route.""" + with tempfile.TemporaryDirectory() as tmp_dir: + static_path = Path(tmp_dir) + (static_path / "test.txt").write_text("Custom route test") - # File should be served on custom route - response = client.get("/assets/test.txt") + config = ServerConfig( + static_files=StaticFilesConfig( + enabled=True, directory=static_path, route="/assets" + ) + ) + app = create_app(config=config) + yield app, static_path + + @pytest.fixture + def custom_route_client(self, custom_route_app): + """Create a test client with custom route.""" + app, _ = custom_route_app + return TestClient(app) + + def test_custom_route_works(self, custom_route_client): + """Test that files are served on the custom route.""" + response = custom_route_client.get("/assets/test.txt") assert response.status_code == 200 assert response.text == "Custom route test" - # File should NOT be served on default route - response = client.get("/static/test.txt") + def test_default_route_not_used(self, custom_route_client): + """Test that files are not served on the default route.""" + response = custom_route_client.get("/static/test.txt") assert response.status_code == 404 -def test_static_files_with_api_routes(): - """Test that static files don't interfere with API routes.""" - with tempfile.TemporaryDirectory() as tmp_dir: - static_path = Path(tmp_dir) +class TestStaticFilesWithAPI: + """Tests that static files don't interfere with API routes.""" - # Create a file that might conflict with API routes - (static_path / "info").mkdir() - (static_path / "info" / "test.txt").write_text("Static info") + @pytest.fixture + def app_with_static_and_api(self): + """Create an app with static files and API routes.""" + with tempfile.TemporaryDirectory() as tmp_dir: + static_path = Path(tmp_dir) - config = ServerConfig( - static_files=StaticFilesConfig( - enabled=True, directory=static_path, route="/static" - ) - ) - app = create_app(config=config) - client = TestClient(app) + # Create a file that might conflict with API routes + (static_path / "info").mkdir() + (static_path / "info" / "test.txt").write_text("Static info") - # API endpoints should still work - response = client.get("/info") + config = ServerConfig( + static_files=StaticFilesConfig( + enabled=True, directory=static_path, route="/static" + ) + ) + app = create_app(config=config) + yield app, static_path + + @pytest.fixture + def client_with_static_and_api(self, app_with_static_and_api): + """Create a test client.""" + app, _ = app_with_static_and_api + return TestClient(app) + + def test_api_routes_still_work(self, client_with_static_and_api): + """Test that API endpoints still work.""" + response = client_with_static_and_api.get("/info") assert response.status_code == 200 assert "data" in response.json() - # Static files should work too - response = client.get("/static/info/test.txt") + def test_static_routes_still_work(self, client_with_static_and_api): + """Test that static endpoints still work.""" + response = client_with_static_and_api.get("/static/info/test.txt") assert response.status_code == 200 assert response.text == "Static info" -def test_static_files_with_gzip(): - """Test that static files work with gzip middleware.""" - with tempfile.TemporaryDirectory() as tmp_dir: - static_path = Path(tmp_dir) +class TestStaticFilesWithGzip: + """Tests for static files with gzip middleware.""" - # Create a large file to test gzip - (static_path / "large.txt").write_text("x" * 1000) - (static_path / "small.txt").write_text("Small file") + @pytest.fixture + def app_with_gzip(self): + """Create an app with static files and gzip enabled.""" + with tempfile.TemporaryDirectory() as tmp_dir: + static_path = Path(tmp_dir) - config = ServerConfig( - static_files=StaticFilesConfig( - enabled=True, directory=static_path, route="/static" - ), - gzip={"enabled": True, "minimum_size": 100}, - ) - app = create_app(config=config) - client = TestClient(app) + (static_path / "large.txt").write_text("x" * 1000) + (static_path / "small.txt").write_text("Small file") - # Large file should work (may or may not be gzipped) - response = client.get("/static/large.txt") + config = ServerConfig( + static_files=StaticFilesConfig( + enabled=True, directory=static_path, route="/static" + ), + gzip={"enabled": True, "minimum_size": 100}, + ) + app = create_app(config=config) + yield app, static_path + + @pytest.fixture + def client_with_gzip(self, app_with_gzip): + """Create a test client with gzip.""" + app, _ = app_with_gzip + return TestClient(app) + + def test_large_file_with_gzip(self, client_with_gzip): + """Test serving a large file with gzip.""" + response = client_with_gzip.get("/static/large.txt") assert response.status_code == 200 assert len(response.content) == 1000 - # Small file should work - response = client.get("/static/small.txt") + def test_small_file_with_gzip(self, client_with_gzip): + """Test serving a small file with gzip.""" + response = client_with_gzip.get("/static/small.txt") assert response.status_code == 200 assert response.text == "Small file" -def test_directory_must_exist(): - """Test that providing a non-existent directory raises an error.""" - with pytest.raises(ValueError, match="Static files directory does not exist"): - StaticFilesConfig( - enabled=True, - directory=Path("/tmp/definitely/does/not/exist/12345"), - route="/static", - ) - - -def test_config_from_environment(monkeypatch): - """Test that static files can be configured via environment variables.""" - with tempfile.TemporaryDirectory() as tmp_dir: - monkeypatch.delenv("OPTIMADE_CONFIG_FILE", raising=False) - - monkeypatch.setenv("OPTIMADE_STATIC_FILES__ENABLED", "true") - monkeypatch.setenv("OPTIMADE_STATIC_FILES__DIRECTORY", tmp_dir) - monkeypatch.setenv("OPTIMADE_STATIC_FILES__ROUTE", "/assets") - - # Load config from environment - config = ServerConfig() - - # Create a config file instead - config_file = Path(tmp_dir) / "config.json" - config_file.write_text(f""" - {{ - "static_files": {{ - "enabled": true, - "directory": "{tmp_dir}", - "route": "/assets" - }} - }} - """) - - # Set the config file env var - monkeypatch.setenv("OPTIMADE_CONFIG_FILE", str(config_file)) +class TestStaticFilesValidation: + """Tests for static files validation.""" - # Reload config from file - config = ServerConfig() + def test_directory_must_exist(self): + """Test that providing a non-existent directory raises an error.""" + with pytest.raises(ValueError, match="Static files directory does not exist"): + StaticFilesConfig( + enabled=True, + directory=Path("/tmp/definitely/does/not/exist/12345"), + route="/static", + ) - assert config.static_files.enabled is True - assert config.static_files.directory == Path(tmp_dir) - assert config.static_files.route == "/assets" - # Verify it actually works - (Path(tmp_dir) / "test.txt").write_text("Hello from env") - app = create_app(config=config) - client = TestClient(app) - response = client.get("/assets/test.txt") - assert response.status_code == 200 - assert response.text == "Hello from env" - - -def test_config_from_file(tmp_path): - """Test that static files can be configured via a config file.""" - with tempfile.TemporaryDirectory() as tmp_dir: - # Create config file - config_file = tmp_path / "config.json" - config_file.write_text(f""" - {{ - "static_files": {{ - "enabled": true, - "directory": "{tmp_dir}", - "route": "/assets" +class TestStaticFilesConfiguration: + """Tests for static files configuration sources.""" + + @pytest.fixture(autouse=True) + def clean_env(self): + """Clean up environment variables before and after each test.""" + original_config = os.environ.get("OPTIMADE_CONFIG_FILE") + if "OPTIMADE_CONFIG_FILE" in os.environ: + del os.environ["OPTIMADE_CONFIG_FILE"] + yield + if original_config is not None: + os.environ["OPTIMADE_CONFIG_FILE"] = original_config + + def test_config_from_file(self, tmp_path, monkeypatch): + """Test that static files can be configured via a config file.""" + with tempfile.TemporaryDirectory() as tmp_dir: + config_file = tmp_path / "config.json" + config_file.write_text(f""" + {{ + "static_files": {{ + "enabled": true, + "directory": "{tmp_dir}", + "route": "/assets" + }} }} - }} - """) + """) - # Set environment to use config file - os.environ["OPTIMADE_CONFIG_FILE"] = str(config_file) + monkeypatch.setenv("OPTIMADE_CONFIG_FILE", str(config_file)) - # Load config - config = ServerConfig() + config = ServerConfig() - assert config.static_files.enabled is True - assert config.static_files.directory == Path(tmp_dir) - assert config.static_files.route == "/assets" + assert config.static_files.enabled is True + assert config.static_files.directory == Path(tmp_dir) + assert config.static_files.route == "/assets" - # Verify it works - (Path(tmp_dir) / "test.txt").write_text("Hello from config file") - app = create_app(config=config) - client = TestClient(app) - response = client.get("/assets/test.txt") - assert response.status_code == 200 - assert response.text == "Hello from config file" + def test_config_directly_in_python(self): + """Test that static files can be configured directly in Python.""" + with tempfile.TemporaryDirectory() as tmp_dir: + config = ServerConfig( + static_files=StaticFilesConfig( + enabled=True, + directory=Path(tmp_dir), + route="/custom", + ) + ) + assert config.static_files.enabled is True + assert config.static_files.directory == Path(tmp_dir) + assert config.static_files.route == "/custom" -def test_no_reach_outside(): - """Test that files outside of the current route are not accessible.""" - # For some reason i need this - unclear why... - if "OPTIMADE_CONFIG_FILE" in os.environ: - del os.environ["OPTIMADE_CONFIG_FILE"] - with tempfile.TemporaryDirectory() as tmp_dir: - static_path = Path(tmp_dir) / "static" - outside_path = Path(tmp_dir) / "outside" +class TestStaticFilesSecurity: + """Security tests for static files.""" - static_path.mkdir() - outside_path.mkdir() + @pytest.fixture + def security_app(self): + """Create an app with security testing setup.""" + with tempfile.TemporaryDirectory() as tmp_dir: + static_path = Path(tmp_dir) / "static" + outside_path = Path(tmp_dir) / "outside" - (static_path / "test.txt").write_text("Inside static dir") - (outside_path / "secret.txt").write_text("This should not be accessible") + static_path.mkdir() + outside_path.mkdir() - config = ServerConfig( - static_files=StaticFilesConfig( - enabled=True, directory=static_path, route="/assets" - ) - ) - app = create_app(config=config) - client = TestClient(app) + (static_path / "test.txt").write_text("Inside static dir") + (outside_path / "secret.txt").write_text("This should not be accessible") - # File inside static directory should work - response = client.get("/assets/test.txt") + config = ServerConfig( + static_files=StaticFilesConfig( + enabled=True, directory=static_path, route="/assets" + ) + ) + app = create_app(config=config) + yield app, static_path, outside_path + + @pytest.fixture + def security_client(self, security_app): + """Create a test client for security tests.""" + app, _, _ = security_app + return TestClient(app) + + def test_file_inside_mounted_directory_accessible(self, security_client): + """Test that files inside the mounted directory are accessible.""" + response = security_client.get("/assets/test.txt") assert response.status_code == 200 assert response.text == "Inside static dir" - # File outside static directory should NOT be accessible - response = client.get("/assets/../outside/secret.txt") + def test_file_outside_mounted_directory_not_accessible(self, security_client): + """Test that files outside the mounted directory are not accessible.""" + response = security_client.get("/assets/../outside/secret.txt") assert response.status_code in [404, 403] - response = client.get("/assets/../../outside/secret.txt") + def test_double_dot_traversal_blocked(self, security_client): + """Test that double dot path traversal is blocked.""" + response = security_client.get("/assets/../../outside/secret.txt") assert response.status_code in [404, 403] - # URL encoded traversal - response = client.get("/assets/%2e%2e/outside/secret.txt") + def test_url_encoded_traversal_blocked(self, security_client): + """Test that URL encoded path traversal is blocked.""" + response = security_client.get("/assets/%2e%2e/outside/secret.txt") assert response.status_code in [404, 403]