from datetime import timedelta from django.contrib.auth import get_user_model from django.contrib.auth.models import Permission from django.core.exceptions import ValidationError from django.core.files.uploadedfile import SimpleUploadedFile from django.test import TestCase, override_settings from django.urls import reverse from django.utils import timezone from .forms import ClaimDecisionForm from .models import Claim, ClaimLog, Project from .validators import validate_receipt_file from .views import SubmitClaimView class ReceiptValidatorTests(TestCase): def test_accepts_valid_pdf(self): file_obj = SimpleUploadedFile( "receipt.pdf", b"%PDF-1.4\nsample", content_type="application/pdf", ) try: validate_receipt_file(file_obj) except ValidationError as exc: # pragma: no cover - explicit failure message self.fail(f"Valid PDF rejected: {exc}") def test_rejects_disallowed_extension(self): file_obj = SimpleUploadedFile( "script.exe", b"MZ fake exe", content_type="application/octet-stream", ) with self.assertRaises(ValidationError): validate_receipt_file(file_obj) @override_settings(CLAIMS_MAX_RECEIPT_BYTES=1024) def test_rejects_too_large_file(self): big_payload = b"%PDF-1.4\n" + b"a" * 2048 file_obj = SimpleUploadedFile( "large.pdf", big_payload, content_type="application/pdf", ) with self.assertRaises(ValidationError): validate_receipt_file(file_obj) def test_rejects_signature_mismatch(self): file_obj = SimpleUploadedFile( "fake.pdf", b"\x89PNG\r\n\x1a\nnot a pdf", content_type="application/pdf", ) with self.assertRaises(ValidationError): validate_receipt_file(file_obj) class ClaimFormsetLimitTests(TestCase): def test_default_formset_has_single_row(self): view = SubmitClaimView() formset = view.build_formset(extra=1) self.assertEqual(formset.total_form_count(), 1) def test_cannot_submit_more_than_max_forms(self): view = SubmitClaimView() data = { "claim_lines-TOTAL_FORMS": "6", "claim_lines-INITIAL_FORMS": "0", "claim_lines-MIN_NUM_FORMS": "1", "claim_lines-MAX_NUM_FORMS": "5", } formset = view.build_formset(data=data) self.assertFalse(formset.is_valid()) self.assertTrue(formset.non_form_errors()) class DashboardViewTests(TestCase): def setUp(self): User = get_user_model() self.user = User.objects.create_user(username="admin", password="test123", email="admin@example.com") view_perm = Permission.objects.get(codename="view_claim") change_perm = Permission.objects.get(codename="change_claim") self.user.user_permissions.add(view_perm, change_perm) self.client.force_login(self.user) def _create_claim(self, **kwargs): defaults = { "full_name": "Test User", "email": "test@example.com", "amount": 123, "currency": Claim.Currency.SEK, "description": "Taxi", "account_number": "123-456", } defaults.update(kwargs) claim = Claim.objects.create(**defaults) return claim def test_dashboard_summary_counts(self): recent_pending = self._create_claim() recent_approved = self._create_claim(status=Claim.Status.APPROVED) paid_claim = self._create_claim(status=Claim.Status.APPROVED, amount=500) paid_claim.paid_at = timezone.now() paid_claim.save(update_fields=["paid_at"]) old_claim = self._create_claim(status=Claim.Status.REJECTED) Claim.objects.filter(pk=old_claim.pk).update(created_at=timezone.now() - timedelta(days=10)) response = self.client.get(reverse("claims:admin-list")) self.assertEqual(response.status_code, 200) summary = response.context["summary"] self.assertEqual(summary["total_claims"], 4) self.assertEqual(summary["last_week_claims"], 3) self.assertEqual(summary["pending_count"], 1) self.assertEqual(summary["approved_count"], 2) self.assertEqual(summary["ready_to_pay"], 1) self.assertTrue(response.context["has_filtered_claims"]) response = self.client.get(reverse("claims:admin-list") + "?status=rejected") self.assertTrue(response.context["has_filtered_claims"]) def test_has_filtered_claims_false_when_no_matching_status(self): self._create_claim(status=Claim.Status.PENDING) response = self.client.get(reverse("claims:admin-list") + "?status=approved") self.assertFalse(response.context["has_filtered_claims"]) def test_attester_can_reset_claim_to_pending(self): claim = self._create_claim(status=Claim.Status.APPROVED) response = self.client.post( reverse("claims:admin-list"), { "action_type": "decision", "claim_id": claim.id, "action": ClaimDecisionForm.ACTION_PENDING, "decision_note": "Behöver komplettering", }, follow=True, ) self.assertEqual(response.status_code, 200) claim.refresh_from_db() self.assertEqual(claim.status, Claim.Status.PENDING) log = claim.logs.filter(action=ClaimLog.Action.STATUS_CHANGED).first() self.assertIsNotNone(log) self.assertEqual(log.from_status, Claim.Status.APPROVED) self.assertEqual(log.to_status, Claim.Status.PENDING) def test_attester_can_edit_details(self): project = Project.objects.create(name="Event", is_active=True) claim = self._create_claim(project=project, amount=100) response = self.client.post( reverse("claims:admin-list"), { "action_type": "edit", "edit_claim_id": claim.id, "full_name": "Changed Name", "email": "changed@example.com", "account_number": "789-000", "amount": "555.55", "currency": Claim.Currency.EUR, "project": "", "description": "Updated description", }, follow=True, ) self.assertEqual(response.status_code, 200) claim.refresh_from_db() self.assertEqual(claim.full_name, "Changed Name") self.assertEqual(claim.email, "changed@example.com") self.assertEqual(claim.currency, Claim.Currency.EUR) self.assertIsNone(claim.project) edit_log = claim.logs.filter(action=ClaimLog.Action.DETAILS_EDITED).first() self.assertIsNotNone(edit_log) self.assertIn("Namn", edit_log.note) self.assertIn("Changed Name", edit_log.note) def test_edit_blocked_for_non_pending_claims(self): claim = self._create_claim(status=Claim.Status.APPROVED) response = self.client.post( reverse("claims:admin-list"), { "action_type": "edit", "edit_claim_id": claim.id, "full_name": "Blocked", "email": "blocked@example.com", "account_number": "456", "amount": "200", "currency": Claim.Currency.SEK, "project": "", "description": "Blocked edit", }, follow=True, ) self.assertEqual(response.status_code, 200) claim.refresh_from_db() self.assertNotEqual(claim.full_name, "Blocked") self.assertFalse(claim.logs.filter(action=ClaimLog.Action.DETAILS_EDITED).exists())