Ctrl K
ComunidadGuías…

tests/test_downloads_clouddrive_organize.py

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""已声明来源的官方移动:冲突、成员核验、确定性拒绝与状态未知、路径边界。"""
import ssl
import unittest
from pathlib import PureWindowsPath
from types import SimpleNamespace
from unittest.mock import Mock, patch
from urllib.parse import unquote, urlsplit

import httpx
import grpc

from peach.organize_clouddrive import (ACCEPTED, DAV_ORIGIN, REJECTED, MoveNotExecuted, PikPakDav,
                                       UnconfirmedMove)
from peach import organize_clouddrive
from peach.task_retry import transient_delay

MAPPING = organize_clouddrive.PikPakPaths('A:\\', '/Pikpak')


class Status(grpc.RpcError):
    def __init__(self, code):
        self._code = code

    def code(self):
        return self._code


class ConfiguredPaths(unittest.TestCase):
    def setUp(self):
        self.mapping = organize_clouddrive.PikPakPaths('P:\\Library', '/My cloud/Movies', '/Movies')

    def test_parent_creation_keeps_server_retry_after_but_stops_for_authentication(self):
        for status, delay in ((429, 1200), (503, 1200), (401, None), (403, None)):
            with self.subTest(status=status), httpx.Client(transport=httpx.MockTransport(
                    lambda request, status=status: httpx.Response(status, headers={'Retry-After': '1200'}))) as http:
                dav = PikPakDav(http, self.mapping, Mock(), listing=lambda path: [], rename=Mock(), move_remote=Mock())
                with self.assertRaises(OSError) as failure:
                    dav.ensure_parent('P:\\Library\\new\\film.mp4')
                self.assertEqual(transient_delay(failure.exception), delay)

    def test_certificate_failure_does_not_become_a_retryable_file_error(self):
        error = httpx.ConnectError('证书校验失败')
        error.__cause__ = ssl.SSLCertVerificationError('证书不可信')
        with httpx.Client(transport=httpx.MockTransport(Mock(side_effect=error))) as http:
            dav = PikPakDav(http, self.mapping, Mock(), listing=lambda path: [], rename=Mock(), move_remote=Mock())
            with self.assertRaises(OSError) as failure:
                dav.ensure_parent('P:\\Library\\new\\film.mp4')
            self.assertIsNone(transient_delay(failure.exception))

    def test_directory_network_failure_retries_but_permission_failure_stops(self):
        for code, delay in ((grpc.StatusCode.UNAVAILABLE, 0), (grpc.StatusCode.DEADLINE_EXCEEDED, 0),
                            (grpc.StatusCode.PERMISSION_DENIED, None)):
            with self.subTest(code=code):
                stream = Mock()
                stream.__iter__ = Mock(side_effect=Status(code))
                cloud = Mock(token='test-token')
                cloud.channel.unary_stream.return_value.return_value = stream
                with self.assertRaises(OSError) as failure:
                    organize_clouddrive._cloud_listing(cloud, 'P:\\Library', self.mapping)
                self.assertEqual(transient_delay(failure.exception), delay)
                stream.cancel.assert_called_once()

    def test_custom_drive_subdirectory_and_remote_name(self):
        self.assertEqual(self.mapping.cloud_path('p:\\library\\人名\\a.mp4'), '/My cloud/Movies/人名/a.mp4')
        self.assertEqual(self.mapping.dav_path('P:\\Library\\人名\\a.mp4'), '/Movies/人名/a.mp4')
        dav = PikPakDav(Mock(), self.mapping, Mock(), listing=Mock(), rename=Mock(), move_remote=Mock())
        self.assertEqual(unquote(dav.url('P:\\Library\\a b.mp4')), DAV_ORIGIN + '/Movies/a b.mp4')
        dav.ensure_parent('P:\\Library\\a.mp4')
        dav.listing.assert_not_called()

    def test_paths_outside_the_declared_directory_are_rejected(self):
        for path in ('A:\\a.mp4', 'P:\\LibraryOther\\a.mp4', 'P:\\Library\\..\\a.mp4',
                     'P:Library\\a.mp4', 'P:\\Library\\a.mp4:secret'):
            with self.subTest(path=path), self.assertRaises(ValueError):
                self.mapping.cloud_path(path)

    def test_mapping_comes_from_the_samples_declared_root(self):
        roots = {'pikpak': ('P:\\Library', 'Q:\\Second')}
        prefixes = (organize_clouddrive.CloudPrefix('/My cloud/Movies', 'P:\\Library'),
                    organize_clouddrive.CloudPrefix('/Another account', 'Q:\\Second'))
        selected = organize_clouddrive._configured_paths('Q:\\Second\\a.mp4', roots, prefixes)
        self.assertEqual((selected.root, selected.remote), ('Q:\\Second', '/Another account'))
        for mappings in ((), prefixes + (organize_clouddrive.CloudPrefix('/Duplicate', 'Q:\\Second'),)):
            with self.subTest(mappings=mappings), self.assertRaises(ValueError):
                organize_clouddrive._configured_paths('Q:\\Second\\a.mp4', roots, mappings)

    def test_protocol_requests_use_the_configured_remote_directory(self):
        cloud = Mock(token='test-token')
        cloud.channel.unary_unary.return_value.return_value = SimpleNamespace(success=True)
        organize_clouddrive._move_remote(cloud, 'P:\\Library\\old\\a.mp4',
                                        'P:\\Library\\new\\a.mp4', self.mapping)
        request = cloud.channel.unary_unary.return_value.call_args.args[0]
        self.assertEqual(list(request.theFilePaths), ['/My cloud/Movies/old/a.mp4'])
        self.assertEqual(request.destPath, '/My cloud/Movies/new')
        organize_clouddrive._rename_remote(cloud, 'P:\\Library\\old\\a.mp4',
                                          'P:\\Library\\old\\b.mp4', self.mapping)
        request = cloud.channel.unary_unary.return_value.call_args.args[0]
        self.assertEqual((request.path, request.newName), ('/My cloud/Movies/old/a.mp4', 'b.mp4'))

    def test_custom_root_directory_listing_stays_within_mapping(self):
        cloud = Mock(token='test-token')
        stream = OfficialDirectoryEvidence.Stream([
            SimpleNamespace(fullPathName='/My cloud/Movies/a.mp4', size=20, isDirectory=False)])
        cloud.channel.unary_stream.return_value.return_value = stream
        self.assertEqual(organize_clouddrive._cloud_listing(cloud, 'P:\\Library', self.mapping),
                         [dict(path='P:\\Library\\a.mp4', size=20, directory=False)])
        request = cloud.channel.unary_stream.return_value.call_args.args[0]
        self.assertEqual(request.path, '/My cloud/Movies')
        self.assertTrue(stream.cancelled)

    def test_custom_root_authentication_checks_account_relative_path(self):
        cloud = Mock(token='test-token')
        cloud.channel.unary_unary.return_value.return_value = SimpleNamespace(
            directUrl=DAV_ORIGIN + '/Movies/a.mp4', userAgent='',
            additionalHeaders={'Authorization': 'test-credential'})
        headers = organize_clouddrive._download_headers(cloud, 'P:\\Library\\a.mp4', self.mapping)
        self.assertEqual(headers, {'Authorization': 'test-credential'})
        request = cloud.channel.unary_unary.return_value.call_args.args[0]
        self.assertEqual(request.path, '/My cloud/Movies/a.mp4')

    def test_declared_root_cannot_be_a_move_target(self):
        cloud = Mock(token='test-token')
        with self.assertRaises(ValueError):
            organize_clouddrive._move_remote(cloud, 'P:\\Library\\nested\\Library',
                                            'P:\\Library', self.mapping)
        cloud.channel.unary_unary.assert_not_called()

    def test_channel_uses_the_configured_webdav_account_subdirectory(self):
        from peach import downloads_clouddrive_pb2 as pb
        roots = {'pikpak': ('P:\\Library',)}
        prefix = organize_clouddrive.CloudPrefix('/Renamed account/Movies', 'P:\\Library')
        with patch.object(organize_clouddrive, 'PushDiscoverySettings') as settings, \
                patch.object(organize_clouddrive, 'CredentialStore') as credentials, \
                patch.object(organize_clouddrive, 'DownloadSettings') as downloads, \
                patch.object(organize_clouddrive, 'CloudDriveClient') as client, \
                patch.object(organize_clouddrive, '_download_headers', return_value={}) as headers, \
                patch.object(organize_clouddrive, 'http_client'), \
                patch.object(organize_clouddrive.organize, 'verified_renames'):
            settings.return_value.load.return_value = SimpleNamespace(prefixes=(prefix,))
            credentials.return_value.load.return_value = SimpleNamespace(values={'token': 'test-token'})
            downloads.return_value.load.return_value = SimpleNamespace(clouddrive_address='http://example.test')
            cloud = client.return_value.__enter__.return_value
            cloud.find.return_value = pb.CloudDriveFile(isDirectory=True,
                CloudAPI=pb.CloudAPI(name='WebDAV', path='/Renamed account'))
            with organize_clouddrive.pikpak_renames('P:\\Library\\a.mp4', roots) as dav:
                self.assertEqual(dav.url('P:\\Library\\a.mp4'), DAV_ORIGIN + '/Movies/a.mp4')
            cloud.find.assert_called_once_with('/Renamed account/Movies')
            self.assertEqual(headers.call_args.args[2].dav_root, '/Movies')


class OfficialMoves(unittest.TestCase):
    def setUp(self):
        self.entries = {'A:\\old': True, 'A:\\old\\film.mp4': 20, 'A:\\new': True}
        self.requests = []
        self.refresh = Mock()
        self.http = httpx.Client(transport=httpx.MockTransport(self.handle), follow_redirects=False)
        self.addCleanup(self.http.close)
        self.dav = PikPakDav(self.http, MAPPING, self.refresh, listing=self.listing,
                             rename=Mock(side_effect=self.relocate),
                             move_remote=Mock(side_effect=self.relocate))

    def handle(self, request):
        path = 'A:' + unquote(urlsplit(str(request.url)).path).replace('/', '\\').rstrip('\\')
        self.requests.append((request.method, path))
        if request.method != 'MKCOL':
            return httpx.Response(405)
        self.entries[path] = True
        return httpx.Response(201)

    def listing(self, path):
        if path != 'A:\\' and self.entries.get(path) is not True:
            raise FileNotFoundError('官方父目录未列出所需目录')
        return [dict(path=name, size=0 if value is True else value, directory=value is True)
                for name, value in self.entries.items()
                if name != path and str(PureWindowsPath(name).parent) == path]

    def relocate(self, source, target):
        for name in list(self.entries):
            if name == source or name.startswith(source + '\\'):
                self.entries[target + name[len(source):]] = self.entries.pop(name)
        return ACCEPTED

    def test_official_move_preserves_size_and_refreshes_both_parents(self):
        self.dav.move('A:\\old\\film.mp4', 'A:\\new\\film.mp4')
        self.assertEqual(self.entries['A:\\new\\film.mp4'], 20)
        self.assertNotIn('A:\\old\\film.mp4', self.entries)
        self.dav.move_remote.assert_called_once_with('A:\\old\\film.mp4', 'A:\\new\\film.mp4')
        self.dav.rename.assert_not_called()
        self.assertEqual({c.args[0] for c in self.refresh.call_args_list}, {'A:\\old', 'A:\\new'})

    def test_target_collision_never_submits_move(self):
        self.entries['A:\\new\\film.mp4'] = 40
        with self.assertRaises(FileExistsError):
            self.dav.move('A:\\old\\film.mp4', 'A:\\new\\film.mp4')
        self.dav.move_remote.assert_not_called()
        self.assertEqual(self.entries['A:\\new\\film.mp4'], 40)

    def test_rejected_submission_is_a_failure_with_the_source_in_place(self):
        self.dav.move_remote = Mock(return_value=REJECTED)
        self.dav.confirm_attempts = 4
        with patch.object(organize_clouddrive.time, 'sleep') as sleep, self.assertRaises(MoveNotExecuted):
            self.dav.move('A:\\old\\film.mp4', 'A:\\new\\film.mp4')
        self.dav.move_remote.assert_called_once()
        sleep.assert_not_called()
        self.assertEqual(self.entries['A:\\old\\film.mp4'], 20)

    def test_unchanged_source_after_the_confirmation_budget_is_a_failure(self):
        for status in (None, ACCEPTED):
            with self.subTest(status=status):
                self.dav.move_remote = Mock(return_value=status)
                self.dav.confirm_attempts = 4
                with patch.object(organize_clouddrive.time, 'sleep') as sleep, \
                        self.assertRaises(MoveNotExecuted):
                    self.dav.move('A:\\old\\film.mp4', 'A:\\new\\film.mp4')
                self.dav.move_remote.assert_called_once()
                self.assertEqual(sleep.call_count, 3)
                self.assertEqual(self.entries['A:\\old\\film.mp4'], 20)

    def test_rename_acknowledgement_without_path_change_is_a_failure(self):
        self.dav.rename = Mock(return_value=ACCEPTED)
        with self.assertRaises(MoveNotExecuted):
            self.dav.move('A:\\old\\film.mp4', 'A:\\old\\title.mp4')
        self.dav.rename.assert_called_once()
        self.assertEqual(self.entries['A:\\old\\film.mp4'], 20)

    def test_same_parent_rename_uses_the_official_rename(self):
        self.dav.move('A:\\old\\film.mp4', 'A:\\old\\title.mp4')
        self.dav.rename.assert_called_once_with('A:\\old\\film.mp4', 'A:\\old\\title.mp4')
        self.dav.move_remote.assert_not_called()
        self.assertEqual(self.entries['A:\\old\\title.mp4'], 20)

    def test_collection_move_checks_all_members(self):
        self.dav.move('A:\\old', 'A:\\new\\old')
        self.assertEqual(self.entries['A:\\new\\old\\film.mp4'], 20)
        self.assertNotIn('A:\\old', self.entries)

    def test_incomplete_collection_stops_without_repeating_move(self):
        def lossy(source, target):
            self.relocate(source, target)
            self.entries.pop(target + '\\film.mp4')
            return ACCEPTED
        self.dav.move_remote = Mock(side_effect=lossy)
        with self.assertRaises(UnconfirmedMove):
            self.dav.move('A:\\old', 'A:\\new\\old')
        self.dav.move_remote.assert_called_once()

    def test_source_and_target_both_present_stop_the_batch(self):
        def copy(source, target):
            self.entries[target] = self.entries[source]
            return ACCEPTED
        self.dav.move_remote = Mock(side_effect=copy)
        with self.assertRaises(UnconfirmedMove):
            self.dav.move('A:\\old\\film.mp4', 'A:\\new\\film.mp4')
        self.dav.move_remote.assert_called_once()

    def test_source_and_target_both_absent_stop_the_batch(self):
        def lose(source, target):
            self.entries.pop(source)
            return None
        self.dav.move_remote = Mock(side_effect=lose)
        with self.assertRaises(UnconfirmedMove):
            self.dav.move('A:\\old\\film.mp4', 'A:\\new\\film.mp4')
        self.dav.move_remote.assert_called_once()

    def test_empty_collection_requires_destination_entry(self):
        del self.entries['A:\\old\\film.mp4']
        self.dav.move('A:\\old', 'A:\\new\\old')
        self.assertIs(self.entries['A:\\new\\old'], True)

    def test_nested_parent_is_created_in_order(self):
        self.dav.move('A:\\old\\film.mp4', 'A:\\deep\\nested\\film.mp4')
        self.assertEqual([r[1] for r in self.requests if r[0] == 'MKCOL'], ['A:\\deep', 'A:\\deep\\nested'])

    def test_cross_drive_is_rejected_before_network(self):
        with self.assertRaises(ValueError):
            self.dav.move('A:\\old\\film.mp4', 'R:\\film.mp4')
        self.assertFalse(self.requests)

    def test_cross_parent_rename_is_rejected_before_submission(self):
        with self.assertRaises(ValueError):
            self.dav.move('A:\\old\\film.mp4', 'A:\\new\\title.mp4')
        self.dav.move_remote.assert_not_called()
        self.dav.rename.assert_not_called()
        self.assertFalse(self.requests)

    def test_unicode_path_is_utf8_percent_encoded(self):
        self.assertEqual(self.dav.url('A:\\创作者\\a b.mp4'), DAV_ORIGIN + '/%E5%88%9B%E4%BD%9C%E8%80%85/a%20b.mp4')

    def test_creator_handle_keeps_the_literal_at_sign(self):
        self.assertEqual(self.dav.url('A:\\old\\creator@handle'), DAV_ORIGIN + '/old/creator@handle')

    def test_delayed_listing_only_rechecks_the_single_submitted_move(self):
        self.dav.move_remote = Mock(return_value=ACCEPTED)
        self.dav.confirm_attempts = 4
        reads = 0
        def refresh(path):
            nonlocal reads
            reads += 1
            if reads == 3:
                self.entries['A:\\new\\film.mp4'] = self.entries.pop('A:\\old\\film.mp4')
        self.dav.refresh = refresh
        with patch.object(organize_clouddrive.time, 'sleep'):
            self.dav.move('A:\\old\\film.mp4', 'A:\\new\\film.mp4')
        self.dav.move_remote.assert_called_once()
        self.assertEqual(self.entries['A:\\new\\film.mp4'], 20)

    def test_cache_failure_after_submission_preserves_intent(self):
        self.dav.move_remote = Mock(return_value=ACCEPTED)
        self.dav.refresh = Mock(side_effect=OSError('未取得'))
        with self.assertRaises(UnconfirmedMove):
            self.dav.move('A:\\old\\film.mp4', 'A:\\new\\film.mp4')
        self.dav.move_remote.assert_called_once()


class OfficialMoveProtocol(unittest.TestCase):
    def test_conflict_skip_and_false_optional_flags_are_encoded(self):
        cloud = Mock(token='test-token')
        cloud.channel.unary_unary.return_value.return_value = SimpleNamespace(success=True)
        self.assertEqual(organize_clouddrive._move_remote(cloud, 'A:\\old\\film.mp4', 'A:\\new\\film.mp4', MAPPING), ACCEPTED)
        request = cloud.channel.unary_unary.return_value.call_args.args[0]
        self.assertEqual(list(request.theFilePaths), ['/Pikpak/old/film.mp4'])
        self.assertEqual(request.destPath, '/Pikpak/new')
        self.assertEqual(request.conflictPolicy, 2)
        for flag in ('moveAcrossClouds', 'handleConflictRecursively'):
            self.assertTrue(request.HasField(flag))
            self.assertFalse(getattr(request, flag))

    def test_path_boundaries_stop_before_rpc(self):
        cloud = Mock(token='test-token')
        for target in ('R:\\film.mp4', 'A:\\new\\other.mp4', 'A:\\old\\film.mp4', 'A:\\new\\..\\film.mp4'):
            with self.subTest(target=target), self.assertRaises(ValueError):
                organize_clouddrive._move_remote(cloud, 'A:\\old\\film.mp4', target, MAPPING)
        for target in ('A:\\new\\title.mp4', 'R:\\old\\title.mp4'):
            with self.subTest(target=target), self.assertRaises(ValueError):
                organize_clouddrive._rename_remote(cloud, 'A:\\old\\film.mp4', target, MAPPING)
        cloud.channel.unary_unary.assert_not_called()

    def test_rpc_timeout_has_no_automatic_resubmission(self):
        for submit in (organize_clouddrive._move_remote, organize_clouddrive._rename_remote):
            with self.subTest(submit=submit.__name__):
                cloud = Mock(token='test-token')
                cloud.channel.unary_unary.return_value.side_effect = Status(grpc.StatusCode.DEADLINE_EXCEEDED)
                target = 'A:\\new\\film.mp4' if submit is organize_clouddrive._move_remote else 'A:\\old\\title.mp4'
                self.assertIsNone(submit(cloud, 'A:\\old\\film.mp4', target, MAPPING))
                cloud.channel.unary_unary.return_value.assert_called_once()

    def test_request_rejections_are_deterministic(self):
        for code in (grpc.StatusCode.INVALID_ARGUMENT, grpc.StatusCode.PERMISSION_DENIED):
            for submit, target in ((organize_clouddrive._move_remote, 'A:\\new\\film.mp4'),
                                   (organize_clouddrive._rename_remote, 'A:\\old\\title.mp4')):
                with self.subTest(code=code, submit=submit.__name__):
                    cloud = Mock(token='test-token')
                    cloud.channel.unary_unary.return_value.side_effect = Status(code)
                    self.assertEqual(submit(cloud, 'A:\\old\\film.mp4', target, MAPPING), REJECTED)

    def test_rename_reply_maps_to_submission_status(self):
        for success, expected in ((True, ACCEPTED), (False, REJECTED)):
            with self.subTest(success=success):
                cloud = Mock(token='test-token')
                cloud.channel.unary_unary.return_value.return_value = SimpleNamespace(success=success)
                self.assertEqual(organize_clouddrive._rename_remote(cloud, 'A:\\old\\film.mp4', 'A:\\old\\title.mp4', MAPPING), expected)
                request = cloud.channel.unary_unary.return_value.call_args.args[0]
                self.assertEqual((request.path, request.newName), ('/Pikpak/old/film.mp4', 'title.mp4'))

    def test_cache_scope_rejects_another_drive(self):
        cloud = Mock(token='test-token')
        with self.assertRaises(ValueError):
            organize_clouddrive._refresh_listing(cloud, 'B:\\MVP', MAPPING)
        cloud.channel.unary_unary.assert_not_called()


class OfficialDirectoryEvidence(unittest.TestCase):
    class Stream:
        def __init__(self, rows):
            self.rows, self.cancelled = rows, False
        def __iter__(self):
            yield SimpleNamespace(subFiles=self.rows)
        def cancel(self):
            self.cancelled = True

    def setUp(self):
        self.paths, self.streams = [], []
        self.entries = {'/Pikpak': [('old', True, 0)], '/Pikpak/old': [('creator@handle', True, 0)],
                        '/Pikpak/old/creator@handle': [('film.mp4', False, 20)]}
        def read(request, **kwargs):
            self.paths.append((request.path, request.forceRefresh))
            rows = [SimpleNamespace(fullPathName=request.path + '/' + name, isDirectory=directory, size=size)
                    for name, directory, size in self.entries[request.path]]
            stream = self.Stream(rows)
            self.streams.append(stream)
            return stream
        self.cloud = Mock(token='test-token')
        self.cloud.channel.unary_stream.return_value = read

    def test_ancestor_listings_confirm_the_complete_unicode_directory(self):
        rows = organize_clouddrive._cloud_listing(self.cloud, 'A:\\old\\creator@handle', MAPPING)
        self.assertEqual(rows, [dict(path='A:\\old\\creator@handle\\film.mp4', directory=False, size=20)])
        self.assertEqual(self.paths, [('/Pikpak', True), ('/Pikpak/old', True), ('/Pikpak/old/creator@handle', True)])
        self.assertTrue(all(stream.cancelled for stream in self.streams))

    def test_missing_ancestor_does_not_read_an_unverified_child(self):
        self.entries['/Pikpak/old'] = []
        with self.assertRaises(FileNotFoundError):
            organize_clouddrive._cloud_listing(self.cloud, 'A:\\old\\creator@handle', MAPPING)
        self.assertEqual(len(self.paths), 2)

    def test_foreign_drive_never_uses_the_existing_credential(self):
        with self.assertRaises(ValueError):
            organize_clouddrive._cloud_listing(self.cloud, 'R:\\Media', MAPPING)
        self.assertFalse(self.paths)

    def test_duplicate_members_stop_and_cancel_the_stream(self):
        self.entries['/Pikpak/old/creator@handle'] *= 2
        with self.assertRaises(ValueError):
            organize_clouddrive._cloud_listing(self.cloud, 'A:\\old\\creator@handle', MAPPING)
        self.assertTrue(all(stream.cancelled for stream in self.streams))


class AuthenticationSamples(unittest.TestCase):
    class Missing(grpc.RpcError):
        def code(self):
            return grpc.StatusCode.NOT_FOUND

    def test_moved_sample_uses_existing_catalog_sample(self):
        cloud = Mock(token='test-token')
        reply = SimpleNamespace(directUrl=DAV_ORIGIN + '/available/image.jpg', userAgent='', additionalHeaders={'Authorization': 'test-credential'})
        stub = cloud.channel.unary_unary.return_value
        stub.side_effect = [self.Missing(), reply]
        with patch.object(organize_clouddrive, '_fallback_samples', return_value=['A:\\available\\image.jpg']):
            headers = organize_clouddrive._download_headers(cloud, 'A:\\moved\\image.jpg', MAPPING)
        self.assertEqual(headers, {'Authorization': 'test-credential'})
        self.assertEqual([c.args[0].path for c in stub.call_args_list], ['/Pikpak/moved/image.jpg', '/Pikpak/available/image.jpg'])

    def test_missing_samples_stop_after_three_calls(self):
        cloud = Mock(token='test-token')
        stub = cloud.channel.unary_unary.return_value
        stub.side_effect = self.Missing()
        with patch.object(organize_clouddrive, '_fallback_samples', return_value=['A:\\first.jpg', 'A:\\second.jpg']), self.assertRaises(ValueError):
            organize_clouddrive._download_headers(cloud, 'A:\\moved.jpg', MAPPING)
        self.assertEqual(stub.call_count, 3)

    def test_foreign_download_source_is_rejected(self):
        cloud = Mock(token='test-token')
        cloud.channel.unary_unary.return_value.return_value = SimpleNamespace(directUrl='https://example.test/image.jpg', additionalHeaders={}, userAgent='')
        with self.assertRaises(ValueError):
            organize_clouddrive._download_headers(cloud, 'A:\\image.jpg', MAPPING)