Files
shelfmark/tests/prowlarr/test_cache.py
T
Alex a99dc1501d Prowlarr and IRC sources, Google Books, book series support + more (#361)
## Headline features 

### Prowlarr plugin - search trackers and download usenet/torrent books

- Search any usenet/torrent tracker via Prowlarr, returns books within
Universal search
- Configure download clients in the app settings (Qbittorrent, Deluge,
Transmission, NZBget, SABnzbd)
- Unified download and file handling within the app, same as AA. 

### IRC plugin 
- Search IRCHighway #ebooks channel for books and download right in the
app.
- No setup needed
- Credit to OpenBooks for the broad idea and inspiration for best
practices for ebook-specific search and download.

### Google Books Metadata Provider
- Create a Google Cloud API key and use Google Books as a metadata
provider
- Not the best source (Hardcover is still recommended), but another
option and further redundancy for universal search

### Book series support
  - New "Series" search field in Hardcover provider
  - "Series order" sort option - lists books in reading order
  - "View Series" button in book details modal to search the full series
  - Series info display (e.g., "3 of 12 in The Wheel of Time")

## Others: 

- Better format filtering, helpful errors when formats rejected (e.g.,
"Found 3 ebooks but format not supported (.pdf). Enable in Settings >
Formats."
- Directory processing - Handles multi-file torrent/usenet downloads
properly
- Expand search toggle - Skip ISBN search to find more editions
- Filtered authors - Uses primary authors only (excludes
translators/narrators) for better search results
- Language multi-select - Filter releases by multiple languages

 Docker / Build / Testing

  - pip cache mounts - Faster Docker builds via BuildKit cache
  - npm cache mounts - Faster frontend builds
  - APT cleanup - Smaller final image size
  - Added make restart command for quick restarts without rebuild
- New pytest-based test framework with proper configuration
(pyproject.toml)
- Unit tests for all download clients (qBittorrent, Transmission,
Deluge, NZBGet, SABnzbd)
  - Bencode parsing tests
  - Cache tests
  - Integration tests for Prowlarr handler
  - E2E test framework
2025-12-27 14:59:06 +00:00

122 lines
4.2 KiB
Python

"""
Tests for the Prowlarr release cache.
"""
import time
import pytest
# Import the cache module
from cwa_book_downloader.release_sources.prowlarr import cache
class TestProwlarrCache:
"""Tests for release caching functionality."""
def setup_method(self):
"""Clear cache before each test."""
# Clear the internal cache
cache._cache.clear()
def test_cache_release_stores_data(self):
"""Test that cache_release stores data correctly."""
release_data = {"title": "Test Book", "size": 1024}
cache.cache_release("test-id", release_data)
assert "test-id" in cache._cache
stored_data, timestamp = cache._cache["test-id"]
assert stored_data == release_data
assert isinstance(timestamp, float)
def test_get_release_returns_cached_data(self):
"""Test that get_release returns cached data."""
release_data = {"title": "Test Book", "format": "epub"}
cache.cache_release("get-test-id", release_data)
result = cache.get_release("get-test-id")
assert result == release_data
def test_get_release_returns_none_for_missing_id(self):
"""Test that get_release returns None for non-existent IDs."""
result = cache.get_release("non-existent-id")
assert result is None
def test_get_release_returns_none_for_expired(self, monkeypatch):
"""Test that get_release returns None for expired entries."""
release_data = {"title": "Old Book"}
cache.cache_release("expired-id", release_data)
# Make the entry appear old by modifying the TTL check
original_ttl = cache.RELEASE_CACHE_TTL
monkeypatch.setattr(cache, "RELEASE_CACHE_TTL", 0)
# Wait a tiny bit to ensure expiration
time.sleep(0.01)
result = cache.get_release("expired-id")
assert result is None
# Restore TTL
monkeypatch.setattr(cache, "RELEASE_CACHE_TTL", original_ttl)
def test_remove_release_deletes_entry(self):
"""Test that remove_release removes cached entries."""
cache.cache_release("remove-id", {"title": "Book to Remove"})
assert "remove-id" in cache._cache
cache.remove_release("remove-id")
assert "remove-id" not in cache._cache
def test_remove_release_ignores_missing_ids(self):
"""Test that remove_release doesn't raise for missing IDs."""
# Should not raise any exception
cache.remove_release("never-existed")
def test_cleanup_expired_removes_old_entries(self, monkeypatch):
"""Test that cleanup_expired removes old entries."""
# Add some entries
cache.cache_release("keep-id", {"title": "Keep This"})
cache.cache_release("old-id", {"title": "Remove This"})
# Make old-id appear expired by manipulating its timestamp
cache._cache["old-id"] = ({"title": "Remove This"}, time.time() - 7200) # 2 hours old
removed = cache.cleanup_expired()
assert removed == 1
assert "keep-id" in cache._cache
assert "old-id" not in cache._cache
def test_get_cache_stats_returns_correct_info(self):
"""Test that get_cache_stats returns accurate information."""
cache.cache_release("stat-1", {"title": "Book 1"})
cache.cache_release("stat-2", {"title": "Book 2"})
stats = cache.get_cache_stats()
assert stats["size"] == 2
assert "stat-1" in stats["entries"]
assert "stat-2" in stats["entries"]
def test_cache_is_thread_safe(self):
"""Test that cache operations are thread-safe."""
import threading
errors = []
def cache_operations():
try:
for i in range(100):
cache.cache_release(f"thread-{threading.current_thread().name}-{i}", {"data": i})
cache.get_release(f"thread-{threading.current_thread().name}-{i}")
except Exception as e:
errors.append(e)
threads = [threading.Thread(target=cache_operations, name=f"T{i}") for i in range(5)]
for t in threads:
t.start()
for t in threads:
t.join()
assert len(errors) == 0, f"Thread errors: {errors}"