image_server/tests/test_navigate.py

335 lines
14 KiB
Python

"""Tests for the /navigate/{path} endpoint."""
from httpx import AsyncClient
import main
class TestNavigatePage:
"""Tests for GET /navigate/{path}."""
async def test_returns_html_page(self, client_zip_navigate: AsyncClient) -> None:
"""Returns an HTML page for a valid internal ZIP path."""
response = await client_zip_navigate.get("/navigate/top.txt")
assert response.status_code == 200
assert "text/html" in response.headers["content-type"]
async def test_page_contains_image_url(
self, client_zip_navigate: AsyncClient
) -> None:
"""HTML page contains the image URL using hash-based /api endpoint."""
response = await client_zip_navigate.get("/navigate/top.txt")
# The image URL should use the hash-based /api endpoint
assert "/api/" in response.text
assert "/data" in response.text
async def test_page_contains_navigate_nav_links(
self, client_zip_navigate: AsyncClient
) -> None:
"""HTML page contains /navigate/ URLs for prev and next navigation."""
response = await client_zip_navigate.get("/navigate/top.txt")
assert "/navigate/" in response.text
async def test_page_contains_prev_next_buttons(
self, client_zip_navigate: AsyncClient
) -> None:
"""HTML page contains prev and next navigation buttons."""
response = await client_zip_navigate.get("/navigate/top.txt")
assert 'class="chevron left"' in response.text
assert 'class="chevron right"' in response.text
async def test_page_contains_play_button(
self, client_zip_navigate: AsyncClient
) -> None:
"""HTML page contains play button with /navigate query param URL."""
response = await client_zip_navigate.get("/navigate/top.txt")
assert "/navigate/" in response.text
assert "?order=next&delay=5" in response.text
async def test_subdirectory_path(self, client_zip_navigate: AsyncClient) -> None:
"""Paths with subdirectories work correctly."""
response = await client_zip_navigate.get("/navigate/folder/deep.txt")
assert response.status_code == 200
assert "text/html" in response.headers["content-type"]
async def test_returns_404_for_invalid_path(
self, client_zip_navigate: AsyncClient
) -> None:
"""Returns 404 for a path that doesn't exist in the zip."""
response = await client_zip_navigate.get("/navigate/nonexistent.txt")
assert response.status_code == 404
async def test_returns_folder_index_for_directory_path(
self, client_zip_navigate: AsyncClient
) -> None:
"""Returns folder index page for a directory path."""
response = await client_zip_navigate.get("/navigate/folder/")
assert response.status_code == 200
assert "text/html" in response.headers["content-type"]
assert "index-item" in response.text
async def test_returns_404_for_empty_directory_path(
self, client_zip_navigate: AsyncClient
) -> None:
"""Returns 404 for a non-existent directory path."""
response = await client_zip_navigate.get("/navigate/nonexistent/")
assert response.status_code == 404
async def test_returns_root_folder_index(
self, client_zip_navigate: AsyncClient
) -> None:
"""Returns folder index page for root path."""
response = await client_zip_navigate.get("/navigate/")
assert response.status_code == 200
assert "text/html" in response.headers["content-type"]
assert "index-item" in response.text
assert "folder/" in response.text
async def test_folder_index_has_breadcrumb(
self, client_zip_navigate: AsyncClient
) -> None:
"""Folder index page has breadcrumb navigation."""
response = await client_zip_navigate.get("/navigate/folder/")
assert "breadcrumb" in response.text
async def test_folder_index_shows_subfolder_links(
self, client_zip_navigate: AsyncClient
) -> None:
"""Root folder index shows subfolder links."""
response = await client_zip_navigate.get("/navigate/")
assert "/navigate/folder/" in response.text
async def test_folder_index_shows_file_links(
self, client_zip_navigate: AsyncClient
) -> None:
"""Folder index shows file links."""
response = await client_zip_navigate.get("/navigate/")
assert "/navigate/top.txt" in response.text
async def test_subfolder_index_shows_nested_files(
self, client_zip_navigate: AsyncClient
) -> None:
"""Subfolder index shows files within that folder."""
response = await client_zip_navigate.get("/navigate/folder/")
assert response.status_code == 200
assert "/navigate/folder/deep.txt" in response.text
assert "/navigate/folder/image.png" in response.text
async def test_subfolder_index_shows_only_current_folder(
self, client_zip_navigate: AsyncClient
) -> None:
"""Subfolder index shows only that folder's contents, not the full tree."""
response = await client_zip_navigate.get("/navigate/folder/")
assert response.status_code == 200
# Should show folder's own files
assert "/navigate/folder/deep.txt" in response.text
assert "/navigate/folder/image.png" in response.text
# Should NOT show root-level items
assert "/navigate/top.txt" not in response.text
async def test_file_page_has_sidebar(
self, client_zip_navigate: AsyncClient
) -> None:
"""File page in navigate mode has folder index sidebar."""
response = await client_zip_navigate.get("/navigate/top.txt")
assert "sidebar" in response.text
assert "index-item" in response.text
async def test_hash_page_has_no_sidebar(
self, client_zip_navigate: AsyncClient
) -> None:
"""Hash page does not show folder index sidebar content."""
# Get a valid hash from the zip
response = await client_zip_navigate.get("/navigate/top.txt")
assert response.status_code == 200
# Extract hash from the response
import re
match = re.search(r'data-hash="([^"]+)"', response.text)
assert match is not None
file_hash = match.group(1)
# Now visit the hash page
hash_response = await client_zip_navigate.get(f"/{file_hash}")
assert hash_response.status_code == 200
# Sidebar should be hidden on hash pages
assert 'id="sidebar" class="hidden"' in hash_response.text
# No folder index content in sidebar
assert "📁" not in hash_response.text
assert "📄" not in hash_response.text
async def test_returns_404_on_directory_indexer(
self, client_dir: AsyncClient
) -> None:
"""Returns 404 when server has only directory indexers (no ZIP)."""
response = await client_dir.get("/navigate/some/path.txt")
assert response.status_code == 404
async def test_returns_404_when_navigate_disabled(
self, client_zip: AsyncClient
) -> None:
"""Returns 404 when navigate flag is not enabled."""
response = await client_zip.get("/navigate/top.txt")
assert response.status_code == 404
async def test_page_has_data_hash_attribute(
self, client_zip_navigate: AsyncClient
) -> None:
"""HTML page has data-hash attribute on image for toggle support."""
response = await client_zip_navigate.get("/navigate/top.txt")
assert "data-hash=" in response.text
async def test_page_has_data_path_attribute(
self, client_zip_navigate: AsyncClient
) -> None:
"""HTML page has data-path attribute on image for toggle support."""
response = await client_zip_navigate.get("/navigate/top.txt")
assert 'data-path="top.txt"' in response.text
async def test_toggle_url_points_to_hash_mode(
self, client_zip_navigate: AsyncClient
) -> None:
"""Navigate page toggle URL points to hash-based URL."""
import re
response = await client_zip_navigate.get("/navigate/top.txt")
match = re.search(r'id="toggle-link" href="(/[^/][^"]*?)"', response.text)
assert match is not None
toggle_href = match.group(1)
assert not toggle_href.startswith("/navigate/")
async def test_toggle_url_preserves_query_params(
self, client_zip_navigate: AsyncClient
) -> None:
"""Navigate page toggle URL preserves order/delay query params."""
import re
response = await client_zip_navigate.get(
"/navigate/top.txt", params={"order": "next", "delay": 5}
)
match = re.search(r'id="toggle-link" href="(/[^"]*?)"', response.text)
assert match is not None
toggle_href = match.group(1)
assert "order=next" in toggle_href
assert "delay=5" in toggle_href
class TestNavigatePageWithRefresh:
"""Tests for GET /navigate/{path}?order=...&delay=... (auto-refresh mode)."""
async def test_next_order_returns_html(
self, client_zip_navigate: AsyncClient
) -> None:
"""Next order returns HTML with refresh meta tag."""
response = await client_zip_navigate.get(
"/navigate/top.txt", params={"order": "next", "delay": 5}
)
assert response.status_code == 200
assert 'http-equiv="refresh"' in response.text
async def test_random_order_returns_html(
self, client_zip_navigate: AsyncClient
) -> None:
"""Random order returns HTML with refresh meta tag."""
response = await client_zip_navigate.get(
"/navigate/top.txt", params={"order": "random", "delay": 3}
)
assert response.status_code == 200
assert 'http-equiv="refresh"' in response.text
async def test_invalid_order_returns_400(
self, client_zip_navigate: AsyncClient
) -> None:
"""Invalid order parameter returns 400."""
response = await client_zip_navigate.get(
"/navigate/top.txt", params={"order": "shuffle", "delay": 5}
)
assert response.status_code == 400
async def test_returns_404_for_invalid_path(
self, client_zip_navigate: AsyncClient
) -> None:
"""Returns 404 for a path that doesn't exist."""
response = await client_zip_navigate.get(
"/navigate/nonexistent.txt", params={"order": "next", "delay": 5}
)
assert response.status_code == 404
async def test_refresh_url_uses_navigate_path(
self, client_zip_navigate: AsyncClient
) -> None:
"""Refresh meta tag uses /navigate/ URLs with paths."""
response = await client_zip_navigate.get(
"/navigate/top.txt", params={"order": "next", "delay": 5}
)
assert "/navigate/" in response.text
assert "order=next" in response.text
assert "delay=5" in response.text
async def test_pause_button_uses_navigate_url(
self, client_zip_navigate: AsyncClient
) -> None:
"""Pause button links to /navigate/{path} without query params."""
response = await client_zip_navigate.get(
"/navigate/top.txt", params={"order": "next", "delay": 5}
)
assert '/navigate/top.txt" class="play-btn"' in response.text
class TestNavigateHelperFunctions:
"""Tests for navigate helper functions."""
def test_find_hash_by_path_returns_hash(self, initialized_zip: None) -> None:
"""_find_hash_by_path returns the correct hash for a valid path."""
file_hash = main._find_hash_by_path("top.txt")
assert file_hash is not None
assert file_hash in main.file_mapping
def test_find_hash_by_path_returns_none_for_invalid(
self, initialized_zip: None
) -> None:
"""_find_hash_by_path returns None for a path that doesn't exist."""
file_hash = main._find_hash_by_path("nonexistent.txt")
assert file_hash is None
def test_find_hash_by_path_returns_none_for_directory(
self, initialized_zip: None
) -> None:
"""_find_hash_by_path returns None for a directory path."""
file_hash = main._find_hash_by_path("folder/")
assert file_hash is None
def test_get_path_from_hash(self, initialized_zip: None) -> None:
"""_get_path_from_hash returns the correct path for a valid hash."""
# Find a hash that maps to a known path
for file_hash, filename in main.indexers[0]._file_mapping.items():
result = main._get_path_from_hash(file_hash)
assert result == filename
break
def test_build_navigate_url_no_params(self) -> None:
"""_build_navigate_url builds correct URL without query params."""
url = main._build_navigate_url("folder/deep.txt")
assert url == "/navigate/folder/deep.txt"
def test_build_navigate_url_with_params(self) -> None:
"""_build_navigate_url builds correct URL with query params."""
url = main._build_navigate_url("folder/deep.txt", order="next", delay=5)
assert url == "/navigate/folder/deep.txt?order=next&delay=5"
def test_get_navigation_data_by_path(self, initialized_zip: None) -> None:
"""_get_navigation_data_by_path returns correct navigation data."""
data = main._get_navigation_data_by_path("top.txt")
assert data is not None
assert "file_hash" in data
assert "next_path" in data
assert "prev_path" in data
assert data["filename"] == "top.txt"
def test_get_navigation_data_by_path_returns_none_for_invalid(
self, initialized_zip: None
) -> None:
"""_get_navigation_data_by_path returns None for invalid path."""
data = main._get_navigation_data_by_path("nonexistent.txt")
assert data is None