mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-01-07 14:06:06 +01:00
Add comprehensive unit tests for AI deletion manager and DeletionRequest model
Co-authored-by: dawnsystem <42047891+dawnsystem@users.noreply.github.com>
This commit is contained in:
parent
a31305b330
commit
d31bdaaab8
2 changed files with 1281 additions and 0 deletions
582
src/documents/tests/test_ai_deletion_manager.py
Normal file
582
src/documents/tests/test_ai_deletion_manager.py
Normal file
|
|
@ -0,0 +1,582 @@
|
|||
"""
|
||||
Unit tests for AI Deletion Manager (ai_deletion_manager.py)
|
||||
|
||||
Tests cover:
|
||||
- create_deletion_request() with impact analysis
|
||||
- _analyze_impact() with different document scenarios
|
||||
- format_deletion_request_for_user() with various scenarios
|
||||
- get_pending_requests() with filters
|
||||
- can_ai_delete_automatically() security constraint
|
||||
- Complete deletion workflows
|
||||
- Audit trail and tracking
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from unittest import mock
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase
|
||||
from django.utils import timezone
|
||||
|
||||
from documents.ai_deletion_manager import AIDeletionManager
|
||||
from documents.models import (
|
||||
Correspondent,
|
||||
DeletionRequest,
|
||||
Document,
|
||||
DocumentType,
|
||||
Tag,
|
||||
)
|
||||
|
||||
|
||||
class TestAIDeletionManagerCreateRequest(TestCase):
|
||||
"""Test create_deletion_request() functionality."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.user = User.objects.create_user(username="testuser", password="testpass")
|
||||
|
||||
# Create test documents with various metadata
|
||||
self.correspondent = Correspondent.objects.create(name="Test Corp")
|
||||
self.doc_type = DocumentType.objects.create(name="Invoice")
|
||||
self.tag1 = Tag.objects.create(name="Important")
|
||||
self.tag2 = Tag.objects.create(name="2024")
|
||||
|
||||
self.doc1 = Document.objects.create(
|
||||
title="Test Document 1",
|
||||
content="Test content 1",
|
||||
checksum="checksum1",
|
||||
mime_type="application/pdf",
|
||||
correspondent=self.correspondent,
|
||||
document_type=self.doc_type,
|
||||
)
|
||||
self.doc1.tags.add(self.tag1, self.tag2)
|
||||
|
||||
self.doc2 = Document.objects.create(
|
||||
title="Test Document 2",
|
||||
content="Test content 2",
|
||||
checksum="checksum2",
|
||||
mime_type="application/pdf",
|
||||
correspondent=self.correspondent,
|
||||
)
|
||||
self.doc2.tags.add(self.tag1)
|
||||
|
||||
def test_create_deletion_request_basic(self):
|
||||
"""Test creating a basic deletion request."""
|
||||
documents = [self.doc1, self.doc2]
|
||||
reason = "Duplicate documents detected"
|
||||
|
||||
request = AIDeletionManager.create_deletion_request(
|
||||
documents=documents,
|
||||
reason=reason,
|
||||
user=self.user,
|
||||
)
|
||||
|
||||
self.assertIsNotNone(request)
|
||||
self.assertIsInstance(request, DeletionRequest)
|
||||
self.assertEqual(request.ai_reason, reason)
|
||||
self.assertEqual(request.user, self.user)
|
||||
self.assertEqual(request.status, DeletionRequest.STATUS_PENDING)
|
||||
self.assertTrue(request.requested_by_ai)
|
||||
self.assertEqual(request.documents.count(), 2)
|
||||
|
||||
def test_create_deletion_request_with_impact_analysis(self):
|
||||
"""Test that deletion request includes impact analysis."""
|
||||
documents = [self.doc1, self.doc2]
|
||||
reason = "Test deletion"
|
||||
|
||||
request = AIDeletionManager.create_deletion_request(
|
||||
documents=documents,
|
||||
reason=reason,
|
||||
user=self.user,
|
||||
)
|
||||
|
||||
impact = request.impact_summary
|
||||
self.assertIsNotNone(impact)
|
||||
self.assertEqual(impact["document_count"], 2)
|
||||
self.assertIn("documents", impact)
|
||||
self.assertIn("affected_tags", impact)
|
||||
self.assertIn("affected_correspondents", impact)
|
||||
self.assertIn("affected_types", impact)
|
||||
self.assertIn("date_range", impact)
|
||||
|
||||
def test_create_deletion_request_with_custom_impact(self):
|
||||
"""Test creating request with pre-computed impact analysis."""
|
||||
documents = [self.doc1]
|
||||
reason = "Test deletion"
|
||||
custom_impact = {
|
||||
"document_count": 1,
|
||||
"custom_field": "custom_value",
|
||||
}
|
||||
|
||||
request = AIDeletionManager.create_deletion_request(
|
||||
documents=documents,
|
||||
reason=reason,
|
||||
user=self.user,
|
||||
impact_analysis=custom_impact,
|
||||
)
|
||||
|
||||
self.assertEqual(request.impact_summary, custom_impact)
|
||||
self.assertEqual(request.impact_summary["custom_field"], "custom_value")
|
||||
|
||||
def test_create_deletion_request_empty_documents(self):
|
||||
"""Test creating request with empty document list."""
|
||||
documents = []
|
||||
reason = "Test deletion"
|
||||
|
||||
request = AIDeletionManager.create_deletion_request(
|
||||
documents=documents,
|
||||
reason=reason,
|
||||
user=self.user,
|
||||
)
|
||||
|
||||
self.assertIsNotNone(request)
|
||||
self.assertEqual(request.documents.count(), 0)
|
||||
self.assertEqual(request.impact_summary["document_count"], 0)
|
||||
|
||||
|
||||
class TestAIDeletionManagerAnalyzeImpact(TestCase):
|
||||
"""Test _analyze_impact() functionality."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.correspondent1 = Correspondent.objects.create(name="Corp A")
|
||||
self.correspondent2 = Correspondent.objects.create(name="Corp B")
|
||||
self.doc_type1 = DocumentType.objects.create(name="Invoice")
|
||||
self.doc_type2 = DocumentType.objects.create(name="Receipt")
|
||||
self.tag1 = Tag.objects.create(name="Important")
|
||||
self.tag2 = Tag.objects.create(name="Archive")
|
||||
self.tag3 = Tag.objects.create(name="2024")
|
||||
|
||||
def test_analyze_impact_single_document(self):
|
||||
"""Test impact analysis for a single document."""
|
||||
doc = Document.objects.create(
|
||||
title="Test Document",
|
||||
content="Test content",
|
||||
checksum="checksum1",
|
||||
mime_type="application/pdf",
|
||||
correspondent=self.correspondent1,
|
||||
document_type=self.doc_type1,
|
||||
)
|
||||
doc.tags.add(self.tag1, self.tag2)
|
||||
|
||||
impact = AIDeletionManager._analyze_impact([doc])
|
||||
|
||||
self.assertEqual(impact["document_count"], 1)
|
||||
self.assertEqual(len(impact["documents"]), 1)
|
||||
self.assertEqual(impact["documents"][0]["id"], doc.id)
|
||||
self.assertEqual(impact["documents"][0]["title"], "Test Document")
|
||||
self.assertIn("Corp A", impact["affected_correspondents"])
|
||||
self.assertIn("Invoice", impact["affected_types"])
|
||||
self.assertIn("Important", impact["affected_tags"])
|
||||
self.assertIn("Archive", impact["affected_tags"])
|
||||
|
||||
def test_analyze_impact_multiple_documents(self):
|
||||
"""Test impact analysis for multiple documents."""
|
||||
doc1 = Document.objects.create(
|
||||
title="Document 1",
|
||||
content="Content 1",
|
||||
checksum="checksum1",
|
||||
mime_type="application/pdf",
|
||||
correspondent=self.correspondent1,
|
||||
document_type=self.doc_type1,
|
||||
)
|
||||
doc1.tags.add(self.tag1)
|
||||
|
||||
doc2 = Document.objects.create(
|
||||
title="Document 2",
|
||||
content="Content 2",
|
||||
checksum="checksum2",
|
||||
mime_type="application/pdf",
|
||||
correspondent=self.correspondent2,
|
||||
document_type=self.doc_type2,
|
||||
)
|
||||
doc2.tags.add(self.tag2, self.tag3)
|
||||
|
||||
impact = AIDeletionManager._analyze_impact([doc1, doc2])
|
||||
|
||||
self.assertEqual(impact["document_count"], 2)
|
||||
self.assertEqual(len(impact["documents"]), 2)
|
||||
self.assertIn("Corp A", impact["affected_correspondents"])
|
||||
self.assertIn("Corp B", impact["affected_correspondents"])
|
||||
self.assertIn("Invoice", impact["affected_types"])
|
||||
self.assertIn("Receipt", impact["affected_types"])
|
||||
self.assertEqual(len(impact["affected_tags"]), 3)
|
||||
|
||||
def test_analyze_impact_document_without_metadata(self):
|
||||
"""Test impact analysis for document without correspondent/type."""
|
||||
doc = Document.objects.create(
|
||||
title="Basic Document",
|
||||
content="Content",
|
||||
checksum="checksum1",
|
||||
mime_type="application/pdf",
|
||||
)
|
||||
|
||||
impact = AIDeletionManager._analyze_impact([doc])
|
||||
|
||||
self.assertEqual(impact["document_count"], 1)
|
||||
self.assertEqual(impact["documents"][0]["correspondent"], None)
|
||||
self.assertEqual(impact["documents"][0]["document_type"], None)
|
||||
self.assertEqual(impact["documents"][0]["tags"], [])
|
||||
self.assertEqual(len(impact["affected_correspondents"]), 0)
|
||||
self.assertEqual(len(impact["affected_types"]), 0)
|
||||
self.assertEqual(len(impact["affected_tags"]), 0)
|
||||
|
||||
def test_analyze_impact_date_range(self):
|
||||
"""Test that date range is properly calculated."""
|
||||
# Create documents with different dates
|
||||
doc1 = Document.objects.create(
|
||||
title="Old Document",
|
||||
content="Content",
|
||||
checksum="checksum1",
|
||||
mime_type="application/pdf",
|
||||
)
|
||||
# Force set the created date to an earlier time
|
||||
doc1.created = timezone.make_aware(datetime(2023, 1, 1))
|
||||
doc1.save()
|
||||
|
||||
doc2 = Document.objects.create(
|
||||
title="New Document",
|
||||
content="Content",
|
||||
checksum="checksum2",
|
||||
mime_type="application/pdf",
|
||||
)
|
||||
doc2.created = timezone.make_aware(datetime(2024, 12, 31))
|
||||
doc2.save()
|
||||
|
||||
impact = AIDeletionManager._analyze_impact([doc1, doc2])
|
||||
|
||||
self.assertIsNotNone(impact["date_range"]["earliest"])
|
||||
self.assertIsNotNone(impact["date_range"]["latest"])
|
||||
# Check that dates are ISO formatted strings
|
||||
self.assertIn("2023-01-01", impact["date_range"]["earliest"])
|
||||
self.assertIn("2024-12-31", impact["date_range"]["latest"])
|
||||
|
||||
def test_analyze_impact_empty_list(self):
|
||||
"""Test impact analysis with empty document list."""
|
||||
impact = AIDeletionManager._analyze_impact([])
|
||||
|
||||
self.assertEqual(impact["document_count"], 0)
|
||||
self.assertEqual(len(impact["documents"]), 0)
|
||||
self.assertEqual(len(impact["affected_correspondents"]), 0)
|
||||
self.assertEqual(len(impact["affected_types"]), 0)
|
||||
self.assertEqual(len(impact["affected_tags"]), 0)
|
||||
|
||||
|
||||
class TestAIDeletionManagerFormatRequest(TestCase):
|
||||
"""Test format_deletion_request_for_user() functionality."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.user = User.objects.create_user(username="testuser", password="testpass")
|
||||
|
||||
self.correspondent = Correspondent.objects.create(name="Test Corp")
|
||||
self.doc_type = DocumentType.objects.create(name="Invoice")
|
||||
self.tag = Tag.objects.create(name="Important")
|
||||
|
||||
self.doc = Document.objects.create(
|
||||
title="Test Document",
|
||||
content="Test content",
|
||||
checksum="checksum1",
|
||||
mime_type="application/pdf",
|
||||
correspondent=self.correspondent,
|
||||
document_type=self.doc_type,
|
||||
)
|
||||
self.doc.tags.add(self.tag)
|
||||
|
||||
def test_format_deletion_request_basic(self):
|
||||
"""Test basic formatting of deletion request."""
|
||||
request = AIDeletionManager.create_deletion_request(
|
||||
documents=[self.doc],
|
||||
reason="Test reason for deletion",
|
||||
user=self.user,
|
||||
)
|
||||
|
||||
message = AIDeletionManager.format_deletion_request_for_user(request)
|
||||
|
||||
self.assertIsInstance(message, str)
|
||||
self.assertIn("AI DELETION REQUEST", message)
|
||||
self.assertIn("Test reason for deletion", message)
|
||||
self.assertIn("Test Document", message)
|
||||
self.assertIn("REQUIRED ACTION", message)
|
||||
|
||||
def test_format_deletion_request_includes_impact_summary(self):
|
||||
"""Test that formatted message includes impact summary."""
|
||||
doc2 = Document.objects.create(
|
||||
title="Document 2",
|
||||
content="Content 2",
|
||||
checksum="checksum2",
|
||||
mime_type="application/pdf",
|
||||
)
|
||||
|
||||
request = AIDeletionManager.create_deletion_request(
|
||||
documents=[self.doc, doc2],
|
||||
reason="Multiple documents",
|
||||
user=self.user,
|
||||
)
|
||||
|
||||
message = AIDeletionManager.format_deletion_request_for_user(request)
|
||||
|
||||
self.assertIn("Number of documents: 2", message)
|
||||
self.assertIn("Test Corp", message)
|
||||
self.assertIn("Invoice", message)
|
||||
self.assertIn("Important", message)
|
||||
|
||||
def test_format_deletion_request_with_no_metadata(self):
|
||||
"""Test formatting when documents have no metadata."""
|
||||
doc = Document.objects.create(
|
||||
title="Basic Document",
|
||||
content="Content",
|
||||
checksum="checksum1",
|
||||
mime_type="application/pdf",
|
||||
)
|
||||
|
||||
request = AIDeletionManager.create_deletion_request(
|
||||
documents=[doc],
|
||||
reason="Test deletion",
|
||||
user=self.user,
|
||||
)
|
||||
|
||||
message = AIDeletionManager.format_deletion_request_for_user(request)
|
||||
|
||||
self.assertIn("Basic Document", message)
|
||||
self.assertIn("None", message) # Should show None for missing metadata
|
||||
|
||||
def test_format_deletion_request_shows_security_warning(self):
|
||||
"""Test that formatted message emphasizes user approval requirement."""
|
||||
request = AIDeletionManager.create_deletion_request(
|
||||
documents=[self.doc],
|
||||
reason="Test",
|
||||
user=self.user,
|
||||
)
|
||||
|
||||
message = AIDeletionManager.format_deletion_request_for_user(request)
|
||||
|
||||
self.assertIn("explicit approval", message.lower())
|
||||
self.assertIn("no files will be deleted until you confirm", message.lower())
|
||||
|
||||
|
||||
class TestAIDeletionManagerGetPendingRequests(TestCase):
|
||||
"""Test get_pending_requests() functionality."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.user1 = User.objects.create_user(username="user1", password="pass1")
|
||||
self.user2 = User.objects.create_user(username="user2", password="pass2")
|
||||
|
||||
self.doc = Document.objects.create(
|
||||
title="Test Document",
|
||||
content="Content",
|
||||
checksum="checksum1",
|
||||
mime_type="application/pdf",
|
||||
)
|
||||
|
||||
def test_get_pending_requests_for_user(self):
|
||||
"""Test getting pending requests for a specific user."""
|
||||
# Create requests for user1
|
||||
req1 = AIDeletionManager.create_deletion_request(
|
||||
documents=[self.doc],
|
||||
reason="Reason 1",
|
||||
user=self.user1,
|
||||
)
|
||||
req2 = AIDeletionManager.create_deletion_request(
|
||||
documents=[self.doc],
|
||||
reason="Reason 2",
|
||||
user=self.user1,
|
||||
)
|
||||
|
||||
# Create request for user2
|
||||
AIDeletionManager.create_deletion_request(
|
||||
documents=[self.doc],
|
||||
reason="Reason 3",
|
||||
user=self.user2,
|
||||
)
|
||||
|
||||
pending = AIDeletionManager.get_pending_requests(self.user1)
|
||||
|
||||
self.assertEqual(len(pending), 2)
|
||||
self.assertIn(req1, pending)
|
||||
self.assertIn(req2, pending)
|
||||
|
||||
def test_get_pending_requests_excludes_approved(self):
|
||||
"""Test that approved requests are not returned."""
|
||||
req1 = AIDeletionManager.create_deletion_request(
|
||||
documents=[self.doc],
|
||||
reason="Reason 1",
|
||||
user=self.user1,
|
||||
)
|
||||
req2 = AIDeletionManager.create_deletion_request(
|
||||
documents=[self.doc],
|
||||
reason="Reason 2",
|
||||
user=self.user1,
|
||||
)
|
||||
|
||||
# Approve one request
|
||||
req1.approve(self.user1, "Approved")
|
||||
|
||||
pending = AIDeletionManager.get_pending_requests(self.user1)
|
||||
|
||||
self.assertEqual(len(pending), 1)
|
||||
self.assertNotIn(req1, pending)
|
||||
self.assertIn(req2, pending)
|
||||
|
||||
def test_get_pending_requests_excludes_rejected(self):
|
||||
"""Test that rejected requests are not returned."""
|
||||
req1 = AIDeletionManager.create_deletion_request(
|
||||
documents=[self.doc],
|
||||
reason="Reason 1",
|
||||
user=self.user1,
|
||||
)
|
||||
req2 = AIDeletionManager.create_deletion_request(
|
||||
documents=[self.doc],
|
||||
reason="Reason 2",
|
||||
user=self.user1,
|
||||
)
|
||||
|
||||
# Reject one request
|
||||
req1.reject(self.user1, "Rejected")
|
||||
|
||||
pending = AIDeletionManager.get_pending_requests(self.user1)
|
||||
|
||||
self.assertEqual(len(pending), 1)
|
||||
self.assertNotIn(req1, pending)
|
||||
self.assertIn(req2, pending)
|
||||
|
||||
def test_get_pending_requests_empty(self):
|
||||
"""Test getting pending requests when none exist."""
|
||||
pending = AIDeletionManager.get_pending_requests(self.user1)
|
||||
|
||||
self.assertEqual(len(pending), 0)
|
||||
|
||||
|
||||
class TestAIDeletionManagerSecurityConstraints(TestCase):
|
||||
"""Test security constraints and AI deletion prevention."""
|
||||
|
||||
def test_can_ai_delete_automatically_always_false(self):
|
||||
"""Test that AI can never delete automatically."""
|
||||
# This is a critical security test
|
||||
result = AIDeletionManager.can_ai_delete_automatically()
|
||||
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_deletion_request_requires_pending_status(self):
|
||||
"""Test that all new deletion requests start as pending."""
|
||||
user = User.objects.create_user(username="testuser", password="pass")
|
||||
doc = Document.objects.create(
|
||||
title="Test",
|
||||
content="Content",
|
||||
checksum="checksum1",
|
||||
mime_type="application/pdf",
|
||||
)
|
||||
|
||||
request = AIDeletionManager.create_deletion_request(
|
||||
documents=[doc],
|
||||
reason="Test",
|
||||
user=user,
|
||||
)
|
||||
|
||||
self.assertEqual(request.status, DeletionRequest.STATUS_PENDING)
|
||||
|
||||
def test_deletion_request_marked_as_ai_initiated(self):
|
||||
"""Test that deletion requests are marked as AI-initiated."""
|
||||
user = User.objects.create_user(username="testuser", password="pass")
|
||||
doc = Document.objects.create(
|
||||
title="Test",
|
||||
content="Content",
|
||||
checksum="checksum1",
|
||||
mime_type="application/pdf",
|
||||
)
|
||||
|
||||
request = AIDeletionManager.create_deletion_request(
|
||||
documents=[doc],
|
||||
reason="Test",
|
||||
user=user,
|
||||
)
|
||||
|
||||
self.assertTrue(request.requested_by_ai)
|
||||
|
||||
|
||||
class TestAIDeletionManagerWorkflow(TestCase):
|
||||
"""Test complete deletion workflow."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.user = User.objects.create_user(username="testuser", password="pass")
|
||||
self.approver = User.objects.create_user(username="approver", password="pass")
|
||||
|
||||
self.doc1 = Document.objects.create(
|
||||
title="Document 1",
|
||||
content="Content 1",
|
||||
checksum="checksum1",
|
||||
mime_type="application/pdf",
|
||||
)
|
||||
self.doc2 = Document.objects.create(
|
||||
title="Document 2",
|
||||
content="Content 2",
|
||||
checksum="checksum2",
|
||||
mime_type="application/pdf",
|
||||
)
|
||||
|
||||
def test_complete_approval_workflow(self):
|
||||
"""Test complete workflow from creation to approval."""
|
||||
# Step 1: Create deletion request
|
||||
request = AIDeletionManager.create_deletion_request(
|
||||
documents=[self.doc1, self.doc2],
|
||||
reason="Duplicates detected",
|
||||
user=self.user,
|
||||
)
|
||||
|
||||
self.assertEqual(request.status, DeletionRequest.STATUS_PENDING)
|
||||
self.assertIsNone(request.reviewed_at)
|
||||
self.assertIsNone(request.reviewed_by)
|
||||
|
||||
# Step 2: Approve request
|
||||
success = request.approve(self.approver, "Looks good")
|
||||
|
||||
self.assertTrue(success)
|
||||
self.assertEqual(request.status, DeletionRequest.STATUS_APPROVED)
|
||||
self.assertIsNotNone(request.reviewed_at)
|
||||
self.assertEqual(request.reviewed_by, self.approver)
|
||||
self.assertEqual(request.review_comment, "Looks good")
|
||||
|
||||
def test_complete_rejection_workflow(self):
|
||||
"""Test complete workflow from creation to rejection."""
|
||||
# Step 1: Create deletion request
|
||||
request = AIDeletionManager.create_deletion_request(
|
||||
documents=[self.doc1],
|
||||
reason="Should be deleted",
|
||||
user=self.user,
|
||||
)
|
||||
|
||||
self.assertEqual(request.status, DeletionRequest.STATUS_PENDING)
|
||||
|
||||
# Step 2: Reject request
|
||||
success = request.reject(self.approver, "Not a duplicate")
|
||||
|
||||
self.assertTrue(success)
|
||||
self.assertEqual(request.status, DeletionRequest.STATUS_REJECTED)
|
||||
self.assertIsNotNone(request.reviewed_at)
|
||||
self.assertEqual(request.reviewed_by, self.approver)
|
||||
self.assertEqual(request.review_comment, "Not a duplicate")
|
||||
|
||||
def test_workflow_audit_trail(self):
|
||||
"""Test that workflow maintains complete audit trail."""
|
||||
request = AIDeletionManager.create_deletion_request(
|
||||
documents=[self.doc1],
|
||||
reason="Test deletion",
|
||||
user=self.user,
|
||||
)
|
||||
|
||||
# Record initial state
|
||||
created_at = request.created_at
|
||||
self.assertIsNotNone(created_at)
|
||||
|
||||
# Approve
|
||||
request.approve(self.approver, "Approved")
|
||||
|
||||
# Verify audit trail
|
||||
self.assertIsNotNone(request.created_at)
|
||||
self.assertIsNotNone(request.updated_at)
|
||||
self.assertIsNotNone(request.reviewed_at)
|
||||
self.assertEqual(request.reviewed_by, self.approver)
|
||||
self.assertTrue(request.requested_by_ai)
|
||||
self.assertEqual(request.user, self.user)
|
||||
699
src/documents/tests/test_deletion_request_model.py
Normal file
699
src/documents/tests/test_deletion_request_model.py
Normal file
|
|
@ -0,0 +1,699 @@
|
|||
"""
|
||||
Unit tests for DeletionRequest Model
|
||||
|
||||
Tests cover:
|
||||
- Model creation and field validation
|
||||
- approve() method with different states
|
||||
- reject() method with different states
|
||||
- Status transitions and constraints
|
||||
- Complete workflow scenarios
|
||||
- Audit trail validation
|
||||
- Model relationships and data integrity
|
||||
"""
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase
|
||||
from django.utils import timezone
|
||||
|
||||
from documents.models import (
|
||||
Correspondent,
|
||||
DeletionRequest,
|
||||
Document,
|
||||
DocumentType,
|
||||
Tag,
|
||||
)
|
||||
|
||||
|
||||
class TestDeletionRequestModelCreation(TestCase):
|
||||
"""Test DeletionRequest model creation and basic functionality."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.user = User.objects.create_user(username="testuser", password="pass")
|
||||
self.doc = Document.objects.create(
|
||||
title="Test Document",
|
||||
content="Content",
|
||||
checksum="checksum1",
|
||||
mime_type="application/pdf",
|
||||
)
|
||||
|
||||
def test_create_deletion_request_basic(self):
|
||||
"""Test creating a basic deletion request."""
|
||||
request = DeletionRequest.objects.create(
|
||||
requested_by_ai=True,
|
||||
ai_reason="Test reason",
|
||||
user=self.user,
|
||||
status=DeletionRequest.STATUS_PENDING,
|
||||
)
|
||||
|
||||
self.assertIsNotNone(request)
|
||||
self.assertTrue(request.requested_by_ai)
|
||||
self.assertEqual(request.ai_reason, "Test reason")
|
||||
self.assertEqual(request.user, self.user)
|
||||
self.assertEqual(request.status, DeletionRequest.STATUS_PENDING)
|
||||
|
||||
def test_deletion_request_auto_timestamps(self):
|
||||
"""Test that timestamps are automatically set."""
|
||||
request = DeletionRequest.objects.create(
|
||||
requested_by_ai=True,
|
||||
ai_reason="Test",
|
||||
user=self.user,
|
||||
)
|
||||
|
||||
self.assertIsNotNone(request.created_at)
|
||||
self.assertIsNotNone(request.updated_at)
|
||||
|
||||
def test_deletion_request_default_status(self):
|
||||
"""Test that default status is pending."""
|
||||
request = DeletionRequest.objects.create(
|
||||
requested_by_ai=True,
|
||||
ai_reason="Test",
|
||||
user=self.user,
|
||||
)
|
||||
|
||||
self.assertEqual(request.status, DeletionRequest.STATUS_PENDING)
|
||||
|
||||
def test_deletion_request_with_documents(self):
|
||||
"""Test adding documents to deletion request."""
|
||||
request = DeletionRequest.objects.create(
|
||||
requested_by_ai=True,
|
||||
ai_reason="Test",
|
||||
user=self.user,
|
||||
)
|
||||
|
||||
doc2 = Document.objects.create(
|
||||
title="Document 2",
|
||||
content="Content 2",
|
||||
checksum="checksum2",
|
||||
mime_type="application/pdf",
|
||||
)
|
||||
|
||||
request.documents.add(self.doc, doc2)
|
||||
|
||||
self.assertEqual(request.documents.count(), 2)
|
||||
self.assertIn(self.doc, request.documents.all())
|
||||
self.assertIn(doc2, request.documents.all())
|
||||
|
||||
def test_deletion_request_impact_summary_default(self):
|
||||
"""Test that impact_summary defaults to empty dict."""
|
||||
request = DeletionRequest.objects.create(
|
||||
requested_by_ai=True,
|
||||
ai_reason="Test",
|
||||
user=self.user,
|
||||
)
|
||||
|
||||
self.assertIsInstance(request.impact_summary, dict)
|
||||
self.assertEqual(request.impact_summary, {})
|
||||
|
||||
def test_deletion_request_impact_summary_json(self):
|
||||
"""Test storing JSON data in impact_summary."""
|
||||
impact = {
|
||||
"document_count": 5,
|
||||
"affected_tags": ["tag1", "tag2"],
|
||||
"metadata": {"key": "value"},
|
||||
}
|
||||
|
||||
request = DeletionRequest.objects.create(
|
||||
requested_by_ai=True,
|
||||
ai_reason="Test",
|
||||
user=self.user,
|
||||
impact_summary=impact,
|
||||
)
|
||||
|
||||
self.assertEqual(request.impact_summary["document_count"], 5)
|
||||
self.assertEqual(request.impact_summary["affected_tags"], ["tag1", "tag2"])
|
||||
|
||||
def test_deletion_request_str_representation(self):
|
||||
"""Test string representation of deletion request."""
|
||||
request = DeletionRequest.objects.create(
|
||||
requested_by_ai=True,
|
||||
ai_reason="Test",
|
||||
user=self.user,
|
||||
)
|
||||
request.documents.add(self.doc)
|
||||
|
||||
str_repr = str(request)
|
||||
|
||||
self.assertIn("Deletion Request", str_repr)
|
||||
self.assertIn(str(request.id), str_repr)
|
||||
self.assertIn("1 documents", str_repr)
|
||||
self.assertIn("pending", str_repr)
|
||||
|
||||
|
||||
class TestDeletionRequestApprove(TestCase):
|
||||
"""Test approve() method functionality."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.user = User.objects.create_user(username="user1", password="pass")
|
||||
self.approver = User.objects.create_user(username="approver", password="pass")
|
||||
self.doc = Document.objects.create(
|
||||
title="Test Document",
|
||||
content="Content",
|
||||
checksum="checksum1",
|
||||
mime_type="application/pdf",
|
||||
)
|
||||
|
||||
def test_approve_pending_request(self):
|
||||
"""Test approving a pending request."""
|
||||
request = DeletionRequest.objects.create(
|
||||
requested_by_ai=True,
|
||||
ai_reason="Test",
|
||||
user=self.user,
|
||||
status=DeletionRequest.STATUS_PENDING,
|
||||
)
|
||||
|
||||
result = request.approve(self.approver, "Approved")
|
||||
|
||||
self.assertTrue(result)
|
||||
self.assertEqual(request.status, DeletionRequest.STATUS_APPROVED)
|
||||
self.assertEqual(request.reviewed_by, self.approver)
|
||||
self.assertIsNotNone(request.reviewed_at)
|
||||
self.assertEqual(request.review_comment, "Approved")
|
||||
|
||||
def test_approve_with_empty_comment(self):
|
||||
"""Test approving without a comment."""
|
||||
request = DeletionRequest.objects.create(
|
||||
requested_by_ai=True,
|
||||
ai_reason="Test",
|
||||
user=self.user,
|
||||
status=DeletionRequest.STATUS_PENDING,
|
||||
)
|
||||
|
||||
result = request.approve(self.approver)
|
||||
|
||||
self.assertTrue(result)
|
||||
self.assertEqual(request.review_comment, "")
|
||||
|
||||
def test_approve_already_approved_request(self):
|
||||
"""Test that approving an already approved request fails."""
|
||||
request = DeletionRequest.objects.create(
|
||||
requested_by_ai=True,
|
||||
ai_reason="Test",
|
||||
user=self.user,
|
||||
status=DeletionRequest.STATUS_APPROVED,
|
||||
reviewed_by=self.user,
|
||||
reviewed_at=timezone.now(),
|
||||
)
|
||||
|
||||
result = request.approve(self.approver, "Trying to approve again")
|
||||
|
||||
self.assertFalse(result)
|
||||
self.assertEqual(request.reviewed_by, self.user) # Should not change
|
||||
|
||||
def test_approve_rejected_request(self):
|
||||
"""Test that approving a rejected request fails."""
|
||||
request = DeletionRequest.objects.create(
|
||||
requested_by_ai=True,
|
||||
ai_reason="Test",
|
||||
user=self.user,
|
||||
status=DeletionRequest.STATUS_REJECTED,
|
||||
reviewed_by=self.user,
|
||||
reviewed_at=timezone.now(),
|
||||
)
|
||||
|
||||
result = request.approve(self.approver, "Trying to approve rejected")
|
||||
|
||||
self.assertFalse(result)
|
||||
self.assertEqual(request.status, DeletionRequest.STATUS_REJECTED)
|
||||
|
||||
def test_approve_cancelled_request(self):
|
||||
"""Test that approving a cancelled request fails."""
|
||||
request = DeletionRequest.objects.create(
|
||||
requested_by_ai=True,
|
||||
ai_reason="Test",
|
||||
user=self.user,
|
||||
status=DeletionRequest.STATUS_CANCELLED,
|
||||
)
|
||||
|
||||
result = request.approve(self.approver, "Trying to approve cancelled")
|
||||
|
||||
self.assertFalse(result)
|
||||
self.assertEqual(request.status, DeletionRequest.STATUS_CANCELLED)
|
||||
|
||||
def test_approve_completed_request(self):
|
||||
"""Test that approving a completed request fails."""
|
||||
request = DeletionRequest.objects.create(
|
||||
requested_by_ai=True,
|
||||
ai_reason="Test",
|
||||
user=self.user,
|
||||
status=DeletionRequest.STATUS_COMPLETED,
|
||||
)
|
||||
|
||||
result = request.approve(self.approver, "Trying to approve completed")
|
||||
|
||||
self.assertFalse(result)
|
||||
self.assertEqual(request.status, DeletionRequest.STATUS_COMPLETED)
|
||||
|
||||
def test_approve_sets_timestamp(self):
|
||||
"""Test that approve() sets the reviewed_at timestamp."""
|
||||
request = DeletionRequest.objects.create(
|
||||
requested_by_ai=True,
|
||||
ai_reason="Test",
|
||||
user=self.user,
|
||||
status=DeletionRequest.STATUS_PENDING,
|
||||
)
|
||||
|
||||
before_approval = timezone.now()
|
||||
result = request.approve(self.approver, "Approved")
|
||||
after_approval = timezone.now()
|
||||
|
||||
self.assertTrue(result)
|
||||
self.assertIsNotNone(request.reviewed_at)
|
||||
self.assertGreaterEqual(request.reviewed_at, before_approval)
|
||||
self.assertLessEqual(request.reviewed_at, after_approval)
|
||||
|
||||
|
||||
class TestDeletionRequestReject(TestCase):
|
||||
"""Test reject() method functionality."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.user = User.objects.create_user(username="user1", password="pass")
|
||||
self.reviewer = User.objects.create_user(username="reviewer", password="pass")
|
||||
|
||||
def test_reject_pending_request(self):
|
||||
"""Test rejecting a pending request."""
|
||||
request = DeletionRequest.objects.create(
|
||||
requested_by_ai=True,
|
||||
ai_reason="Test",
|
||||
user=self.user,
|
||||
status=DeletionRequest.STATUS_PENDING,
|
||||
)
|
||||
|
||||
result = request.reject(self.reviewer, "Not necessary")
|
||||
|
||||
self.assertTrue(result)
|
||||
self.assertEqual(request.status, DeletionRequest.STATUS_REJECTED)
|
||||
self.assertEqual(request.reviewed_by, self.reviewer)
|
||||
self.assertIsNotNone(request.reviewed_at)
|
||||
self.assertEqual(request.review_comment, "Not necessary")
|
||||
|
||||
def test_reject_with_empty_comment(self):
|
||||
"""Test rejecting without a comment."""
|
||||
request = DeletionRequest.objects.create(
|
||||
requested_by_ai=True,
|
||||
ai_reason="Test",
|
||||
user=self.user,
|
||||
status=DeletionRequest.STATUS_PENDING,
|
||||
)
|
||||
|
||||
result = request.reject(self.reviewer)
|
||||
|
||||
self.assertTrue(result)
|
||||
self.assertEqual(request.review_comment, "")
|
||||
|
||||
def test_reject_already_rejected_request(self):
|
||||
"""Test that rejecting an already rejected request fails."""
|
||||
request = DeletionRequest.objects.create(
|
||||
requested_by_ai=True,
|
||||
ai_reason="Test",
|
||||
user=self.user,
|
||||
status=DeletionRequest.STATUS_REJECTED,
|
||||
reviewed_by=self.user,
|
||||
reviewed_at=timezone.now(),
|
||||
)
|
||||
|
||||
result = request.reject(self.reviewer, "Trying to reject again")
|
||||
|
||||
self.assertFalse(result)
|
||||
self.assertEqual(request.reviewed_by, self.user) # Should not change
|
||||
|
||||
def test_reject_approved_request(self):
|
||||
"""Test that rejecting an approved request fails."""
|
||||
request = DeletionRequest.objects.create(
|
||||
requested_by_ai=True,
|
||||
ai_reason="Test",
|
||||
user=self.user,
|
||||
status=DeletionRequest.STATUS_APPROVED,
|
||||
reviewed_by=self.user,
|
||||
reviewed_at=timezone.now(),
|
||||
)
|
||||
|
||||
result = request.reject(self.reviewer, "Trying to reject approved")
|
||||
|
||||
self.assertFalse(result)
|
||||
self.assertEqual(request.status, DeletionRequest.STATUS_APPROVED)
|
||||
|
||||
def test_reject_cancelled_request(self):
|
||||
"""Test that rejecting a cancelled request fails."""
|
||||
request = DeletionRequest.objects.create(
|
||||
requested_by_ai=True,
|
||||
ai_reason="Test",
|
||||
user=self.user,
|
||||
status=DeletionRequest.STATUS_CANCELLED,
|
||||
)
|
||||
|
||||
result = request.reject(self.reviewer, "Trying to reject cancelled")
|
||||
|
||||
self.assertFalse(result)
|
||||
self.assertEqual(request.status, DeletionRequest.STATUS_CANCELLED)
|
||||
|
||||
def test_reject_completed_request(self):
|
||||
"""Test that rejecting a completed request fails."""
|
||||
request = DeletionRequest.objects.create(
|
||||
requested_by_ai=True,
|
||||
ai_reason="Test",
|
||||
user=self.user,
|
||||
status=DeletionRequest.STATUS_COMPLETED,
|
||||
)
|
||||
|
||||
result = request.reject(self.reviewer, "Trying to reject completed")
|
||||
|
||||
self.assertFalse(result)
|
||||
self.assertEqual(request.status, DeletionRequest.STATUS_COMPLETED)
|
||||
|
||||
def test_reject_sets_timestamp(self):
|
||||
"""Test that reject() sets the reviewed_at timestamp."""
|
||||
request = DeletionRequest.objects.create(
|
||||
requested_by_ai=True,
|
||||
ai_reason="Test",
|
||||
user=self.user,
|
||||
status=DeletionRequest.STATUS_PENDING,
|
||||
)
|
||||
|
||||
before_rejection = timezone.now()
|
||||
result = request.reject(self.reviewer, "Rejected")
|
||||
after_rejection = timezone.now()
|
||||
|
||||
self.assertTrue(result)
|
||||
self.assertIsNotNone(request.reviewed_at)
|
||||
self.assertGreaterEqual(request.reviewed_at, before_rejection)
|
||||
self.assertLessEqual(request.reviewed_at, after_rejection)
|
||||
|
||||
|
||||
class TestDeletionRequestWorkflowScenarios(TestCase):
|
||||
"""Test complete workflow scenarios."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.user = User.objects.create_user(username="user1", password="pass")
|
||||
self.approver = User.objects.create_user(username="approver", password="pass")
|
||||
|
||||
self.correspondent = Correspondent.objects.create(name="Test Corp")
|
||||
self.doc_type = DocumentType.objects.create(name="Invoice")
|
||||
self.tag = Tag.objects.create(name="Important")
|
||||
|
||||
self.doc1 = Document.objects.create(
|
||||
title="Document 1",
|
||||
content="Content 1",
|
||||
checksum="checksum1",
|
||||
mime_type="application/pdf",
|
||||
correspondent=self.correspondent,
|
||||
document_type=self.doc_type,
|
||||
)
|
||||
self.doc1.tags.add(self.tag)
|
||||
|
||||
self.doc2 = Document.objects.create(
|
||||
title="Document 2",
|
||||
content="Content 2",
|
||||
checksum="checksum2",
|
||||
mime_type="application/pdf",
|
||||
)
|
||||
|
||||
def test_workflow_pending_to_approved(self):
|
||||
"""Test workflow transition from pending to approved."""
|
||||
request = DeletionRequest.objects.create(
|
||||
requested_by_ai=True,
|
||||
ai_reason="Duplicate documents",
|
||||
user=self.user,
|
||||
status=DeletionRequest.STATUS_PENDING,
|
||||
impact_summary={"document_count": 2},
|
||||
)
|
||||
request.documents.add(self.doc1, self.doc2)
|
||||
|
||||
# Verify initial state
|
||||
self.assertEqual(request.status, DeletionRequest.STATUS_PENDING)
|
||||
self.assertIsNone(request.reviewed_by)
|
||||
self.assertIsNone(request.reviewed_at)
|
||||
|
||||
# Approve
|
||||
success = request.approve(self.approver, "Confirmed duplicates")
|
||||
|
||||
# Verify final state
|
||||
self.assertTrue(success)
|
||||
self.assertEqual(request.status, DeletionRequest.STATUS_APPROVED)
|
||||
self.assertEqual(request.reviewed_by, self.approver)
|
||||
self.assertIsNotNone(request.reviewed_at)
|
||||
self.assertEqual(request.review_comment, "Confirmed duplicates")
|
||||
|
||||
def test_workflow_pending_to_rejected(self):
|
||||
"""Test workflow transition from pending to rejected."""
|
||||
request = DeletionRequest.objects.create(
|
||||
requested_by_ai=True,
|
||||
ai_reason="Suspected duplicates",
|
||||
user=self.user,
|
||||
status=DeletionRequest.STATUS_PENDING,
|
||||
)
|
||||
request.documents.add(self.doc1)
|
||||
|
||||
# Verify initial state
|
||||
self.assertEqual(request.status, DeletionRequest.STATUS_PENDING)
|
||||
|
||||
# Reject
|
||||
success = request.reject(self.approver, "Not duplicates")
|
||||
|
||||
# Verify final state
|
||||
self.assertTrue(success)
|
||||
self.assertEqual(request.status, DeletionRequest.STATUS_REJECTED)
|
||||
self.assertEqual(request.reviewed_by, self.approver)
|
||||
self.assertIsNotNone(request.reviewed_at)
|
||||
self.assertEqual(request.review_comment, "Not duplicates")
|
||||
|
||||
def test_workflow_cannot_approve_after_rejection(self):
|
||||
"""Test that request cannot be approved after rejection."""
|
||||
request = DeletionRequest.objects.create(
|
||||
requested_by_ai=True,
|
||||
ai_reason="Test",
|
||||
user=self.user,
|
||||
status=DeletionRequest.STATUS_PENDING,
|
||||
)
|
||||
|
||||
# Reject first
|
||||
request.reject(self.user, "Rejected")
|
||||
self.assertEqual(request.status, DeletionRequest.STATUS_REJECTED)
|
||||
|
||||
# Try to approve
|
||||
success = request.approve(self.approver, "Changed my mind")
|
||||
|
||||
# Should fail
|
||||
self.assertFalse(success)
|
||||
self.assertEqual(request.status, DeletionRequest.STATUS_REJECTED)
|
||||
|
||||
def test_workflow_cannot_reject_after_approval(self):
|
||||
"""Test that request cannot be rejected after approval."""
|
||||
request = DeletionRequest.objects.create(
|
||||
requested_by_ai=True,
|
||||
ai_reason="Test",
|
||||
user=self.user,
|
||||
status=DeletionRequest.STATUS_PENDING,
|
||||
)
|
||||
|
||||
# Approve first
|
||||
request.approve(self.approver, "Approved")
|
||||
self.assertEqual(request.status, DeletionRequest.STATUS_APPROVED)
|
||||
|
||||
# Try to reject
|
||||
success = request.reject(self.user, "Changed my mind")
|
||||
|
||||
# Should fail
|
||||
self.assertFalse(success)
|
||||
self.assertEqual(request.status, DeletionRequest.STATUS_APPROVED)
|
||||
|
||||
|
||||
class TestDeletionRequestAuditTrail(TestCase):
|
||||
"""Test audit trail and tracking functionality."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.user = User.objects.create_user(username="user1", password="pass")
|
||||
self.approver = User.objects.create_user(username="approver", password="pass")
|
||||
|
||||
def test_audit_trail_records_creator(self):
|
||||
"""Test that audit trail records the user who needs to approve."""
|
||||
request = DeletionRequest.objects.create(
|
||||
requested_by_ai=True,
|
||||
ai_reason="Test",
|
||||
user=self.user,
|
||||
)
|
||||
|
||||
self.assertEqual(request.user, self.user)
|
||||
|
||||
def test_audit_trail_records_reviewer(self):
|
||||
"""Test that audit trail records who reviewed the request."""
|
||||
request = DeletionRequest.objects.create(
|
||||
requested_by_ai=True,
|
||||
ai_reason="Test",
|
||||
user=self.user,
|
||||
status=DeletionRequest.STATUS_PENDING,
|
||||
)
|
||||
|
||||
request.approve(self.approver, "Approved")
|
||||
|
||||
self.assertEqual(request.reviewed_by, self.approver)
|
||||
self.assertNotEqual(request.reviewed_by, request.user)
|
||||
|
||||
def test_audit_trail_records_timestamps(self):
|
||||
"""Test that all timestamps are properly recorded."""
|
||||
request = DeletionRequest.objects.create(
|
||||
requested_by_ai=True,
|
||||
ai_reason="Test",
|
||||
user=self.user,
|
||||
)
|
||||
|
||||
created_at = request.created_at
|
||||
|
||||
# Approve the request
|
||||
request.approve(self.approver, "Approved")
|
||||
|
||||
# Verify timestamps
|
||||
self.assertIsNotNone(request.created_at)
|
||||
self.assertIsNotNone(request.updated_at)
|
||||
self.assertIsNotNone(request.reviewed_at)
|
||||
self.assertGreaterEqual(request.reviewed_at, created_at)
|
||||
|
||||
def test_audit_trail_preserves_ai_reason(self):
|
||||
"""Test that AI's original reason is preserved."""
|
||||
original_reason = "AI detected duplicates based on content similarity"
|
||||
|
||||
request = DeletionRequest.objects.create(
|
||||
requested_by_ai=True,
|
||||
ai_reason=original_reason,
|
||||
user=self.user,
|
||||
)
|
||||
|
||||
# Approve with different comment
|
||||
request.approve(self.approver, "User confirmed")
|
||||
|
||||
# Original AI reason should be preserved
|
||||
self.assertEqual(request.ai_reason, original_reason)
|
||||
self.assertEqual(request.review_comment, "User confirmed")
|
||||
|
||||
def test_audit_trail_completion_details(self):
|
||||
"""Test that completion details can be stored."""
|
||||
request = DeletionRequest.objects.create(
|
||||
requested_by_ai=True,
|
||||
ai_reason="Test",
|
||||
user=self.user,
|
||||
status=DeletionRequest.STATUS_COMPLETED,
|
||||
completion_details={
|
||||
"deleted_count": 5,
|
||||
"failed_count": 0,
|
||||
"completed_by": "system",
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(request.completion_details["deleted_count"], 5)
|
||||
self.assertEqual(request.completion_details["failed_count"], 0)
|
||||
|
||||
def test_audit_trail_multiple_requests_same_user(self):
|
||||
"""Test audit trail with multiple requests for same user."""
|
||||
request1 = DeletionRequest.objects.create(
|
||||
requested_by_ai=True,
|
||||
ai_reason="Reason 1",
|
||||
user=self.user,
|
||||
)
|
||||
|
||||
request2 = DeletionRequest.objects.create(
|
||||
requested_by_ai=True,
|
||||
ai_reason="Reason 2",
|
||||
user=self.user,
|
||||
)
|
||||
|
||||
# Approve one, reject another
|
||||
request1.approve(self.approver, "Approved")
|
||||
request2.reject(self.approver, "Rejected")
|
||||
|
||||
# Verify each has its own audit trail
|
||||
self.assertEqual(request1.status, DeletionRequest.STATUS_APPROVED)
|
||||
self.assertEqual(request2.status, DeletionRequest.STATUS_REJECTED)
|
||||
self.assertEqual(request1.review_comment, "Approved")
|
||||
self.assertEqual(request2.review_comment, "Rejected")
|
||||
|
||||
|
||||
class TestDeletionRequestModelRelationships(TestCase):
|
||||
"""Test model relationships and data integrity."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.user = User.objects.create_user(username="user1", password="pass")
|
||||
|
||||
def test_user_deletion_cascades_to_requests(self):
|
||||
"""Test that deleting a user deletes their deletion requests."""
|
||||
request = DeletionRequest.objects.create(
|
||||
requested_by_ai=True,
|
||||
ai_reason="Test",
|
||||
user=self.user,
|
||||
)
|
||||
|
||||
request_id = request.id
|
||||
self.assertEqual(DeletionRequest.objects.filter(id=request_id).count(), 1)
|
||||
|
||||
# Delete user
|
||||
self.user.delete()
|
||||
|
||||
# Request should be deleted
|
||||
self.assertEqual(DeletionRequest.objects.filter(id=request_id).count(), 0)
|
||||
|
||||
def test_document_relationship_many_to_many(self):
|
||||
"""Test many-to-many relationship with documents."""
|
||||
doc1 = Document.objects.create(
|
||||
title="Doc 1",
|
||||
content="Content",
|
||||
checksum="checksum1",
|
||||
mime_type="application/pdf",
|
||||
)
|
||||
doc2 = Document.objects.create(
|
||||
title="Doc 2",
|
||||
content="Content",
|
||||
checksum="checksum2",
|
||||
mime_type="application/pdf",
|
||||
)
|
||||
|
||||
request = DeletionRequest.objects.create(
|
||||
requested_by_ai=True,
|
||||
ai_reason="Test",
|
||||
user=self.user,
|
||||
)
|
||||
|
||||
request.documents.add(doc1, doc2)
|
||||
|
||||
self.assertEqual(request.documents.count(), 2)
|
||||
self.assertEqual(doc1.deletion_requests.count(), 1)
|
||||
self.assertEqual(doc2.deletion_requests.count(), 1)
|
||||
|
||||
def test_reviewed_by_nullable(self):
|
||||
"""Test that reviewed_by can be null."""
|
||||
request = DeletionRequest.objects.create(
|
||||
requested_by_ai=True,
|
||||
ai_reason="Test",
|
||||
user=self.user,
|
||||
status=DeletionRequest.STATUS_PENDING,
|
||||
)
|
||||
|
||||
self.assertIsNone(request.reviewed_by)
|
||||
|
||||
def test_reviewed_by_set_null_on_delete(self):
|
||||
"""Test that reviewed_by is set to null when reviewer is deleted."""
|
||||
approver = User.objects.create_user(username="approver", password="pass")
|
||||
|
||||
request = DeletionRequest.objects.create(
|
||||
requested_by_ai=True,
|
||||
ai_reason="Test",
|
||||
user=self.user,
|
||||
status=DeletionRequest.STATUS_PENDING,
|
||||
)
|
||||
|
||||
request.approve(approver, "Approved")
|
||||
self.assertEqual(request.reviewed_by, approver)
|
||||
|
||||
# Delete approver
|
||||
approver.delete()
|
||||
|
||||
# Refresh request
|
||||
request.refresh_from_db()
|
||||
|
||||
# reviewed_by should be null
|
||||
self.assertIsNone(request.reviewed_by)
|
||||
# But the request should still exist
|
||||
self.assertEqual(request.status, DeletionRequest.STATUS_APPROVED)
|
||||
Loading…
Add table
Add a link
Reference in a new issue