Coverage for claims\tests.py: 100%

132 statements  

« 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 

6 

7from .models import Claim, ClaimDetail, Note 

8from .services import ClaimDataIngestor 

9from unittest.mock import patch, mock_open 

10 

11class ClaimDataIngestorTests(TestCase): 

12 """ 

13 Tests for the ClaimDataIngestor service. 

14 """ 

15 

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" 

21 

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) 

25 

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 } 

32 

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

36 

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 ) 

48 

49 summary, errors = self._run_ingestor_with_string_io(claims_csv, details_csv) 

50 

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) 

56 

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

62 

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" 

71 

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

75 

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 ) 

80 

81 summary, errors = self._run_ingestor_with_string_io(updated_claims_csv, initial_details_csv, mode="overwrite") 

82 

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

89 

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" 

98 

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) 

103 

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) 

108 

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" 

118 

119 summary, errors = self._run_ingestor_with_string_io(claims_csv, details_csv) 

120 

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

125 

126class ClaimsModelsTests(TestCase): 

127 """ 

128 Tests for the models in the claims app. 

129 """ 

130 

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 ) 

152 

153 def test_claim_str(self): 

154 self.assertEqual(str(self.claim), "Claim 1 for John Doe") 

155 

156 def test_underpayment_property(self): 

157 self.assertEqual(self.claim.underpayment, Decimal('200.00')) 

158 

159 def test_claim_detail_str(self): 

160 self.assertEqual(str(self.claim_detail), "Details for Claim 1") 

161 

162 def test_note_str(self): 

163 self.assertIn("Note on 1", str(self.note)) 

164 

165class ClaimsViewsTests(TestCase): 

166 """ 

167 Tests for the views in the claims app. 

168 """ 

169 

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 ) 

192 

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

198 

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

204 

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

210 

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

216 

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

221 

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) 

228 

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) 

234 

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) 

240 

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

251 

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)