Runs after NFO refresh during scheduled rescans. Renames folders that are missing a year (e.g. 'Naruto' → 'Naruto (1999)') using the year from the database record. Safety: _build_target_folder() always strips any existing year suffix first, preventing double/triple year accumulation like 'Naruto (1999) (1999) (1999)'. Changes: - New FolderNamingService (folder_naming_service.py) with safe target name construction, DB update, and in-memory cache update - New SchedulerConfig field: folder_naming_after_nfo_scan (default True) - Integrated as step 3 in scheduler _perform_rescan() after NFO scan - Runtime UI: existing 'folder-scan-enabled' checkbox in index.html now wired to toggle the feature (app.js + scheduler-config.js) - Setup screen: new checkbox in setup.html Scheduler Settings section - API: scheduler config endpoint returns all scan toggles - Tests: 39 unit tests covering static helpers, rename logic, safety guard, and integration cases (folder_naming_service.py) - Docs: testing guide updated with FolderNamingService examples
76 lines
2.5 KiB
Markdown
76 lines
2.5 KiB
Markdown
|
|
### Testing FolderNamingService
|
|
|
|
FolderNamingService fixes missing years in anime folder names after NFO refresh. Test file: `tests/unit/test_folder_naming_service.py`.
|
|
|
|
Key methods tested:
|
|
- `_build_target_folder()` — constructs safe "Title (YYYY)" names, strips existing year suffix first (prevents double-year like "Naruto (1999) (1999)")
|
|
- `_folder_has_year()` — detects existing `(YYYY)` pattern
|
|
- `_extract_year_from_folder_name()` — parses `(YYYY)` suffix
|
|
- `_extract_title_from_folder_name()` — strips year suffix
|
|
|
|
```python
|
|
# Safe: repeated calls never accumulate years
|
|
r1 = FolderNamingService._build_target_folder("Naruto", 1999)
|
|
r2 = FolderNamingService._build_target_folder(r1, 1999)
|
|
assert r1 == r2 == "Naruto (1999)"
|
|
|
|
# Safe: existing year is replaced, not appended
|
|
result = FolderNamingService._build_target_folder("Naruto (2020)", 1999)
|
|
assert result == "Naruto (1999)"
|
|
assert "2020" not in result
|
|
```
|
|
|
|
The service is also tested end-to-end with mocked filesystem and database:
|
|
- Renames folder and updates DB when year is missing from folder name
|
|
- Skips rename when folder already has a year
|
|
- Skips rename when DB has no year
|
|
- Skips when target folder already exists on disk
|
|
- Safety guard detects and skips if target folder year mismatches DB year
|
|
|
|
### Mocking aiohttp Sessions
|
|
|
|
When testing code that uses `aiohttp.ClientSession`:
|
|
|
|
```python
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
from aiohttp import ClientSession
|
|
|
|
# Mock aiohttp session for testing
|
|
class MockAiohttpSession:
|
|
def __init__(self):
|
|
self.closed = False
|
|
|
|
async def close(self):
|
|
self.closed = True
|
|
|
|
def get(self, url, **kwargs):
|
|
mock_response = AsyncMock()
|
|
mock_response.status = 200
|
|
mock_response.json = AsyncMock(return_value={"data": "test"})
|
|
mock_response.__aenter__ = AsyncMock(return_value=mock_response)
|
|
mock_response.__aexit__ = AsyncMock(return_value=None)
|
|
return mock_response
|
|
|
|
# Use in fixture
|
|
@pytest.fixture
|
|
async def mock_tmdb_session():
|
|
session = MockAiohttpSession()
|
|
yield session
|
|
# Cleanup verification
|
|
assert session.closed, "Session was not closed"
|
|
```
|
|
|
|
**Key points:**
|
|
- Always verify `session.closed` is `True` after context manager exits
|
|
- Mock `__aenter__` and `__aexit__` for response context managers
|
|
- Set `closed = False` on mock session for unclosed warning tests
|
|
|
|
7. Coverage Requirements
|
|
8. CI/CD Integration
|
|
9. Writing Good Tests
|
|
- Arrange-Act-Assert pattern
|
|
- Test isolation
|
|
- Edge cases
|
|
10. Common Pitfalls to Avoid
|