Files
shelfmark/tests/prowlarr/test_bencode.py
Alex 7fdf55f5fd Enhancements to ABB handling (#621)
- Migrate download client handling from /prowlarr to /download. Moves
all torrent/usenet handling to app-level and gives ABB this
functionality.
- ABB Scraper now uses shared HTTP infrastructure instead of raw
requests, adding retry and proxy support
- Added author, age and bitrate info to ABB search results
- Added "best match" sorting option for releases
- Added size and bitrate sorting options for ABB
- Removed bundled default ABB hostname, must be configured by the user
- Added URL normalisation for ABB hostname
- Rearranged settings UI, moved download clients to its own section. 
- More tests
2026-02-16 14:52:46 +00:00

199 lines
6.7 KiB
Python

"""
Tests for bencode encoding/decoding in the torrent utilities.
"""
import pytest
from shelfmark.download.clients.torrent_utils import (
bencode_encode as _bencode_encode,
bencode_decode as _bencode_decode,
extract_info_hash_from_torrent as _extract_info_hash_from_torrent,
)
class TestBencodeDecode:
"""Tests for bencode decoding."""
def test_decode_integer(self):
"""Test decoding integers."""
result, remaining = _bencode_decode(b"i42e")
assert result == 42
assert remaining == b""
def test_decode_negative_integer(self):
"""Test decoding negative integers."""
result, remaining = _bencode_decode(b"i-42e")
assert result == -42
def test_decode_zero(self):
"""Test decoding zero."""
result, remaining = _bencode_decode(b"i0e")
assert result == 0
def test_decode_string(self):
"""Test decoding byte strings."""
result, remaining = _bencode_decode(b"5:hello")
assert result == b"hello"
assert remaining == b""
def test_decode_empty_string(self):
"""Test decoding empty string."""
result, remaining = _bencode_decode(b"0:")
assert result == b""
def test_decode_list(self):
"""Test decoding lists."""
result, remaining = _bencode_decode(b"li1ei2ei3ee")
assert result == [1, 2, 3]
assert remaining == b""
def test_decode_empty_list(self):
"""Test decoding empty list."""
result, remaining = _bencode_decode(b"le")
assert result == []
def test_decode_nested_list(self):
"""Test decoding nested lists."""
result, remaining = _bencode_decode(b"lli1eeli2eee")
assert result == [[1], [2]]
def test_decode_dict(self):
"""Test decoding dictionaries."""
result, remaining = _bencode_decode(b"d3:key5:valuee")
assert result == {b"key": b"value"}
assert remaining == b""
def test_decode_empty_dict(self):
"""Test decoding empty dictionary."""
result, remaining = _bencode_decode(b"de")
assert result == {}
def test_decode_complex_structure(self):
"""Test decoding complex nested structures."""
# Dict with string, int, and list values
data = b"d3:agei25e4:name4:John5:itemsli1ei2ei3eee"
result, remaining = _bencode_decode(data)
assert result == {
b"age": 25,
b"name": b"John",
b"items": [1, 2, 3],
}
class TestBencodeEncode:
"""Tests for bencode encoding."""
def test_encode_integer(self):
"""Test encoding integers."""
assert _bencode_encode(42) == b"i42e"
assert _bencode_encode(-42) == b"i-42e"
assert _bencode_encode(0) == b"i0e"
def test_encode_bytes(self):
"""Test encoding byte strings."""
assert _bencode_encode(b"hello") == b"5:hello"
assert _bencode_encode(b"") == b"0:"
def test_encode_string(self):
"""Test encoding regular strings (UTF-8 encoded)."""
assert _bencode_encode("hello") == b"5:hello"
assert _bencode_encode("") == b"0:"
def test_encode_list(self):
"""Test encoding lists."""
assert _bencode_encode([1, 2, 3]) == b"li1ei2ei3ee"
assert _bencode_encode([]) == b"le"
def test_encode_dict(self):
"""Test encoding dictionaries."""
result = _bencode_encode({b"key": b"value"})
assert result == b"d3:key5:valuee"
def test_encode_dict_keys_sorted(self):
"""Test that dictionary keys are sorted."""
# Keys should be sorted: b < z
result = _bencode_encode({b"z": 1, b"a": 2, b"m": 3})
# a=2, m=3, z=1
assert result == b"d1:ai2e1:mi3e1:zi1ee"
def test_encode_nested_structure(self):
"""Test encoding nested structures."""
data = {b"list": [1, 2, 3], b"num": 42}
result = _bencode_encode(data)
# Keys sorted: "list" < "num"
assert result == b"d4:listli1ei2ei3ee3:numi42ee"
class TestBencodeRoundTrip:
"""Tests for encoding then decoding (roundtrip)."""
def test_roundtrip_integer(self):
"""Test roundtrip for integers."""
original = 12345
encoded = _bencode_encode(original)
decoded, _ = _bencode_decode(encoded)
assert decoded == original
def test_roundtrip_bytes(self):
"""Test roundtrip for byte strings."""
original = b"hello world"
encoded = _bencode_encode(original)
decoded, _ = _bencode_decode(encoded)
assert decoded == original
def test_roundtrip_list(self):
"""Test roundtrip for lists."""
original = [1, 2, b"three", [4, 5]]
encoded = _bencode_encode(original)
decoded, _ = _bencode_decode(encoded)
assert decoded == original
def test_roundtrip_dict(self):
"""Test roundtrip for dictionaries."""
original = {b"name": b"test", b"value": 123}
encoded = _bencode_encode(original)
decoded, _ = _bencode_decode(encoded)
assert decoded == original
class TestExtractInfoHash:
"""Tests for extracting info hash from torrent files."""
def test_extract_hash_from_simple_torrent(self):
"""Test extracting hash from a simple v1 torrent structure."""
# Create a minimal valid v1 torrent structure (has 'pieces' key)
info_dict = {b"name": b"test.txt", b"length": 100, b"pieces": b"\x00" * 20}
torrent = {b"info": info_dict}
torrent_bytes = _bencode_encode(torrent)
result = _extract_info_hash_from_torrent(torrent_bytes)
# V1 torrents return SHA-1 hash (40-character hex string)
assert result is not None
assert len(result) == 40
assert all(c in "0123456789abcdef" for c in result)
def test_extract_hash_returns_none_for_invalid(self):
"""Test that invalid data returns None."""
assert _extract_info_hash_from_torrent(b"not a torrent") is None
assert _extract_info_hash_from_torrent(b"") is None
def test_extract_hash_returns_none_without_info(self):
"""Test that torrent without info dict returns None."""
torrent = {b"announce": b"http://tracker.example.com"}
torrent_bytes = _bencode_encode(torrent)
result = _extract_info_hash_from_torrent(torrent_bytes)
assert result is None
def test_extract_hash_is_consistent(self):
"""Test that same torrent always produces same hash."""
info_dict = {b"name": b"consistent.txt", b"length": 500}
torrent = {b"info": info_dict}
torrent_bytes = _bencode_encode(torrent)
hash1 = _extract_info_hash_from_torrent(torrent_bytes)
hash2 = _extract_info_hash_from_torrent(torrent_bytes)
assert hash1 == hash2