Clean up tests for query param conversion and remove auth

- Deleted test_auth.py (auth no longer exists)
- Rewrote TestOrderDelayRoute -> TestRootRedirectWithOrderDelay using query params
- Updated TestHashPageWithRefresh to use ?order=...&delay=... URLs
- Added play button query param assertion in TestHashPage
- Removed password=None from test_navigation.py seeded_indexers fixture
- Formatted with black, all 59 tests passing
This commit is contained in:
Timothy Farrell 2026-04-25 05:35:27 -05:00
parent 22bf57f896
commit f856c92394
6 changed files with 56 additions and 221 deletions

Binary file not shown.

14
TODO.md
View File

@ -26,19 +26,19 @@ Remove all authentication from the server and convert `order`/`delay` from path
- [x] Remove `password` field from `args_directory`/`args_zip` fixtures (or keep as None if still in argparse)
### 4. Update `test_auth.py`
- [ ] Delete the entire `test_auth.py` file (all auth tests are no longer relevant)
- [x] Delete the entire `test_auth.py` file (all auth tests are no longer relevant)
### 5. Update `test_endpoints.py`
- [ ] Update `TestOrderDelayRoute` tests — remove or rewrite for query param routes
- [ ] Update `TestHashPageWithRefresh` tests to use query param URLs (`/{hash}?order=next&delay=5`)
- [ ] Update `TestHashPage` tests if needed (play button URLs changed)
- [x] Update `TestOrderDelayRoute` tests — remove or rewrite for query param routes
- [x] Update `TestHashPageWithRefresh` tests to use query param URLs (`/{hash}?order=next&delay=5`)
- [x] Update `TestHashPage` tests if needed (play button URLs changed)
### 6. Update `test_navigation.py`
- [ ] Remove `password=None` from `seeded_indexers` fixture args (if `--password` arg is removed)
- [x] Remove `password=None` from `seeded_indexers` fixture args (if `--password` arg is removed)
### 7. Format and verify
- [ ] Run `uv run black .` to format all code
- [ ] Run `uv run pytest` to verify all tests pass
- [x] Run `uv run black .` to format all code
- [x] Run `uv run pytest` to verify all tests pass
## Notes
- After removing auth, the `--password` CLI arg is gone entirely

14
main.py
View File

@ -253,9 +253,7 @@ def _render_page(
@app.get("/{file_hash}")
async def hash_page(
file_hash: str, order: str | None = None, delay: int | None = None
):
async def hash_page(file_hash: str, order: str | None = None, delay: int | None = None):
"""Serve a page for a specific file hash with optional auto-refresh navigation.
Args:
@ -275,16 +273,18 @@ async def hash_page(
if order is not None and delay is not None:
# Timer mode: auto-refresh with query params
refresh_url = _build_url(
navigation_data["next_hash"], order=order, delay=delay
refresh_url = _build_url(navigation_data["next_hash"], order=order, delay=delay)
refresh_meta = (
f'<meta http-equiv="refresh" content="{delay};url={refresh_url}">'
)
refresh_meta = f'<meta http-equiv="refresh" content="{delay};url={refresh_url}">'
image_click_url = _build_url(file_hash)
# Create pause button to stop auto-refresh
pause_button = '<a href="{file_hash}" class="play-btn" title="Pause">⏸</a>'.format(
pause_button = (
'<a href="{file_hash}" class="play-btn" title="Pause">⏸</a>'.format(
file_hash=file_hash
)
)
return _render_page(
navigation_data,

View File

@ -1,189 +0,0 @@
"""Tests for authentication."""
import argparse
import base64
from pathlib import Path
import pytest
from httpx import ASGITransport, AsyncClient
import main
def _basic_auth_header(username: str, password: str) -> str:
"""Create a Basic Auth header value."""
creds = f"{username}:{password}"
return f"Basic {base64.b64encode(creds.encode()).decode()}"
def _make_args(tmp_path: Path) -> argparse.Namespace:
"""Create an argparse.Namespace for the given path."""
return argparse.Namespace(
source=str(tmp_path),
host="127.0.0.1",
port=0,
salt="auth-salt",
password=None,
)
@pytest.fixture
def auth_setup(tmp_path: Path) -> tuple[str, str]:
"""Set up server with sample files and password protection.
Returns:
Tuple of (username, password).
"""
(tmp_path / "test.txt").write_text("hello")
main.initialize_server(_make_args(tmp_path))
main.set_auth_password("secret123")
return ("user", "secret123")
class TestNoPasswordSet:
"""Tests when no password is configured.
Note: HTTPBasic() always requires an Authorization header.
When expected_password is None, any credentials pass.
"""
async def test_health_always_open(self, client_dir: AsyncClient) -> None:
"""Health check has no auth dependency — always accessible."""
response = await client_dir.get("/api/health")
assert response.status_code == 200
async def test_protected_endpoint_requires_auth_header(
self, initialized_dir: None
) -> None:
"""Even with no password, HTTPBasic requires an auth header."""
file_hash = list(main.file_mapping.keys())[0]
# No auth header → 401 from HTTPBasic
transport = ASGITransport(app=main.app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
response = await ac.get(f"/api/{file_hash}/data")
assert response.status_code == 401
async def test_any_credentials_pass_when_no_password(
self, client_dir: AsyncClient
) -> None:
"""Any credentials pass when no password is set."""
file_hash = list(main.file_mapping.keys())[0]
transport = ASGITransport(app=main.app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
ac.headers["Authorization"] = _basic_auth_header("any", "thing")
response = await ac.get(f"/api/{file_hash}/data")
assert response.status_code == 200
async def test_root_requires_auth_header(self, initialized_dir: None) -> None:
"""Root endpoint requires auth header even with no password."""
transport = ASGITransport(app=main.app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
response = await ac.get("/", follow_redirects=False)
assert response.status_code == 401
class TestCorrectPassword:
"""Tests with correct password."""
async def test_health_with_correct_password(
self, auth_setup: tuple[str, str]
) -> None:
"""Health check works (it has no auth, always 200)."""
transport = ASGITransport(app=main.app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
response = await ac.get("/api/health")
assert response.status_code == 200
async def test_file_access_with_correct_password(
self, auth_setup: tuple[str, str]
) -> None:
"""File access works with correct password."""
username, password = auth_setup
transport = ASGITransport(app=main.app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
ac.headers["Authorization"] = _basic_auth_header(username, password)
file_hash = list(main.file_mapping.keys())[0]
response = await ac.get(f"/api/{file_hash}/data")
assert response.status_code == 200
async def test_root_with_correct_password(
self, auth_setup: tuple[str, str]
) -> None:
"""Root redirect works with correct password."""
username, password = auth_setup
transport = ASGITransport(app=main.app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
ac.headers["Authorization"] = _basic_auth_header(username, password)
response = await ac.get("/", follow_redirects=False)
assert response.status_code in (307, 302, 301)
async def test_hash_page_with_correct_password(
self, auth_setup: tuple[str, str]
) -> None:
"""Hash page works with correct password."""
username, password = auth_setup
transport = ASGITransport(app=main.app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
ac.headers["Authorization"] = _basic_auth_header(username, password)
file_hash = list(main.file_mapping.keys())[0]
response = await ac.get(f"/{file_hash}")
assert response.status_code == 200
class TestWrongPassword:
"""Tests with incorrect password."""
async def test_file_access_with_wrong_password(
self, auth_setup: tuple[str, str]
) -> None:
"""File access returns 401 with wrong password."""
transport = ASGITransport(app=main.app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
ac.headers["Authorization"] = _basic_auth_header("user", "wrong")
file_hash = list(main.file_mapping.keys())[0]
response = await ac.get(f"/api/{file_hash}/data")
assert response.status_code == 401
async def test_root_with_wrong_password(self, auth_setup: tuple[str, str]) -> None:
"""Root redirect returns 401 with wrong password."""
transport = ASGITransport(app=main.app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
ac.headers["Authorization"] = _basic_auth_header("user", "wrong")
response = await ac.get("/", follow_redirects=False)
assert response.status_code == 401
async def test_no_auth_header_returns_401(
self, auth_setup: tuple[str, str]
) -> None:
"""Missing auth header returns 401."""
transport = ASGITransport(app=main.app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
file_hash = list(main.file_mapping.keys())[0]
response = await ac.get(f"/api/{file_hash}/data")
assert response.status_code == 401
async def test_includes_www_authenticate_header(
self, auth_setup: tuple[str, str]
) -> None:
"""401 response includes WWW-Authenticate header."""
transport = ASGITransport(app=main.app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
file_hash = list(main.file_mapping.keys())[0]
response = await ac.get(f"/api/{file_hash}/data")
assert response.status_code == 401
assert "www-authenticate" in response.headers
class TestSetAuthPassword:
"""Tests for set_auth_password function."""
def test_sets_password(self) -> None:
"""Password is set correctly."""
main.set_auth_password("newpass")
assert main.expected_password == "newpass"
def test_clears_password_with_none(self) -> None:
"""Passing None clears the password."""
main.set_auth_password("something")
main.set_auth_password(None)
assert main.expected_password is None

View File

@ -101,20 +101,28 @@ class TestRootRedirect:
assert hash_from_url in main.file_mapping
class TestOrderDelayRoute:
"""Tests for GET /{order}/{delay}."""
class TestRootRedirectWithOrderDelay:
"""Tests for GET / with order/delay query parameters."""
async def test_next_order_redirects(self, client_dir: AsyncClient) -> None:
"""/next/5 redirects to /next/5/{hash}."""
response = await client_dir.get("/next/5", follow_redirects=False)
"""/?order=next&delay=5 redirects to /{hash}?order=next&delay=5."""
response = await client_dir.get(
"/", params={"order": "next", "delay": 5}, follow_redirects=False
)
assert response.status_code in (307, 302, 301)
assert "/next/5/" in response.headers["location"]
location = response.headers["location"]
assert "order=next" in location
assert "delay=5" in location
async def test_random_order_redirects(self, client_dir: AsyncClient) -> None:
"""/random/3 redirects to /random/3/{hash}."""
response = await client_dir.get("/random/3", follow_redirects=False)
"""/?order=random&delay=3 redirects to /{hash}?order=random&delay=3."""
response = await client_dir.get(
"/", params={"order": "random", "delay": 3}, follow_redirects=False
)
assert response.status_code in (307, 302, 301)
assert "/random/3/" in response.headers["location"]
location = response.headers["location"]
assert "order=random" in location
assert "delay=3" in location
class TestHashPage:
@ -142,6 +150,12 @@ class TestHashPage:
assert 'class="chevron left"' in response.text
assert 'class="chevron right"' in response.text
async def test_page_contains_play_button(self, client_dir: AsyncClient) -> None:
"""HTML page contains play button with query param URL."""
file_hash = list(main.file_mapping.keys())[0]
response = await client_dir.get(f"/{file_hash}")
assert "?order=next&delay=5" in response.text
async def test_returns_404_for_invalid_hash(self, client_dir: AsyncClient) -> None:
"""Returns 404 for a hash that doesn't exist."""
response = await client_dir.get("/nonexistent-hash")
@ -149,37 +163,48 @@ class TestHashPage:
class TestHashPageWithRefresh:
"""Tests for GET /{order}/{delay}/{file_hash}."""
"""Tests for GET /{file_hash}?order=...&delay=... (auto-refresh mode)."""
async def test_next_order_returns_html(self, client_dir: AsyncClient) -> None:
"""Next order returns HTML with refresh meta tag."""
file_hash = list(main.file_mapping.keys())[0]
response = await client_dir.get(f"/next/5/{file_hash}")
response = await client_dir.get(
f"/{file_hash}", 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_dir: AsyncClient) -> None:
"""Random order returns HTML with refresh meta tag."""
file_hash = list(main.file_mapping.keys())[0]
response = await client_dir.get(f"/random/3/{file_hash}")
response = await client_dir.get(
f"/{file_hash}", 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_dir: AsyncClient) -> None:
"""Invalid order parameter returns 400."""
file_hash = list(main.file_mapping.keys())[0]
response = await client_dir.get(f"/shuffle/5/{file_hash}")
response = await client_dir.get(
f"/{file_hash}", params={"order": "shuffle", "delay": 5}
)
assert response.status_code == 400
async def test_returns_404_for_invalid_hash(self, client_dir: AsyncClient) -> None:
"""Returns 404 for a hash that doesn't exist."""
response = await client_dir.get("/next/5/nonexistent-hash")
response = await client_dir.get(
"/nonexistent-hash", params={"order": "next", "delay": 5}
)
assert response.status_code == 404
async def test_refresh_url_points_to_next_file(
self, client_dir: AsyncClient
) -> None:
"""Refresh meta tag points to the next file in sequence."""
"""Refresh meta tag points to the next file in sequence with query params."""
file_hash = list(main.file_mapping.keys())[0]
response = await client_dir.get(f"/next/5/{file_hash}")
assert "url=/next/5/" in response.text
response = await client_dir.get(
f"/{file_hash}", params={"order": "next", "delay": 5}
)
assert "order=next" in response.text
assert "delay=5" in response.text

View File

@ -16,7 +16,6 @@ def seeded_indexers(sample_files: dict[str, Path], tmp_path: Path) -> None:
host="127.0.0.1",
port=0,
salt="nav-test-salt",
password=None,
)
main.initialize_server(args)