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)