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:
parent
22bf57f896
commit
f856c92394
BIN
2024_seasonal_wallpapers_pack.zip
Normal file
BIN
2024_seasonal_wallpapers_pack.zip
Normal file
Binary file not shown.
14
TODO.md
14
TODO.md
@ -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
14
main.py
@ -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,
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user