Files
x360-web-queue/tests/test_network_fix.py
T
2026-10-15 14:15:10 +02:00

165 lines
6.2 KiB
Python

import asyncio
import json
import os
import sys
from pathlib import Path
import httpx
import pytest
from app import archive_auth, archive_download, bt, main, store
@pytest.fixture
def isolated(tmp_path, monkeypatch):
monkeypatch.setattr(store, 'DATA', tmp_path)
monkeypatch.setattr(store, 'DB_PATH', tmp_path / 'queue.sqlite3')
monkeypatch.setenv('ARIA2_SECRET', 'test')
monkeypatch.setenv('ARCHIVE_CONNECTIONS', '1') # Old single-stream behavior is covered separately.
store.init_db()
return tmp_path
def create_archive(url='https://archive.org/download/item/Game.iso'):
return main.new_job('url', url, 'Game', 'raw')
def fake_http_client(monkeypatch, handler):
transport = httpx.MockTransport(handler)
monkeypatch.setattr(archive_download, '_client', lambda: httpx.Client(
transport=transport, cookies=archive_auth.session_cookies(), follow_redirects=False))
monkeypatch.setattr(archive_download.time, 'sleep', lambda delay: None)
class BytesStream(httpx.SyncByteStream):
def __init__(self, data):
self.data = data
def __iter__(self):
yield self.data
def test_archive_transient_http_500_is_retried_and_logged(isolated, monkeypatch):
archive_auth._save_session('user@example.org', {
'logged-in-user': 'user', 'logged-in-sig': 'cookie-secret'})
requests = []
def handler(request):
requests.append(request)
if len(requests) < 3:
return httpx.Response(500, text='upstream temporarily broken')
return httpx.Response(200, stream=BytesStream(b'ISO-CONTENT'), headers={'Content-Length': '11'})
fake_http_client(monkeypatch, handler)
job = create_archive()
archive_download._transfer_one(job)
assert store.get(job['id'])['state'] == 'preparing'
assert (isolated / 'raw' / job['id'] / 'Game.iso').read_bytes() == b'ISO-CONTENT'
assert len(requests) == 3
assert all('cookie-secret' in req.headers.get('cookie', '') for req in requests)
text = (isolated / 'logs' / (job['id'] + '.download.log')).read_text()
assert 'HTTP 500' in text and 'Ukończono' in text
assert 'cookie-secret' not in text
def test_archive_range_resume_with_redirect(isolated, monkeypatch):
requests = []
def handler(request):
requests.append((request.url.host, request.headers.get('range')))
if request.url.host == 'archive.org':
return httpx.Response(302, headers={'Location': 'https://ia444.us.archive.org/download/item/Game.iso'})
return httpx.Response(206, stream=BytesStream(b'DEF'), headers={'Content-Range': 'bytes 3-5/6',
'Content-Length': '3'})
fake_http_client(monkeypatch, handler)
job = create_archive()
raw = isolated / 'raw' / job['id']
raw.mkdir(parents=True)
(raw / 'Game.iso.part').write_bytes(b'ABC')
archive_download._transfer_one(job)
assert (raw / 'Game.iso').read_bytes() == b'ABCDEF'
assert not (raw / 'Game.iso.part').exists()
assert requests == [('archive.org', 'bytes=3-'), ('ia444.us.archive.org', 'bytes=3-')]
def test_archive_403_is_explicit_and_not_retried(isolated, monkeypatch):
seen = []
def handler(request):
seen.append(1)
return httpx.Response(403, text='denied')
fake_http_client(monkeypatch, handler)
job = create_archive()
with pytest.raises(archive_download.ArchiveDownloadError, match='HTTP 403'):
archive_download._transfer_one(job)
assert len(seen) == 1
def test_archive_persistent_500_reports_origin_failure(isolated, monkeypatch):
seen = []
def handler(request):
seen.append(1)
return httpx.Response(500)
fake_http_client(monkeypatch, handler)
job = create_archive()
with pytest.raises(archive_download.ArchiveDownloadError, match='HTTP 500 po 5 próbach'):
archive_download._transfer_one(job)
assert len(seen) == 5
def test_archive_cancel_keeps_partial_files(isolated, monkeypatch):
job = create_archive()
store.update(job['id'], state='paused')
fake_http_client(monkeypatch, lambda req: pytest.fail('Do not download paused job'))
with pytest.raises(archive_download.ArchiveStopped):
archive_download._transfer_one(job)
def test_bt_helper_subprocess_inspection_and_fetch(isolated, monkeypatch):
"""Exercise process supervisor and status semantics without real peers."""
from tests.test_features import sample_torrent
helper = isolated / 'fake-helper'
# Tests the Python supervisor lifecycle, not the compiled Go / network engine.
helper.write_text('#!' + sys.executable + '\n' + '''
import json, os, sys
from pathlib import Path
args = sys.argv[1:]
mode = args[0]
def param(x): return args[args.index(x)+1]
state = Path(param('-status'))
state.parent.mkdir(parents=True, exist_ok=True)
if mode == 'inspect':
Path(param('-out')).write_bytes(bytes.fromhex(os.environ['FAKE_TORRENT_HEX']))
state.write_text(json.dumps({'status':'ready','connections':2}))
else:
output=Path(param('-out'))
output.mkdir(parents=True, exist_ok=True)
(output/'Game.iso').write_bytes(b'foo')
state.write_text(json.dumps({'status':'done','downloaded':3,'total':3}))
''')
helper.chmod(0o755)
monkeypatch.setenv('FAKE_TORRENT_HEX', sample_torrent().hex())
monkeypatch.setenv('BT_HELPER', str(helper))
monkeypatch.setattr(bt, '_DOWNLOADS', {})
monkeypatch.setattr(bt, '_INSPECTIONS', {})
async def check():
pid = 'a' * 32
folder = isolated / 'metadata' / pid
folder.mkdir(parents=True)
proc = await bt.start_inspection(pid, 'magnet:?xt=urn:btih:' + 'a'*40)
assert await proc.wait() == 0
assert bt.inspect_state(pid)['status'] == 'ready'
await bt.stop_inspection(pid)
job = main.new_job('torrent_selected', json.dumps({'selection':'1'}), 'Game', 'raw')
(isolated/'torrents').mkdir()
(isolated/'torrents'/(job['id']+'.torrent')).write_bytes(sample_torrent())
await bt.start_download(job)
proc = bt._DOWNLOADS[job['id']]
assert await proc.wait() == 0
await bt.poll_download(store.get(job['id']))
assert store.get(job['id'])['state'] == 'preparing'
await bt.shutdown()
asyncio.run(check())