200 lines
7.7 KiB
Python
200 lines
7.7 KiB
Python
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())
|