"""Run: USE_SQLITE=true python manage.py test drive.test_space_reparent_and_access""" import tempfile import uuid from pathlib import Path from django.contrib.auth import get_user_model from django.test import TestCase, override_settings from rest_framework.test import APIClient from .models import DriveItem, DriveSpace, SpaceGrant from .permissions import space_rights from .spaces import _make_numbered_space, ensure_space, ensure_private_space from .quotas import occupancy class SpaceReparentingTests(TestCase): def setUp(self): tmp = tempfile.TemporaryDirectory() self.addCleanup(tmp.cleanup) self.storage = Path(tmp.name) override = override_settings(DRIVE_STORAGE_ROOT=self.storage, FILE_UPLOAD_TEMP_DIR=str(self.storage / '.uploads')) override.enable() self.addCleanup(override.disable) User = get_user_model() self.admin = User.objects.create_superuser('space-admin', 'admin@example.com', 'password-for-tests') self.member = User.objects.create_user('space-member', password='password-for-tests') self.client = APIClient() self.client.force_authenticate(self.admin) self.root = ensure_space(self.admin) gb = 1024 ** 3 self.root.quota_bytes = 100 * gb self.root.save(update_fields=['quota_bytes']) self.a = _make_numbered_space(self.root, managed_by=self.admin, display_name='A', quota_bytes=60 * gb) self.b = _make_numbered_space(self.root, managed_by=self.admin, display_name='B', quota_bytes=50 * gb) self.child = _make_numbered_space(self.a, managed_by=self.admin, display_name='Child', quota_bytes=30 * gb) self.nested = _make_numbered_space(self.child, managed_by=self.admin, display_name='Nested', quota_bytes=10 * gb) self.old_path = self.child.relative_path self.file = DriveItem.objects.create(owner=self.admin, space=self.nested, name='example.pdf', size=15, storage_key=str(uuid.uuid4()), storage_location=f'{self.nested.relative_path}/example.pdf') filepath = self.storage / self.file.storage_location filepath.parent.mkdir(parents=True, exist_ok=True) filepath.write_bytes(b'important bytes') def test_move_subtree_preserves_files_and_nested_spaces(self): res = self.client.patch(f'/api/drive/spaces/{self.child.pk}/', {'parent_space': self.b.pk}, format='json') self.assertEqual(res.status_code, 200, res.data) self.child.refresh_from_db() self.nested.refresh_from_db() self.file.refresh_from_db() expected = f'{self.b.relative_path}/{self.child.directory_name}' self.assertEqual(self.child.parent_id, self.b.pk) self.assertEqual(self.child.relative_path, expected) self.assertEqual(self.nested.relative_path, f'{expected}/{self.nested.directory_name}') self.assertEqual(self.file.storage_location, f'{self.nested.relative_path}/example.pdf') self.assertEqual((self.storage / self.file.storage_location).read_bytes(), b'important bytes') self.assertFalse((self.storage / self.old_path).exists()) self.assertEqual(occupancy(self.b)[0], 15) self.assertEqual(occupancy(self.a)[0], 0) def test_move_refused_if_it_would_create_cycle(self): res = self.client.patch(f'/api/drive/spaces/{self.a.pk}/', {'parent_space': self.nested.pk}, format='json') self.assertEqual(res.status_code, 400, res.data) self.a.refresh_from_db() self.assertEqual(self.a.parent_id, self.root.pk) self.assertTrue((self.storage / self.old_path).exists()) def test_move_rolls_back_when_new_quota_is_insufficient(self): res = self.client.patch(f'/api/drive/spaces/{self.child.pk}/', {'parent_space': self.b.pk, 'quota_bytes': 0}, format='json') self.assertEqual(res.status_code, 400, res.data) self.child.refresh_from_db() self.file.refresh_from_db() self.assertEqual(self.child.parent_id, self.a.pk) self.assertEqual(self.child.relative_path, self.old_path) self.assertTrue((self.storage / self.file.storage_location).is_file()) def test_manage_space_without_create_right_on_target_cannot_move(self): SpaceGrant.objects.create(user=self.member, space=self.child, permissions=['view','manage_space']) SpaceGrant.objects.create(user=self.member, space=self.b, permissions=['view']) self.client.force_authenticate(self.member) res = self.client.patch(f'/api/drive/spaces/{self.child.pk}/', {'parent_space': self.b.pk}, format='json') self.assertEqual(res.status_code, 403, res.data) self.child.refresh_from_db() self.assertEqual(self.child.parent_id, self.a.pk) def test_private_space_keeps_its_separate_physical_storage(self): SpaceGrant.objects.create(user=self.member, space=self.child, permissions=['view','download']) private = ensure_private_space(self.member) self.assertTrue(private.relative_path.startswith('privates/')) res = self.client.patch(f'/api/drive/spaces/{self.child.pk}/', {'parent_space': self.b.pk}, format='json') self.assertEqual(res.status_code, 200, res.data) private.refresh_from_db() self.assertEqual(private.parent_id, self.child.pk) self.assertTrue((self.storage/private.relative_path).is_dir()) def test_assigned_space_move_does_not_grant_parent_permissions(self): assigned = _make_numbered_space(self.a, user=self.member, display_name='Assigned', quota_bytes=10 * 1024**3, can_view_parent=True, can_write_parent=True, can_invite=True, parent_permissions=['view','download','add','manage_access']) self.assertIn('manage_access', space_rights(self.member,self.a)) res = self.client.patch(f'/api/drive/spaces/{assigned.pk}/', {'parent_space':self.b.pk}, format='json') self.assertEqual(res.status_code,200,res.data) assigned.refresh_from_db() self.assertFalse(assigned.can_view_parent) self.assertFalse(assigned.can_invite) self.assertFalse(space_rights(self.member,self.b)) def test_move_is_blocked_by_occupied_destination_quota(self): huge = 50 * 1024**3 DriveItem.objects.create(owner=self.admin,space=self.b,name='full.bin',size=huge, storage_key=str(uuid.uuid4()),storage_location='legacy-file') res = self.client.patch(f'/api/drive/spaces/{self.child.pk}/', {'parent_space':self.b.pk},format='json') self.assertEqual(res.status_code,400,res.data) self.child.refresh_from_db() self.assertEqual(self.child.parent_id,self.a.pk) self.assertTrue((self.storage/self.old_path).is_dir()) def test_admin_edits_direct_and_assigned_access_without_creating_duplicates(self): assigned = _make_numbered_space(self.root, user=self.member, display_name='Assigned', quota_bytes=10 * 1024**3) listing = self.client.get(f'/api/drive/admin/users/{self.member.pk}/access/') self.assertEqual(listing.status_code, 200, listing.data) self.assertEqual(sum(a['space_id'] == assigned.pk for a in listing.data['accesses']), 1) url = f'/api/drive/admin/users/{self.member.pk}/access/owner/{assigned.pk}/' saved = self.client.patch(url, {'permissions':['view','download']}, format='json') self.assertEqual(saved.status_code, 200, saved.data) self.assertEqual(space_rights(self.member, assigned), {'view','download'}) denied = self.client.patch(url, {'permissions':['modify']}, format='json') self.assertEqual(denied.status_code, 400, denied.data) listing = self.client.get(f'/api/drive/admin/users/{self.member.pk}/access/') self.assertEqual(sum(a['space_id'] == assigned.pk for a in listing.data['accesses']), 1) self.assertEqual(next(access['permissions'] for access in listing.data['accesses'] if access['space_id'] == assigned.pk), ['view','download']) revoked = self.client.delete(url) self.assertEqual(revoked.status_code, 204) self.assertFalse(SpaceGrant.objects.filter(user=self.member,space=assigned).exists())