Files
Aniworld/Docs/TESTING.md
Lukas 5f46d2e802 feat: add folder naming service to fix missing years in anime folder names
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
2026-07-26 21:45:08 +02:00

2.5 KiB

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
# 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:

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
  1. Coverage Requirements
  2. CI/CD Integration
  3. Writing Good Tests
    • Arrange-Act-Assert pattern
    • Test isolation
    • Edge cases
  4. Common Pitfalls to Avoid