Add navigation by path
This commit is contained in:
parent
1618a5a037
commit
b969902ac5
@ -56,10 +56,11 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="container">
|
<div id="container">
|
||||||
<a href="$image_click_url"><div id="img" title="$filename"></div></a>
|
<a href="$image_click_url"><div id="img" title="$filename" data-hash="$file_hash" data-path="$file_path"></div></a>
|
||||||
<a href="$prev_url" class="chevron left" id="prev-btn">‹</a>
|
<a href="$prev_url" class="chevron left" id="prev-btn">‹</a>
|
||||||
<a href="$next_url" class="chevron right" id="next-btn">›</a>
|
<a href="$next_url" class="chevron right" id="next-btn">›</a>
|
||||||
$play_button
|
$play_button
|
||||||
|
<a id="toggle-link" href="$toggle_url" class="hidden"></a>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
function getRefreshParams() {
|
function getRefreshParams() {
|
||||||
@ -103,6 +104,8 @@
|
|||||||
var newOrder = params.order === 'next' ? 'random' : 'next';
|
var newOrder = params.order === 'next' ? 'random' : 'next';
|
||||||
window.location.href = buildUrl(params.hash, newOrder, params.delay);
|
window.location.href = buildUrl(params.hash, newOrder, params.delay);
|
||||||
}
|
}
|
||||||
|
} else if (e.key.toLowerCase() === 'n') {
|
||||||
|
document.getElementById('toggle-link').click();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
234
main.py
234
main.py
@ -144,6 +144,20 @@ class ZipFileIndexer(FileIndexer):
|
|||||||
return None
|
return None
|
||||||
return self._file_mapping[file_hash]
|
return self._file_mapping[file_hash]
|
||||||
|
|
||||||
|
def get_hash_by_path(self, path: str) -> str | None:
|
||||||
|
"""Reverse-lookup: given an internal ZIP filename, find its hash.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: The internal filename inside the zip.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The hash if found, None otherwise.
|
||||||
|
"""
|
||||||
|
for file_hash, filename in self._file_mapping.items():
|
||||||
|
if filename == path:
|
||||||
|
return file_hash
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
INDEXER_MAP = {".zip": ZipFileIndexer}
|
INDEXER_MAP = {".zip": ZipFileIndexer}
|
||||||
|
|
||||||
@ -297,16 +311,41 @@ def _render_page(
|
|||||||
play_button: str = "",
|
play_button: str = "",
|
||||||
current_order: str | None = None,
|
current_order: str | None = None,
|
||||||
current_delay: int | None = None,
|
current_delay: int | None = None,
|
||||||
|
use_navigate_urls: bool = False,
|
||||||
) -> HTMLResponse:
|
) -> HTMLResponse:
|
||||||
"""Render the frontend page with navigation data"""
|
"""Render the frontend page with navigation data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
navigation_data: Dictionary with navigation info.
|
||||||
|
extra_meta: Extra <meta> tags (e.g., auto-refresh).
|
||||||
|
image_click_url: URL navigated to on image click.
|
||||||
|
play_button: Play/pause button HTML.
|
||||||
|
current_order: Current navigation order ('next' or 'random').
|
||||||
|
current_delay: Delay in seconds for auto-navigation.
|
||||||
|
use_navigate_urls: If True, use /navigate/{path} URLs for prev/next
|
||||||
|
instead of /{hash} URLs.
|
||||||
|
"""
|
||||||
with open("frontend.html") as f:
|
with open("frontend.html") as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
|
|
||||||
template = string.Template(content)
|
template = string.Template(content)
|
||||||
|
|
||||||
# Generate navigation URLs based on current mode
|
# Generate navigation URLs based on current mode
|
||||||
|
if use_navigate_urls:
|
||||||
|
next_path = navigation_data["next_path"]
|
||||||
|
prev_path = navigation_data["prev_path"]
|
||||||
|
if current_order is not None:
|
||||||
|
next_url = _build_navigate_url(
|
||||||
|
next_path, order=current_order, delay=current_delay
|
||||||
|
)
|
||||||
|
prev_url = _build_navigate_url(
|
||||||
|
prev_path, order=current_order, delay=current_delay
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
next_url = f"/navigate/{next_path}"
|
||||||
|
prev_url = f"/navigate/{prev_path}"
|
||||||
|
else:
|
||||||
if current_order is not None:
|
if current_order is not None:
|
||||||
# Timer mode: preserve current order and delay via query params
|
|
||||||
next_url = _build_url(
|
next_url = _build_url(
|
||||||
navigation_data["next_hash"],
|
navigation_data["next_hash"],
|
||||||
order=current_order,
|
order=current_order,
|
||||||
@ -318,16 +357,33 @@ def _render_page(
|
|||||||
delay=current_delay,
|
delay=current_delay,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Browse mode: generate browse mode URLs
|
|
||||||
next_url = "/{next_hash}".format(next_hash=navigation_data["next_hash"])
|
next_url = "/{next_hash}".format(next_hash=navigation_data["next_hash"])
|
||||||
prev_url = "/{prev_hash}".format(prev_hash=navigation_data["prev_hash"])
|
prev_url = "/{prev_hash}".format(prev_hash=navigation_data["prev_hash"])
|
||||||
|
|
||||||
|
# Build the toggle URL (alternate view mode)
|
||||||
|
file_path = navigation_data.get("filename", "")
|
||||||
|
if use_navigate_urls:
|
||||||
|
toggle_url = _build_url(
|
||||||
|
navigation_data["file_hash"],
|
||||||
|
order=current_order,
|
||||||
|
delay=current_delay,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
toggle_url = _build_navigate_url(
|
||||||
|
file_path,
|
||||||
|
order=current_order,
|
||||||
|
delay=current_delay,
|
||||||
|
)
|
||||||
|
|
||||||
content = template.substitute(
|
content = template.substitute(
|
||||||
img_url="/api/{file_hash}/data".format(file_hash=navigation_data["file_hash"]),
|
img_url="/api/{file_hash}/data".format(file_hash=navigation_data["file_hash"]),
|
||||||
image_click_url=image_click_url or _get_random_hash(),
|
image_click_url=image_click_url or _get_random_hash(),
|
||||||
next_url=next_url,
|
next_url=next_url,
|
||||||
prev_url=prev_url,
|
prev_url=prev_url,
|
||||||
filename=navigation_data["filename"],
|
filename=navigation_data["filename"],
|
||||||
|
file_hash=navigation_data["file_hash"],
|
||||||
|
file_path=file_path,
|
||||||
|
toggle_url=toggle_url,
|
||||||
extra_meta=extra_meta,
|
extra_meta=extra_meta,
|
||||||
play_button=play_button,
|
play_button=play_button,
|
||||||
)
|
)
|
||||||
@ -467,6 +523,178 @@ def _get_random_hash() -> str:
|
|||||||
return random.choice(keys)
|
return random.choice(keys)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_random_navigate_path() -> str | None:
|
||||||
|
"""Get a random internal path from ZipFileIndexers only."""
|
||||||
|
zip_paths: list[str] = []
|
||||||
|
for indexer in indexers:
|
||||||
|
if isinstance(indexer, ZipFileIndexer):
|
||||||
|
zip_paths.extend(indexer._file_mapping.values())
|
||||||
|
if not zip_paths:
|
||||||
|
return None
|
||||||
|
return random.choice(zip_paths)
|
||||||
|
|
||||||
|
|
||||||
|
def _find_hash_by_path(path: str) -> str | None:
|
||||||
|
"""Find the hash for a given internal ZIP path across all ZipFileIndexers.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: The internal filename inside a zip.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The hash if found, None otherwise.
|
||||||
|
"""
|
||||||
|
for idx in indexers:
|
||||||
|
if isinstance(idx, ZipFileIndexer):
|
||||||
|
file_hash = idx.get_hash_by_path(path)
|
||||||
|
if file_hash is not None:
|
||||||
|
return file_hash
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_path_from_hash(file_hash: str) -> str | None:
|
||||||
|
"""Get the internal ZIP path for a given hash.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_hash: The hash of the file.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The internal filename if found, None otherwise.
|
||||||
|
"""
|
||||||
|
indexer = _find_indexer_for_hash(file_hash)
|
||||||
|
if indexer is not None:
|
||||||
|
return indexer.get_filename_by_hash(file_hash)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _build_navigate_url(
|
||||||
|
path: str, order: str | None = None, delay: int | None = None
|
||||||
|
) -> str:
|
||||||
|
"""Build a /navigate URL with optional order/delay query parameters."""
|
||||||
|
base = f"/navigate/{path}"
|
||||||
|
if order is not None and delay is not None:
|
||||||
|
return f"{base}?order={order}&delay={delay}"
|
||||||
|
return base
|
||||||
|
|
||||||
|
|
||||||
|
def _get_navigation_data_by_path(
|
||||||
|
path: str, order: str | None = None
|
||||||
|
) -> dict[str, str] | None:
|
||||||
|
"""Get navigation data for a file identified by its internal ZIP path.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: The internal filename inside a zip.
|
||||||
|
order: Navigation order - 'next' for sequential, 'random' for random,
|
||||||
|
or None for default browse mode.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with navigation paths and filename, or None if path not found.
|
||||||
|
"""
|
||||||
|
file_hash = _find_hash_by_path(path)
|
||||||
|
if file_hash is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
keys = list(file_mapping.keys())
|
||||||
|
idx = keys.index(file_hash)
|
||||||
|
|
||||||
|
if order == "random":
|
||||||
|
next_hash = _get_random_hash()
|
||||||
|
prev_hash = _get_random_hash()
|
||||||
|
else:
|
||||||
|
next_hash = keys[(idx + 1) % len(keys)]
|
||||||
|
prev_hash = keys[idx - 1] if idx > 0 else keys[-1]
|
||||||
|
|
||||||
|
next_path = _get_path_from_hash(next_hash) or ""
|
||||||
|
prev_path = _get_path_from_hash(prev_hash) or ""
|
||||||
|
|
||||||
|
return {
|
||||||
|
"file_hash": file_hash,
|
||||||
|
"next_path": next_path,
|
||||||
|
"prev_path": prev_path,
|
||||||
|
"filename": path,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/navigate/{path:path}")
|
||||||
|
async def navigate_page(
|
||||||
|
path: str,
|
||||||
|
order: str | None = None,
|
||||||
|
delay: int | None = None,
|
||||||
|
request: Request = None,
|
||||||
|
):
|
||||||
|
"""Serve a page for a file identified by its internal ZIP path.
|
||||||
|
|
||||||
|
Navigation links use /navigate/{path} URLs instead of hashed URLs.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: The internal filename inside a zip.
|
||||||
|
order: Navigation order - 'next' for sequential, 'random' for random.
|
||||||
|
delay: Delay in seconds before auto-navigating to next file.
|
||||||
|
"""
|
||||||
|
if order is not None and order not in ("next", "random"):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400, detail="Invalid order. Must be 'next' or 'random'"
|
||||||
|
)
|
||||||
|
|
||||||
|
navigation_data = _get_navigation_data_by_path(path, order=order)
|
||||||
|
if navigation_data is None:
|
||||||
|
raise HTTPException(status_code=404, detail="File not found")
|
||||||
|
|
||||||
|
file_hash = navigation_data["file_hash"]
|
||||||
|
|
||||||
|
# Check if the file is encrypted and requires auth
|
||||||
|
if request is not None:
|
||||||
|
password = _get_zip_password(file_hash, request)
|
||||||
|
if password is None:
|
||||||
|
indexer = _find_indexer_for_hash(file_hash)
|
||||||
|
if isinstance(indexer, ZipFileIndexer) and indexer.is_file_encrypted(
|
||||||
|
file_hash
|
||||||
|
):
|
||||||
|
_raise_unauthorized()
|
||||||
|
|
||||||
|
if order is not None and delay is not None:
|
||||||
|
# Timer mode: auto-refresh with query params
|
||||||
|
refresh_url = _build_navigate_url(
|
||||||
|
navigation_data["next_path"], order=order, delay=delay
|
||||||
|
)
|
||||||
|
refresh_meta = (
|
||||||
|
f'<meta http-equiv="refresh" content="{delay};url={refresh_url}">'
|
||||||
|
)
|
||||||
|
image_click_url = _build_navigate_url(path)
|
||||||
|
|
||||||
|
# Create pause button to stop auto-refresh
|
||||||
|
pause_button = (
|
||||||
|
f'<a href="/navigate/{path}" class="play-btn" title="Pause">⏸</a>'
|
||||||
|
)
|
||||||
|
|
||||||
|
return _render_page(
|
||||||
|
navigation_data,
|
||||||
|
refresh_meta,
|
||||||
|
image_click_url,
|
||||||
|
play_button=pause_button,
|
||||||
|
current_order=order,
|
||||||
|
current_delay=delay,
|
||||||
|
use_navigate_urls=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Browse mode
|
||||||
|
play_button = (
|
||||||
|
f'<a href="/navigate/{path}?order=next&delay=5" '
|
||||||
|
'class="play-btn" title="Play next 5">⏵</a>'
|
||||||
|
)
|
||||||
|
random_path = _get_random_navigate_path()
|
||||||
|
image_click_url = (
|
||||||
|
f"/navigate/{random_path}" if random_path else _get_random_hash()
|
||||||
|
)
|
||||||
|
return _render_page(
|
||||||
|
navigation_data,
|
||||||
|
image_click_url=image_click_url,
|
||||||
|
play_button=play_button,
|
||||||
|
current_order=None,
|
||||||
|
current_delay=None,
|
||||||
|
use_navigate_urls=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser(description="Run the file server")
|
parser = argparse.ArgumentParser(description="Run the file server")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
|
|||||||
@ -160,6 +160,19 @@ class TestHashPage:
|
|||||||
response = await client_dir.get("/nonexistent-hash")
|
response = await client_dir.get("/nonexistent-hash")
|
||||||
assert response.status_code == 404
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
async def test_toggle_url_points_to_navigate_mode(
|
||||||
|
self, client_zip: AsyncClient
|
||||||
|
) -> None:
|
||||||
|
"""Hash page toggle URL points to navigate-based URL."""
|
||||||
|
import re
|
||||||
|
|
||||||
|
file_hash = list(main.file_mapping.keys())[0]
|
||||||
|
response = await client_zip.get(f"/{file_hash}")
|
||||||
|
match = re.search(r'id="toggle-link" href="(/[^"]*?)"', response.text)
|
||||||
|
assert match is not None
|
||||||
|
toggle_href = match.group(1)
|
||||||
|
assert toggle_href.startswith("/navigate/")
|
||||||
|
|
||||||
|
|
||||||
class TestHashPageWithRefresh:
|
class TestHashPageWithRefresh:
|
||||||
"""Tests for GET /{file_hash}?order=...&delay=... (auto-refresh mode)."""
|
"""Tests for GET /{file_hash}?order=...&delay=... (auto-refresh mode)."""
|
||||||
|
|||||||
217
tests/test_navigate.py
Normal file
217
tests/test_navigate.py
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
"""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: AsyncClient) -> None:
|
||||||
|
"""Returns an HTML page for a valid internal ZIP path."""
|
||||||
|
response = await client_zip.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: AsyncClient) -> None:
|
||||||
|
"""HTML page contains the image URL using hash-based /api endpoint."""
|
||||||
|
response = await client_zip.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: AsyncClient
|
||||||
|
) -> None:
|
||||||
|
"""HTML page contains /navigate/ URLs for prev and next navigation."""
|
||||||
|
response = await client_zip.get("/navigate/top.txt")
|
||||||
|
assert "/navigate/" in response.text
|
||||||
|
|
||||||
|
async def test_page_contains_prev_next_buttons(
|
||||||
|
self, client_zip: AsyncClient
|
||||||
|
) -> None:
|
||||||
|
"""HTML page contains prev and next navigation buttons."""
|
||||||
|
response = await client_zip.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: AsyncClient) -> None:
|
||||||
|
"""HTML page contains play button with /navigate query param URL."""
|
||||||
|
response = await client_zip.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: AsyncClient) -> None:
|
||||||
|
"""Paths with subdirectories work correctly."""
|
||||||
|
response = await client_zip.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: AsyncClient) -> None:
|
||||||
|
"""Returns 404 for a path that doesn't exist in the zip."""
|
||||||
|
response = await client_zip.get("/navigate/nonexistent.txt")
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
async def test_returns_404_for_directory_path(
|
||||||
|
self, client_zip: AsyncClient
|
||||||
|
) -> None:
|
||||||
|
"""Returns 404 for a directory path (not a file)."""
|
||||||
|
response = await client_zip.get("/navigate/folder/")
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
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_page_has_data_hash_attribute(self, client_zip: AsyncClient) -> None:
|
||||||
|
"""HTML page has data-hash attribute on image for toggle support."""
|
||||||
|
response = await client_zip.get("/navigate/top.txt")
|
||||||
|
assert "data-hash=" in response.text
|
||||||
|
|
||||||
|
async def test_page_has_data_path_attribute(self, client_zip: AsyncClient) -> None:
|
||||||
|
"""HTML page has data-path attribute on image for toggle support."""
|
||||||
|
response = await client_zip.get("/navigate/top.txt")
|
||||||
|
assert 'data-path="top.txt"' in response.text
|
||||||
|
|
||||||
|
async def test_toggle_url_points_to_hash_mode(
|
||||||
|
self, client_zip: AsyncClient
|
||||||
|
) -> None:
|
||||||
|
"""Navigate page toggle URL points to hash-based URL."""
|
||||||
|
import re
|
||||||
|
|
||||||
|
response = await client_zip.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: AsyncClient
|
||||||
|
) -> None:
|
||||||
|
"""Navigate page toggle URL preserves order/delay query params."""
|
||||||
|
import re
|
||||||
|
|
||||||
|
response = await client_zip.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: AsyncClient) -> None:
|
||||||
|
"""Next order returns HTML with refresh meta tag."""
|
||||||
|
response = await client_zip.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: AsyncClient) -> None:
|
||||||
|
"""Random order returns HTML with refresh meta tag."""
|
||||||
|
response = await client_zip.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: AsyncClient) -> None:
|
||||||
|
"""Invalid order parameter returns 400."""
|
||||||
|
response = await client_zip.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: AsyncClient) -> None:
|
||||||
|
"""Returns 404 for a path that doesn't exist."""
|
||||||
|
response = await client_zip.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: AsyncClient
|
||||||
|
) -> None:
|
||||||
|
"""Refresh meta tag uses /navigate/ URLs with paths."""
|
||||||
|
response = await client_zip.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: AsyncClient
|
||||||
|
) -> None:
|
||||||
|
"""Pause button links to /navigate/{path} without query params."""
|
||||||
|
response = await client_zip.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
|
||||||
Loading…
x
Reference in New Issue
Block a user