165 lines
6.2 KiB
Python
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())
|