1+ import { vi , describe , test , expect , beforeEach , afterEach } from 'vitest' ;
2+ import { uploadReport , ReportError , fetchLatestReports , fetchAllReports , markReportAsRead } from '../reportService' ;
3+ import { ReportCategory , ReportStatus } from '../../models/medicalReport' ;
4+ import axios from 'axios' ;
5+
6+ // Mock axios
7+ vi . mock ( 'axios' , ( ) => ( {
8+ default : {
9+ post : vi . fn ( ) ,
10+ get : vi . fn ( ) ,
11+ patch : vi . fn ( ) ,
12+ isAxiosError : vi . fn ( ( ) => true )
13+ }
14+ } ) ) ;
15+
16+ // Mock response data
17+ const mockReports = [
18+ {
19+ id : '1' ,
20+ title : 'heart-report' ,
21+ status : ReportStatus . UNREAD ,
22+ category : ReportCategory . HEART ,
23+ documentUrl : 'http://example.com/heart-report.pdf' ,
24+ date : '2024-03-24' ,
25+ } ,
26+ {
27+ id : '2' ,
28+ title : 'brain-scan' ,
29+ status : ReportStatus . UNREAD ,
30+ category : ReportCategory . NEUROLOGICAL ,
31+ documentUrl : 'http://example.com/brain-scan.pdf' ,
32+ date : '2024-03-24' ,
33+ }
34+ ] ;
35+
36+ describe ( 'reportService' , ( ) => {
37+ const mockFile = new File ( [ 'test content' ] , 'test-report.pdf' , { type : 'application/pdf' } ) ;
38+ let progressCallback : ( progress : number ) => void ;
39+
40+ beforeEach ( ( ) => {
41+ vi . resetAllMocks ( ) ;
42+ progressCallback = vi . fn ( ) ;
43+ } ) ;
44+
45+ describe ( 'uploadReport' , ( ) => {
46+ // Create a mock implementation for FormData
47+ let mockFormData : { append : ReturnType < typeof vi . fn > } ;
48+
49+ beforeEach ( ( ) => {
50+ // Mock the internal timers used in uploadReport
51+ vi . spyOn ( global , 'setTimeout' ) . mockImplementation ( ( fn ) => {
52+ if ( typeof fn === 'function' ) fn ( ) ;
53+ return 123 as unknown as NodeJS . Timeout ;
54+ } ) ;
55+
56+ vi . spyOn ( global , 'setInterval' ) . mockImplementation ( ( ) => {
57+ return 456 as unknown as NodeJS . Timeout ;
58+ } ) ;
59+
60+ vi . spyOn ( global , 'clearInterval' ) . mockImplementation ( ( ) => { } ) ;
61+
62+ // Setup mock FormData
63+ mockFormData = {
64+ append : vi . fn ( )
65+ } ;
66+
67+ // Mock FormData constructor
68+ global . FormData = vi . fn ( ( ) => mockFormData as unknown as FormData ) ;
69+ } ) ;
70+
71+ test ( 'should upload file successfully' , async ( ) => {
72+ const report = await uploadReport ( mockFile , progressCallback ) ;
73+
74+ // Check the returned data matches our expectations
75+ expect ( report ) . toBeDefined ( ) ;
76+ expect ( report . title ) . toBe ( 'test-report' ) ;
77+ expect ( report . status ) . toBe ( ReportStatus . UNREAD ) ;
78+
79+ // Verify form data was created with the correct file
80+ expect ( FormData ) . toHaveBeenCalled ( ) ;
81+ expect ( mockFormData . append ) . toHaveBeenCalledWith ( 'file' , mockFile ) ;
82+
83+ // Check the progress callback was called
84+ expect ( progressCallback ) . toHaveBeenCalled ( ) ;
85+ } ) ;
86+
87+ test ( 'should determine category based on filename' , async ( ) => {
88+ const heartFile = new File ( [ 'test' ] , 'heart-report.pdf' , { type : 'application/pdf' } ) ;
89+ const heartReport = await uploadReport ( heartFile ) ;
90+ expect ( heartReport . category ) . toBe ( ReportCategory . HEART ) ;
91+
92+ // Reset mocks for the second file
93+ vi . resetAllMocks ( ) ;
94+ mockFormData = { append : vi . fn ( ) } ;
95+ global . FormData = vi . fn ( ( ) => mockFormData as unknown as FormData ) ;
96+
97+ // Recreate timer mocks for the second upload
98+ vi . spyOn ( global , 'setTimeout' ) . mockImplementation ( ( fn ) => {
99+ if ( typeof fn === 'function' ) fn ( ) ;
100+ return 123 as unknown as NodeJS . Timeout ;
101+ } ) ;
102+
103+ vi . spyOn ( global , 'setInterval' ) . mockImplementation ( ( ) => {
104+ return 456 as unknown as NodeJS . Timeout ;
105+ } ) ;
106+
107+ vi . spyOn ( global , 'clearInterval' ) . mockImplementation ( ( ) => { } ) ;
108+
109+ const neuroFile = new File ( [ 'test' ] , 'brain-scan.pdf' , { type : 'application/pdf' } ) ;
110+ const neuroReport = await uploadReport ( neuroFile ) ;
111+ expect ( neuroReport . category ) . toBe ( ReportCategory . NEUROLOGICAL ) ;
112+ } ) ;
113+
114+ test ( 'should handle upload without progress callback' , async ( ) => {
115+ const report = await uploadReport ( mockFile ) ;
116+ expect ( report ) . toBeDefined ( ) ;
117+ expect ( report . title ) . toBe ( 'test-report' ) ;
118+ } ) ;
119+
120+ test ( 'should throw ReportError on upload failure' , async ( ) => {
121+ // Restore the original FormData
122+ const originalFormData = global . FormData ;
123+
124+ // Mock FormData to throw an error
125+ global . FormData = vi . fn ( ( ) => {
126+ throw new Error ( 'FormData construction failed' ) ;
127+ } ) ;
128+
129+ await expect ( uploadReport ( mockFile , progressCallback ) )
130+ . rejects
131+ . toThrow ( ReportError ) ;
132+
133+ // Restore the previous mock
134+ global . FormData = originalFormData ;
135+ } ) ;
136+
137+ afterEach ( ( ) => {
138+ vi . restoreAllMocks ( ) ;
139+ } ) ;
140+ } ) ;
141+
142+ describe ( 'fetchLatestReports' , ( ) => {
143+ beforeEach ( ( ) => {
144+ // Setup axios mock response
145+ ( axios . get as ReturnType < typeof vi . fn > ) . mockResolvedValue ( {
146+ data : mockReports . slice ( 0 , 2 )
147+ } ) ;
148+ } ) ;
149+
150+ test ( 'should fetch latest reports with default limit' , async ( ) => {
151+ const reports = await fetchLatestReports ( ) ;
152+
153+ expect ( axios . get ) . toHaveBeenCalled ( ) ;
154+ expect ( reports ) . toHaveLength ( 2 ) ;
155+ expect ( reports [ 0 ] ) . toEqual ( expect . objectContaining ( {
156+ id : expect . any ( String ) ,
157+ title : expect . any ( String )
158+ } ) ) ;
159+ } ) ;
160+
161+ test ( 'should fetch latest reports with custom limit' , async ( ) => {
162+ const limit = 1 ;
163+ ( axios . get as ReturnType < typeof vi . fn > ) . mockResolvedValue ( {
164+ data : mockReports . slice ( 0 , 1 )
165+ } ) ;
166+
167+ const reports = await fetchLatestReports ( limit ) ;
168+
169+ expect ( axios . get ) . toHaveBeenCalled ( ) ;
170+ expect ( reports ) . toHaveLength ( 1 ) ;
171+ } ) ;
172+
173+ test ( 'should throw ReportError on fetch failure' , async ( ) => {
174+ ( axios . get as ReturnType < typeof vi . fn > ) . mockRejectedValue ( new Error ( 'Network error' ) ) ;
175+
176+ await expect ( fetchLatestReports ( ) )
177+ . rejects
178+ . toThrow ( ReportError ) ;
179+ } ) ;
180+ } ) ;
181+
182+ describe ( 'fetchAllReports' , ( ) => {
183+ beforeEach ( ( ) => {
184+ ( axios . get as ReturnType < typeof vi . fn > ) . mockResolvedValue ( {
185+ data : mockReports
186+ } ) ;
187+ } ) ;
188+
189+ test ( 'should fetch all reports' , async ( ) => {
190+ const reports = await fetchAllReports ( ) ;
191+
192+ expect ( axios . get ) . toHaveBeenCalled ( ) ;
193+ expect ( reports ) . toEqual ( mockReports ) ;
194+ } ) ;
195+
196+ test ( 'should throw ReportError on fetch failure' , async ( ) => {
197+ ( axios . get as ReturnType < typeof vi . fn > ) . mockRejectedValue ( new Error ( 'Network error' ) ) ;
198+
199+ await expect ( fetchAllReports ( ) )
200+ . rejects
201+ . toThrow ( ReportError ) ;
202+ } ) ;
203+ } ) ;
204+
205+ describe ( 'markReportAsRead' , ( ) => {
206+ beforeEach ( ( ) => {
207+ const updatedReport = {
208+ ...mockReports [ 0 ] ,
209+ status : ReportStatus . READ
210+ } ;
211+
212+ ( axios . patch as ReturnType < typeof vi . fn > ) . mockResolvedValue ( {
213+ data : updatedReport
214+ } ) ;
215+ } ) ;
216+
217+ test ( 'should mark a report as read' , async ( ) => {
218+ const updatedReport = await markReportAsRead ( '1' ) ;
219+
220+ expect ( axios . patch ) . toHaveBeenCalled ( ) ;
221+ expect ( updatedReport . status ) . toBe ( ReportStatus . READ ) ;
222+ } ) ;
223+
224+ test ( 'should throw error when report not found' , async ( ) => {
225+ ( axios . patch as ReturnType < typeof vi . fn > ) . mockRejectedValue ( new Error ( 'Report not found' ) ) ;
226+
227+ await expect ( markReportAsRead ( 'non-existent-id' ) )
228+ . rejects
229+ . toThrow ( ReportError ) ;
230+ } ) ;
231+ } ) ;
232+ } ) ;
0 commit comments