|
4 | 4 | from addons.osfstorage.models import OsfStorageFile, OsfStorageFolder |
5 | 5 | from osf.models import NodeLog, BaseFileNode |
6 | 6 | from osf.models.files import TrashedFileNode, TrashedFolder |
7 | | -from osf.management.commands.force_archive import get_file_obj_from_log, build_file_tree, DEFAULT_PERMISSIBLE_ADDONS |
| 7 | +from osf.management.commands.force_archive import get_file_obj_from_log, build_file_tree, handle_file_operation, DEFAULT_PERMISSIBLE_ADDONS |
8 | 8 | from osf_tests.factories import NodeFactory, RegistrationFactory |
9 | 9 |
|
10 | 10 |
|
@@ -192,3 +192,100 @@ def get_root(self): |
192 | 192 | names = [child['object'].name for child in tree['children']] |
193 | 193 | assert 'file1.txt' in names |
194 | 194 | assert 'file2.txt' not in names |
| 195 | + |
| 196 | + @pytest.mark.django_db |
| 197 | + def test_obj_cache_includes_folders(self, node, reg, permissible_addons): |
| 198 | + """ |
| 199 | + Regression: n.files is a GenericRelation to OsfStorageFile only, so folder _ids were |
| 200 | + never in obj_cache. The fix uses BaseFileNode.objects.filter(...) which includes folders. |
| 201 | + """ |
| 202 | + from django.contrib.contenttypes.models import ContentType |
| 203 | + |
| 204 | + folder = OsfStorageFolder.create(target=node, name='myfolder') |
| 205 | + folder.save() |
| 206 | + root_folder = OsfStorageFolder.create(target=node, name='') |
| 207 | + root_folder.save() |
| 208 | + |
| 209 | + # Demonstrate the BUG: n.files (GenericRelation to OsfStorageFile) omits folders |
| 210 | + old_obj_cache = set(node.files.values_list('_id', flat=True)) |
| 211 | + assert folder._id not in old_obj_cache, 'Folders must NOT appear via n.files (demonstrating the bug)' |
| 212 | + assert root_folder._id not in old_obj_cache, 'Root folder must NOT appear via n.files (demonstrating the bug)' |
| 213 | + |
| 214 | + # Demonstrate the FIX: BaseFileNode.objects.filter(...) includes files AND folders |
| 215 | + ct_id = ContentType.objects.get_for_model(node.__class__()).id |
| 216 | + new_obj_cache = set( |
| 217 | + BaseFileNode.objects.filter( |
| 218 | + target_object_id=node.id, |
| 219 | + target_content_type_id=ct_id, |
| 220 | + ).values_list('_id', flat=True) |
| 221 | + ) |
| 222 | + assert folder._id in new_obj_cache, 'Folders must appear in fixed obj_cache' |
| 223 | + assert root_folder._id in new_obj_cache, 'Root folder must appear in fixed obj_cache' |
| 224 | + |
| 225 | + |
| 226 | +class TestHandleFileOperation: |
| 227 | + |
| 228 | + @pytest.fixture |
| 229 | + def node(self): |
| 230 | + return NodeFactory(title='Test Node', category='project') |
| 231 | + |
| 232 | + @pytest.fixture |
| 233 | + def reg(self, node): |
| 234 | + return RegistrationFactory(project=node, registered_date=timezone.now()) |
| 235 | + |
| 236 | + @pytest.mark.django_db |
| 237 | + def test_addon_file_moved_from_root_dir(self, node, reg): |
| 238 | + """ |
| 239 | + Regression: when materialized='/' (root dir moved between nodes), the old code did: |
| 240 | + '/{}'.format('/').rstrip('/') -> '' |
| 241 | + ''.split('/') -> [''] (only 1 element) |
| 242 | + [''][-2] -> IndexError: list index out of range |
| 243 | + The fix detects the root-dir case and looks up the folder by name='' directly. |
| 244 | + """ |
| 245 | + from django.contrib.contenttypes.models import ContentType |
| 246 | + |
| 247 | + root_folder = OsfStorageFolder.create(target=node, name='') |
| 248 | + root_folder.save() |
| 249 | + file = OsfStorageFile.create(target=node, name='file.txt') |
| 250 | + file.save() |
| 251 | + file.move_under(root_folder) |
| 252 | + |
| 253 | + ct_id = ContentType.objects.get_for_model(node.__class__()).id |
| 254 | + obj_cache = set( |
| 255 | + BaseFileNode.objects.filter( |
| 256 | + target_object_id=node.id, |
| 257 | + target_content_type_id=ct_id, |
| 258 | + ).values_list('_id', flat=True) |
| 259 | + ) |
| 260 | + |
| 261 | + file_tree = { |
| 262 | + 'object': root_folder, |
| 263 | + 'name': '', |
| 264 | + 'deleted': False, |
| 265 | + 'version': None, |
| 266 | + 'children': [ |
| 267 | + {'object': file, 'name': 'file.txt', 'deleted': False, 'version': None, 'children': []} |
| 268 | + ] |
| 269 | + } |
| 270 | + |
| 271 | + # materialized='/' is the actual crash case: moving a root dir between nodes |
| 272 | + log = NodeLog.objects.create( |
| 273 | + node=node, |
| 274 | + action='addon_file_moved', |
| 275 | + params={ |
| 276 | + 'source': { |
| 277 | + 'materialized': '/', # root dir: triggers IndexError in old code |
| 278 | + 'name': '', |
| 279 | + }, |
| 280 | + 'destination': { |
| 281 | + 'materialized': '/', |
| 282 | + 'name': '', |
| 283 | + } |
| 284 | + }, |
| 285 | + date=timezone.now(), |
| 286 | + ) |
| 287 | + |
| 288 | + # Old code: '/{}'.format('/').rstrip('/') = '' -> ''.split('/')[-2] -> IndexError |
| 289 | + # Fixed code: detects no '/' in materialized.rstrip('/') and uses name='' directly |
| 290 | + result_tree, noop = handle_file_operation(file_tree, reg, file, log, obj_cache) |
| 291 | + assert result_tree is not None |
0 commit comments