@@ -5,7 +5,6 @@ import {afterEach, beforeEach, describe, expect, it, type Mock, vi} from 'vitest
55
66import { useAuthState } from '../hooks/auth/useAuthState'
77import { useWindowConnection } from '../hooks/comlink/useWindowConnection'
8- import { useSanityInstance } from '../hooks/context/useSanityInstance'
98import { ComlinkTokenRefreshProvider } from './ComlinkTokenRefresh'
109import { ResourceProvider } from './ResourceProvider'
1110
@@ -27,32 +26,20 @@ vi.mock('../hooks/comlink/useWindowConnection', () => ({
2726 useWindowConnection : vi . fn ( ) ,
2827} ) )
2928
30- vi . mock ( '../hooks/context/useSanityInstance' , ( ) => ( {
31- useSanityInstance : vi . fn ( ) ,
32- } ) )
33-
3429// Use simpler mock typings
3530const mockGetIsInDashboardState = getIsInDashboardState as Mock
3631const mockSetAuthToken = setAuthToken as Mock
3732const mockUseAuthState = useAuthState as Mock
3833const mockUseWindowConnection = useWindowConnection as Mock
39- const mockUseSanityInstance = useSanityInstance as unknown as Mock
4034
4135const mockFetch = vi . fn ( )
42- // eslint-disable-next-line @typescript-eslint/no-explicit-any
43- const mockSanityInstance : any = {
44- projectId : 'test' ,
45- dataset : 'test' ,
46- config : { studioMode : { enabled : false } } ,
47- }
4836
4937describe ( 'ComlinkTokenRefresh' , ( ) => {
5038 beforeEach ( ( ) => {
5139 vi . useFakeTimers ( )
5240 mockGetIsInDashboardState . mockReturnValue ( { getCurrent : vi . fn ( ( ) => false ) } )
5341 mockUseAuthState . mockReturnValue ( { type : AuthStateType . LOGGED_IN } )
5442 mockUseWindowConnection . mockReturnValue ( { fetch : mockFetch } )
55- mockUseSanityInstance . mockReturnValue ( mockSanityInstance )
5643 } )
5744
5845 afterEach ( ( ) => {
@@ -127,6 +114,15 @@ describe('ComlinkTokenRefresh', () => {
127114 } )
128115 mockFetch . mockResolvedValueOnce ( { token : 'new-token' } )
129116
117+ // Insert an Unauthorized error container that should be removed on success
118+ const errorContainer = document . createElement ( 'div' )
119+ errorContainer . id = '__sanityError'
120+ const child = document . createElement ( 'div' )
121+ child . textContent =
122+ 'Uncaught error: Unauthorized - A valid session is required for this endpoint'
123+ errorContainer . appendChild ( child )
124+ document . body . appendChild ( errorContainer )
125+
130126 render (
131127 < ResourceProvider projectId = "test-project" dataset = "test-dataset" fallback = { null } >
132128 < ComlinkTokenRefreshProvider >
@@ -141,6 +137,14 @@ describe('ComlinkTokenRefresh', () => {
141137
142138 expect ( mockSetAuthToken ) . toHaveBeenCalledWith ( expect . any ( Object ) , 'new-token' )
143139 expect ( mockFetch ) . toHaveBeenCalledTimes ( 1 )
140+ expect ( mockFetch ) . toHaveBeenCalledWith ( 'dashboard/v1/auth/tokens/create' )
141+ // Assert setAuthToken was called with instance matching provider config
142+ const instanceArg = mockSetAuthToken . mock . calls [ 0 ] [ 0 ]
143+ expect ( instanceArg . config ) . toEqual (
144+ expect . objectContaining ( { projectId : 'test-project' , dataset : 'test-dataset' } ) ,
145+ )
146+ // Unauthorized error container should be removed
147+ expect ( document . getElementById ( '__sanityError' ) ) . toBeNull ( )
144148 } )
145149
146150 it ( 'should not set auth token if received token is null when not in studio mode' , async ( ) => {
@@ -243,20 +247,100 @@ describe('ComlinkTokenRefresh', () => {
243247 expect ( mockFetch ) . toHaveBeenCalledWith ( 'dashboard/v1/auth/tokens/create' )
244248 } )
245249
246- describe ( 'when in studio mode' , ( ) => {
247- beforeEach ( ( ) => {
248- // Make the instance report studio mode enabled
249- mockUseSanityInstance . mockReturnValue ( {
250- ...mockSanityInstance ,
251- config : { studioMode : { enabled : true } } ,
252- } )
250+ it ( 'dedupes multiple 401 errors while a refresh is in progress' , async ( ) => {
251+ mockUseAuthState . mockReturnValue ( {
252+ type : AuthStateType . ERROR ,
253+ error : { statusCode : 401 , message : 'Unauthorized' } ,
253254 } )
255+ // Return a promise we resolve later to keep in-progress true for a bit
256+ let resolveFetch : ( v : { token : string | null } ) => void
257+ mockFetch . mockImplementation (
258+ ( ) =>
259+ new Promise < { token : string | null } > ( ( resolve ) => {
260+ resolveFetch = resolve
261+ } ) ,
262+ )
254263
255- it ( 'should not render DashboardTokenRefresh when studio mode enabled' , ( ) => {
256- render (
264+ const { rerender} = render (
265+ < ResourceProvider fallback = { null } >
266+ < ComlinkTokenRefreshProvider >
267+ < div > Test</ div >
268+ </ ComlinkTokenRefreshProvider >
269+ </ ResourceProvider > ,
270+ )
271+
272+ // Trigger a second 401 while the first request is still in progress
273+ mockUseAuthState . mockReturnValue ( {
274+ type : AuthStateType . ERROR ,
275+ error : { statusCode : 401 , message : 'Unauthorized again' } ,
276+ } )
277+ act ( ( ) => {
278+ rerender (
279+ < ResourceProvider fallback = { null } >
280+ < ComlinkTokenRefreshProvider >
281+ < div > Test</ div >
282+ </ ComlinkTokenRefreshProvider >
283+ </ ResourceProvider > ,
284+ )
285+ } )
286+
287+ // Only one fetch should be in-flight
288+ expect ( mockFetch ) . toHaveBeenCalledTimes ( 1 )
289+
290+ // Finish the first fetch
291+ await act ( async ( ) => {
292+ resolveFetch ! ( { token : null } )
293+ } )
294+ } )
295+
296+ it ( 'requests again after timeout if previous request did not resolve' , async ( ) => {
297+ mockUseAuthState . mockReturnValue ( {
298+ type : AuthStateType . ERROR ,
299+ error : { statusCode : 401 , message : 'Unauthorized' } ,
300+ } )
301+ // First call never resolves
302+ mockFetch . mockImplementationOnce ( ( ) => new Promise ( ( ) => { } ) )
303+
304+ const { rerender} = render (
305+ < ResourceProvider fallback = { null } >
257306 < ComlinkTokenRefreshProvider >
258307 < div > Test</ div >
259- </ ComlinkTokenRefreshProvider > ,
308+ </ ComlinkTokenRefreshProvider >
309+ </ ResourceProvider > ,
310+ )
311+
312+ expect ( mockFetch ) . toHaveBeenCalledTimes ( 1 )
313+
314+ // After timeout elapses, a subsequent 401 should trigger another fetch
315+ await act ( async ( ) => {
316+ await vi . advanceTimersByTimeAsync ( 10000 )
317+ } )
318+
319+ mockUseAuthState . mockReturnValue ( {
320+ type : AuthStateType . ERROR ,
321+ error : { statusCode : 401 , message : 'Unauthorized again' } ,
322+ } )
323+ act ( ( ) => {
324+ rerender (
325+ < ResourceProvider fallback = { null } >
326+ < ComlinkTokenRefreshProvider >
327+ < div > Test</ div >
328+ </ ComlinkTokenRefreshProvider >
329+ </ ResourceProvider > ,
330+ )
331+ } )
332+
333+ expect ( mockFetch ) . toHaveBeenCalledTimes ( 2 )
334+ } )
335+
336+ describe ( 'when in studio mode' , ( ) => {
337+ it ( 'should not render DashboardTokenRefresh when studio mode enabled' , ( ) => {
338+ render (
339+ < ResourceProvider fallback = { null } studioMode = { { enabled : true } } >
340+ < ComlinkTokenRefreshProvider >
341+ < div > Test</ div >
342+ </ ComlinkTokenRefreshProvider >
343+ </ ResourceProvider > ,
260344 )
261345
262346 // In studio mode, provider should return children directly
0 commit comments