first commit
This commit is contained in:
commit
58cfd95600
35 files changed
+5357
No files matched your search
@@ -0,0 +1,164 @@
|
||||
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())
|
||||
Reference in new issue
Block a user