Skip to content

Commit 0a5e1aa

Browse files
authored
chore: adjust ComlinkTokenRefresh test to have less mocking (#658)
1 parent 60de2d1 commit 0a5e1aa

File tree

1 file changed

+107
-23
lines changed

1 file changed

+107
-23
lines changed

packages/react/src/context/ComlinkTokenRefresh.test.tsx

Lines changed: 107 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {afterEach, beforeEach, describe, expect, it, type Mock, vi} from 'vitest
55

66
import {useAuthState} from '../hooks/auth/useAuthState'
77
import {useWindowConnection} from '../hooks/comlink/useWindowConnection'
8-
import {useSanityInstance} from '../hooks/context/useSanityInstance'
98
import {ComlinkTokenRefreshProvider} from './ComlinkTokenRefresh'
109
import {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
3530
const mockGetIsInDashboardState = getIsInDashboardState as Mock
3631
const mockSetAuthToken = setAuthToken as Mock
3732
const mockUseAuthState = useAuthState as Mock
3833
const mockUseWindowConnection = useWindowConnection as Mock
39-
const mockUseSanityInstance = useSanityInstance as unknown as Mock
4034

4135
const 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

4937
describe('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

Comments
 (0)