deploy: sync server CloudSearch 0.5.6
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from cloudsearch_transfer.adapter.aliyun import AliyunAdapter
|
||||
from cloudsearch_transfer.adapter.baidu import BaiduAdapter
|
||||
from cloudsearch_transfer.adapter.xunlei import XunleiAdapter
|
||||
from cloudsearch_transfer.config import PlatformConfig, TransferConfig
|
||||
from cloudsearch_transfer.adapter.base import FileInfo
|
||||
|
||||
|
||||
class DummyCredential:
|
||||
cookie = "k=xxxxxxxx"
|
||||
refresh_token = "rt"
|
||||
def validate(self):
|
||||
return True
|
||||
def get_bdstoken(self):
|
||||
return "bdstoken"
|
||||
def get_drive_id(self):
|
||||
return "drive-id"
|
||||
def get_headers(self):
|
||||
return {}
|
||||
def get_auth_headers(self):
|
||||
return {}
|
||||
|
||||
|
||||
def make_config(**kwargs):
|
||||
data = dict(enabled=True, cookie="k=" + "x" * 80, refresh_token="rt", account_name="test", save_dir="/")
|
||||
data.update(kwargs)
|
||||
return PlatformConfig(**data)
|
||||
|
||||
|
||||
def make_transfer_config():
|
||||
return TransferConfig(request_timeout=1, max_retries=0, ad_filter_enabled=False, task_poll_interval=0, task_poll_max_attempts=2)
|
||||
|
||||
|
||||
class P1DriveApiCapabilityTests(unittest.TestCase):
|
||||
def make_aliyun(self):
|
||||
with patch("cloudsearch_transfer.adapter.aliyun.AliyunCredentialManager", return_value=DummyCredential()):
|
||||
return AliyunAdapter(make_config(), make_transfer_config())
|
||||
|
||||
def make_baidu(self):
|
||||
with patch("cloudsearch_transfer.adapter.baidu.BaiduCredentialManager", return_value=DummyCredential()):
|
||||
return BaiduAdapter(make_config(), make_transfer_config())
|
||||
|
||||
def make_xunlei(self):
|
||||
with patch("cloudsearch_transfer.adapter.xunlei.XunleiCredentialManager", return_value=DummyCredential()):
|
||||
return XunleiAdapter(make_config(), make_transfer_config())
|
||||
|
||||
def test_p1_adapters_declare_callable_core_drive_api_methods(self):
|
||||
expectations = {
|
||||
"aliyun": (self.make_aliyun(), ["ensure_dir", "save_files", "rename", "move_files", "delete_files"]),
|
||||
"baidu": (self.make_baidu(), ["ensure_dir", "save_files", "rename", "move_files", "delete_files"]),
|
||||
"xunlei": (self.make_xunlei(), ["ensure_dir", "save_files", "poll_task", "rename", "move_files"]),
|
||||
}
|
||||
for platform, (adapter, required) in expectations.items():
|
||||
with self.subTest(platform=platform):
|
||||
for capability in required:
|
||||
self.assertTrue(adapter.capabilities.get(capability), f"{platform}.{capability} capability not declared")
|
||||
for method in ["ensure_dir", "mkdir", "rename", "move_files"]:
|
||||
self.assertNotEqual(getattr(type(adapter), method), getattr(adapter.__class__.__mro__[1], method, None), f"{platform}.{method} not overridden")
|
||||
if adapter.capabilities.get("delete_files"):
|
||||
self.assertNotEqual(getattr(type(adapter), "delete_files"), getattr(adapter.__class__.__mro__[1], "delete_files", None), f"{platform}.delete_files not overridden")
|
||||
|
||||
def test_aliyun_save_files_resolves_path_to_file_id_before_copy(self):
|
||||
adapter = self.make_aliyun()
|
||||
adapter.ensure_dir = lambda path: "target-folder-id"
|
||||
captured = {}
|
||||
transfer = adapter._get_transfer()
|
||||
def fake_batch_copy(share_id, share_token, file_ids, to_parent):
|
||||
captured["to_parent"] = to_parent
|
||||
return ["new-file"]
|
||||
transfer._batch_copy = fake_batch_copy
|
||||
detail = {"share_token": "share-token", "files": [{"file_id": "src-file"}]}
|
||||
self.assertEqual(adapter._save_files("share-id", detail, "/Media"), ["new-file"])
|
||||
self.assertEqual(captured["to_parent"], "target-folder-id")
|
||||
|
||||
def test_aliyun_ensure_dir_creates_nested_paths_progressively(self):
|
||||
adapter = self.make_aliyun()
|
||||
existing = {"/A": "fid-A"}
|
||||
created = []
|
||||
adapter.get_fids = lambda paths: [{"file_path": p, "fid": existing[p]} for p in paths if p in existing]
|
||||
def fake_mkdir(path):
|
||||
created.append(path)
|
||||
existing[path] = "fid-" + path.rsplit("/", 1)[-1]
|
||||
return {"code": 0, "data": {"fid": existing[path]}}
|
||||
adapter.mkdir = fake_mkdir
|
||||
self.assertEqual(adapter.ensure_dir("/A/B"), "fid-B")
|
||||
self.assertEqual(created, ["/A/B"])
|
||||
|
||||
def test_baidu_save_files_ensures_path_before_transfer_and_matching(self):
|
||||
adapter = self.make_baidu()
|
||||
adapter.ensure_dir = lambda path: "/CloudSearch/Media"
|
||||
captured = {}
|
||||
adapter.credential.get_bdstoken = lambda: "bdstoken"
|
||||
def fake_transfer_files(shareid, uk, fs_ids, save_dir, token):
|
||||
captured["transfer_dir"] = save_dir
|
||||
adapter._transfer._transfer_files = fake_transfer_files
|
||||
def fake_list_and_match(save_dir, filenames, token):
|
||||
captured["list_dir"] = save_dir
|
||||
return ["1001"]
|
||||
adapter._transfer._list_and_match = fake_list_and_match
|
||||
detail = {"shareid": "sid", "uk": "uk", "fs_ids": ["old"], "filenames": ["demo.mkv"]}
|
||||
self.assertEqual(adapter._save_files("pwd", detail, "/CloudSearch/Media"), ["1001"])
|
||||
self.assertEqual(captured["transfer_dir"], "/CloudSearch/Media")
|
||||
self.assertEqual(captured["list_dir"], "/CloudSearch/Media")
|
||||
|
||||
def test_baidu_ensure_dir_creates_nested_paths_progressively(self):
|
||||
adapter = self.make_baidu()
|
||||
dirs = {"/A"}
|
||||
created = []
|
||||
adapter.get_files = lambda parent="/": [FileInfo(fid=parent.rstrip("/") + "/A", name="A", is_dir=True)] if parent == "/" else []
|
||||
def fake_mkdir(path):
|
||||
created.append(path)
|
||||
dirs.add(path)
|
||||
return {"code": 0, "data": {"path": path}}
|
||||
adapter.mkdir = fake_mkdir
|
||||
self.assertEqual(adapter.ensure_dir("/A/B"), "/A/B")
|
||||
self.assertEqual(created, ["/A/B"])
|
||||
|
||||
def test_xunlei_save_files_resolves_path_and_polls_restore_task(self):
|
||||
adapter = self.make_xunlei()
|
||||
adapter.ensure_dir = lambda path: "target-parent-id"
|
||||
transfer = adapter._transfer
|
||||
transfer._restore_files = lambda share_id, token, fids, parent_id="": "task-1" if parent_id == "target-parent-id" else (_ for _ in ()).throw(AssertionError(parent_id))
|
||||
transfer._poll_restore_task = lambda task_id: {"old-1": "new-1"}
|
||||
detail = {"share_id": "share", "pass_code_token": "pct", "files": [{"id": "old-1", "name": "demo"}]}
|
||||
self.assertEqual(adapter._save_files("share", detail, "/Media"), ["new-1"])
|
||||
|
||||
def test_xunlei_query_task_delegates_to_restore_poll(self):
|
||||
adapter = self.make_xunlei()
|
||||
adapter._transfer._poll_restore_task = lambda task_id: {"old": "new"}
|
||||
self.assertEqual(adapter.query_task("task-1"), {"code": 0, "status": 200, "data": {"old": "new"}})
|
||||
|
||||
def test_xunlei_does_not_advertise_permanent_delete_as_drive_delete_capability(self):
|
||||
adapter = self.make_xunlei()
|
||||
self.assertFalse(adapter.capabilities.get("delete_files"), "迅雷 batchDelete is permanent and must not be advertised as generic delete_files")
|
||||
|
||||
def test_xunlei_get_share_detail_passes_extraction_code_to_transfer_engine(self):
|
||||
adapter = self.make_xunlei()
|
||||
captured = {}
|
||||
def fake_get_share_info(share_id, passcode=""):
|
||||
captured["share_id"] = share_id
|
||||
captured["passcode"] = passcode
|
||||
return {"title": "demo", "files": [{"id": "old"}], "pass_code_token": "pct"}
|
||||
adapter._transfer._get_share_info = fake_get_share_info
|
||||
detail = adapter._get_share_detail("share-id", "abcd")
|
||||
self.assertEqual(detail["title"], "demo")
|
||||
self.assertEqual(captured, {"share_id": "share-id", "passcode": "abcd"})
|
||||
|
||||
def test_xunlei_create_share_forwards_requested_share_password(self):
|
||||
adapter = self.make_xunlei()
|
||||
captured = {}
|
||||
def fake_create_share(file_ids, password=""):
|
||||
captured["file_ids"] = file_ids
|
||||
captured["password"] = password
|
||||
return ("https://pan.xunlei.com/s/new", password)
|
||||
adapter._transfer._create_share = fake_create_share
|
||||
self.assertEqual(adapter._create_share(["fid-1"], "demo", "xy12"), ("https://pan.xunlei.com/s/new", "xy12"))
|
||||
self.assertEqual(captured, {"file_ids": ["fid-1"], "password": "xy12"})
|
||||
|
||||
def test_xunlei_poll_task_alias_matches_declared_capability(self):
|
||||
adapter = self.make_xunlei()
|
||||
adapter._transfer._poll_restore_task = lambda task_id: {"old": "new"}
|
||||
self.assertEqual(adapter.poll_task("task-1"), {"code": 0, "status": 200, "data": {"old": "new"}})
|
||||
|
||||
def test_xunlei_get_fids_is_read_only_and_does_not_create_missing_paths(self):
|
||||
adapter = self.make_xunlei()
|
||||
calls = []
|
||||
def fake_get_files(parent="0"):
|
||||
calls.append(("get_files", parent))
|
||||
if parent in ("0", ""):
|
||||
return [FileInfo(fid="fid-A", name="A", is_dir=True)]
|
||||
if parent == "fid-A":
|
||||
return [FileInfo(fid="fid-B", name="B", is_dir=False)]
|
||||
return []
|
||||
def fail_mkdir(*args, **kwargs):
|
||||
raise AssertionError("get_fids must not create directories")
|
||||
adapter.get_files = fake_get_files
|
||||
adapter.mkdir = fail_mkdir
|
||||
self.assertEqual(adapter.get_fids(["/A/B", "/A/Missing"]), [{"file_path": "/A/B", "fid": "fid-B"}])
|
||||
self.assertEqual(calls, [("get_files", "0"), ("get_files", "fid-A"), ("get_files", "0"), ("get_files", "fid-A")])
|
||||
|
||||
def test_baidu_get_fids_checks_existing_parent_listing(self):
|
||||
adapter = self.make_baidu()
|
||||
calls = []
|
||||
def fake_get_files(parent="/"):
|
||||
calls.append(parent)
|
||||
if parent == "/A":
|
||||
return [FileInfo(fid="1001", name="movie.mkv", is_dir=False)]
|
||||
return []
|
||||
adapter.get_files = fake_get_files
|
||||
self.assertEqual(adapter.get_fids(["/A/movie.mkv", "/A/missing.mkv"]), [{"file_path": "/A/movie.mkv", "fid": "1001", "path": "/A/movie.mkv"}])
|
||||
self.assertEqual(calls, ["/A", "/A"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,135 @@
|
||||
import subprocess
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class TransferServerRuntimeTests(unittest.TestCase):
|
||||
def test_transfer_image_imports_server_module_from_app_workdir(self):
|
||||
result = subprocess.run(
|
||||
[
|
||||
"docker", "run", "--rm",
|
||||
"-e", "TRANSFER_CONFIG_PATH=/tmp/transfer_config.json",
|
||||
"cloudsearch-transfer:test",
|
||||
"python", "-c",
|
||||
"import cloudsearch_transfer.server as s; print(hasattr(s, 'app'))",
|
||||
],
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
timeout=20,
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertIn("True", result.stdout)
|
||||
|
||||
def test_update_platform_creates_config_with_package_import(self):
|
||||
result = subprocess.run(
|
||||
[
|
||||
"docker", "run", "--rm",
|
||||
"-e", "TRANSFER_CONFIG_PATH=/tmp/transfer_config_server_test.json",
|
||||
"cloudsearch-transfer:test",
|
||||
"python", "-c",
|
||||
"""
|
||||
import os
|
||||
os.environ['TRANSFER_API_TOKEN'] = 'test-token'
|
||||
from cloudsearch_transfer.server import app
|
||||
client = app.test_client()
|
||||
resp = client.put('/api/config/platforms/newplatform', headers={'X-Transfer-Token': 'test-token'}, json={'enabled': True, 'save_dir': '/Media'})
|
||||
print(resp.status_code)
|
||||
print(resp.get_data(as_text=True))
|
||||
""",
|
||||
],
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
timeout=20,
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertIn("200", result.stdout)
|
||||
self.assertIn("newplatform", result.stdout)
|
||||
|
||||
def test_dockerfile_healthcheck_uses_http_health_probe(self):
|
||||
dockerfile = Path(__file__).resolve().parents[1] / "Dockerfile"
|
||||
content = dockerfile.read_text()
|
||||
healthcheck_block = content.split("HEALTHCHECK", 1)[1].split("\n\nCMD", 1)[0]
|
||||
self.assertIn("/health", healthcheck_block)
|
||||
self.assertNotIn('python -m cloudsearch_transfer.server', healthcheck_block)
|
||||
|
||||
def test_config_update_requires_api_token_when_configured(self):
|
||||
script = """
|
||||
import os
|
||||
os.environ['TRANSFER_API_TOKEN'] = 'secret-token'
|
||||
os.environ['TRANSFER_CONFIG_PATH'] = '/tmp/transfer_config_auth_test.json'
|
||||
from cloudsearch_transfer.server import app
|
||||
client = app.test_client()
|
||||
unauth = client.put('/api/config/platforms/newplatform', json={'enabled': True})
|
||||
auth = client.put('/api/config/platforms/newplatform', headers={'X-Transfer-Token': 'secret-token'}, json={'enabled': True, 'save_dir': '/Media'})
|
||||
print(unauth.status_code)
|
||||
print(auth.status_code)
|
||||
"""
|
||||
result = subprocess.run(
|
||||
[
|
||||
"docker", "run", "--rm",
|
||||
"-e", "TRANSFER_CONFIG_PATH=/tmp/transfer_config_auth_test.json",
|
||||
"cloudsearch-transfer:test",
|
||||
"python", "-c", script,
|
||||
],
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
timeout=20,
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertIn("401", result.stdout)
|
||||
self.assertIn("200", result.stdout)
|
||||
|
||||
def test_config_update_requires_token_by_default(self):
|
||||
script = """
|
||||
import os
|
||||
os.environ.pop('TRANSFER_API_TOKEN', None)
|
||||
os.environ['TRANSFER_CONFIG_PATH'] = '/tmp/transfer_config_default_auth_test.json'
|
||||
from cloudsearch_transfer.server import app
|
||||
client = app.test_client()
|
||||
resp = client.put('/api/config/platforms/newplatform', json={'enabled': True})
|
||||
print(resp.status_code)
|
||||
"""
|
||||
result = subprocess.run(
|
||||
[
|
||||
"docker", "run", "--rm",
|
||||
"-e", "TRANSFER_CONFIG_PATH=/tmp/transfer_config_default_auth_test.json",
|
||||
"cloudsearch-transfer:test",
|
||||
"python", "-c", script,
|
||||
],
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
timeout=20,
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertIn("401", result.stdout)
|
||||
|
||||
def test_config_save_persists_full_credentials_for_runtime_reload(self):
|
||||
script = """
|
||||
from cloudsearch_transfer.config import ConfigManager, PlatformConfig
|
||||
path = '/tmp/transfer_config_persist_test.json'
|
||||
cfg = ConfigManager(path)
|
||||
cfg.platforms['quark'] = PlatformConfig(enabled=True, cookie='cookie-value-that-is-longer-than-twenty-characters', refresh_token='refresh-token-that-is-longer-than-twenty-characters')
|
||||
cfg.save()
|
||||
reloaded = ConfigManager(path)
|
||||
print(reloaded.platforms['quark'].cookie)
|
||||
print(reloaded.platforms['quark'].refresh_token)
|
||||
"""
|
||||
result = subprocess.run(
|
||||
["docker", "run", "--rm", "cloudsearch-transfer:test", "python", "-c", script],
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
timeout=20,
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertIn("cookie-value-that-is-longer-than-twenty-characters", result.stdout)
|
||||
self.assertIn("refresh-token-that-is-longer-than-twenty-characters", result.stdout)
|
||||
self.assertNotIn("...", result.stdout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user