Merge pull request #39 from dawnsystem/copilot/add-tests-for-ai-deletion-manager

Add unit tests for AI Deletion Manager and DeletionRequest model
This commit is contained in:
dawnsystem 2025-11-12 16:06:10 +01:00 committed by GitHub
commit c7a338ef0c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 1280 additions and 0 deletions

View file

@ -0,0 +1,581 @@
"""
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 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)

View 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)