Coverage for claims\tests.py: 100%
132 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-14 18:28 -0400
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-14 18:28 -0400
1from decimal import Decimal
2from django.test import TestCase
3from django.contrib.auth.models import User
4from django.urls import reverse
5from pathlib import Path
7from .models import Claim, ClaimDetail, Note
8from .services import ClaimDataIngestor
9from unittest.mock import patch, mock_open
11class ClaimDataIngestorTests(TestCase):
12 """
13 Tests for the ClaimDataIngestor service.
14 """
16 def setUp(self):
17 # Use simple strings. The mock doesn't care about the actual path,
18 # only that the key used to create the mock matches the key used to look it up.
19 self.dummy_claims_path = "dummy_claims.csv"
20 self.dummy_details_path = "dummy_details.csv"
22 def _run_ingestor_with_string_io(self, claims_csv_content: str, details_csv_content: str, *, mode: str = "append"):
23 """Helper to run the ingestor with in-memory CSV data and a specific mode."""
24 ingestor = ClaimDataIngestor(self.dummy_claims_path, self.dummy_details_path, mode=mode)
26 # The keys for the mock_files dictionary MUST be Path objects,
27 # because the service now uses Path objects internally to call open().
28 mock_files = {
29 Path(self.dummy_claims_path): mock_open(read_data=claims_csv_content).return_value,
30 Path(self.dummy_details_path): mock_open(read_data=details_csv_content).return_value,
31 }
33 # The 'file' argument received here will be a Path object.
34 with patch("builtins.open", lambda file, **kwargs: mock_files[file]):
35 return ingestor.run()
37 def test_successful_data_load(self):
38 """Tests a clean, successful import of new data."""
39 claim_id = 1
40 claims_csv = (
41 "id,patient_name,billed_amount,paid_amount,status,insurer_name,discharge_date\n"
42 f"{claim_id},John Doe,1200.50,1000.00,PAID,Acme Insurance,2025-09-01"
43 )
44 details_csv = (
45 "id,claim_id,cpt_codes,denial_reason\n"
46 f'1,{claim_id},"99214,99215","Prior authorization required"'
47 )
49 summary, errors = self._run_ingestor_with_string_io(claims_csv, details_csv)
51 self.assertEqual(len(errors), 0)
52 self.assertEqual(summary["claims_created"], 1)
53 self.assertEqual(summary["details_created"], 1)
54 self.assertEqual(Claim.objects.count(), 1)
55 self.assertEqual(ClaimDetail.objects.count(), 1)
57 claim = Claim.objects.get(id=claim_id)
58 self.assertEqual(claim.patient_name, "John Doe")
59 self.assertEqual(claim.billed_amount, Decimal("1200.50"))
60 self.assertIsNotNone(claim.details)
61 self.assertEqual(claim.details.cpt_codes, "99214,99215")
63 def test_overwrite_replaces_existing_data(self):
64 """Re-running with overwrite should replace existing data (purge then load)."""
65 claim_id = 1
66 initial_claims_csv = (
67 "id,patient_name,billed_amount,paid_amount,status,insurer_name,discharge_date\n"
68 f"{claim_id},Jane Smith,500.00,0.00,DENIED,Aetna,2025-08-15"
69 )
70 initial_details_csv = "id,claim_id,cpt_codes,denial_reason\n"
72 self._run_ingestor_with_string_io(initial_claims_csv, initial_details_csv)
73 self.assertEqual(Claim.objects.count(), 1)
74 self.assertEqual(Claim.objects.get(id=claim_id).status, "DENIED")
76 updated_claims_csv = (
77 "id,patient_name,billed_amount,paid_amount,status,insurer_name,discharge_date\n"
78 f"{claim_id},Jane Smith,500.00,500.00,PAID,CIGMA,2025-08-15"
79 )
81 summary, errors = self._run_ingestor_with_string_io(updated_claims_csv, initial_details_csv, mode="overwrite")
83 self.assertEqual(len(errors), 0)
84 self.assertEqual(summary["claims_updated"], 0)
85 self.assertEqual(summary["claims_created"], 1)
86 self.assertEqual(Claim.objects.count(), 1)
87 self.assertEqual(Claim.objects.get(id=claim_id).status, "PAID")
88 self.assertEqual(Claim.objects.get(id=claim_id).paid_amount, Decimal("500.00"))
90 def test_append_skips_existing_data(self):
91 """Append mode should not modify existing records; it skips duplicates."""
92 claim_id = 2
93 claims_csv = (
94 "id,patient_name,billed_amount,paid_amount,status,insurer_name,discharge_date\n"
95 f"{claim_id},Alpha,100.00,0.00,DENIED,Ins,2025-08-01"
96 )
97 details_csv = "id,claim_id,cpt_codes,denial_reason\n"
99 # initial load creates one
100 summary, errors = self._run_ingestor_with_string_io(claims_csv, details_csv, mode="append")
101 self.assertEqual(len(errors), 0)
102 self.assertEqual(summary["claims_created"], 1)
104 # re-load same record in append mode: should skip
105 summary2, errors2 = self._run_ingestor_with_string_io(claims_csv, details_csv, mode="append")
106 self.assertEqual(len(errors2), 0)
107 self.assertEqual(summary2.get("claims_skipped", 0), 1)
109 def test_handles_bad_data_gracefully(self):
110 """Tests that rows with errors are skipped and reported."""
111 claim_id = 1
112 claims_csv = (
113 "id,patient_name,billed_amount,paid_amount,status,insurer_name,discharge_date\n"
114 f"{claim_id},Kiryu Kazuma,100.00,50.00,PAID,CVS,2025-09-02\n"
115 "bad-id,Majima Goro,not-a-decimal,50.00,PENDING,United,2025-09-03"
116 )
117 details_csv = "id,claim_id,cpt_codes,denial_reason\n"
119 summary, errors = self._run_ingestor_with_string_io(claims_csv, details_csv)
121 self.assertEqual(Claim.objects.count(), 1)
122 self.assertEqual(summary["claims_created"], 1)
123 self.assertEqual(len(errors), 1)
124 self.assertIn("invalid literal for int() with base 10: 'bad-id'", errors[0])
126class ClaimsModelsTests(TestCase):
127 """
128 Tests for the models in the claims app.
129 """
131 def setUp(self):
132 self.user = User.objects.create_user(username='testuser', password='password')
133 self.claim = Claim.objects.create(
134 id=1,
135 patient_name='John Doe',
136 billed_amount=Decimal('1000.00'),
137 paid_amount=Decimal('800.00'),
138 status='PAID',
139 insurer_name='Acme Insurance',
140 discharge_date='2025-09-01'
141 )
142 self.claim_detail = ClaimDetail.objects.create(
143 claim=self.claim,
144 cpt_codes='99214,99215',
145 denial_reason='Prior authorization required'
146 )
147 self.note = Note.objects.create(
148 claim=self.claim,
149 note='This is a test note.',
150 user=self.user
151 )
153 def test_claim_str(self):
154 self.assertEqual(str(self.claim), "Claim 1 for John Doe")
156 def test_underpayment_property(self):
157 self.assertEqual(self.claim.underpayment, Decimal('200.00'))
159 def test_claim_detail_str(self):
160 self.assertEqual(str(self.claim_detail), "Details for Claim 1")
162 def test_note_str(self):
163 self.assertIn("Note on 1", str(self.note))
165class ClaimsViewsTests(TestCase):
166 """
167 Tests for the views in the claims app.
168 """
170 def setUp(self):
171 self.user = User.objects.create_user(username='testuser', password='password')
172 self.client.login(username='testuser', password='password')
173 self.claim1 = Claim.objects.create(
174 id=1,
175 patient_name='John Doe',
176 billed_amount=Decimal('1000.00'),
177 paid_amount=Decimal('800.00'),
178 status='PAID',
179 insurer_name='Acme Insurance',
180 discharge_date='2025-09-01'
181 )
182 self.claim2 = Claim.objects.create(
183 id=2,
184 patient_name='Jane Smith',
185 billed_amount=Decimal('2000.00'),
186 paid_amount=Decimal('1500.00'),
187 status='DENIED',
188 insurer_name='Beta Insurance',
189 discharge_date='2025-09-02',
190 is_flagged=True
191 )
193 def test_claim_list_view(self):
194 response = self.client.get(reverse('claims:claim-list'))
195 self.assertEqual(response.status_code, 200)
196 self.assertContains(response, "John Doe")
197 self.assertContains(response, "Jane Smith")
199 def test_claim_list_view_search(self):
200 response = self.client.get(reverse('claims:claim-list') + '?search=Acme')
201 self.assertEqual(response.status_code, 200)
202 self.assertContains(response, "John Doe")
203 self.assertNotContains(response, "Jane Smith")
205 def test_claim_list_view_filter_by_status(self):
206 response = self.client.get(reverse('claims:claim-list') + '?status=DENIED')
207 self.assertEqual(response.status_code, 200)
208 self.assertNotContains(response, "John Doe")
209 self.assertContains(response, "Jane Smith")
211 def test_claim_list_view_filter_by_flagged(self):
212 response = self.client.get(reverse('claims:claim-list') + '?flagged=true')
213 self.assertEqual(response.status_code, 200)
214 self.assertNotContains(response, "John Doe")
215 self.assertContains(response, "Jane Smith")
217 def test_claim_detail_view(self):
218 response = self.client.get(reverse('claims:claim-detail', args=[self.claim1.id]))
219 self.assertEqual(response.status_code, 200)
220 self.assertContains(response, "John Doe")
222 def test_toggle_flag_view(self):
223 self.assertFalse(self.claim1.is_flagged)
224 response = self.client.post(reverse('claims:toggle-flag', args=[self.claim1.id]))
225 self.assertEqual(response.status_code, 200)
226 self.claim1.refresh_from_db()
227 self.assertTrue(self.claim1.is_flagged)
229 def test_add_note_view(self):
230 self.assertEqual(self.claim1.notes.count(), 0)
231 response = self.client.post(reverse('claims:add-note', args=[self.claim1.id]), {'note': 'This is a new note.'})
232 self.assertEqual(response.status_code, 200)
233 self.assertEqual(self.claim1.notes.count(), 1)
235class RegistrationFlowTests(TestCase):
236 def test_register_with_weak_password_and_login(self):
237 # GET register page
238 resp = self.client.get(reverse('register'))
239 self.assertEqual(resp.status_code, 200)
241 # POST weak password
242 data = {
243 'username': 'weakuser',
244 'password1': '123',
245 'password2': '123',
246 }
247 resp = self.client.post(reverse('register'), data)
248 # should redirect to login
249 self.assertEqual(resp.status_code, 302)
250 self.assertEqual(resp.url, reverse('login'))
252 # User exists and can login
253 self.assertTrue(User.objects.filter(username='weakuser').exists())
254 login_ok = self.client.login(username='weakuser', password='123')
255 self.assertTrue(login_ok)