test fixes

This commit is contained in:
Lukas 2025-10-19 19:57:42 +02:00
parent d698ae50a2
commit d87ec398bb
10 changed files with 943 additions and 153 deletions

21
data/config.json Normal file
View File

@ -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"
}

693
fix_test_instruction.md Normal file
View File

@ -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 = <AsyncMock name='mock.send_json' id='...'>.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!

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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)

54
tests/conftest.py Normal file
View File

@ -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

View File

@ -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"
)

View File

@ -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."""