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") edit_perm = Permission.objects.get(codename="edit_claim_details") pay_perm = Permission.objects.get(codename="mark_claim_paid") self.user.user_permissions.add(view_perm, change_perm, edit_perm, pay_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()) def test_edit_requires_permission(self): self.user.user_permissions.remove(Permission.objects.get(codename="edit_claim_details")) claim = self._create_claim() response = self.client.post( reverse("claims:admin-list"), { "action_type": "edit", "edit_claim_id": claim.id, "full_name": "Nope", "email": "nope@example.com", "account_number": "456", "amount": "200", "currency": Claim.Currency.SEK, "project": "", "description": "Should fail", }, follow=True, ) self.assertEqual(response.status_code, 200) claim.refresh_from_db() self.assertNotEqual(claim.full_name, "Nope") self.assertFalse(claim.logs.filter(action=ClaimLog.Action.DETAILS_EDITED).exists()) @override_settings(CLAIMS_ENABLE_INTERNAL_PAYMENTS=True) def test_mark_paid_requires_permission(self): claim = self._create_claim(status=Claim.Status.APPROVED) self.user.user_permissions.remove(Permission.objects.get(codename="mark_claim_paid")) response = self.client.post( reverse("claims:admin-list"), { "action_type": "payment", "payment_claim_id": claim.id, }, follow=True, ) self.assertEqual(response.status_code, 200) claim.refresh_from_db() self.assertIsNone(claim.paid_at) self.assertFalse(claim.logs.filter(action=ClaimLog.Action.MARKED_PAID).exists()) class UserManagementViewTests(TestCase): def setUp(self): User = get_user_model() self.admin = User.objects.create_user(username="manager", password="test123", email="manager@example.com") perms = Permission.objects.filter(codename__in=["view_user", "change_user"]) self.admin.user_permissions.add(*perms) self.client.force_login(self.admin) self.target = User.objects.create_user(username="editor", password="oldpass123", email="old@example.com") def test_admin_can_update_profile_and_password(self): response = self.client.post( reverse("claims:user-manage"), { "action": "update", "user_id": self.target.id, "is_staff": "on", "grant_view": "on", "grant_change": "", "grant_edit": "on", "grant_pay": "on", "first_name": "New", "last_name": "Name", "email": "new@example.com", "new_password1": "StrongPass123!", "new_password2": "StrongPass123!", }, follow=True, ) self.assertEqual(response.status_code, 200) target = get_user_model().objects.get(pk=self.target.pk) self.assertEqual(target.first_name, "New") self.assertEqual(target.last_name, "Name") self.assertEqual(target.email, "new@example.com") self.assertTrue(target.is_staff) self.assertTrue(target.check_password("StrongPass123!"))