"""Tests for --root-path URL prefix support.""" import argparse from collections.abc import Generator from pathlib import Path import pytest from httpx import ASGITransport, AsyncClient import main @pytest.fixture def args_directory_with_root( sample_files: dict[str, Path], tmp_path: Path ) -> argparse.Namespace: """Argparse namespace with root_path set.""" return argparse.Namespace( source=str(tmp_path), host="127.0.0.1", port=0, salt="test-salt", navigate=False, root_path="/images", ) @pytest.fixture def initialized_dir_with_root(args_directory_with_root: argparse.Namespace) -> None: """Initialize the server with sample directory files and root_path.""" main.initialize_server(args_directory_with_root) @pytest.fixture async def client_dir_with_root( initialized_dir_with_root: None, ) -> Generator[AsyncClient]: """Async HTTP client against the app initialized with root_path.""" transport = ASGITransport(app=main.app) async with AsyncClient(transport=transport, base_url="http://test") as ac: yield ac class TestRootPathGlobal: """Tests that root_path is set correctly on initialization.""" def test_root_path_set_from_args(self, args_directory_with_root): """root_path global is set from args.""" main.initialize_server(args_directory_with_root) assert main.root_path == "/images" def test_root_path_normalized(self, sample_files, tmp_path): """root_path is normalized (leading/trailing slashes handled).""" args = argparse.Namespace( source=str(tmp_path), host="127.0.0.1", port=0, salt="test-salt", navigate=False, root_path="images/", ) main.initialize_server(args) assert main.root_path == "/images" def test_root_path_empty_when_not_provided(self, args_directory): """root_path is empty string when not provided.""" main.initialize_server(args_directory) assert main.root_path == "" def test_app_root_path_set(self, args_directory_with_root): """FastAPI app.root_path is set for proper URL generation.""" main.initialize_server(args_directory_with_root) assert main.app.root_path == "/images" class TestRootPathURLBuilders: """Tests that URL builder functions include root_path.""" def test_build_url_includes_root_path(self, initialized_dir_with_root): """_build_url includes root_path prefix.""" url = main._build_url("abc123") assert url == "/images/abc123" def test_build_url_with_params_includes_root_path(self, initialized_dir_with_root): """_build_url with order/delay includes root_path prefix.""" url = main._build_url("abc123", order="next", delay=5) assert url == "/images/abc123?order=next&delay=5" def test_build_navigate_url_includes_root_path(self, initialized_dir_with_root): """_build_navigate_url includes root_path prefix.""" url = main._build_navigate_url("folder/file.txt") assert url == "/images/navigate/folder/file.txt" def test_build_navigate_url_with_params_includes_root_path( self, initialized_dir_with_root ): """_build_navigate_url with order/delay includes root_path prefix.""" url = main._build_navigate_url("folder/file.txt", order="random", delay=3) assert url == "/images/navigate/folder/file.txt?order=random&delay=3" def test_build_url_no_root_when_empty(self, initialized_dir): """_build_url has no prefix when root_path is empty.""" url = main._build_url("abc123") assert url == "/abc123" class TestRootPathEndpoints: """Tests that endpoint responses include root_path in URLs.""" async def test_root_redirect_includes_root_path( self, client_dir_with_root: AsyncClient ): """Root redirect location includes root_path.""" response = await client_dir_with_root.get("/", follow_redirects=False) assert response.status_code == 307 location = response.headers["location"] assert location.startswith("/images/") async def test_root_redirect_with_params_includes_root_path( self, client_dir_with_root: AsyncClient ): """Root redirect with order/delay includes root_path.""" response = await client_dir_with_root.get( "/", params={"order": "next", "delay": 5}, follow_redirects=False ) assert response.status_code == 307 location = response.headers["location"] assert location.startswith("/images/") assert "order=next" in location assert "delay=5" in location async def test_hash_page_image_url_includes_root_path( self, client_dir_with_root: AsyncClient ): """Hash page img_url includes root_path.""" file_hash = list(main.file_mapping.keys())[0] response = await client_dir_with_root.get(f"/{file_hash}") assert response.status_code == 200 assert "/images/api/" in response.text async def test_hash_page_nav_urls_include_root_path( self, client_dir_with_root: AsyncClient ): """Hash page prev/next URLs include root_path.""" file_hash = list(main.file_mapping.keys())[0] response = await client_dir_with_root.get(f"/{file_hash}") assert response.status_code == 200 # Check that chevron links use root_path import re # Find href values on chevron elements chevrons = re.findall( r'class="chevron (left|right)"[^>]*href="([^"]*)"', response.text ) for direction, href in chevrons: assert href.startswith( "/images/" ), f"Chevron {direction} href {href} missing root_path" async def test_hash_page_play_button_includes_root_path( self, client_dir_with_root: AsyncClient ): """Hash page play button URL includes root_path.""" file_hash = list(main.file_mapping.keys())[0] response = await client_dir_with_root.get(f"/{file_hash}") assert response.status_code == 200 # Play button should have root_path in its href assert "/images/" in response.text async def test_hash_page_refresh_includes_root_path( self, client_dir_with_root: AsyncClient ): """Hash page in refresh mode includes root_path in meta refresh.""" file_hash = list(main.file_mapping.keys())[0] response = await client_dir_with_root.get( f"/{file_hash}", params={"order": "next", "delay": 5} ) assert response.status_code == 200 assert 'http-equiv="refresh"' in response.text # The refresh URL should include root_path assert "/images/" in response.text