From d530fa9013df30b5c016f366e945033ec0ad8768 Mon Sep 17 00:00:00 2001 From: Jacob Rief Date: Thu, 9 Jan 2025 16:10:22 +0100 Subject: [PATCH] refactor folder unittests out of demoapp --- .../{unittests.yml => unit-tests.yml} | 17 +- demoapp/unittests/test_folder_admin.py | 153 ---------- finder/admin/folder.py | 48 +-- finder/admin/inode.py | 18 +- demoapp/pytest.ini => pytest.ini | 2 +- {demoapp/unittests => unittests}/__init__.py | 0 {demoapp/unittests => unittests}/conftest.py | 8 +- {demoapp => unittests}/fonts/Courier.ttf | Bin unittests/settings.py | 82 +++++ unittests/test_folder_admin.py | 282 ++++++++++++++++++ unittests/urls.py | 7 + {demoapp => unittests}/utils.py | 0 12 files changed, 418 insertions(+), 199 deletions(-) rename .github/workflows/{unittests.yml => unit-tests.yml} (67%) delete mode 100644 demoapp/unittests/test_folder_admin.py rename demoapp/pytest.ini => pytest.ini (57%) rename {demoapp/unittests => unittests}/__init__.py (100%) rename {demoapp/unittests => unittests}/conftest.py (80%) rename {demoapp => unittests}/fonts/Courier.ttf (100%) create mode 100644 unittests/settings.py create mode 100644 unittests/test_folder_admin.py create mode 100644 unittests/urls.py rename {demoapp => unittests}/utils.py (100%) diff --git a/.github/workflows/unittests.yml b/.github/workflows/unit-tests.yml similarity index 67% rename from .github/workflows/unittests.yml rename to .github/workflows/unit-tests.yml index ddfa4c23c..c3e7e8c02 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unit-tests.yml @@ -23,29 +23,22 @@ jobs: matrix: python-version: ["3.11", "3.12"] django-version: ["5.2.*"] - node-version: ["18.x"] + with-cte: [true, false] steps: - uses: actions/checkout@v3 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v2 - with: - node-version: ${{ matrix.node-version }} - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - npm install --include=dev python -m pip install --upgrade pip python -m pip install https://github.com/django/django/archive/refs/heads/main.zip - python -m pip install django-cte django-entangled ffmpeg-python pillow reportlab svglib + python -m pip install django-entangled ffmpeg-python pillow reportlab svglib python -m pip install beautifulsoup4 coverage Faker lxml pytest pytest-django pytest-cov - - name: Build Client - run: | - npm run compilescss - npm run esbuild + - if: ${{ matrix.with-cte }} + run: python -m pip install django-cte - name: Test with pytest run: | - python -m pytest -v demoapp/unittests + python -m pytest -v unittests diff --git a/demoapp/unittests/test_folder_admin.py b/demoapp/unittests/test_folder_admin.py deleted file mode 100644 index 871d3b36d..000000000 --- a/demoapp/unittests/test_folder_admin.py +++ /dev/null @@ -1,153 +0,0 @@ -import json -import os -from uuid import uuid5, NAMESPACE_DNS - -import hashlib -import pytest - -from bs4 import BeautifulSoup - -from django.conf import settings -from django.core.files.uploadedfile import SimpleUploadedFile -from django.test.client import MULTIPART_CONTENT -from django.urls import reverse - -from filer import settings as filer_settings -from finder.contrib.image.models import ImageFileModel -from finder.models.folder import FolderModel -from finder.models.realm import RealmModel - - -@pytest.mark.django_db -def test_create_realm_on_first_access(admin_client): - assert RealmModel.objects.exists() is False - response = admin_client.get(reverse('admin:finder_foldermodel_changelist')) - assert response.status_code == 302 - realm = RealmModel.objects.first() - assert realm is not None - redirected = reverse('admin:finder_inodemodel_change', kwargs={'inode_id': realm.root_folder.id}) - assert response.url == redirected - assert realm.root_folder.is_folder is True - assert realm.root_folder.is_trash is False - assert realm.root_folder.owner == response.wsgi_request.user - assert realm.root_folder.name == '__root__' - assert realm.root_folder.parent is None - assert realm.root_folder.is_root - assert realm.trash_folders.count() == 0 - - -@pytest.mark.django_db -def test_access_root_folder(realm, admin_client): - admin_url = reverse('admin:finder_inodemodel_change', kwargs={'inode_id': realm.root_folder.id}) - response = admin_client.get(admin_url) - assert response.status_code == 200 - soup = BeautifulSoup(response.content, 'html.parser') - assert soup.title.string == "Root | Change Folder | Django site admin" - script_element = soup.find(id='finder-settings') - assert script_element.name == 'script' - finder_settings = json.loads(script_element.string) - finder_settings.pop('csrf_token') - finder_settings.pop('favorite_folders') - finder_settings.pop('menu_extensions') - assert finder_settings == { - 'name': '__root__', - 'is_folder': True, - 'folder_id': str(realm.root_folder.id), - 'parent_id': None, - 'parent_url': None, - 'is_root': True, - 'is_trash': False, - 'folder_url': admin_url, - 'base_url': reverse('admin:finder_foldermodel_changelist'), - 'ancestors': [str(realm.root_folder.id)], - } - - -@pytest.mark.django_db -def test_access_folder_not_found(admin_client): - not_inode_id = uuid5(NAMESPACE_DNS, 'not-found') - admin_url = reverse('admin:finder_inodemodel_change', kwargs={'inode_id': not_inode_id}) - response = admin_client.get(admin_url) - assert response.status_code == 404 - - - -@pytest.mark.django_db -def test_folder_upload_file(realm, admin_client): - admin_url = reverse('admin:finder_inodemodel_change', kwargs={'inode_id': realm.root_folder.id}) - sha1 = hashlib.sha1() - with open(settings.BASE_DIR / 'workdir/assets/image_0.png', 'rb') as file_handle: - response = admin_client.post( - f'{admin_url}/upload', - {'upload_file': file_handle, 'filename': 'image_0.png'}, - content_type=MULTIPART_CONTENT % {'boundary': 'BoUnDaRyStRiNg'}, - ) - file_handle.seek(0) - while chunk := file_handle.read(4096): - sha1.update(chunk) - assert response.status_code == 200 - file_info = response.json()['file_info'] - id = file_info['id'] - assert file_info['name'] == 'image_0.png' - assert file_info['file_size'] == os.stat(settings.BASE_DIR / 'workdir/assets/image_0.png').st_size - assert file_info['sha1'] == sha1.hexdigest() - assert file_info['mime_type'] == 'image/png' - filer_public = filer_settings.FILER_STORAGES['public']['main']['UPLOAD_TO_PREFIX'] - assert file_info['download_url'] == f'{settings.MEDIA_URL}{filer_public}/{id[0:2]}/{id[2:4]}/{id}/image_0.png' - filer_public_thumbnails = filer_settings.FILER_STORAGES['public']['thumbnails']['THUMBNAIL_OPTIONS']['base_dir'] - assert file_info['thumbnail_url'] == f'{settings.MEDIA_URL}{filer_public_thumbnails}/{id[0:2]}/{id[2:4]}/{id}/image_0__180x180.png' - assert ImageFileModel.objects.filter(id=id).exists() - - -@pytest.fixture -def uploaded_images(realm, admin_user): - images = [] - for counter in range(10): - file_name = f'image_{counter}.png' - with open(settings.BASE_DIR / f'workdir/assets/{file_name}', 'rb') as file_handle: - uploaded_file = SimpleUploadedFile(file_name, file_handle.read(), content_type='image/png') - images.append( - ImageFileModel.objects.create_from_upload( - uploaded_file, - folder=realm.root_folder, - owner=admin_user, - ) - ) - return images - - -@pytest.mark.django_db -def test_folder_fetch(realm, uploaded_images, admin_client): - admin_url = reverse('admin:finder_inodemodel_change', kwargs={'inode_id': realm.root_folder.id}) - response = admin_client.get(f'{admin_url}/fetch') - assert response.status_code == 200 - inodes = response.json()['inodes'] - image_ids = {str(im.id) for im in ImageFileModel.objects.all()} - assert {inode['id'] for inode in inodes} == image_ids - - -@pytest.mark.django_db -def test_create_sub_folder(realm, admin_client): - admin_url = reverse('admin:finder_inodemodel_change', kwargs={'inode_id': realm.root_folder.id}) - response = admin_client.post(f'{admin_url}/add_folder', {'name': "Sub Folder"}, content_type='application/json') - assert response.status_code == 200 - new_folder = response.json()['new_folder'] - assert new_folder['name'] == "Sub Folder" - sub_folder = FolderModel.objects.get(id=new_folder['id']) - response = admin_client.get(f'{admin_url}/fetch') - assert response.status_code == 200 - inodes = response.json()['inodes'] - assert inodes[0]['id'] == str(sub_folder.id) - assert inodes[0]['name'] == "Sub Folder" - assert inodes[0]['is_folder'] is True - assert inodes[0]['parent'] == str(realm.root_folder.id) - assert inodes[0]['thumbnail_url'] == f'{settings.STATIC_URL}filer/icons/folder.svg' - - # with missing content-type - response = admin_client.post(f'{admin_url}/add_folder') - assert response.status_code == 400 - - # add a second folder with the same name - response = admin_client.post(f'{admin_url}/add_folder', {'name': "Sub Folder"}, content_type='application/json') - assert response.status_code == 409 - assert response.content.decode() == "A folder named “Sub Folder” already exists." diff --git a/finder/admin/folder.py b/finder/admin/folder.py index 5893cede5..3b330a514 100644 --- a/finder/admin/folder.py +++ b/finder/admin/folder.py @@ -5,7 +5,7 @@ from django.db.models import QuerySet, Subquery from django.forms.widgets import Media -from django.http.response import HttpResponse, HttpResponseBadRequest, HttpResponseNotFound, JsonResponse +from django.http.response import HttpResponse, HttpResponseNotAllowed, HttpResponseNotFound, JsonResponse from django.templatetags.static import static from django.urls import path, reverse from django.utils.translation import gettext @@ -175,8 +175,12 @@ def get_model_admin(self, mime_type): return self._model_admin_cache[mime_type] def fetch_inodes(self, request, folder_id): - if not (current_folder := self.get_object(request, folder_id)): - return HttpResponseNotFound(f"Folder {folder_id} not found.") + if request.method != 'GET': + return HttpResponseNotAllowed(f"Method {request.method} not allowed. Only GET requests are allowed.") + try: + current_folder = self.get_object(request, folder_id) + except ObjectDoesNotExist: + return HttpResponseNotFound(f"FolderModel<{folder_id}> not found.") if search_query := request.GET.get('q'): inode_qs = self.search_for_inodes(request, current_folder, search_query) else: @@ -204,11 +208,13 @@ def search_for_inodes(self, request, current_folder, search_query, **lookup): def upload_file(self, request, folder_id): if request.method != 'POST': - return HttpResponseBadRequest(f"Method {request.method} not allowed. Only POST requests are allowed.") - if not (folder := self.get_object(request, folder_id)): - return HttpResponseNotFound(f"Folder {folder_id} not found.") + return HttpResponseNotAllowed(f"Method {request.method} not allowed. Only POST requests are allowed.") + try: + folder = self.get_object(request, folder_id) + except ObjectDoesNotExist: + return HttpResponseNotFound(f"FolderModel<{folder_id}> not found.") if request.content_type != 'multipart/form-data' or 'upload_file' not in request.FILES: - return HttpResponseBadRequest("Bad encoding type or missing payload.") + return HttpResponse("Bad encoding type or missing payload.", status=415) model = FileModel.objects.get_model_for(request.FILES['upload_file'].content_type) new_file = model.objects.create_from_upload( request.FILES['upload_file'], @@ -223,17 +229,17 @@ def update_inode(self, request, folder_id): body = json.loads(request.body) try: obj = self.get_object(request, body['id']) - except (InodeModel.DoesNotExist, KeyError): - return HttpResponseNotFound(f"Inode(id={body.get('id', '')}) not found.") + except (ObjectDoesNotExist, KeyError): + return HttpResponseNotFound(f"InodeModel')}> not found.") current_folder = self.get_object(request, folder_id) inode_name = body['name'] try: filename_validator(inode_name) except ValidationError as exc: - return HttpResponseBadRequest(exc.messages[0], status=409) + return HttpResponse(exc.messages[0], status=409) if current_folder.listdir(name=inode_name, is_folder=True).exists(): msg = gettext("A folder named “{name}” already exists.") - return HttpResponseBadRequest(msg.format(name=inode_name), status=409) + return HttpResponse(msg.format(name=inode_name), status=409) update_values = {} for field in self.get_fields(request, obj): if field in body and body[field] != getattr(obj, field): @@ -241,15 +247,9 @@ def update_inode(self, request, folder_id): update_values[field] = body[field] if update_values: obj.save(update_fields=list(update_values.keys())) - favorite_folders = self.get_favorite_folders(request, current_folder) - if update_values: - for folder in favorite_folders: - if folder['id'] == obj.id: - folder.update(update_values) - break return JsonResponse({ 'new_inode': self.serialize_inode(obj), - 'favorite_folders': favorite_folders, + 'favorite_folders': self.get_favorite_folders(request, current_folder), }) def copy_inodes(self, request, folder_id): @@ -263,7 +263,7 @@ def copy_inodes(self, request, folder_id): try: inode.copy_to(current_folder, owner=request.user) except RecursionError as exc: - return HttpResponseBadRequest(str(exc), status=409) + return HttpResponse(str(exc), status=409) return JsonResponse({ 'inodes': list(self.get_inodes(request, parent=current_folder)), }) @@ -291,7 +291,7 @@ def move_inodes(self, request, folder_id): proxy_obj.validate_constraints() proxy_obj._meta.model.objects.filter(id=entry['id']).update(parent=target_folder) except ValidationError as exc: - return HttpResponseBadRequest(exc.messages[0], status=409) + return HttpResponse(exc.messages[0], status=409) return JsonResponse({ 'inodes': list(self.get_inodes(request, parent=target_folder)), }) @@ -303,7 +303,7 @@ def delete_inodes(self, request, folder_id): current_folder = self.get_object(request, folder_id) trash_folder = self.get_trash_folder(request) if current_folder.id == trash_folder.id: - return HttpResponseBadRequest("Cannot move inodes from trash folder into itself.") + return HttpResponse("Cannot move inodes from trash folder into itself.", status=409) inode_ids = body.get('inode_ids', []) for entry in FolderModel.objects.filter_unified(id__in=inode_ids): inode = FolderModel.objects.get_inode(id=entry['id']) @@ -324,7 +324,7 @@ def delete_inodes(self, request, folder_id): def undo_discarded_inodes(self, request): if request.method != 'POST': - return HttpResponseBadRequest(f"Method {request.method} not allowed. Only POST requests are allowed.") + return HttpResponseNotAllowed(f"Method {request.method} not allowed. Only POST requests are allowed.") body = json.loads(request.body) trash_folder = self.get_trash_folder(request) discarded_inodes = DiscardedInode.objects.filter(inode__in=body.get('inode_ids', [])) @@ -337,7 +337,7 @@ def undo_discarded_inodes(self, request): def erase_trash_folder(self, request): if request.method != 'DELETE': - return HttpResponseBadRequest(f"Method {request.method} not allowed. Only DELETE requests are allowed.") + return HttpResponseNotAllowed(f"Method {request.method} not allowed. Only DELETE requests are allowed.") trash_folder_entries = self.get_trash_folder(request).listdir() DiscardedInode.objects.filter(inode__in=list(trash_folder_entries.values_list('id', flat=True))).delete() for entry in trash_folder_entries: @@ -360,7 +360,7 @@ def add_folder(self, request, folder_id): body = json.loads(request.body) if parent_folder.listdir(name=body['name'], is_folder=True).exists(): msg = gettext("A folder named “{name}” already exists.") - return HttpResponseBadRequest(msg.format(name=body['name']), status=409) + return HttpResponse(msg.format(name=body['name']), status=409) new_folder = FolderModel.objects.create( name=body['name'], parent=parent_folder, diff --git a/finder/admin/inode.py b/finder/admin/inode.py index 89004b18c..6ceb4861c 100644 --- a/finder/admin/inode.py +++ b/finder/admin/inode.py @@ -1,10 +1,15 @@ import json +from django.core.exceptions import ObjectDoesNotExist + from django.contrib import admin from django.contrib.sites.shortcuts import get_current_site from django.db.models.expressions import F, Value from django.db.models.fields import BooleanField -from django.http.response import HttpResponseBadRequest, HttpResponseNotFound, HttpResponseRedirect, JsonResponse +from django.http.response import ( + HttpResponse, HttpResponseBadRequest, HttpResponseNotAllowed, HttpResponseNotFound, HttpResponseRedirect, + JsonResponse, +) from django.middleware.csrf import get_token from django.template.response import TemplateResponse from django.urls import path, reverse @@ -32,10 +37,15 @@ def get_object(self, request, inode_id, *args): def check_for_valid_post_request(self, request, folder_id): if request.method != 'POST': - return HttpResponseBadRequest(f"Method {request.method} not allowed. Only POST requests are allowed.") + return HttpResponseNotAllowed(f"Method {request.method} not allowed. Only POST requests are allowed.") if request.content_type != 'application/json': - return HttpResponseBadRequest(f"Invalid content-type {request.content_type}. Only application/json is allowed.") - if self.get_object(request, folder_id) is None: + return HttpResponse( + f"Invalid content-type {request.content_type}. Only application/json is allowed.", + status=415, + ) + try: + self.get_object(request, folder_id) + except ObjectDoesNotExist: return HttpResponseNotFound(f"Folder with id “{folder_id}” not found.") def toggle_pin(self, request, folder_id): diff --git a/demoapp/pytest.ini b/pytest.ini similarity index 57% rename from demoapp/pytest.ini rename to pytest.ini index d6e8c50eb..a301c221e 100644 --- a/demoapp/pytest.ini +++ b/pytest.ini @@ -1,4 +1,4 @@ [pytest] -DJANGO_SETTINGS_MODULE = demoapp.settings +DJANGO_SETTINGS_MODULE = unittests.settings django_find_project = false addopts = --tb=native diff --git a/demoapp/unittests/__init__.py b/unittests/__init__.py similarity index 100% rename from demoapp/unittests/__init__.py rename to unittests/__init__.py diff --git a/demoapp/unittests/conftest.py b/unittests/conftest.py similarity index 80% rename from demoapp/unittests/conftest.py rename to unittests/conftest.py index f6a40c380..c22507a03 100644 --- a/demoapp/unittests/conftest.py +++ b/unittests/conftest.py @@ -6,10 +6,9 @@ from django.contrib.admin.sites import site as admin_site from django.urls import reverse -from finder.models.realm import RealmModel from finder.models.folder import FolderModel -from ..utils import create_random_image +from .utils import create_random_image os.environ.setdefault('DJANGO_ALLOW_ASYNC_UNSAFE', 'true') @@ -17,9 +16,8 @@ @pytest.fixture(autouse=True, scope='session') def create_assets(): os.makedirs(settings.BASE_DIR / 'workdir/assets', exist_ok=True) - for counter in range(10): - image = create_random_image() - image.save(settings.BASE_DIR / 'workdir/assets' / f'image_{counter:01d}.png') + image = create_random_image() + image.save(settings.BASE_DIR / 'workdir/assets' / f'demo_image.png') @pytest.fixture(scope='session') diff --git a/demoapp/fonts/Courier.ttf b/unittests/fonts/Courier.ttf similarity index 100% rename from demoapp/fonts/Courier.ttf rename to unittests/fonts/Courier.ttf diff --git a/unittests/settings.py b/unittests/settings.py new file mode 100644 index 000000000..f66d841a3 --- /dev/null +++ b/unittests/settings.py @@ -0,0 +1,82 @@ +import os +from pathlib import Path + +BASE_DIR = Path(__file__).resolve().parent.parent + +SECRET_KEY = os.getenv('DJANGO_SECRET_KEY', 'secret_key') + +DEBUG = os.getenv('DJANGO_DEBUG') in ['true', 'True', '1', 'yes', 'Yes', 'y', 'on', 'On'] + +ALLOWED_HOSTS = ['*'] + +SITE_ID = 1 + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.sites', + 'django.contrib.staticfiles', + 'finder', + 'finder.contrib.archive', + 'finder.contrib.audio', + 'finder.contrib.common', + 'finder.contrib.image.pil', + 'finder.contrib.image.svg', + 'finder.contrib.video', +] + +MIDDLEWARE = [ + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', +] + +ROOT_URLCONF = 'unittests.urls' + +TEMPLATES = [{ + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, +}] + +WSGI_APPLICATION = 'wsgi.application' + + +if os.getenv('USE_POSTGRES', False) in ['1', 'True', 'true']: + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': 'finder', + 'USER': 'finder', + 'PASSWORD': '', + 'HOST': 'localhost', + 'PORT': 5432, + }, + } +else: + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'workdir/db.sqlite3', + }, + } + + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +USE_TZ = True + +TIME_ZONE = 'UTC' + +USE_I18N = True + +STATIC_URL = '/static/' + +MEDIA_ROOT = Path(os.getenv('DJANGO_MEDIA_ROOT', BASE_DIR / 'workdir/media')) + +MEDIA_URL = '/media/' diff --git a/unittests/test_folder_admin.py b/unittests/test_folder_admin.py new file mode 100644 index 000000000..404dee873 --- /dev/null +++ b/unittests/test_folder_admin.py @@ -0,0 +1,282 @@ +import json +import os +from uuid import uuid5, NAMESPACE_DNS + +import hashlib +import pytest + +from bs4 import BeautifulSoup + +from django.conf import settings +from django.core.files.uploadedfile import SimpleUploadedFile +from django.test.client import MULTIPART_CONTENT +from django.urls import reverse + +from filer import settings as filer_settings +from finder.contrib.image.models import ImageFileModel +from finder.models.folder import FolderModel +from finder.models.realm import RealmModel + + +@pytest.mark.django_db +def test_create_realm_on_first_access(admin_client): + assert RealmModel.objects.exists() is False + response = admin_client.get(reverse('admin:finder_foldermodel_changelist')) + assert response.status_code == 302 + realm = RealmModel.objects.first() + assert realm is not None + redirected = reverse('admin:finder_inodemodel_change', kwargs={'inode_id': realm.root_folder.id}) + assert response.url == redirected + assert realm.root_folder.is_folder is True + assert realm.root_folder.is_trash is False + assert realm.root_folder.owner == response.wsgi_request.user + assert realm.root_folder.name == '__root__' + assert realm.root_folder.parent is None + assert realm.root_folder.is_root + assert realm.trash_folders.count() == 0 + + +@pytest.mark.django_db +def test_access_root_folder(realm, admin_client): + admin_url = reverse('admin:finder_inodemodel_change', kwargs={'inode_id': realm.root_folder.id}) + response = admin_client.get(admin_url) + assert response.status_code == 200 + soup = BeautifulSoup(response.content, 'html.parser') + assert soup.title.string == "Root | Change Folder | Django site admin" + script_element = soup.find(id='finder-settings') + assert script_element.name == 'script' + finder_settings = json.loads(script_element.string) + finder_settings.pop('csrf_token') + finder_settings.pop('favorite_folders') + finder_settings.pop('menu_extensions') + assert finder_settings == { + 'name': '__root__', + 'is_folder': True, + 'folder_id': str(realm.root_folder.id), + 'parent_id': None, + 'parent_url': None, + 'is_root': True, + 'is_trash': False, + 'folder_url': admin_url, + 'base_url': reverse('admin:finder_foldermodel_changelist'), + 'ancestors': [str(realm.root_folder.id)], + } + + +@pytest.mark.django_db +def test_access_folder_not_found(admin_client): + not_inode_id = uuid5(NAMESPACE_DNS, 'not-found') + admin_url = reverse('admin:finder_inodemodel_change', kwargs={'inode_id': not_inode_id}) + response = admin_client.get(admin_url) + assert response.status_code == 404 + + +@pytest.mark.django_db +def test_folder_upload_file(realm, admin_client): + admin_url = reverse('admin:finder_inodemodel_change', kwargs={'inode_id': realm.root_folder.id}) + sha1 = hashlib.sha1() + with open(settings.BASE_DIR / 'workdir/assets/demo_image.png', 'rb') as file_handle: + response = admin_client.post( + f'{admin_url}/upload', + {'upload_file': file_handle, 'filename': 'demo_image.png'}, + content_type=MULTIPART_CONTENT % {'boundary': 'BoUnDaRyStRiNg'}, + ) + file_handle.seek(0) + while chunk := file_handle.read(4096): + sha1.update(chunk) + assert response.status_code == 200 + file_info = response.json()['file_info'] + id = file_info['id'] + assert file_info['name'] == 'demo_image.png' + assert file_info['file_size'] == os.stat(settings.BASE_DIR / 'workdir/assets/demo_image.png').st_size + assert file_info['sha1'] == sha1.hexdigest() + assert file_info['mime_type'] == 'image/png' + filer_public = filer_settings.FILER_STORAGES['public']['main']['UPLOAD_TO_PREFIX'] + assert file_info['download_url'] == f'{settings.MEDIA_URL}{filer_public}/{id[0:2]}/{id[2:4]}/{id}/demo_image.png' + filer_public_thumbnails = filer_settings.FILER_STORAGES['public']['thumbnails']['THUMBNAIL_OPTIONS']['base_dir'] + assert file_info['thumbnail_url'] == f'{settings.MEDIA_URL}{filer_public_thumbnails}/{id[0:2]}/{id[2:4]}/{id}/demo_image__180x180.png' + assert ImageFileModel.objects.filter(id=id).exists() + + # with wrong method + response = admin_client.get(admin_url + '/upload') + assert response.status_code == 405 + + # with wrong encoding + response = admin_client.post(admin_url + '/upload', content_type='application/json') + assert response.status_code == 415 + + # with missing folder + missing_folder_id = uuid5(NAMESPACE_DNS, 'missing-folder') + response = admin_client.post( + reverse('admin:finder_inodemodel_change', kwargs={'inode_id': missing_folder_id}) + '/upload', + ) + assert response.status_code == 404 + + +@pytest.fixture +def uploaded_image(realm, admin_user): + file_name = f'demo_image.png' + with open(settings.BASE_DIR / 'workdir/assets' / file_name, 'rb') as file_handle: + uploaded_file = SimpleUploadedFile(file_name, file_handle.read(), content_type='image/png') + return ImageFileModel.objects.create_from_upload( + uploaded_file, + folder=realm.root_folder, + owner=admin_user, + ) + + +@pytest.fixture +def sub_folder(realm, admin_user): + return FolderModel.objects.create( + parent=realm.root_folder, + name='Sub Folder', + owner=admin_user, + ) + + +@pytest.mark.django_db +def test_folder_fetch(realm, uploaded_image, admin_client): + admin_url = reverse('admin:finder_inodemodel_change', kwargs={'inode_id': realm.root_folder.id}) + response = admin_client.get(f'{admin_url}/fetch') + assert response.status_code == 200 + inodes = response.json()['inodes'] + assert ImageFileModel.objects.filter(parent=realm.root_folder, id=inodes[0]['id']).exists() + + # found using search query + response = admin_client.get(f'{admin_url}/fetch?q=demo') + assert response.status_code == 200 + inodes = response.json()['inodes'] + assert ImageFileModel.objects.filter(parent=realm.root_folder, id=inodes[0]['id']).exists() + + # not found using search query + response = admin_client.get(f'{admin_url}/fetch?q=nemo') + assert response.status_code == 200 + inodes = response.json()['inodes'] + assert len(inodes) == 0 + + # with missing folder + missing_folder_id = uuid5(NAMESPACE_DNS, 'missing-folder') + response = admin_client.get( + reverse('admin:finder_inodemodel_change', kwargs={'inode_id': missing_folder_id}) + '/fetch', + ) + assert response.status_code == 404 + + # with wrong method + response = admin_client.head(f'{admin_url}/fetch') + assert response.status_code == 405 + + +@pytest.fixture +def update_inode_url(realm): + return reverse('admin:finder_inodemodel_change', kwargs={'inode_id': realm.root_folder.id}) + '/update' + + +@pytest.mark.django_db +def test_update_inode_nothing_changed(update_inode_url, uploaded_image, admin_client): + response = admin_client.post( + update_inode_url, + {'id': str(uploaded_image.id), 'name': 'demo_image.png'}, + content_type='application/json', + ) + assert response.status_code == 200 + + +@pytest.mark.django_db +def test_update_inode_update_filename(update_inode_url, uploaded_image, admin_client): + response = admin_client.post( + update_inode_url, + {'id': str(uploaded_image.id), 'name': 'renamed_image.png'}, + content_type='application/json', + ) + assert response.status_code == 200 + uploaded_image.refresh_from_db() + assert uploaded_image.name == 'renamed_image.png' + + +@pytest.mark.django_db +def test_update_inode_update_using_invalid_filename(update_inode_url, uploaded_image, admin_client): + response = admin_client.post( + update_inode_url, + {'id': str(uploaded_image.id), 'name': 'invalid:name'}, + content_type='application/json', + ) + assert response.status_code == 409 + + +@pytest.mark.django_db +def test_update_inode_using_existing_folder_name(update_inode_url, uploaded_image, sub_folder, admin_client): + response = admin_client.post( + update_inode_url, + {'id': str(uploaded_image.id), 'name': "Sub Folder"}, + content_type='application/json', + ) + assert response.status_code == 409 + + +@pytest.mark.django_db +def test_update_inode_rename_folder(update_inode_url, sub_folder, admin_client): + response = admin_client.post( + update_inode_url, + {'id': str(sub_folder.id), 'name': "Renamed Folder"}, + content_type='application/json', + ) + assert response.status_code == 200 + sub_folder.refresh_from_db() + assert sub_folder.name == "Renamed Folder" + + +@pytest.mark.django_db +def test_update_inode_update_with_missing_content_type(update_inode_url, uploaded_image, admin_client): + response = admin_client.post( + update_inode_url, + {'id': str(uploaded_image.id), 'name': 'renamed_image.png'}, + ) + assert response.status_code == 415 + + +@pytest.mark.django_db +def test_update_inode_update_with_missing_folder(admin_client): + missing_folder_id = uuid5(NAMESPACE_DNS, 'missing-folder') + response = admin_client.post( + reverse('admin:finder_inodemodel_change', kwargs={'inode_id': missing_folder_id}) + '/update', + content_type='application/json', + ) + assert response.status_code == 404 + + +@pytest.mark.django_db +def test_update_inode_update_with_missing_file(update_inode_url, admin_client): + missing_file_id = uuid5(NAMESPACE_DNS, 'missing-file') + response = admin_client.post( + update_inode_url, + {'id': str(missing_file_id)}, + content_type='application/json', + ) + assert response.status_code == 404 + + +@pytest.mark.django_db +def test_create_sub_folder(realm, admin_client): + admin_url = reverse('admin:finder_inodemodel_change', kwargs={'inode_id': realm.root_folder.id}) + response = admin_client.post(f'{admin_url}/add_folder', {'name': "Sub Folder"}, content_type='application/json') + assert response.status_code == 200 + new_folder = response.json()['new_folder'] + assert new_folder['name'] == "Sub Folder" + sub_folder = FolderModel.objects.get(id=new_folder['id']) + response = admin_client.get(f'{admin_url}/fetch') + assert response.status_code == 200 + inodes = response.json()['inodes'] + assert inodes[0]['id'] == str(sub_folder.id) + assert inodes[0]['name'] == "Sub Folder" + assert inodes[0]['is_folder'] is True + assert inodes[0]['parent'] == str(realm.root_folder.id) + assert inodes[0]['thumbnail_url'] == f'{settings.STATIC_URL}filer/icons/folder.svg' + + # with missing Content-Type + response = admin_client.post(f'{admin_url}/add_folder') + assert response.status_code == 415 + + # add a second folder with the same name + response = admin_client.post(f'{admin_url}/add_folder', {'name': "Sub Folder"}, content_type='application/json') + assert response.status_code == 409 + assert response.content.decode() == "A folder named “Sub Folder” already exists." diff --git a/unittests/urls.py b/unittests/urls.py new file mode 100644 index 000000000..5bcbcb851 --- /dev/null +++ b/unittests/urls.py @@ -0,0 +1,7 @@ +from django.contrib import admin +from django.urls import path + + +urlpatterns = [ + path('admin/', admin.site.urls), +] diff --git a/demoapp/utils.py b/unittests/utils.py similarity index 100% rename from demoapp/utils.py rename to unittests/utils.py