|
| 1 | +"""Tests for drive_service.create_folder (with cache-update + timestamps) |
| 2 | +and drive_service.update_file (the WebDAV PUT-on-existing path). |
| 3 | +""" |
| 4 | +from unittest.mock import patch |
| 5 | + |
| 6 | +import pytest |
| 7 | + |
| 8 | +from services.drive import drive_service |
| 9 | + |
| 10 | + |
| 11 | +@pytest.fixture(autouse=True) |
| 12 | +def _reset(): |
| 13 | + drive_service.folder_content_cache.clear() |
| 14 | + drive_service._mem_reserved = 0 |
| 15 | + yield |
| 16 | + drive_service.folder_content_cache.clear() |
| 17 | + drive_service._mem_reserved = 0 |
| 18 | + |
| 19 | + |
| 20 | +@pytest.fixture |
| 21 | +def fake_creds(): |
| 22 | + return { |
| 23 | + 'user': { |
| 24 | + 'rootFolderId': 'root-uuid', |
| 25 | + 'bucket': '00' * 12, |
| 26 | + 'mnemonic': ('abandon abandon abandon abandon abandon abandon ' |
| 27 | + 'abandon abandon abandon abandon abandon about'), |
| 28 | + 'bridgeUser': 'u@example.com', |
| 29 | + 'userId': 'u-42', |
| 30 | + }, |
| 31 | + } |
| 32 | + |
| 33 | + |
| 34 | +# ---------- create_folder ---------- |
| 35 | + |
| 36 | +def test_create_folder_uses_root_when_no_parent_given(fake_creds): |
| 37 | + """If parent_folder_uuid is None, must default to user's rootFolderId.""" |
| 38 | + captured = {} |
| 39 | + def fake_api_create(payload): |
| 40 | + captured['payload'] = payload |
| 41 | + return {'uuid': 'new-uuid', 'plainName': payload['plainName']} |
| 42 | + |
| 43 | + with patch.object(drive_service.auth, 'get_auth_details', |
| 44 | + return_value=fake_creds), \ |
| 45 | + patch.object(drive_service.api, 'create_folder', |
| 46 | + side_effect=fake_api_create): |
| 47 | + result = drive_service.create_folder('NewDir') |
| 48 | + |
| 49 | + assert captured['payload']['parentFolderUuid'] == 'root-uuid' |
| 50 | + assert captured['payload']['plainName'] == 'NewDir' |
| 51 | + assert result['uuid'] == 'new-uuid' |
| 52 | + |
| 53 | + |
| 54 | +def test_create_folder_with_explicit_parent(fake_creds): |
| 55 | + captured = {} |
| 56 | + def fake_api_create(payload): |
| 57 | + captured['payload'] = payload |
| 58 | + return {'uuid': 'new-uuid'} |
| 59 | + with patch.object(drive_service.auth, 'get_auth_details', |
| 60 | + return_value=fake_creds), \ |
| 61 | + patch.object(drive_service.api, 'create_folder', |
| 62 | + side_effect=fake_api_create): |
| 63 | + drive_service.create_folder('Sub', parent_folder_uuid='custom-parent') |
| 64 | + assert captured['payload']['parentFolderUuid'] == 'custom-parent' |
| 65 | + |
| 66 | + |
| 67 | +def test_create_folder_includes_timestamps_when_provided(fake_creds): |
| 68 | + captured = {} |
| 69 | + def fake_api_create(payload): |
| 70 | + captured['payload'] = payload |
| 71 | + return {'uuid': 'new-uuid'} |
| 72 | + with patch.object(drive_service.auth, 'get_auth_details', |
| 73 | + return_value=fake_creds), \ |
| 74 | + patch.object(drive_service.api, 'create_folder', |
| 75 | + side_effect=fake_api_create): |
| 76 | + drive_service.create_folder( |
| 77 | + 'Dated', parent_folder_uuid='p', |
| 78 | + creation_time='2025-01-01T00:00:00Z', |
| 79 | + modification_time='2025-06-01T00:00:00Z', |
| 80 | + ) |
| 81 | + assert captured['payload']['creationTime'] == '2025-01-01T00:00:00Z' |
| 82 | + assert captured['payload']['modificationTime'] == '2025-06-01T00:00:00Z' |
| 83 | + |
| 84 | + |
| 85 | +def test_create_folder_omits_timestamps_when_none(fake_creds): |
| 86 | + captured = {} |
| 87 | + def fake_api_create(payload): |
| 88 | + captured['payload'] = payload |
| 89 | + return {'uuid': 'new-uuid'} |
| 90 | + with patch.object(drive_service.auth, 'get_auth_details', |
| 91 | + return_value=fake_creds), \ |
| 92 | + patch.object(drive_service.api, 'create_folder', |
| 93 | + side_effect=fake_api_create): |
| 94 | + drive_service.create_folder('Plain', parent_folder_uuid='p') |
| 95 | + assert 'creationTime' not in captured['payload'] |
| 96 | + assert 'modificationTime' not in captured['payload'] |
| 97 | + |
| 98 | + |
| 99 | +def test_create_folder_updates_parent_cache_when_present(fake_creds): |
| 100 | + """If parent is already cached, the new folder appears in subsequent listings |
| 101 | + without a re-fetch.""" |
| 102 | + drive_service.folder_content_cache['parent-uuid'] = ( |
| 103 | + 9999999999.0, {'folders': [], 'files': []}, |
| 104 | + ) |
| 105 | + |
| 106 | + new_folder = {'uuid': 'new-uuid', 'plainName': 'NewDir'} |
| 107 | + with patch.object(drive_service.auth, 'get_auth_details', |
| 108 | + return_value=fake_creds), \ |
| 109 | + patch.object(drive_service.api, 'create_folder', |
| 110 | + return_value=new_folder): |
| 111 | + drive_service.create_folder('NewDir', parent_folder_uuid='parent-uuid') |
| 112 | + |
| 113 | + _, content = drive_service.folder_content_cache['parent-uuid'] |
| 114 | + assert any(f.get('uuid') == 'new-uuid' for f in content['folders']) |
| 115 | + |
| 116 | + |
| 117 | +def test_create_folder_skips_cache_update_when_parent_not_cached(fake_creds): |
| 118 | + """If parent isn't cached, no cache mutation — the next get_folder_content |
| 119 | + call will fetch fresh.""" |
| 120 | + new_folder = {'uuid': 'new-uuid', 'plainName': 'NewDir'} |
| 121 | + with patch.object(drive_service.auth, 'get_auth_details', |
| 122 | + return_value=fake_creds), \ |
| 123 | + patch.object(drive_service.api, 'create_folder', |
| 124 | + return_value=new_folder): |
| 125 | + drive_service.create_folder('NewDir', parent_folder_uuid='unknown-parent') |
| 126 | + # Cache stays empty (no entry was created) |
| 127 | + assert 'unknown-parent' not in drive_service.folder_content_cache |
| 128 | + |
| 129 | + |
| 130 | +def test_create_folder_raises_when_no_root_id(): |
| 131 | + """If creds have no rootFolderId AND no explicit parent → ValueError.""" |
| 132 | + bad_creds = {'user': {}} |
| 133 | + with patch.object(drive_service.auth, 'get_auth_details', |
| 134 | + return_value=bad_creds): |
| 135 | + with pytest.raises(ValueError, match="No root folder"): |
| 136 | + drive_service.create_folder('X') |
| 137 | + |
| 138 | + |
| 139 | +# ---------- update_file (WebDAV PUT-on-existing) ---------- |
| 140 | + |
| 141 | +def test_update_file_full_cycle(tmp_path, fake_creds): |
| 142 | + """update_file: read local file → encrypt → start upload → upload chunk → |
| 143 | + finish upload → replace_file metadata. Verify each step is called.""" |
| 144 | + local = tmp_path / "doc.txt" |
| 145 | + local.write_bytes(b"updated content") |
| 146 | + |
| 147 | + with patch.object(drive_service.api, 'get_file_metadata', |
| 148 | + return_value={'plainName': 'doc'}), \ |
| 149 | + patch.object(drive_service.auth, 'get_auth_details', |
| 150 | + return_value=fake_creds), \ |
| 151 | + patch.object(drive_service.api, 'start_upload', |
| 152 | + return_value={'uploads': [{ |
| 153 | + 'index': 0, 'size': 100, |
| 154 | + 'url': 'https://upload', 'uuid': 'net-uuid', |
| 155 | + }]}), \ |
| 156 | + patch.object(drive_service.api, 'upload_chunk') as mock_chunk, \ |
| 157 | + patch.object(drive_service.api, 'finish_upload', |
| 158 | + return_value={'id': 'new-net-id'}), \ |
| 159 | + patch.object(drive_service.api, 'replace_file', |
| 160 | + return_value={'success': True}) as mock_replace, \ |
| 161 | + patch.object(drive_service, '_clear_parent_cache_for_item'): |
| 162 | + result = drive_service.update_file('file-uuid', str(local)) |
| 163 | + |
| 164 | + assert result['success'] is True |
| 165 | + mock_chunk.assert_called_once() |
| 166 | + # replace_file gets the new network file id and the local file size |
| 167 | + args, _ = mock_replace.call_args |
| 168 | + assert args[0] == 'file-uuid' |
| 169 | + assert args[1]['fileId'] == 'new-net-id' |
| 170 | + assert args[1]['size'] == len(b"updated content") |
| 171 | + |
| 172 | + |
| 173 | +def test_update_file_clears_parent_cache(tmp_path, fake_creds): |
| 174 | + """After the update, parent cache must be invalidated so listings refresh.""" |
| 175 | + local = tmp_path / "doc.txt" |
| 176 | + local.write_bytes(b"x") |
| 177 | + |
| 178 | + with patch.object(drive_service.api, 'get_file_metadata', |
| 179 | + return_value={'plainName': 'doc'}), \ |
| 180 | + patch.object(drive_service.auth, 'get_auth_details', |
| 181 | + return_value=fake_creds), \ |
| 182 | + patch.object(drive_service.api, 'start_upload', |
| 183 | + return_value={'uploads': [{ |
| 184 | + 'index': 0, 'size': 1, |
| 185 | + 'url': 'u', 'uuid': 'n', |
| 186 | + }]}), \ |
| 187 | + patch.object(drive_service.api, 'upload_chunk'), \ |
| 188 | + patch.object(drive_service.api, 'finish_upload', |
| 189 | + return_value={'id': 'nid'}), \ |
| 190 | + patch.object(drive_service.api, 'replace_file', |
| 191 | + return_value={}), \ |
| 192 | + patch.object(drive_service, '_clear_parent_cache_for_item') as mock_clear: |
| 193 | + drive_service.update_file('file-uuid', str(local)) |
| 194 | + |
| 195 | + mock_clear.assert_called_once_with('file-uuid', 'file') |
| 196 | + |
| 197 | + |
| 198 | +def test_update_file_wraps_errors(): |
| 199 | + with patch.object(drive_service.api, 'get_file_metadata', |
| 200 | + side_effect=ConnectionError("net")): |
| 201 | + with pytest.raises(Exception, match="Failed to update file"): |
| 202 | + drive_service.update_file('file-uuid', '/tmp/nope') |
0 commit comments