1+ // src/api/OpenProcessing.test.ts
2+
3+ import { getCurationSketches , getSketch , getSketchSize , priorityIds , type OpenProcessingCurationResponse } from '@/src/api/OpenProcessing' ;
4+ import { describe , it , expect , vi , beforeEach } from 'vitest' ;
5+
6+ const mockFetch = vi . fn ( ) ;
7+
8+ vi . stubGlobal ( 'fetch' , mockFetch ) ;
9+
10+ // Test data: first item is mock data, second uses actual priority ID from current curation
11+ const getCurationSketchesData : OpenProcessingCurationResponse = [ {
12+ visualID : 101 ,
13+ title : 'Sketch One' ,
14+ description : 'Description One' ,
15+ instructions : 'Instructions One' ,
16+ mode : 'p5js' ,
17+ createdOn : '2025-01-01' ,
18+ userID : 'User One' ,
19+ submittedOn : '2025-01-01' ,
20+ fullname : 'Fullname One'
21+ } ,
22+ {
23+ visualID : Number ( priorityIds [ 0 ] ) , // Real ID from current curation priority list
24+ title : 'Sketch Two' ,
25+ description : 'Description Two' ,
26+ instructions : 'Instructions Two' ,
27+ mode : 'p5js' ,
28+ createdOn : '2025-01-01' ,
29+ userID : 'User Two' ,
30+ submittedOn : '2025-01-01' ,
31+ fullname : 'Fullname Two'
32+ } ]
33+
34+ describe ( 'OpenProcessing API Caching' , ( ) => {
35+
36+ beforeEach ( ( ) => {
37+ vi . clearAllMocks ( ) ;
38+
39+ getCurationSketches . cache . clear ?.( ) ;
40+ getSketch . cache . clear ?.( ) ;
41+ getSketchSize . cache . clear ?.( ) ;
42+ } ) ;
43+
44+ // Case 1: Verify caching for getCurationSketches
45+ it ( 'should only call the API once even if getCurationSketches is called multiple times' , async ( ) => {
46+
47+ mockFetch . mockResolvedValue ( {
48+ ok : true ,
49+ json : ( ) => Promise . resolve ( getCurationSketchesData ) ,
50+ } ) ;
51+
52+ await getCurationSketches ( ) ;
53+ await getCurationSketches ( ) ;
54+
55+ // Check if fetch was called exactly 2 times (for the two curation IDs).
56+ // If this number becomes 4, it means the caching is broken.
57+ expect ( mockFetch ) . toHaveBeenCalledTimes ( 2 ) ;
58+ } ) ;
59+
60+ // Case 2: Verify getSketch uses cached data from getCurationSketches
61+ it ( 'should use cached data from getCurationSketches for getSketch calls' , async ( ) => {
62+
63+ mockFetch . mockResolvedValueOnce ( { // for curationId
64+ ok : true ,
65+ json : ( ) => Promise . resolve ( [ getCurationSketchesData [ 0 ] ] ) ,
66+ } ) . mockResolvedValueOnce ( { // for newCurationId
67+ ok : true ,
68+ json : ( ) => Promise . resolve ( [ getCurationSketchesData [ 1 ] ] ) ,
69+ } ) ;
70+
71+ // Call the main function to populate the cache.
72+ await getCurationSketches ( ) ;
73+ // At this point, fetch has been called twice.
74+ expect ( mockFetch ) . toHaveBeenCalledTimes ( 2 ) ;
75+
76+ // Now, call getSketch with an ID that should be in the cache.
77+ const sketch = await getSketch ( getCurationSketchesData [ 0 ] . visualID ) ;
78+ expect ( sketch . title ) . toBe ( 'Sketch One' ) ;
79+ const sketch2 = await getSketch ( getCurationSketchesData [ 1 ] . visualID ) ;
80+ expect ( sketch2 . title ) . toBe ( 'Sketch Two' ) ;
81+
82+ // Verify that no additional fetch calls were made.
83+ // The call count should still be 2 because the data came from the cache.
84+ expect ( mockFetch ) . toHaveBeenCalledTimes ( 2 ) ;
85+ } ) ;
86+
87+ // Case 3: Verify getSketch fetches individual sketch when not in cache
88+ it ( 'should fetch individual sketch when not in cache' , async ( ) => {
89+
90+ // for curationId
91+ mockFetch . mockResolvedValueOnce ( {
92+ ok : true ,
93+ json : ( ) => Promise . resolve ( [ ] )
94+ } ) . mockResolvedValueOnce ( { // for newCurationId
95+ ok : true ,
96+ json : ( ) => Promise . resolve ( [ ] )
97+ } ) ;
98+
99+ await getCurationSketches ( ) ; // Create empty cache
100+
101+ // Individual sketch API call
102+ mockFetch . mockResolvedValueOnce ( {
103+ ok : true ,
104+ json : ( ) => Promise . resolve ( { visualID : 999 , title : 'Individual Sketch' } )
105+ } ) ;
106+
107+ const sketch = await getSketch ( 999 ) ;
108+ expect ( sketch . title ) . toBe ( 'Individual Sketch' ) ;
109+ expect ( mockFetch ) . toHaveBeenCalledTimes ( 3 ) ; // 2 for empty curations in getCurationSketches + 1 for individual call in getSketch
110+ } ) ;
111+
112+ // Case 4: Overall regression test for total sketch page generation
113+ it ( 'should not exceed the expected number of API calls during a build simulation' , async ( ) => {
114+
115+ // Mock the responses for getCurationSketches.
116+ mockFetch . mockResolvedValueOnce ( { // for curationId
117+ ok : true ,
118+ json : ( ) => Promise . resolve ( [ getCurationSketchesData [ 0 ] ] ) ,
119+ } ) . mockResolvedValueOnce ( { // for newCurationId
120+ ok : true ,
121+ json : ( ) => Promise . resolve ( [ getCurationSketchesData [ 1 ] ] ) ,
122+ } ) ;
123+
124+ // 2. Mock the response for getSketchSize calls.
125+ mockFetch . mockResolvedValue ( { // for all subsequent calls
126+ ok : true ,
127+ json : ( ) => Promise . resolve ( [ { code : 'createCanvas(400, 400);' } ] ) ,
128+ } ) ;
129+
130+ // --- sketch page build simulation ---
131+ // This simulates what happens during `getStaticPaths`.
132+ const sketches = await getCurationSketches ( ) ; // Makes 2 API calls.
133+
134+ // This simulates what happens as each page is generated.
135+ for ( const sketch of sketches ) {
136+ // Inside the page component, getSketch and getSketchSize would be called.
137+ await getSketch ( sketch . visualID ) ; // Uses cache (0 new calls).
138+ await getSketchSize ( sketch . visualID ) ; // Makes 1 new API call.
139+ }
140+ // --- simulation end ---
141+
142+ // Calculate the total expected calls.
143+ // 2 for getCurationSketches + 1 for each sketch's getSketchSize call.
144+ const expectedCalls = 2 + sketches . length ;
145+ expect ( mockFetch ) . toHaveBeenCalledTimes ( expectedCalls ) ;
146+
147+ } ) ;
148+ } ) ;
149+
150+ describe ( 'Error Handling' , ( ) => {
151+
152+ beforeEach ( ( ) => {
153+ vi . clearAllMocks ( ) ;
154+
155+ getCurationSketches . cache . clear ?.( ) ;
156+ getSketch . cache . clear ?.( ) ;
157+ getSketchSize . cache . clear ?.( ) ;
158+ } ) ;
159+
160+ it ( 'should throw an error when getCurationSketches API call fails' , async ( ) => {
161+ mockFetch . mockResolvedValue ( {
162+ ok : false ,
163+ status : 500 ,
164+ statusText : 'Internal Server Error' ,
165+ } ) ;
166+
167+ await expect ( getCurationSketches ( ) ) . rejects . toThrow (
168+ 'getCurationSketches: 500 Internal Server Error'
169+ ) ;
170+ } ) ;
171+
172+ it ( 'should throw an error when rate limit is exceeded (429)' , async ( ) => {
173+ mockFetch . mockResolvedValue ( {
174+ ok : false ,
175+ status : 429 ,
176+ statusText : 'Too Many Requests' ,
177+ } ) ;
178+
179+ await expect ( getCurationSketches ( ) ) . rejects . toThrow (
180+ 'getCurationSketches: 429 Too Many Requests'
181+ ) ;
182+ } ) ;
183+
184+ it ( 'should throw an error when getSketch API call fails for individual sketch' , async ( ) => {
185+ // Setup empty curations first
186+ mockFetch . mockResolvedValueOnce ( {
187+ ok : true ,
188+ json : ( ) => Promise . resolve ( [ ] )
189+ } ) . mockResolvedValueOnce ( {
190+ ok : true ,
191+ json : ( ) => Promise . resolve ( [ ] )
192+ } ) ;
193+
194+ await getCurationSketches ( ) ; // Create empty cache
195+
196+ // Individual sketch API call fails with 429
197+ mockFetch . mockResolvedValueOnce ( {
198+ ok : false ,
199+ status : 429 ,
200+ statusText : 'Too Many Requests'
201+ } ) ;
202+
203+ await expect ( getSketch ( 999 ) ) . rejects . toThrow (
204+ 'getSketch: 999 429 Too Many Requests'
205+ ) ;
206+ } ) ;
207+
208+ it ( 'should throw an error when getSketchSize API call fails' , async ( ) => {
209+ // Setup sketch data first
210+ mockFetch . mockResolvedValueOnce ( {
211+ ok : true ,
212+ json : ( ) => Promise . resolve ( [ getCurationSketchesData [ 0 ] ] )
213+ } ) . mockResolvedValueOnce ( {
214+ ok : true ,
215+ json : ( ) => Promise . resolve ( [ ] )
216+ } ) ;
217+
218+ await getCurationSketches ( ) ;
219+
220+ // getSketchSize API call fails with rate limit
221+ mockFetch . mockResolvedValueOnce ( {
222+ ok : false ,
223+ status : 429 ,
224+ statusText : 'Too Many Requests'
225+ } ) ;
226+
227+ await expect ( getSketchSize ( getCurationSketchesData [ 0 ] . visualID ) ) . rejects . toThrow (
228+ `getSketchSize: ${ getCurationSketchesData [ 0 ] . visualID } 429 Too Many Requests`
229+ ) ;
230+ } ) ;
231+ } ) ;
0 commit comments