diff --git a/2024_seasonal_wallpapers_pack.zip b/2024_seasonal_wallpapers_pack.zip
new file mode 100644
index 0000000..632e446
Binary files /dev/null and b/2024_seasonal_wallpapers_pack.zip differ
diff --git a/TODO.md b/TODO.md
index 5395121..1cad32f 100644
--- a/TODO.md
+++ b/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
diff --git a/main.py b/main.py
index 4f98325..be12a26 100644
--- a/main.py
+++ b/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,15 +273,17 @@ 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''
)
- refresh_meta = f''
image_click_url = _build_url(file_hash)
# Create pause button to stop auto-refresh
- pause_button = '⏸'.format(
- file_hash=file_hash
+ pause_button = (
+ '⏸'.format(
+ file_hash=file_hash
+ )
)
return _render_page(
diff --git a/tests/test_auth.py b/tests/test_auth.py
deleted file mode 100644
index dd5eb62..0000000
--- a/tests/test_auth.py
+++ /dev/null
@@ -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
diff --git a/tests/test_endpoints.py b/tests/test_endpoints.py
index 0198043..a2eb5e2 100644
--- a/tests/test_endpoints.py
+++ b/tests/test_endpoints.py
@@ -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
diff --git a/tests/test_navigation.py b/tests/test_navigation.py
index 4ca0583..f2e2372 100644
--- a/tests/test_navigation.py
+++ b/tests/test_navigation.py
@@ -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)