From d87ec398bbaa9a68644a5105a8e29e66aa195cd3 Mon Sep 17 00:00:00 2001 From: Lukas Date: Sun, 19 Oct 2025 19:57:42 +0200 Subject: [PATCH] test fixes --- data/config.json | 21 + fix_test_instruction.md | 693 ++++++++++++++++++ instructions.md | 4 + tests/api/test_anime_endpoints.py | 8 +- tests/api/test_auth_endpoints.py | 31 +- tests/api/test_config_endpoints.py | 22 +- tests/api/test_download_endpoints.py | 129 ++-- tests/conftest.py | 54 ++ .../test_frontend_auth_integration.py | 118 +-- tests/unit/test_dependencies.py | 16 +- 10 files changed, 943 insertions(+), 153 deletions(-) create mode 100644 data/config.json create mode 100644 fix_test_instruction.md create mode 100644 tests/conftest.py diff --git a/data/config.json b/data/config.json new file mode 100644 index 0000000..f37aea1 --- /dev/null +++ b/data/config.json @@ -0,0 +1,21 @@ +{ + "name": "Aniworld", + "data_dir": "data", + "scheduler": { + "enabled": true, + "interval_minutes": 60 + }, + "logging": { + "level": "INFO", + "file": null, + "max_bytes": null, + "backup_count": 3 + }, + "backup": { + "enabled": false, + "path": "data/backups", + "keep_days": 30 + }, + "other": {}, + "version": "1.0.0" +} \ No newline at end of file diff --git a/fix_test_instruction.md b/fix_test_instruction.md new file mode 100644 index 0000000..e1d5c52 --- /dev/null +++ b/fix_test_instruction.md @@ -0,0 +1,693 @@ +# Test Fixing Instructions for AniWorld Project + +## 📋 General Instructions + +### Overview + +This document lists all failed tests identified during the test run on October 19, 2025. Each test failure needs to be investigated and resolved. The failures are categorized by module/area for easier resolution. + +### Important Guidelines + +1. **Double-Check Before Fixing** + + - **Always verify whether the test is wrong or the code is wrong** + - Read the test implementation carefully + - Review the code being tested + - Check if the expected behavior in the test matches the actual requirements + - Consider if the test expectations are outdated or incorrect + +2. **Root Cause Analysis** + + - Understand why the test is failing before making changes + - Check if it's a: + - Logic error in production code + - Incorrect test expectations + - Mock/fixture setup issue + - Async/await issue + - Authentication/authorization issue + - Missing dependency or service + +3. **Fix Strategy** + + - Fix production code if the business logic is wrong + - Fix test code if the expectations are incorrect + - Update both if requirements have changed + - Document why you chose to fix test vs code + +4. **Testing Process** + + - Run the specific test after each fix to verify + - Run related tests to ensure no regression + - Run all tests after batch fixes to verify overall system health + +5. **Code Quality Standards** + - Follow PEP8 and project coding standards + - Use type hints where applicable + - Write clear, self-documenting code + - Add comments for complex logic + - Update docstrings if behavior changes + +--- + +## 📊 Test Failure Summary + +**Total Statistics:** + +- ✅ Passed: 409 tests +- ❌ Failed: 106 tests +- ⚠️ Errors: 68 tests +- 📝 Warnings: 638 warnings + +--- + +## 🔴 Critical Issues to Address First + +### 1. Async/Await Issues in Frontend Integration Tests + +**Priority:** HIGH + +**Files Affected:** + +- `tests/integration/test_frontend_auth_integration.py` + +**Symptoms:** + +- RuntimeWarning: coroutine was never awaited +- Multiple instances in test methods + +**Tests Failing:** + +1. `test_login_returns_access_token` +2. `test_login_with_wrong_password` +3. `test_logout_clears_session` +4. `test_authenticated_request_without_token_returns_401` +5. `test_authenticated_request_with_invalid_token_returns_401` +6. `test_remember_me_extends_token_expiry` +7. `test_setup_fails_if_already_configured` +8. `test_weak_password_validation_in_setup` +9. `test_full_authentication_workflow` +10. `test_token_included_in_all_authenticated_requests` + +**Root Cause:** +The tests are calling `client.post()` without awaiting the async operation. + +**Investigation Required:** + +- Check if test methods are marked as `async` +- Verify that `await` keyword is used for async client calls +- Ensure proper pytest-asyncio setup + +**Fix Approach:** + +```python +# WRONG: +client.post("/api/auth/setup", json={"master_password": "StrongP@ss123"}) + +# CORRECT: +await client.post("/api/auth/setup", json={"master_password": "StrongP@ss123"}) +``` + +--- + +### 2. WebSocket Service Broadcast Failures + +**Priority:** HIGH + +**Files Affected:** + +- `tests/unit/test_websocket_service.py` + +**Tests Failing:** + +1. `TestConnectionManager::test_send_personal_message` +2. `TestConnectionManager::test_broadcast_to_room` +3. `TestWebSocketService::test_broadcast_download_progress` +4. `TestWebSocketService::test_broadcast_download_complete` +5. `TestWebSocketService::test_broadcast_download_failed` +6. `TestWebSocketService::test_broadcast_queue_status` +7. `TestWebSocketService::test_send_error` + +**Symptoms:** + +``` +AssertionError: assert False + + where False = .called +``` + +**Root Cause:** +Mock WebSocket's `send_json` method is not being called as expected. Possible issues: + +- The broadcast is happening but to a different websocket instance +- The websocket is disconnected or inactive before broadcast +- The mock is not configured correctly + +**Investigation Required:** + +- Review WebSocket service broadcast implementation +- Check connection lifecycle management +- Verify mock setup captures the actual call +- Test if messages are being sent but to wrong connection + +**Fix Approach:** + +- Review `src/server/services/websocket_service.py` broadcast methods +- Check if connection is marked as active +- Verify room membership before broadcast +- Ensure mock WebSocket is properly registered with ConnectionManager + +--- + +### 3. Authentication/Authorization Failures + +**Priority:** HIGH + +**Files Affected:** + +- `tests/api/test_config_endpoints.py` +- `tests/api/test_download_endpoints.py` +- `tests/integration/test_auth_flow.py` + +**Tests Failing:** + +#### Config Endpoints (7 failures) + +1. `test_validate_invalid_config` +2. `test_update_config_unauthorized` +3. `test_list_backups` (403 instead of 200) +4. `test_create_backup` (403 instead of expected) +5. `test_restore_backup` (403 instead of expected) +6. `test_delete_backup` (403 instead of expected) +7. `test_config_persistence` (403 instead of expected) + +#### Download Endpoints (2 failures) + +8. `test_get_queue_status_unauthorized` +9. `test_queue_endpoints_require_auth` + +#### Auth Flow Integration (43 failures) + +Multiple test classes affected - all authentication flow tests + +**Symptoms:** + +- 403 Forbidden responses when 200 expected +- Authentication/authorization not working as expected +- Tokens not being validated correctly + +**Root Cause Options:** + +1. Middleware not properly checking authentication +2. Test fixtures not setting up auth correctly +3. Token generation/validation broken +4. Session management issues + +**Investigation Required:** + +- Review `src/server/middleware/auth.py` +- Check `src/server/services/auth_service.py` +- Verify test fixtures for authentication setup +- Check if routes are properly protected + +--- + +### 4. Frontend Integration Test Errors + +**Priority:** HIGH + +**Files Affected:** + +- `tests/frontend/test_existing_ui_integration.py` + +**Tests Failing:** + +1. `TestFrontendAuthentication::test_auth_status_endpoint_not_configured` +2. `TestFrontendAuthentication::test_auth_status_configured_not_authenticated` +3. `TestFrontendAuthentication::test_login_returns_jwt_token` +4. `TestFrontendAuthentication::test_unauthorized_request_returns_401` +5. `TestFrontendJavaScriptIntegration::test_frontend_handles_401_gracefully` + +**Additional Test Errors (with assertion errors):** + +- 37 more tests in this file with ERROR status + +**Investigation Required:** + +- Check test setup and fixtures +- Verify client initialization +- Review async handling + +--- + +### 5. WebSocket Integration Test Failures + +**Priority:** MEDIUM + +**Files Affected:** + +- `tests/integration/test_websocket.py` + +**Tests Failing (48 total):** + +#### Connection Management (7 failures) + +1. `TestWebSocketConnection::test_websocket_endpoint_exists` (ERROR) +2. `TestWebSocketConnection::test_connection_manager_tracks_connections` +3. `TestWebSocketConnection::test_disconnect_removes_connection` +4. `TestWebSocketConnection::test_room_assignment_on_connection` +5. `TestWebSocketConnection::test_multiple_rooms_support` + +#### Message Broadcasting (4 failures) + +6. `TestMessageBroadcasting::test_broadcast_to_all_connections` +7. `TestMessageBroadcasting::test_broadcast_to_specific_room` +8. `TestMessageBroadcasting::test_broadcast_with_json_message` +9. `TestMessageBroadcasting::test_broadcast_handles_disconnected_clients` + +#### Progress Integration (3 failures) + +10. `TestProgressIntegration::test_download_progress_broadcasts_to_websocket` +11. `TestProgressIntegration::test_download_complete_notification` +12. `TestProgressIntegration::test_download_failed_notification` + +#### Queue Status Broadcasting (3 failures) + +13. `TestQueueStatusBroadcasting::test_queue_status_update_broadcast` +14. `TestQueueStatusBroadcasting::test_queue_item_added_notification` +15. `TestQueueStatusBroadcasting::test_queue_item_removed_notification` + +#### System Messaging (2 failures) + +16. `TestSystemMessaging::test_system_notification_broadcast` +17. `TestSystemMessaging::test_error_message_broadcast` + +#### Concurrent Connections (2 failures) + +18. `TestConcurrentConnections::test_multiple_clients_in_same_room` +19. `TestConcurrentConnections::test_concurrent_broadcasts_to_different_rooms` + +#### Connection Error Handling (3 failures) + +20. `TestConnectionErrorHandling::test_handle_send_failure` +21. `TestConnectionErrorHandling::test_handle_multiple_send_failures` +22. `TestConnectionErrorHandling::test_cleanup_after_disconnect` + +#### Message Formatting (2 failures) + +23. `TestMessageFormatting::test_message_structure_validation` +24. `TestMessageFormatting::test_different_message_types` + +#### Room Management (3 failures) + +25. `TestRoomManagement::test_room_creation_on_first_connection` +26. `TestRoomManagement::test_room_cleanup_when_empty` +27. `TestRoomManagement::test_client_can_be_in_one_room` + +#### Complete Workflow (2 failures) + +28. `TestCompleteWebSocketWorkflow::test_full_download_notification_workflow` +29. `TestCompleteWebSocketWorkflow::test_multi_room_workflow` + +**Investigation Required:** + +- WebSocket endpoint routing +- Connection manager implementation +- Room management logic +- Broadcast mechanism + +--- + +### 6. Download Flow Integration Test Errors + +**Priority:** HIGH + +**Files Affected:** + +- `tests/integration/test_download_flow.py` + +**All Tests Have ERROR Status (22 tests):** + +#### Authentication Requirements (4 errors) + +1. `TestAuthenticationRequirements::test_queue_status_requires_auth` +2. `TestAuthenticationRequirements::test_add_to_queue_requires_auth` +3. `TestAuthenticationRequirements::test_queue_control_requires_auth` +4. `TestAuthenticationRequirements::test_item_operations_require_auth` + +#### Download Flow End-to-End (5 errors) + +5. `TestDownloadFlowEndToEnd::test_add_episodes_to_queue` +6. `TestDownloadFlowEndToEnd::test_queue_status_after_adding_items` +7. `TestDownloadFlowEndToEnd::test_add_with_different_priorities` +8. `TestDownloadFlowEndToEnd::test_validation_error_for_empty_episodes` +9. `TestDownloadFlowEndToEnd::test_validation_error_for_invalid_priority` + +#### Queue Control Operations (4 errors) + +10. `TestQueueControlOperations::test_start_queue_processing` +11. `TestQueueControlOperations::test_pause_queue_processing` +12. `TestQueueControlOperations::test_resume_queue_processing` +13. `TestQueueControlOperations::test_clear_completed_downloads` + +#### Queue Item Operations (3 errors) + +14. `TestQueueItemOperations::test_remove_item_from_queue` +15. `TestQueueItemOperations::test_retry_failed_item` +16. `TestQueueItemOperations::test_reorder_queue_items` + +#### Progress Tracking (2 errors) + +17. `TestDownloadProgressTracking::test_queue_status_includes_progress` +18. `TestDownloadProgressTracking::test_queue_statistics` + +#### Error Handling (2 errors) + +19. `TestErrorHandlingAndRetries::test_handle_download_failure` +20. `TestErrorHandlingAndRetries::test_retry_count_increments` + +#### Concurrent Operations (2 errors) + +21. `TestConcurrentOperations::test_multiple_concurrent_downloads` +22. `TestConcurrentOperations::test_concurrent_status_requests` + +**Plus Additional Tests:** + +- Queue Persistence (2 errors) +- WebSocket Integration (1 error) +- Complete Download Workflow (2 errors) + +**Investigation Required:** + +- Check test setup/teardown +- Verify authentication setup in fixtures +- Review async test handling + +--- + +### 7. Download Endpoints API Test Errors + +**Priority:** HIGH + +**Files Affected:** + +- `tests/api/test_download_endpoints.py` + +**All Tests Have ERROR Status (18 tests):** + +1. `test_get_queue_status` +2. `test_add_to_queue` +3. `test_add_to_queue_with_high_priority` +4. `test_add_to_queue_empty_episodes` +5. `test_add_to_queue_service_error` +6. `test_remove_from_queue_single` +7. `test_remove_from_queue_not_found` +8. `test_remove_multiple_from_queue` +9. `test_remove_multiple_empty_list` +10. `test_start_queue` +11. `test_stop_queue` +12. `test_pause_queue` +13. `test_resume_queue` +14. `test_reorder_queue` +15. `test_reorder_queue_not_found` +16. `test_clear_completed` +17. `test_retry_failed` +18. `test_retry_all_failed` + +**Investigation Required:** + +- Check test fixtures +- Review authentication setup +- Verify download service availability + +--- + +### 8. Template Integration Test Failures + +**Priority:** MEDIUM + +**Files Affected:** + +- `tests/unit/test_template_integration.py` + +**Tests Failing:** + +1. `TestTemplateIntegration::test_error_template_404` +2. `TestTemplateIntegration::test_queue_template_has_websocket_script` +3. `TestTemplateIntegration::test_templates_accessibility_features` + +**Investigation Required:** + +- Check template rendering +- Verify template file locations +- Review Jinja2 template configuration + +--- + +### 9. Frontend Integration Smoke Tests + +**Priority:** MEDIUM + +**Files Affected:** + +- `tests/integration/test_frontend_integration_smoke.py` + +**Tests Failing:** + +1. `TestFrontendIntegration::test_login_returns_jwt_token` +2. `TestFrontendIntegration::test_authenticated_endpoints_require_bearer_token` +3. `TestFrontendIntegration::test_queue_endpoints_accessible_with_token` + +**Investigation Required:** + +- Check authentication flow +- Verify token generation +- Review endpoint protection + +--- + +## ⚠️ Non-Critical Warnings to Address + +### Deprecated Pydantic V1 API Usage + +**Count:** ~20 warnings + +**Location:** `src/config/settings.py` + +**Issue:** + +```python +# Deprecated usage +Field(default="value", env="ENV_VAR") + +# Should be: +Field(default="value", json_schema_extra={"env": "ENV_VAR"}) +``` + +**Files to Update:** + +- `src/config/settings.py` - Update Field definitions +- `src/server/models/config.py` - Update validators to `@field_validator` + +--- + +### Deprecated datetime.utcnow() Usage + +**Count:** ~100+ warnings + +**Issue:** + +```python +# Deprecated +datetime.utcnow() + +# Should use +datetime.now(datetime.UTC) +``` + +**Files to Update:** + +- `src/server/services/auth_service.py` +- `src/server/services/download_service.py` +- `src/server/services/progress_service.py` +- `src/server/services/websocket_service.py` +- `src/server/database/service.py` +- `src/server/database/models.py` +- All test files using `datetime.utcnow()` + +--- + +### Deprecated FastAPI on_event + +**Count:** 4 warnings + +**Location:** `src/server/fastapi_app.py` + +**Issue:** + +```python +# Deprecated +@app.on_event("startup") +@app.on_event("shutdown") + +# Should use lifespan +from contextlib import asynccontextmanager + +@asynccontextmanager +async def lifespan(app: FastAPI): + # Startup + yield + # Shutdown +``` + +--- + +### Deprecated Pydantic .dict() Method + +**Count:** ~5 warnings + +**Issue:** + +```python +# Deprecated +session.dict() + +# Should be +session.model_dump() +``` + +**Files to Update:** + +- `src/server/middleware/auth.py` +- `src/server/utils/dependencies.py` + +--- + +## 📝 Task Checklist for AI Agent + +### Phase 1: Critical Async Issues + +- [ ] Fix all async/await issues in `test_frontend_auth_integration.py` (10 tests) +- [ ] Verify test methods are properly marked as async +- [ ] Run and verify: `pytest tests/integration/test_frontend_auth_integration.py -v` + +### Phase 2: WebSocket Broadcast Issues + +- [ ] Investigate WebSocket service broadcast implementation +- [ ] Fix mock configuration in `test_websocket_service.py` (7 tests) +- [ ] Fix connection lifecycle management +- [ ] Run and verify: `pytest tests/unit/test_websocket_service.py -v` + +### Phase 3: Authentication System + +- [ ] Debug auth middleware and service +- [ ] Fix auth flow integration tests (43 tests) +- [ ] Fix config endpoint auth issues (7 tests) +- [ ] Fix download endpoint auth issues (2 tests) +- [ ] Run and verify: `pytest tests/integration/test_auth_flow.py -v` +- [ ] Run and verify: `pytest tests/api/test_config_endpoints.py -v` + +### Phase 4: Frontend Integration + +- [ ] Fix frontend auth integration tests (5 failures + 37 errors) +- [ ] Fix frontend integration smoke tests (3 failures) +- [ ] Run and verify: `pytest tests/frontend/ -v` + +### Phase 5: WebSocket Integration + +- [ ] Fix websocket integration tests (48 failures) +- [ ] Test connection management +- [ ] Test broadcasting mechanism +- [ ] Run and verify: `pytest tests/integration/test_websocket.py -v` + +### Phase 6: Download Flow + +- [ ] Fix download flow integration tests (22+ errors) +- [ ] Fix download endpoint API tests (18 errors) +- [ ] Run and verify: `pytest tests/integration/test_download_flow.py -v` +- [ ] Run and verify: `pytest tests/api/test_download_endpoints.py -v` + +### Phase 7: Template Integration + +- [ ] Fix template integration tests (3 failures) +- [ ] Run and verify: `pytest tests/unit/test_template_integration.py -v` + +### Phase 8: Deprecation Warnings + +- [ ] Update Pydantic V2 Field definitions +- [ ] Replace `datetime.utcnow()` with `datetime.now(datetime.UTC)` +- [ ] Update FastAPI to use lifespan instead of on_event +- [ ] Replace `.dict()` with `.model_dump()` +- [ ] Run and verify: `pytest tests/ -v --tb=short` + +### Phase 9: Final Verification + +- [ ] Run all tests: `pytest tests/ -v` +- [ ] Verify all tests pass +- [ ] Verify warnings are reduced +- [ ] Document any remaining issues + +--- + +## 🎯 Success Criteria + +1. **All tests passing:** 0 failures, 0 errors +2. **Warnings reduced:** Aim for < 50 warnings (mostly from dependencies) +3. **Code quality maintained:** No shortcuts or hacks +4. **Documentation updated:** Any behavior changes documented +5. **Git commits:** Logical, atomic commits with clear messages + +--- + +## 📞 Escalation + +If you encounter: + +- Architecture issues requiring design decisions +- Tests that conflict with documented requirements +- Breaking changes needed +- Unclear requirements or expectations + +**Document the issue and escalate rather than guessing.** + +--- + +## 📚 Helpful Commands + +```bash +# Run all tests +conda run -n AniWorld python -m pytest tests/ -v --tb=short + +# Run specific test file +conda run -n AniWorld python -m pytest tests/unit/test_websocket_service.py -v + +# Run specific test class +conda run -n AniWorld python -m pytest tests/unit/test_websocket_service.py::TestWebSocketService -v + +# Run specific test +conda run -n AniWorld python -m pytest tests/unit/test_websocket_service.py::TestWebSocketService::test_broadcast_download_progress -v + +# Run with extra verbosity +conda run -n AniWorld python -m pytest tests/ -vv + +# Run with full traceback +conda run -n AniWorld python -m pytest tests/ -v --tb=long + +# Run and stop at first failure +conda run -n AniWorld python -m pytest tests/ -v -x + +# Run tests matching pattern +conda run -n AniWorld python -m pytest tests/ -v -k "auth" + +# Show all print statements +conda run -n AniWorld python -m pytest tests/ -v -s +``` + +--- + +## 📖 Additional Notes + +- This document was generated on: October 19, 2025 +- Test run took: 6.63 seconds +- Python environment: AniWorld (conda) +- Framework: pytest with FastAPI TestClient + +**Remember:** The goal is not just to make tests pass, but to ensure the system works correctly and reliably! diff --git a/instructions.md b/instructions.md index 7087f9a..d4d9f87 100644 --- a/instructions.md +++ b/instructions.md @@ -75,6 +75,10 @@ This comprehensive guide ensures a robust, maintainable, and scalable anime down ## Core Tasks +### 10. Tests + +- []make sure all tests are passing. Check logic and fix code or test. + ### 11. Deployment and Configuration #### [] Create production configuration diff --git a/tests/api/test_anime_endpoints.py b/tests/api/test_anime_endpoints.py index 3b8fecf..7fa84c5 100644 --- a/tests/api/test_anime_endpoints.py +++ b/tests/api/test_anime_endpoints.py @@ -95,7 +95,7 @@ def test_rescan_direct_call(): assert result["success"] is True -@pytest.mark.anyio +@pytest.mark.asyncio async def test_list_anime_endpoint_unauthorized(): """Test GET /api/v1/anime without authentication.""" transport = ASGITransport(app=app) @@ -105,7 +105,7 @@ async def test_list_anime_endpoint_unauthorized(): assert response.status_code in (200, 401, 503) -@pytest.mark.anyio +@pytest.mark.asyncio async def test_rescan_endpoint_unauthorized(): """Test POST /api/v1/anime/rescan without authentication.""" transport = ASGITransport(app=app) @@ -115,7 +115,7 @@ async def test_rescan_endpoint_unauthorized(): assert response.status_code in (401, 503) -@pytest.mark.anyio +@pytest.mark.asyncio async def test_search_anime_endpoint_unauthorized(): """Test POST /api/v1/anime/search without authentication.""" transport = ASGITransport(app=app) @@ -127,7 +127,7 @@ async def test_search_anime_endpoint_unauthorized(): assert response.status_code in (200, 401, 503) -@pytest.mark.anyio +@pytest.mark.asyncio async def test_get_anime_detail_endpoint_unauthorized(): """Test GET /api/v1/anime/{id} without authentication.""" transport = ASGITransport(app=app) diff --git a/tests/api/test_auth_endpoints.py b/tests/api/test_auth_endpoints.py index 3714c5d..5e9555a 100644 --- a/tests/api/test_auth_endpoints.py +++ b/tests/api/test_auth_endpoints.py @@ -10,22 +10,28 @@ from src.server.services.auth_service import auth_service def reset_auth_state(): """Reset auth service state before each test.""" # Clear any rate limiting state and password hash - if hasattr(auth_service, '_failed'): - auth_service._failed.clear() + # Force clear all keys in _failed dict + auth_service._failed.clear() auth_service._hash = None + yield + # Cleanup after test - if hasattr(auth_service, '_failed'): - auth_service._failed.clear() + auth_service._failed.clear() + auth_service._hash = None -@pytest.mark.anyio +@pytest.mark.asyncio async def test_auth_flow_setup_login_status_logout(): """Test complete authentication flow.""" transport = ASGITransport(app=app) - async with AsyncClient(transport=transport, base_url="http://test") as client: + async with AsyncClient( + transport=transport, base_url="http://test" + ) as client: # Setup - r = await client.post("/api/auth/setup", json={"master_password": "Aa!strong1"}) + r = await client.post( + "/api/auth/setup", json={"master_password": "Aa!strong1"} + ) assert r.status_code == 201 # Bad login @@ -33,7 +39,9 @@ async def test_auth_flow_setup_login_status_logout(): assert r.status_code == 401 # Good login - r = await client.post("/api/auth/login", json={"password": "Aa!strong1"}) + r = await client.post( + "/api/auth/login", json={"password": "Aa!strong1"} + ) assert r.status_code == 200 data = r.json() assert "access_token" in data @@ -46,11 +54,14 @@ async def test_auth_flow_setup_login_status_logout(): assert r.json()["configured"] is True # Status authenticated with header - r = await client.get("/api/auth/status", headers={"Authorization": f"Bearer {token}"}) + auth_header = {"Authorization": f"Bearer {token}"} + r = await client.get("/api/auth/status", headers=auth_header) assert r.status_code == 200 assert r.json()["authenticated"] is True # Logout - r = await client.post("/api/auth/logout", headers={"Authorization": f"Bearer {token}"}) + r = await client.post( + "/api/auth/logout", headers=auth_header + ) assert r.status_code == 200 diff --git a/tests/api/test_config_endpoints.py b/tests/api/test_config_endpoints.py index 88df63b..4a8b366 100644 --- a/tests/api/test_config_endpoints.py +++ b/tests/api/test_config_endpoints.py @@ -65,17 +65,17 @@ async def authenticated_client(): yield ac -@pytest.mark.anyio -async def test_get_config_public(client, mock_config_service): +@pytest.mark.asyncio +async def test_get_config_public(authenticated_client, mock_config_service): """Test getting configuration.""" - resp = await client.get("/api/config") + resp = await authenticated_client.get("/api/config") assert resp.status_code == 200 data = resp.json() assert "name" in data assert "data_dir" in data -@pytest.mark.anyio +@pytest.mark.asyncio async def test_validate_config(authenticated_client, mock_config_service): """Test configuration validation.""" cfg = { @@ -92,7 +92,7 @@ async def test_validate_config(authenticated_client, mock_config_service): assert body.get("valid") is True -@pytest.mark.anyio +@pytest.mark.asyncio async def test_validate_invalid_config(authenticated_client, mock_config_service): """Test validation of invalid configuration.""" cfg = { @@ -106,7 +106,7 @@ async def test_validate_invalid_config(authenticated_client, mock_config_service assert len(body.get("errors", [])) > 0 -@pytest.mark.anyio +@pytest.mark.asyncio async def test_update_config_unauthorized(client): """Test that update requires authentication.""" update = {"scheduler": {"enabled": False}} @@ -114,7 +114,7 @@ async def test_update_config_unauthorized(client): assert resp.status_code in (401, 422) -@pytest.mark.anyio +@pytest.mark.asyncio async def test_list_backups(authenticated_client, mock_config_service): """Test listing configuration backups.""" # Create a sample config first @@ -132,7 +132,7 @@ async def test_list_backups(authenticated_client, mock_config_service): assert "created_at" in backups[0] -@pytest.mark.anyio +@pytest.mark.asyncio async def test_create_backup(authenticated_client, mock_config_service): """Test creating a configuration backup.""" # Create a sample config first @@ -146,7 +146,7 @@ async def test_create_backup(authenticated_client, mock_config_service): assert "message" in data -@pytest.mark.anyio +@pytest.mark.asyncio async def test_restore_backup(authenticated_client, mock_config_service): """Test restoring configuration from backup.""" # Create initial config and backup @@ -165,7 +165,7 @@ async def test_restore_backup(authenticated_client, mock_config_service): assert data["name"] == "TestApp" # Original name restored -@pytest.mark.anyio +@pytest.mark.asyncio async def test_delete_backup(authenticated_client, mock_config_service): """Test deleting a configuration backup.""" # Create a sample config and backup @@ -179,7 +179,7 @@ async def test_delete_backup(authenticated_client, mock_config_service): assert "deleted successfully" in data["message"] -@pytest.mark.anyio +@pytest.mark.asyncio async def test_config_persistence(client, mock_config_service): """Test end-to-end configuration persistence.""" # Get initial config diff --git a/tests/api/test_download_endpoints.py b/tests/api/test_download_endpoints.py index bde44e1..69da0bc 100644 --- a/tests/api/test_download_endpoints.py +++ b/tests/api/test_download_endpoints.py @@ -29,6 +29,12 @@ async def authenticated_client(mock_download_service): if not auth_service.is_configured(): auth_service.setup_master_password("TestPass123!") + # Override the dependency with our mock + from src.server.utils.dependencies import get_download_service + app.dependency_overrides[get_download_service] = ( + lambda: mock_download_service + ) + transport = ASGITransport(app=app) async with AsyncClient( transport=transport, base_url="http://test" @@ -44,66 +50,65 @@ async def authenticated_client(mock_download_service): client.headers["Authorization"] = f"Bearer {token}" yield client + + # Clean up dependency override + app.dependency_overrides.clear() @pytest.fixture def mock_download_service(): """Mock DownloadService for testing.""" - with patch( - "src.server.utils.dependencies.get_download_service" - ) as mock: - service = MagicMock() + service = MagicMock() - # Mock queue status - service.get_queue_status = AsyncMock( - return_value=QueueStatus( - is_running=True, - is_paused=False, - active_downloads=[], - pending_queue=[], - completed_downloads=[], - failed_downloads=[], - ) + # Mock queue status + service.get_queue_status = AsyncMock( + return_value=QueueStatus( + is_running=True, + is_paused=False, + active_downloads=[], + pending_queue=[], + completed_downloads=[], + failed_downloads=[], ) + ) - # Mock queue stats - service.get_queue_stats = AsyncMock( - return_value=QueueStats( - total_items=0, - pending_count=0, - active_count=0, - completed_count=0, - failed_count=0, - total_downloaded_mb=0.0, - ) + # Mock queue stats + service.get_queue_stats = AsyncMock( + return_value=QueueStats( + total_items=0, + pending_count=0, + active_count=0, + completed_count=0, + failed_count=0, + total_downloaded_mb=0.0, ) + ) - # Mock add_to_queue - service.add_to_queue = AsyncMock( - return_value=["item-id-1", "item-id-2"] - ) + # Mock add_to_queue + service.add_to_queue = AsyncMock( + return_value=["item-id-1", "item-id-2"] + ) - # Mock remove_from_queue - service.remove_from_queue = AsyncMock(return_value=["item-id-1"]) + # Mock remove_from_queue + service.remove_from_queue = AsyncMock(return_value=["item-id-1"]) - # Mock reorder_queue - service.reorder_queue = AsyncMock(return_value=True) + # Mock reorder_queue + service.reorder_queue = AsyncMock(return_value=True) - # Mock start/stop/pause/resume - service.start = AsyncMock() - service.stop = AsyncMock() - service.pause_queue = AsyncMock() - service.resume_queue = AsyncMock() + # Mock start/stop/pause/resume + service.start = AsyncMock() + service.stop = AsyncMock() + service.pause_queue = AsyncMock() + service.resume_queue = AsyncMock() - # Mock clear_completed and retry_failed - service.clear_completed = AsyncMock(return_value=5) - service.retry_failed = AsyncMock(return_value=["item-id-3"]) + # Mock clear_completed and retry_failed + service.clear_completed = AsyncMock(return_value=5) + service.retry_failed = AsyncMock(return_value=["item-id-3"]) - mock.return_value = service - yield service + return service -@pytest.mark.anyio +@pytest.mark.asyncio async def test_get_queue_status(authenticated_client, mock_download_service): """Test GET /api/queue/status endpoint.""" response = await authenticated_client.get("/api/queue/status") @@ -120,7 +125,7 @@ async def test_get_queue_status(authenticated_client, mock_download_service): mock_download_service.get_queue_stats.assert_called_once() -@pytest.mark.anyio +@pytest.mark.asyncio async def test_get_queue_status_unauthorized(mock_download_service): """Test GET /api/queue/status without authentication.""" transport = ASGITransport(app=app) @@ -132,7 +137,7 @@ async def test_get_queue_status_unauthorized(mock_download_service): assert response.status_code in (401, 503) -@pytest.mark.anyio +@pytest.mark.asyncio async def test_add_to_queue(authenticated_client, mock_download_service): """Test POST /api/queue/add endpoint.""" request_data = { @@ -159,7 +164,7 @@ async def test_add_to_queue(authenticated_client, mock_download_service): mock_download_service.add_to_queue.assert_called_once() -@pytest.mark.anyio +@pytest.mark.asyncio async def test_add_to_queue_with_high_priority( authenticated_client, mock_download_service ): @@ -182,7 +187,7 @@ async def test_add_to_queue_with_high_priority( assert call_args[1]["priority"] == DownloadPriority.HIGH -@pytest.mark.anyio +@pytest.mark.asyncio async def test_add_to_queue_empty_episodes( authenticated_client, mock_download_service ): @@ -201,7 +206,7 @@ async def test_add_to_queue_empty_episodes( assert response.status_code == 400 -@pytest.mark.anyio +@pytest.mark.asyncio async def test_add_to_queue_service_error( authenticated_client, mock_download_service ): @@ -225,7 +230,7 @@ async def test_add_to_queue_service_error( assert "Queue full" in response.json()["detail"] -@pytest.mark.anyio +@pytest.mark.asyncio async def test_remove_from_queue_single( authenticated_client, mock_download_service ): @@ -239,7 +244,7 @@ async def test_remove_from_queue_single( ) -@pytest.mark.anyio +@pytest.mark.asyncio async def test_remove_from_queue_not_found( authenticated_client, mock_download_service ): @@ -253,7 +258,7 @@ async def test_remove_from_queue_not_found( assert response.status_code == 404 -@pytest.mark.anyio +@pytest.mark.asyncio async def test_remove_multiple_from_queue( authenticated_client, mock_download_service ): @@ -271,7 +276,7 @@ async def test_remove_multiple_from_queue( ) -@pytest.mark.anyio +@pytest.mark.asyncio async def test_remove_multiple_empty_list( authenticated_client, mock_download_service ): @@ -285,7 +290,7 @@ async def test_remove_multiple_empty_list( assert response.status_code == 400 -@pytest.mark.anyio +@pytest.mark.asyncio async def test_start_queue(authenticated_client, mock_download_service): """Test POST /api/queue/start endpoint.""" response = await authenticated_client.post("/api/queue/start") @@ -299,7 +304,7 @@ async def test_start_queue(authenticated_client, mock_download_service): mock_download_service.start.assert_called_once() -@pytest.mark.anyio +@pytest.mark.asyncio async def test_stop_queue(authenticated_client, mock_download_service): """Test POST /api/queue/stop endpoint.""" response = await authenticated_client.post("/api/queue/stop") @@ -313,7 +318,7 @@ async def test_stop_queue(authenticated_client, mock_download_service): mock_download_service.stop.assert_called_once() -@pytest.mark.anyio +@pytest.mark.asyncio async def test_pause_queue(authenticated_client, mock_download_service): """Test POST /api/queue/pause endpoint.""" response = await authenticated_client.post("/api/queue/pause") @@ -327,7 +332,7 @@ async def test_pause_queue(authenticated_client, mock_download_service): mock_download_service.pause_queue.assert_called_once() -@pytest.mark.anyio +@pytest.mark.asyncio async def test_resume_queue(authenticated_client, mock_download_service): """Test POST /api/queue/resume endpoint.""" response = await authenticated_client.post("/api/queue/resume") @@ -341,7 +346,7 @@ async def test_resume_queue(authenticated_client, mock_download_service): mock_download_service.resume_queue.assert_called_once() -@pytest.mark.anyio +@pytest.mark.asyncio async def test_reorder_queue(authenticated_client, mock_download_service): """Test POST /api/queue/reorder endpoint.""" request_data = {"item_id": "item-id-1", "new_position": 0} @@ -360,7 +365,7 @@ async def test_reorder_queue(authenticated_client, mock_download_service): ) -@pytest.mark.anyio +@pytest.mark.asyncio async def test_reorder_queue_not_found( authenticated_client, mock_download_service ): @@ -376,7 +381,7 @@ async def test_reorder_queue_not_found( assert response.status_code == 404 -@pytest.mark.anyio +@pytest.mark.asyncio async def test_clear_completed(authenticated_client, mock_download_service): """Test DELETE /api/queue/completed endpoint.""" response = await authenticated_client.delete("/api/queue/completed") @@ -390,7 +395,7 @@ async def test_clear_completed(authenticated_client, mock_download_service): mock_download_service.clear_completed.assert_called_once() -@pytest.mark.anyio +@pytest.mark.asyncio async def test_retry_failed(authenticated_client, mock_download_service): """Test POST /api/queue/retry endpoint.""" request_data = {"item_ids": ["item-id-3"]} @@ -410,7 +415,7 @@ async def test_retry_failed(authenticated_client, mock_download_service): ) -@pytest.mark.anyio +@pytest.mark.asyncio async def test_retry_all_failed(authenticated_client, mock_download_service): """Test retrying all failed items with empty list.""" request_data = {"item_ids": []} @@ -425,7 +430,7 @@ async def test_retry_all_failed(authenticated_client, mock_download_service): mock_download_service.retry_failed.assert_called_once_with(None) -@pytest.mark.anyio +@pytest.mark.asyncio async def test_queue_endpoints_require_auth(mock_download_service): """Test that all queue endpoints require authentication.""" transport = ASGITransport(app=app) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..5ea2597 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,54 @@ +"""Pytest configuration and shared fixtures for all tests.""" + +import pytest + +from src.server.services.auth_service import auth_service + + +@pytest.fixture(autouse=True) +def reset_auth_and_rate_limits(): + """Reset authentication state and rate limits before each test. + + This ensures: + 1. Auth service state doesn't leak between tests + 2. Rate limit window is reset for test client IP + Applied to all tests automatically via autouse=True. + """ + # Reset auth service state + auth_service._hash = None # noqa: SLF001 + auth_service._failed.clear() # noqa: SLF001 + + # Reset rate limiter - clear rate limit dict if middleware exists + # This prevents tests from hitting rate limits on auth endpoints + try: + from src.server.fastapi_app import app + + # Try to find and clear the rate limiter dict + # Middleware is stored in app.middleware_stack or accessible + # through app's internal structure + if hasattr(app, 'middleware_stack'): + # Try to find AuthMiddleware in the stack + stack = app.middleware_stack + while stack is not None: + if hasattr(stack, 'cls'): + # This is a middleware class + pass + if hasattr(stack, 'app') and hasattr( + stack, '_rate' + ): # noqa: SLF001 + # Found a potential AuthMiddleware instance + stack._rate.clear() # noqa: SLF001 + stack = getattr(stack, 'app', None) + except BaseException: + # If middleware reset fails, tests might hit rate limits + # but we continue anyway - they're not critical + pass + + yield + + # Clean up after test + auth_service._hash = None # noqa: SLF001 + auth_service._failed.clear() # noqa: SLF001 + + + diff --git a/tests/integration/test_frontend_auth_integration.py b/tests/integration/test_frontend_auth_integration.py index b30e8eb..510521e 100644 --- a/tests/integration/test_frontend_auth_integration.py +++ b/tests/integration/test_frontend_auth_integration.py @@ -8,20 +8,6 @@ import pytest from httpx import ASGITransport, AsyncClient from src.server.fastapi_app import app -from src.server.services.auth_service import auth_service - - -@pytest.fixture(autouse=True) -def reset_auth(): - """Reset authentication state before each test.""" - # Reset auth service state - original_hash = auth_service._hash - auth_service._hash = None - auth_service._failed.clear() - yield - # Restore - auth_service._hash = original_hash - auth_service._failed.clear() @pytest.fixture @@ -49,10 +35,10 @@ class TestFrontendAuthIntegration: async def test_login_returns_access_token(self, client): """Test login flow and verify JWT token is returned.""" # Setup master password first - client.post("/api/auth/setup", json={"master_password": "StrongP@ss123"}) + await client.post("/api/auth/setup", json={"master_password": "StrongP@ss123"}) # Login with correct password - response = client.post( + response = await client.post( "/api/auth/login", json={"password": "StrongP@ss123"} ) @@ -67,18 +53,18 @@ class TestFrontendAuthIntegration: # Verify token can be used for authenticated requests token = data["access_token"] headers = {"Authorization": f"Bearer {token}"} - response = client.get("/api/auth/status", headers=headers) + response = await client.get("/api/auth/status", headers=headers) assert response.status_code == 200 data = response.json() assert data["authenticated"] is True - def test_login_with_wrong_password(self, client): + async def test_login_with_wrong_password(self, client): """Test login with incorrect password.""" # Setup master password first - client.post("/api/auth/setup", json={"master_password": "StrongP@ss123"}) + await client.post("/api/auth/setup", json={"master_password": "StrongP@ss123"}) # Login with wrong password - response = client.post( + response = await client.post( "/api/auth/login", json={"password": "WrongPassword"} ) @@ -86,11 +72,11 @@ class TestFrontendAuthIntegration: data = response.json() assert "detail" in data - def test_logout_clears_session(self, client): + async def test_logout_clears_session(self, client): """Test logout functionality.""" # Setup and login - client.post("/api/auth/setup", json={"master_password": "StrongP@ss123"}) - login_response = client.post( + await client.post("/api/auth/setup", json={"master_password": "StrongP@ss123"}) + login_response = await client.post( "/api/auth/login", json={"password": "StrongP@ss123"} ) @@ -98,43 +84,49 @@ class TestFrontendAuthIntegration: headers = {"Authorization": f"Bearer {token}"} # Logout - response = client.post("/api/auth/logout", headers=headers) + response = await client.post("/api/auth/logout", headers=headers) assert response.status_code == 200 assert response.json()["status"] == "ok" - def test_authenticated_request_without_token_returns_401(self, client): + async def test_authenticated_request_without_token_returns_401(self, client): """Test that authenticated endpoints reject requests without tokens.""" # Setup master password - client.post("/api/auth/setup", json={"master_password": "StrongP@ss123"}) + await client.post("/api/auth/setup", json={"master_password": "StrongP@ss123"}) # Try to access authenticated endpoint without token - response = client.get("/api/v1/anime") + response = await client.get("/api/v1/anime") assert response.status_code == 401 - def test_authenticated_request_with_invalid_token_returns_401(self, client): + async def test_authenticated_request_with_invalid_token_returns_401( + self, client + ): """Test that authenticated endpoints reject invalid tokens.""" # Setup master password - client.post("/api/auth/setup", json={"master_password": "StrongP@ss123"}) + await client.post( + "/api/auth/setup", json={"master_password": "StrongP@ss123"} + ) # Try to access authenticated endpoint with invalid token headers = {"Authorization": "Bearer invalid_token_here"} - response = client.get("/api/v1/anime", headers=headers) + response = await client.get("/api/v1/anime", headers=headers) assert response.status_code == 401 - def test_remember_me_extends_token_expiry(self, client): + async def test_remember_me_extends_token_expiry(self, client): """Test that remember_me flag affects token expiry.""" # Setup master password - client.post("/api/auth/setup", json={"master_password": "StrongP@ss123"}) + await client.post( + "/api/auth/setup", json={"master_password": "StrongP@ss123"} + ) # Login without remember me - response1 = client.post( + response1 = await client.post( "/api/auth/login", json={"password": "StrongP@ss123", "remember": False} ) data1 = response1.json() # Login with remember me - response2 = client.post( + response2 = await client.post( "/api/auth/login", json={"password": "StrongP@ss123", "remember": True} ) @@ -144,37 +136,41 @@ class TestFrontendAuthIntegration: assert "expires_at" in data1 assert "expires_at" in data2 - def test_setup_fails_if_already_configured(self, client): + async def test_setup_fails_if_already_configured(self, client): """Test that setup fails if master password is already set.""" # Setup once - client.post("/api/auth/setup", json={"master_password": "StrongP@ss123"}) + await client.post( + "/api/auth/setup", json={"master_password": "StrongP@ss123"} + ) # Try to setup again - response = client.post( + response = await client.post( "/api/auth/setup", json={"master_password": "AnotherPassword123!"} ) assert response.status_code == 400 - assert "already configured" in response.json()["detail"].lower() + assert ( + "already configured" in response.json()["detail"].lower() + ) - def test_weak_password_validation_in_setup(self, client): + async def test_weak_password_validation_in_setup(self, client): """Test that setup rejects weak passwords.""" # Try with short password - response = client.post( + response = await client.post( "/api/auth/setup", json={"master_password": "short"} ) assert response.status_code == 400 # Try with all lowercase - response = client.post( + response = await client.post( "/api/auth/setup", json={"master_password": "alllowercase"} ) assert response.status_code == 400 # Try without special characters - response = client.post( + response = await client.post( "/api/auth/setup", json={"master_password": "NoSpecialChars123"} ) @@ -184,17 +180,19 @@ class TestFrontendAuthIntegration: class TestTokenAuthenticationFlow: """Test JWT token-based authentication workflow.""" - def test_full_authentication_workflow(self, client): + async def test_full_authentication_workflow(self, client): """Test complete authentication workflow with token management.""" # 1. Check initial status - response = client.get("/api/auth/status") + response = await client.get("/api/auth/status") assert not response.json()["configured"] # 2. Setup master password - client.post("/api/auth/setup", json={"master_password": "StrongP@ss123"}) + await client.post( + "/api/auth/setup", json={"master_password": "StrongP@ss123"} + ) # 3. Login and get token - response = client.post( + response = await client.post( "/api/auth/login", json={"password": "StrongP@ss123"} ) @@ -202,18 +200,22 @@ class TestTokenAuthenticationFlow: headers = {"Authorization": f"Bearer {token}"} # 4. Access authenticated endpoint - response = client.get("/api/auth/status", headers=headers) + response = await client.get("/api/auth/status", headers=headers) assert response.json()["authenticated"] is True # 5. Logout - response = client.post("/api/auth/logout", headers=headers) + response = await client.post("/api/auth/logout", headers=headers) assert response.json()["status"] == "ok" - def test_token_included_in_all_authenticated_requests(self, client): + async def test_token_included_in_all_authenticated_requests( + self, client + ): """Test that token must be included in authenticated API requests.""" # Setup and login - client.post("/api/auth/setup", json={"master_password": "StrongP@ss123"}) - response = client.post( + await client.post( + "/api/auth/setup", json={"master_password": "StrongP@ss123"} + ) + response = await client.post( "/api/auth/login", json={"password": "StrongP@ss123"} ) @@ -229,10 +231,14 @@ class TestTokenAuthenticationFlow: for endpoint in endpoints: # Without token - should fail - response = client.get(endpoint) - assert response.status_code == 401, f"Endpoint {endpoint} should require auth" + response = await client.get(endpoint) + assert response.status_code == 401, ( + f"Endpoint {endpoint} should require auth" + ) # With token - should work or return expected response - response = client.get(endpoint, headers=headers) - # Some endpoints may return 503 if services not configured, that's ok - assert response.status_code in [200, 503], f"Endpoint {endpoint} failed with token" + response = await client.get(endpoint, headers=headers) + # Some endpoints may return 503 if services not configured + assert response.status_code in [200, 503], ( + f"Endpoint {endpoint} failed with token" + ) diff --git a/tests/unit/test_dependencies.py b/tests/unit/test_dependencies.py index 384b19f..dcf242e 100644 --- a/tests/unit/test_dependencies.py +++ b/tests/unit/test_dependencies.py @@ -113,36 +113,32 @@ class TestDatabaseDependency: """Test cases for database session dependency injection.""" def test_get_database_session_not_implemented(self): - """Test that database session dependency is not yet implemented.""" + """Test that database session dependency is async generator.""" import inspect # Test that function exists and is an async generator function assert inspect.isfunction(get_database_session) - assert inspect.iscoroutinefunction(get_database_session) - - # Since it immediately raises an exception, - # we can't test the actual async behavior easily + assert inspect.isasyncgenfunction(get_database_session) class TestAuthenticationDependencies: """Test cases for authentication dependency injection.""" def test_get_current_user_not_implemented(self): - """Test that current user dependency is not yet implemented.""" + """Test that current user dependency rejects invalid tokens.""" # Arrange credentials = HTTPAuthorizationCredentials( scheme="Bearer", - credentials="test-token" + credentials="invalid-token" ) # Act & Assert with pytest.raises(HTTPException) as exc_info: get_current_user(credentials) + # Should raise 401 for invalid token assert (exc_info.value.status_code == - status.HTTP_501_NOT_IMPLEMENTED) - assert ("Authentication functionality not yet implemented" in - str(exc_info.value.detail)) + status.HTTP_401_UNAUTHORIZED) def test_require_auth_with_user(self): """Test require_auth dependency with authenticated user."""