From 0356844058e8bc4d4b1996b439963b5dc10b6d1d Mon Sep 17 00:00:00 2001 From: bruhjeshhh Date: Wed, 7 Jan 2026 02:27:26 +0530 Subject: [PATCH 1/3] Add functionality to delete all versions of a script - Add ScriptDeleteAllVersionsView to delete all versions of a script - Add API endpoint delete_all_versions to ScriptViewSet - Update delete_script.html template with dropdown to delete all versions - Add comprehensive test suite with 9 passing tests - Add testing documentation The feature allows script owners to delete all versions of their script at once, providing a clean interface for complete script removal. The dropdown only appears when a script has multiple versions. --- TESTING_DELETE_ALL_VERSIONS.md | 111 ++++++++++ scripts/templates/delete_script.html | 53 ++++- scripts/urls.py | 5 + scripts/views.py | 20 ++ scripts/viewsets.py | 22 ++ tests/settings.py | 102 +++++++++ tests/test_delete_all_versions.py | 315 +++++++++++++++++++++++++++ 7 files changed, 620 insertions(+), 8 deletions(-) create mode 100644 TESTING_DELETE_ALL_VERSIONS.md create mode 100644 tests/test_delete_all_versions.py diff --git a/TESTING_DELETE_ALL_VERSIONS.md b/TESTING_DELETE_ALL_VERSIONS.md new file mode 100644 index 00000000..9ddc2863 --- /dev/null +++ b/TESTING_DELETE_ALL_VERSIONS.md @@ -0,0 +1,111 @@ +# Testing Guide: Delete All Versions Feature + +This guide covers both manual testing and automated testing for the new "Delete All Versions" functionality. + +## Manual Testing + +### Prerequisites +1. Start your Django development server: + ```bash + python manage.py runserver + ``` + +2. Ensure you have: + - A user account with at least one script that has multiple versions + - Access to the Django admin or ability to create test data + +### Test Scenario 1: Web UI - Delete All Versions (Multiple Versions) + +1. **Navigate to a script page** that has 2+ versions: + - Go to: `http://localhost:8000/script/` + - Or find a script you own with multiple versions + +2. **Verify the dropdown appears**: + - Look for the "Delete Version" button + - If the script has multiple versions, you should see a dropdown arrow next to it + - Click the dropdown to see "Delete All Versions" option + +3. **Test the deletion**: + - Click "Delete All Versions" from the dropdown + - A modal should appear asking for confirmation + - The modal should show: "Are you sure you want to delete all X versions of 'Script Name'?" + - Click "Delete All Versions" in the modal + - You should be redirected to the home page (`/`) + - Verify the script no longer exists in the database + +### Test Scenario 2: Web UI - Single Version Script + +1. **Navigate to a script page** that has only 1 version: + - The "Delete Version" button should NOT have a dropdown + - Only the single version deletion option should be available + +### Test Scenario 3: Web UI - Permission Check + +1. **Try to delete a script you don't own**: + - Navigate to a script owned by another user + - You should NOT see the delete button at all (if `can_delete` is False) + - Or if you somehow access the URL directly, you should get a 403 Forbidden error + +### Test Scenario 4: API Endpoint + +1. **Get authentication credentials**: + ```bash + # You'll need Basic Auth credentials for a user who owns a script + ``` + +2. **Test the API endpoint**: + ```bash + # Replace with an actual ScriptVersion pk + # Replace and with your credentials + curl -X DELETE \ + -u : \ + http://localhost:8000/api/scripts//delete-all-versions/ + ``` + +3. **Expected responses**: + - **Success (204)**: Script and all versions deleted + - **404**: Script version not found + - **403**: Permission denied (not the owner) + - **401**: Authentication required + +### Test Scenario 5: Verify Cascade Deletion + +1. **Before deletion**, check the database: + ```python + # In Django shell: python manage.py shell + from scripts.models import Script, ScriptVersion + script = Script.objects.get(pk=) + version_count = script.versions.count() + print(f"Script has {version_count} versions") + ``` + +2. **Delete all versions** using either method + +3. **After deletion**, verify: + ```python + # Script should be gone + Script.objects.filter(pk=).exists() # Should be False + + # All versions should be gone + ScriptVersion.objects.filter(script_id=).exists() # Should be False + ``` + +## Automated Testing + +### Running Tests + +```bash +# Run all tests +pytest tests/ + +# Run with coverage +pytest tests/ --cov scripts + +# Run specific test file +pytest tests/test_delete_all_versions.py -v +``` + +### Example Test File + +See `tests/test_delete_all_versions.py` for a complete test suite. + diff --git a/scripts/templates/delete_script.html b/scripts/templates/delete_script.html index 6bd1e3bd..dce71584 100644 --- a/scripts/templates/delete_script.html +++ b/scripts/templates/delete_script.html @@ -1,30 +1,67 @@ {% load bootstrap4 %} {% load botc_script_tags %} - + - + +{% if script.versions.all|length > 1 %} + +{% endif %} + +
+ + {% if script.versions.all|length > 1 %} + + + {% endif %} +
diff --git a/scripts/urls.py b/scripts/urls.py index ed34d037..1108e48b 100644 --- a/scripts/urls.py +++ b/scripts/urls.py @@ -69,6 +69,11 @@ name="favourite", ), path("script/", views.ScriptView.as_view(), name="script"), + path( + "script//delete_all", + views.ScriptDeleteAllVersionsView.as_view(), + name="delete_all_script_versions", + ), path( "script///similar", views.get_similar_scripts, diff --git a/scripts/views.py b/scripts/views.py index 5ba3cfef..75c20985 100644 --- a/scripts/views.py +++ b/scripts/views.py @@ -582,6 +582,26 @@ def determine_success_url(self, script): return f"/script/{script.pk}" +class ScriptDeleteAllVersionsView(LoginRequiredMixin, generic.View): + """ + Deletes all versions of a script and the script itself. + """ + + def post(self, request, *args, **kwargs): + try: + script = models.Script.objects.get(pk=kwargs.get("pk")) + except models.Script.DoesNotExist: + raise Http404("Cannot delete a script that does not exist.") + + if script.owner != request.user: + return HttpResponseForbidden() + + # Delete the script, which will cascade delete all versions due to CASCADE relationship + script.delete() + + return HttpResponseRedirect("/") + + class StatisticsView(generic.ListView, FilterView): model = models.ScriptVersion template_name = "statistics.html" diff --git a/scripts/viewsets.py b/scripts/viewsets.py index 768a00c1..b940787b 100644 --- a/scripts/viewsets.py +++ b/scripts/viewsets.py @@ -214,6 +214,28 @@ def destroy(self, request, *args, **kwargs): script.delete() return Response(status=status.HTTP_204_NO_CONTENT) + @authentication_classes([BasicAuthentication]) + @action(methods=["delete"], detail=True, url_path="delete-all-versions") + def delete_all_versions(self, request, pk=None): + """ + Delete all versions of a script. The script itself will also be deleted. + """ + try: + instance = models.ScriptVersion.objects.get(pk=pk) + except models.ScriptVersion.DoesNotExist: + return Response({"error": "Script version not found."}, status=status.HTTP_404_NOT_FOUND) + + script = instance.script + + if script.owner != self.request.user: + return Response( + {"error": "You do not have permission to delete this script."}, status=status.HTTP_403_FORBIDDEN + ) + + # Delete the script, which will cascade delete all versions due to CASCADE relationship + script.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + class TranslateScriptViewSet(viewsets.ReadOnlyModelViewSet): queryset = models.ScriptVersion.objects.all() diff --git a/tests/settings.py b/tests/settings.py index e69de29b..849529cb 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -0,0 +1,102 @@ +""" +Django settings for testing. +""" +from pathlib import Path +import os + +BASE_DIR = Path(__file__).resolve().parent.parent + +SECRET_KEY = "test-secret-key-for-testing-only" + +DEBUG = True + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.sites", + "django.contrib.staticfiles", + "django.contrib.postgres", + "scripts.apps.ScriptsConfig", + "versionfield", + "django_tables2", + "django_filters", + "bootstrap4", + "django_bootstrap_icons", + "rest_framework", + "storages", + "allauth", + "allauth.account", + "allauth.socialaccount", + "markdownify.apps.MarkdownifyConfig", + "corsheaders", + "drf_spectacular", +] + +MIDDLEWARE = [ + "corsheaders.middleware.CorsMiddleware", + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "allauth.account.middleware.AccountMiddleware", +] + +ROOT_URLCONF = "botc.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + "django.template.context_processors.request", + ], + }, + } +] + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": ":memory:", + } +} + +AUTHENTICATION_BACKENDS = [ + "django.contrib.auth.backends.ModelBackend", + "allauth.account.auth_backends.AuthenticationBackend", +] + +SITE_ID = 1 + +USE_TZ = True + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +REST_FRAMEWORK = { + "DEFAULT_PERMISSION_CLASSES": [ + "rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly" + ], + "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", + "DEFAULT_FILTER_BACKENDS": ["django_filters.rest_framework.DjangoFilterBackend"], + "PAGE_SIZE": 10, +} + +CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.locmem.LocMemCache", + "LOCATION": "local-cache", + } +} + diff --git a/tests/test_delete_all_versions.py b/tests/test_delete_all_versions.py new file mode 100644 index 00000000..bd46f880 --- /dev/null +++ b/tests/test_delete_all_versions.py @@ -0,0 +1,315 @@ +""" +Tests for the delete all versions functionality. +""" +import pytest +from django.contrib.auth.models import User +from django.urls import reverse +from django.test import Client +from scripts.models import Script, ScriptVersion +from versionfield import Version + + +@pytest.fixture +def user(db): + """Create a test user with API write permission.""" + from django.contrib.auth.models import Permission + from django.contrib.contenttypes.models import ContentType + + user = User.objects.create_user( + username="testuser", + password="testpass123", + email="test@example.com" + ) + + # Grant API write permission + content_type = ContentType.objects.get_for_model(ScriptVersion) + permission = Permission.objects.get( + codename="api_write_permission", + content_type=content_type + ) + user.user_permissions.add(permission) + + return user + + +@pytest.fixture +def other_user(db): + """Create another test user with API write permission.""" + from django.contrib.auth.models import Permission + from django.contrib.contenttypes.models import ContentType + + user = User.objects.create_user( + username="otheruser", + password="testpass123", + email="other@example.com" + ) + + # Grant API write permission + content_type = ContentType.objects.get_for_model(ScriptVersion) + permission = Permission.objects.get( + codename="api_write_permission", + content_type=content_type + ) + user.user_permissions.add(permission) + + return user + + +@pytest.fixture +def script_with_multiple_versions(db, user): + """Create a script with multiple versions.""" + script = Script.objects.create(name="Test Script", owner=user) + + # Create version 1.0 + ScriptVersion.objects.create( + script=script, + version=Version("1.0"), + content=[{"id": "washerwoman"}], + latest=False, + num_townsfolk=1, + num_outsiders=0, + num_minions=0, + num_demons=0, + num_fabled=0, + num_loric=0, + num_travellers=0, + ) + + # Create version 2.0 (latest) + version_2 = ScriptVersion.objects.create( + script=script, + version=Version("2.0"), + content=[{"id": "washerwoman"}, {"id": "librarian"}], + latest=True, + num_townsfolk=2, + num_outsiders=0, + num_minions=0, + num_demons=0, + num_fabled=0, + num_loric=0, + num_travellers=0, + ) + + return script, version_2 + + +@pytest.fixture +def script_with_single_version(db, user): + """Create a script with a single version.""" + script = Script.objects.create(name="Single Version Script", owner=user) + version = ScriptVersion.objects.create( + script=script, + version=Version("1.0"), + content=[{"id": "washerwoman"}], + latest=True, + num_townsfolk=1, + num_outsiders=0, + num_minions=0, + num_demons=0, + num_fabled=0, + num_loric=0, + num_travellers=0, + ) + return script, version + + +class TestDeleteAllVersionsView: + """Test the web view for deleting all versions.""" + + def test_delete_all_versions_success(self, db, user, script_with_multiple_versions): + """Test successful deletion of all versions.""" + script, version = script_with_multiple_versions + client = Client() + client.force_login(user) + + # Verify script exists with multiple versions + assert Script.objects.filter(pk=script.pk).exists() + assert script.versions.count() == 2 + + # Delete all versions + url = reverse("delete_all_script_versions", kwargs={"pk": script.pk}) + response = client.post(url) + + # Should redirect to home page + assert response.status_code == 302 + assert response.url == "/" + + # Script and all versions should be deleted + assert not Script.objects.filter(pk=script.pk).exists() + assert not ScriptVersion.objects.filter(script_id=script.pk).exists() + + def test_delete_all_versions_permission_denied(self, db, other_user, script_with_multiple_versions): + """Test that non-owners cannot delete all versions.""" + script, version = script_with_multiple_versions + client = Client() + client.force_login(other_user) + + url = reverse("delete_all_script_versions", kwargs={"pk": script.pk}) + response = client.post(url) + + # Should return 403 Forbidden + assert response.status_code == 403 + + # Script should still exist + assert Script.objects.filter(pk=script.pk).exists() + assert script.versions.count() == 2 + + def test_delete_all_versions_unauthenticated(self, db, script_with_multiple_versions): + """Test that unauthenticated users cannot delete.""" + script, version = script_with_multiple_versions + client = Client() + + url = reverse("delete_all_script_versions", kwargs={"pk": script.pk}) + response = client.post(url) + + # Should redirect to login + assert response.status_code == 302 + assert "/login" in response.url or "/account/login" in response.url + + # Script should still exist + assert Script.objects.filter(pk=script.pk).exists() + + def test_delete_all_versions_get_method_not_allowed(self, db, user, script_with_multiple_versions): + """Test that GET method is not allowed (should use POST).""" + script, version = script_with_multiple_versions + client = Client() + client.force_login(user) + + url = reverse("delete_all_script_versions", kwargs={"pk": script.pk}) + response = client.get(url) + + # BaseDeleteView typically allows GET for confirmation, but let's verify behavior + # This might return 200 (showing confirmation) or 405, depending on implementation + assert response.status_code in [200, 405] + + def test_delete_all_versions_nonexistent_script(self, db, user): + """Test deletion of non-existent script.""" + client = Client() + client.force_login(user) + + url = reverse("delete_all_script_versions", kwargs={"pk": 99999}) + response = client.post(url) + + # Should return 404 + assert response.status_code == 404 + + +class TestDeleteAllVersionsAPI: + """Test the API endpoint for deleting all versions.""" + + def test_api_delete_all_versions_success(self, db, user, script_with_multiple_versions): + """Test successful API deletion of all versions.""" + from rest_framework.test import APIClient + from rest_framework.authtoken.models import Token + + script, version = script_with_multiple_versions + client = APIClient() + + # Use Basic Auth - set credentials directly + client.credentials(HTTP_AUTHORIZATION=f'Basic {self._get_basic_auth(user)}') + + # Verify script exists + assert Script.objects.filter(pk=script.pk).exists() + assert script.versions.count() == 2 + + # Delete via API + url = f"/api/scripts/{version.pk}/delete-all-versions/" + response = client.delete(url) + + # Should return 204 No Content + assert response.status_code == 204 + + # Script and all versions should be deleted + assert not Script.objects.filter(pk=script.pk).exists() + assert not ScriptVersion.objects.filter(script_id=script.pk).exists() + + def test_api_delete_all_versions_permission_denied(self, db, other_user, script_with_multiple_versions): + """Test that non-owners cannot delete via API.""" + from rest_framework.test import APIClient + + script, version = script_with_multiple_versions + client = APIClient() + client.credentials(HTTP_AUTHORIZATION=f'Basic {self._get_basic_auth(other_user)}') + + url = f"/api/scripts/{version.pk}/delete-all-versions/" + response = client.delete(url) + + # Should return 403 Forbidden + assert response.status_code == 403 + assert "error" in response.json() + + # Script should still exist + assert Script.objects.filter(pk=script.pk).exists() + + def test_api_delete_all_versions_not_found(self, db, user): + """Test API deletion of non-existent script version.""" + from rest_framework.test import APIClient + + client = APIClient() + client.credentials(HTTP_AUTHORIZATION=f'Basic {self._get_basic_auth(user)}') + + url = "/api/scripts/99999/delete-all-versions/" + response = client.delete(url) + + # Should return 404 + assert response.status_code == 404 + assert "error" in response.json() + + def test_api_delete_all_versions_unauthenticated(self, db, script_with_multiple_versions): + """Test that unauthenticated users cannot delete via API.""" + from rest_framework.test import APIClient + + script, version = script_with_multiple_versions + client = APIClient() + + url = f"/api/scripts/{version.pk}/delete-all-versions/" + response = client.delete(url) + + # Should return 401 Unauthorized or 403 Forbidden (both indicate rejection) + assert response.status_code in [401, 403] + + # Script should still exist + assert Script.objects.filter(pk=script.pk).exists() + + @staticmethod + def _get_basic_auth(user): + """Helper to create basic auth header.""" + import base64 + credentials = f"{user.username}:testpass123" + return base64.b64encode(credentials.encode()).decode() + + +class TestDeleteAllVersionsTemplate: + """Test template rendering and UI elements.""" + + @pytest.mark.skip(reason="Requires PostgreSQL for DISTINCT ON queries in template") + def test_template_shows_dropdown_for_multiple_versions(self, db, user, script_with_multiple_versions): + """Test that dropdown appears when script has multiple versions.""" + script, version = script_with_multiple_versions + client = Client() + client.force_login(user) + + url = reverse("script", kwargs={"pk": script.pk}) + response = client.get(url) + + assert response.status_code == 200 + # Check that delete button and dropdown are present + assert b"Delete Version" in response.content + assert b"Delete All Versions" in response.content or b"dropdown" in response.content.lower() + + @pytest.mark.skip(reason="Requires PostgreSQL for DISTINCT ON queries in template") + def test_template_no_dropdown_for_single_version(self, db, user, script_with_single_version): + """Test that dropdown does not appear for single version.""" + script, version = script_with_single_version + client = Client() + client.force_login(user) + + url = reverse("script", kwargs={"pk": script.pk}) + response = client.get(url) + + assert response.status_code == 200 + # Delete button should exist, but dropdown should not + assert b"Delete Version" in response.content + # The "Delete All Versions" text should not appear for single version + # (or the dropdown should not be rendered) + From 013157e3821d5c46daf40467647fce70ee20273b Mon Sep 17 00:00:00 2001 From: bruhjeshhh Date: Wed, 7 Jan 2026 06:28:55 +0530 Subject: [PATCH 2/3] chore: apply ruff fixes (remove unused imports) --- test_manual.py | 81 +++++++++++++++++++++++++++++++ tests/settings.py | 1 - tests/test_delete_all_versions.py | 1 - 3 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 test_manual.py diff --git a/test_manual.py b/test_manual.py new file mode 100644 index 00000000..f3f99796 --- /dev/null +++ b/test_manual.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python +""" +Quick manual testing script for delete all versions functionality. +Run this with: python manage.py shell < test_manual.py +Or copy-paste into Django shell. +""" +from scripts.models import Script, ScriptVersion +from django.contrib.auth.models import User +from versionfield import Version + +# Create or get a test user +user, created = User.objects.get_or_create( + username="testuser", + defaults={"email": "test@example.com"} +) +if created: + user.set_password("testpass123") + user.save() + print(f"Created user: {user.username}") + +# Create a test script with multiple versions +script, created = Script.objects.get_or_create( + name="Test Script for Deletion", + defaults={"owner": user} +) +if created: + print(f"Created script: {script.name} (ID: {script.pk})") +else: + print(f"Using existing script: {script.name} (ID: {script.pk})") + +# Check current versions +current_versions = script.versions.count() +print(f"Current versions: {current_versions}") + +# Create multiple versions if needed +if current_versions < 2: + for i, version_str in enumerate(["1.0", "2.0", "3.0"], 1): + ScriptVersion.objects.get_or_create( + script=script, + version=Version(version_str), + defaults={ + "content": [{"id": "washerwoman"}] * i, + "latest": (i == 3), # Only last one is latest + "num_townsfolk": i, + "num_outsiders": 0, + "num_minions": 0, + "num_demons": 0, + "num_fabled": 0, + "num_loric": 0, + "num_travellers": 0, + } + ) + print(f"Created test versions. Total versions: {script.versions.count()}") + +# Display script info +print("\n" + "="*50) +print("SCRIPT INFORMATION") +print("="*50) +print(f"Script ID: {script.pk}") +print(f"Script Name: {script.name}") +print(f"Owner: {script.owner.username if script.owner else 'None'}") +print(f"Number of versions: {script.versions.count()}") +print("\nVersions:") +for v in script.versions.all().order_by("version"): + print(f" - Version {v.version} (PK: {v.pk}, Latest: {v.latest})") + +print("\n" + "="*50) +print("TESTING INSTRUCTIONS") +print("="*50) +print("1. Web UI Test:") +print(f" - Go to: http://localhost:8000/script/{script.pk}") +print(" - Look for 'Delete Version' button with dropdown") +print(" - Click dropdown and select 'Delete All Versions'") +print(" - Confirm deletion") +print(" - Should redirect to home page") +print("\n2. API Test:") +print(f" curl -X DELETE -u {user.username}:testpass123 \\") +print(f" http://localhost:8000/api/scripts/{script.versions.first().pk}/delete-all-versions/") +print("\n3. Verify deletion:") +print(f" Script.objects.filter(pk={script.pk}).exists() # Should be False") + diff --git a/tests/settings.py b/tests/settings.py index 849529cb..30666718 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -2,7 +2,6 @@ Django settings for testing. """ from pathlib import Path -import os BASE_DIR = Path(__file__).resolve().parent.parent diff --git a/tests/test_delete_all_versions.py b/tests/test_delete_all_versions.py index bd46f880..7f35b503 100644 --- a/tests/test_delete_all_versions.py +++ b/tests/test_delete_all_versions.py @@ -200,7 +200,6 @@ class TestDeleteAllVersionsAPI: def test_api_delete_all_versions_success(self, db, user, script_with_multiple_versions): """Test successful API deletion of all versions.""" from rest_framework.test import APIClient - from rest_framework.authtoken.models import Token script, version = script_with_multiple_versions client = APIClient() From e6c0ffac2c993e368d4dfc946d523856d64ae918 Mon Sep 17 00:00:00 2001 From: bruhjeshhh Date: Wed, 7 Jan 2026 06:39:17 +0530 Subject: [PATCH 3/3] chore: format code --- test_manual.py | 22 ++--- tests/settings.py | 6 +- tests/test_delete_all_versions.py | 143 ++++++++++++++---------------- 3 files changed, 75 insertions(+), 96 deletions(-) diff --git a/test_manual.py b/test_manual.py index f3f99796..0ac92f08 100644 --- a/test_manual.py +++ b/test_manual.py @@ -4,25 +4,20 @@ Run this with: python manage.py shell < test_manual.py Or copy-paste into Django shell. """ + from scripts.models import Script, ScriptVersion from django.contrib.auth.models import User from versionfield import Version # Create or get a test user -user, created = User.objects.get_or_create( - username="testuser", - defaults={"email": "test@example.com"} -) +user, created = User.objects.get_or_create(username="testuser", defaults={"email": "test@example.com"}) if created: user.set_password("testpass123") user.save() print(f"Created user: {user.username}") # Create a test script with multiple versions -script, created = Script.objects.get_or_create( - name="Test Script for Deletion", - defaults={"owner": user} -) +script, created = Script.objects.get_or_create(name="Test Script for Deletion", defaults={"owner": user}) if created: print(f"Created script: {script.name} (ID: {script.pk})") else: @@ -48,14 +43,14 @@ "num_fabled": 0, "num_loric": 0, "num_travellers": 0, - } + }, ) print(f"Created test versions. Total versions: {script.versions.count()}") # Display script info -print("\n" + "="*50) +print("\n" + "=" * 50) print("SCRIPT INFORMATION") -print("="*50) +print("=" * 50) print(f"Script ID: {script.pk}") print(f"Script Name: {script.name}") print(f"Owner: {script.owner.username if script.owner else 'None'}") @@ -64,9 +59,9 @@ for v in script.versions.all().order_by("version"): print(f" - Version {v.version} (PK: {v.pk}, Latest: {v.latest})") -print("\n" + "="*50) +print("\n" + "=" * 50) print("TESTING INSTRUCTIONS") -print("="*50) +print("=" * 50) print("1. Web UI Test:") print(f" - Go to: http://localhost:8000/script/{script.pk}") print(" - Look for 'Delete Version' button with dropdown") @@ -78,4 +73,3 @@ print(f" http://localhost:8000/api/scripts/{script.versions.first().pk}/delete-all-versions/") print("\n3. Verify deletion:") print(f" Script.objects.filter(pk={script.pk}).exists() # Should be False") - diff --git a/tests/settings.py b/tests/settings.py index 30666718..19b9acea 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -1,6 +1,7 @@ """ Django settings for testing. """ + from pathlib import Path BASE_DIR = Path(__file__).resolve().parent.parent @@ -84,9 +85,7 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" REST_FRAMEWORK = { - "DEFAULT_PERMISSION_CLASSES": [ - "rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly" - ], + "DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly"], "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", "DEFAULT_FILTER_BACKENDS": ["django_filters.rest_framework.DjangoFilterBackend"], "PAGE_SIZE": 10, @@ -98,4 +97,3 @@ "LOCATION": "local-cache", } } - diff --git a/tests/test_delete_all_versions.py b/tests/test_delete_all_versions.py index 7f35b503..baaa0d1d 100644 --- a/tests/test_delete_all_versions.py +++ b/tests/test_delete_all_versions.py @@ -1,6 +1,7 @@ """ Tests for the delete all versions functionality. """ + import pytest from django.contrib.auth.models import User from django.urls import reverse @@ -14,21 +15,14 @@ def user(db): """Create a test user with API write permission.""" from django.contrib.auth.models import Permission from django.contrib.contenttypes.models import ContentType - - user = User.objects.create_user( - username="testuser", - password="testpass123", - email="test@example.com" - ) - + + user = User.objects.create_user(username="testuser", password="testpass123", email="test@example.com") + # Grant API write permission content_type = ContentType.objects.get_for_model(ScriptVersion) - permission = Permission.objects.get( - codename="api_write_permission", - content_type=content_type - ) + permission = Permission.objects.get(codename="api_write_permission", content_type=content_type) user.user_permissions.add(permission) - + return user @@ -37,21 +31,14 @@ def other_user(db): """Create another test user with API write permission.""" from django.contrib.auth.models import Permission from django.contrib.contenttypes.models import ContentType - - user = User.objects.create_user( - username="otheruser", - password="testpass123", - email="other@example.com" - ) - + + user = User.objects.create_user(username="otheruser", password="testpass123", email="other@example.com") + # Grant API write permission content_type = ContentType.objects.get_for_model(ScriptVersion) - permission = Permission.objects.get( - codename="api_write_permission", - content_type=content_type - ) + permission = Permission.objects.get(codename="api_write_permission", content_type=content_type) user.user_permissions.add(permission) - + return user @@ -59,7 +46,7 @@ def other_user(db): def script_with_multiple_versions(db, user): """Create a script with multiple versions.""" script = Script.objects.create(name="Test Script", owner=user) - + # Create version 1.0 ScriptVersion.objects.create( script=script, @@ -74,7 +61,7 @@ def script_with_multiple_versions(db, user): num_loric=0, num_travellers=0, ) - + # Create version 2.0 (latest) version_2 = ScriptVersion.objects.create( script=script, @@ -89,7 +76,7 @@ def script_with_multiple_versions(db, user): num_loric=0, num_travellers=0, ) - + return script, version_2 @@ -115,200 +102,200 @@ def script_with_single_version(db, user): class TestDeleteAllVersionsView: """Test the web view for deleting all versions.""" - + def test_delete_all_versions_success(self, db, user, script_with_multiple_versions): """Test successful deletion of all versions.""" script, version = script_with_multiple_versions client = Client() client.force_login(user) - + # Verify script exists with multiple versions assert Script.objects.filter(pk=script.pk).exists() assert script.versions.count() == 2 - + # Delete all versions url = reverse("delete_all_script_versions", kwargs={"pk": script.pk}) response = client.post(url) - + # Should redirect to home page assert response.status_code == 302 assert response.url == "/" - + # Script and all versions should be deleted assert not Script.objects.filter(pk=script.pk).exists() assert not ScriptVersion.objects.filter(script_id=script.pk).exists() - + def test_delete_all_versions_permission_denied(self, db, other_user, script_with_multiple_versions): """Test that non-owners cannot delete all versions.""" script, version = script_with_multiple_versions client = Client() client.force_login(other_user) - + url = reverse("delete_all_script_versions", kwargs={"pk": script.pk}) response = client.post(url) - + # Should return 403 Forbidden assert response.status_code == 403 - + # Script should still exist assert Script.objects.filter(pk=script.pk).exists() assert script.versions.count() == 2 - + def test_delete_all_versions_unauthenticated(self, db, script_with_multiple_versions): """Test that unauthenticated users cannot delete.""" script, version = script_with_multiple_versions client = Client() - + url = reverse("delete_all_script_versions", kwargs={"pk": script.pk}) response = client.post(url) - + # Should redirect to login assert response.status_code == 302 assert "/login" in response.url or "/account/login" in response.url - + # Script should still exist assert Script.objects.filter(pk=script.pk).exists() - + def test_delete_all_versions_get_method_not_allowed(self, db, user, script_with_multiple_versions): """Test that GET method is not allowed (should use POST).""" script, version = script_with_multiple_versions client = Client() client.force_login(user) - + url = reverse("delete_all_script_versions", kwargs={"pk": script.pk}) response = client.get(url) - + # BaseDeleteView typically allows GET for confirmation, but let's verify behavior # This might return 200 (showing confirmation) or 405, depending on implementation assert response.status_code in [200, 405] - + def test_delete_all_versions_nonexistent_script(self, db, user): """Test deletion of non-existent script.""" client = Client() client.force_login(user) - + url = reverse("delete_all_script_versions", kwargs={"pk": 99999}) response = client.post(url) - + # Should return 404 assert response.status_code == 404 class TestDeleteAllVersionsAPI: """Test the API endpoint for deleting all versions.""" - + def test_api_delete_all_versions_success(self, db, user, script_with_multiple_versions): """Test successful API deletion of all versions.""" from rest_framework.test import APIClient - + script, version = script_with_multiple_versions client = APIClient() - + # Use Basic Auth - set credentials directly - client.credentials(HTTP_AUTHORIZATION=f'Basic {self._get_basic_auth(user)}') - + client.credentials(HTTP_AUTHORIZATION=f"Basic {self._get_basic_auth(user)}") + # Verify script exists assert Script.objects.filter(pk=script.pk).exists() assert script.versions.count() == 2 - + # Delete via API url = f"/api/scripts/{version.pk}/delete-all-versions/" response = client.delete(url) - + # Should return 204 No Content assert response.status_code == 204 - + # Script and all versions should be deleted assert not Script.objects.filter(pk=script.pk).exists() assert not ScriptVersion.objects.filter(script_id=script.pk).exists() - + def test_api_delete_all_versions_permission_denied(self, db, other_user, script_with_multiple_versions): """Test that non-owners cannot delete via API.""" from rest_framework.test import APIClient - + script, version = script_with_multiple_versions client = APIClient() - client.credentials(HTTP_AUTHORIZATION=f'Basic {self._get_basic_auth(other_user)}') - + client.credentials(HTTP_AUTHORIZATION=f"Basic {self._get_basic_auth(other_user)}") + url = f"/api/scripts/{version.pk}/delete-all-versions/" response = client.delete(url) - + # Should return 403 Forbidden assert response.status_code == 403 assert "error" in response.json() - + # Script should still exist assert Script.objects.filter(pk=script.pk).exists() - + def test_api_delete_all_versions_not_found(self, db, user): """Test API deletion of non-existent script version.""" from rest_framework.test import APIClient - + client = APIClient() - client.credentials(HTTP_AUTHORIZATION=f'Basic {self._get_basic_auth(user)}') - + client.credentials(HTTP_AUTHORIZATION=f"Basic {self._get_basic_auth(user)}") + url = "/api/scripts/99999/delete-all-versions/" response = client.delete(url) - + # Should return 404 assert response.status_code == 404 assert "error" in response.json() - + def test_api_delete_all_versions_unauthenticated(self, db, script_with_multiple_versions): """Test that unauthenticated users cannot delete via API.""" from rest_framework.test import APIClient - + script, version = script_with_multiple_versions client = APIClient() - + url = f"/api/scripts/{version.pk}/delete-all-versions/" response = client.delete(url) - + # Should return 401 Unauthorized or 403 Forbidden (both indicate rejection) assert response.status_code in [401, 403] - + # Script should still exist assert Script.objects.filter(pk=script.pk).exists() - + @staticmethod def _get_basic_auth(user): """Helper to create basic auth header.""" import base64 + credentials = f"{user.username}:testpass123" return base64.b64encode(credentials.encode()).decode() class TestDeleteAllVersionsTemplate: """Test template rendering and UI elements.""" - + @pytest.mark.skip(reason="Requires PostgreSQL for DISTINCT ON queries in template") def test_template_shows_dropdown_for_multiple_versions(self, db, user, script_with_multiple_versions): """Test that dropdown appears when script has multiple versions.""" script, version = script_with_multiple_versions client = Client() client.force_login(user) - + url = reverse("script", kwargs={"pk": script.pk}) response = client.get(url) - + assert response.status_code == 200 # Check that delete button and dropdown are present assert b"Delete Version" in response.content assert b"Delete All Versions" in response.content or b"dropdown" in response.content.lower() - + @pytest.mark.skip(reason="Requires PostgreSQL for DISTINCT ON queries in template") def test_template_no_dropdown_for_single_version(self, db, user, script_with_single_version): """Test that dropdown does not appear for single version.""" script, version = script_with_single_version client = Client() client.force_login(user) - + url = reverse("script", kwargs={"pk": script.pk}) response = client.get(url) - + assert response.status_code == 200 # Delete button should exist, but dropdown should not assert b"Delete Version" in response.content # The "Delete All Versions" text should not appear for single version # (or the dropdown should not be rendered) -