@sanity/sdk 2.15.0 → 2.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_chunks-dts/createGroqSearchFilter.d.ts +5 -4
- package/dist/_chunks-dts/createGroqSearchFilter.d.ts.map +1 -1
- package/dist/_chunks-es/createGroqSearchFilter.js +152 -148
- package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -1
- package/dist/_chunks-es/version.js +1 -1
- package/dist/index.d.ts +69 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +79 -32
- package/dist/index.js.map +1 -1
- package/package.json +12 -12
- package/src/_exports/index.ts +2 -0
- package/src/auth/authStore.test.ts +1 -1
- package/src/auth/handleAuthCallback.test.ts +1 -1
- package/src/auth/refreshStampedToken.test.ts +199 -477
- package/src/auth/refreshStampedToken.ts +67 -195
- package/src/client/liveEvents.test.ts +151 -0
- package/src/client/liveEvents.ts +107 -0
- package/src/document/actions.ts +36 -0
- package/src/document/documentStore.test.ts +132 -1
- package/src/document/documentStore.ts +8 -0
- package/src/document/events.ts +32 -0
- package/src/document/listen.ts +8 -0
- package/src/document/processActions/edit.ts +19 -14
- package/src/document/processActions/shared.ts +55 -8
- package/src/document/processActions.test.ts +145 -0
- package/src/document/reducers.test.ts +396 -0
- package/src/document/reducers.ts +62 -1
- package/src/document/sharedListener.test.ts +1 -0
- package/src/document/sharedListener.ts +2 -0
- package/src/query/queryStore.test.ts +232 -38
- package/src/query/queryStore.ts +47 -47
- package/src/query/reducers.test.ts +0 -42
- package/src/query/reducers.ts +0 -16
- package/src/releases/getPerspectiveState.test.ts +3 -8
- package/src/releases/getPerspectiveState.ts +6 -19
- package/src/releases/observeReleases.test.ts +127 -0
- package/src/releases/observeReleases.ts +102 -0
- package/src/releases/releasesStore.test.ts +51 -53
- package/src/releases/releasesStore.ts +32 -32
- package/src/releases/utils/sortReleases.test.ts +36 -0
- package/src/releases/utils/sortReleases.ts +14 -9
|
@@ -1,18 +1,11 @@
|
|
|
1
|
-
// NOTE: vi.mock REMOVED
|
|
2
|
-
|
|
3
1
|
import {of, Subscription, throwError} from 'rxjs'
|
|
4
|
-
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
|
|
2
|
+
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
|
|
5
3
|
|
|
6
4
|
import {createSanityInstance} from '../store/createSanityInstance'
|
|
7
5
|
import {createStoreState} from '../store/createStoreState'
|
|
8
6
|
import {AuthStateType} from './authStateType'
|
|
9
7
|
import {type AuthState, authStore} from './authStore'
|
|
10
|
-
import {
|
|
11
|
-
getLastRefreshTime,
|
|
12
|
-
getNextRefreshDelay,
|
|
13
|
-
refreshStampedToken,
|
|
14
|
-
setLastRefreshTime,
|
|
15
|
-
} from './refreshStampedToken'
|
|
8
|
+
import {refreshStampedToken} from './refreshStampedToken'
|
|
16
9
|
import {createLoggedInAuthState} from './utils'
|
|
17
10
|
|
|
18
11
|
// Mock logger to prevent actual logging during tests
|
|
@@ -30,23 +23,16 @@ vi.mock('../utils/logger', async (importOriginal) => {
|
|
|
30
23
|
}
|
|
31
24
|
})
|
|
32
25
|
|
|
33
|
-
// Type definitions for Web Locks (can be kept if needed for context)
|
|
34
|
-
// ... (Lock, LockOptions, LockGrantedCallback types)
|
|
35
|
-
|
|
36
26
|
describe('refreshStampedToken', () => {
|
|
37
27
|
let mockStorage: Storage
|
|
38
|
-
let originalNavigator: typeof navigator // Restored
|
|
39
28
|
let originalDocument: Document
|
|
40
29
|
let subscriptions: Subscription[]
|
|
41
|
-
// mockLocksRequest removed
|
|
42
30
|
|
|
43
31
|
beforeEach(() => {
|
|
44
32
|
subscriptions = []
|
|
45
33
|
vi.clearAllMocks()
|
|
46
34
|
vi.useFakeTimers()
|
|
47
35
|
|
|
48
|
-
originalNavigator = global.navigator // Restore original navigator setup
|
|
49
|
-
|
|
50
36
|
// Mock document for visibility API
|
|
51
37
|
originalDocument = global.document
|
|
52
38
|
Object.defineProperty(global, 'document', {
|
|
@@ -67,49 +53,10 @@ describe('refreshStampedToken', () => {
|
|
|
67
53
|
key: vi.fn(),
|
|
68
54
|
length: 0,
|
|
69
55
|
}
|
|
70
|
-
// Restore a basic, functional mock for navigator.locks
|
|
71
|
-
// This mock *will* run the callback, including the infinite loop
|
|
72
|
-
// if not handled carefully in tests.
|
|
73
|
-
const mockLocks = {
|
|
74
|
-
request: vi.fn(
|
|
75
|
-
async (
|
|
76
|
-
_name: string,
|
|
77
|
-
_options: LockOptions | LockGrantedCallback<unknown>,
|
|
78
|
-
callback?: LockGrantedCallback<unknown>,
|
|
79
|
-
) => {
|
|
80
|
-
const actualCallback = typeof _options === 'function' ? _options : callback
|
|
81
|
-
if (!actualCallback) return false
|
|
82
|
-
const mockLock: Lock = {name: 'mock-lock', mode: 'exclusive'}
|
|
83
|
-
try {
|
|
84
|
-
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
85
|
-
// CAUTION: This executes the callback provided by acquireTokenRefreshLock
|
|
86
|
-
// which contains the infinite loop. Tests need to avoid triggering
|
|
87
|
-
// timers indefinitely if this runs.
|
|
88
|
-
await actualCallback(mockLock)
|
|
89
|
-
// This return is unlikely to be hit if callback loops
|
|
90
|
-
return true
|
|
91
|
-
} catch (error) {
|
|
92
|
-
// eslint-disable-next-line no-console
|
|
93
|
-
console.error('Mock lock request failed:', error)
|
|
94
|
-
return false
|
|
95
|
-
}
|
|
96
|
-
},
|
|
97
|
-
),
|
|
98
|
-
query: vi.fn(async () => ({held: [], pending: []})),
|
|
99
|
-
}
|
|
100
|
-
Object.defineProperty(global, 'navigator', {
|
|
101
|
-
value: {locks: mockLocks},
|
|
102
|
-
writable: true,
|
|
103
|
-
})
|
|
104
56
|
})
|
|
105
57
|
|
|
106
58
|
afterEach(async () => {
|
|
107
59
|
subscriptions.forEach((sub) => sub.unsubscribe())
|
|
108
|
-
// Restore original navigator
|
|
109
|
-
Object.defineProperty(global, 'navigator', {
|
|
110
|
-
value: originalNavigator,
|
|
111
|
-
writable: true,
|
|
112
|
-
})
|
|
113
60
|
// Restore original document
|
|
114
61
|
Object.defineProperty(global, 'document', {
|
|
115
62
|
value: originalDocument,
|
|
@@ -125,353 +72,222 @@ describe('refreshStampedToken', () => {
|
|
|
125
72
|
vi.useRealTimers()
|
|
126
73
|
})
|
|
127
74
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
dataset: 'd',
|
|
138
|
-
auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
|
|
139
|
-
})
|
|
140
|
-
const initialState = authStore.getInitialState(instance, null)
|
|
141
|
-
initialState.authState = {
|
|
142
|
-
type: AuthStateType.LOGGED_IN,
|
|
143
|
-
token: 'sk-initial-token-st123',
|
|
144
|
-
currentUser: null,
|
|
145
|
-
}
|
|
146
|
-
initialState.dashboardContext = {mode: 'test'}
|
|
147
|
-
const state = createStoreState(initialState)
|
|
148
|
-
|
|
149
|
-
const subscription = refreshStampedToken({state, instance, key: null})
|
|
150
|
-
subscriptions.push(subscription)
|
|
151
|
-
|
|
152
|
-
await vi.advanceTimersToNextTimerAsync()
|
|
153
|
-
|
|
154
|
-
const finalAuthStateDash = state.get().authState
|
|
155
|
-
expect(finalAuthStateDash.type).toBe(AuthStateType.LOGGED_IN)
|
|
156
|
-
// Ensure token was updated
|
|
157
|
-
if (finalAuthStateDash.type === AuthStateType.LOGGED_IN) {
|
|
158
|
-
expect(finalAuthStateDash.token).toBe('sk-refreshed-token-st123')
|
|
159
|
-
}
|
|
160
|
-
// Verify navigator.locks.request was NOT called
|
|
161
|
-
const locksRequest = navigator.locks.request as ReturnType<typeof vi.fn>
|
|
162
|
-
expect(locksRequest).not.toHaveBeenCalled()
|
|
75
|
+
it('refreshes the token when the interval timer fires', async () => {
|
|
76
|
+
const mockClient = {
|
|
77
|
+
observable: {request: vi.fn(() => of({token: 'sk-refreshed-token-st123'}))},
|
|
78
|
+
}
|
|
79
|
+
const mockClientFactory = vi.fn().mockReturnValue(mockClient)
|
|
80
|
+
const instance = createSanityInstance({
|
|
81
|
+
projectId: 'p',
|
|
82
|
+
dataset: 'd',
|
|
83
|
+
auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
|
|
163
84
|
})
|
|
85
|
+
const initialState = authStore.getInitialState(instance, null)
|
|
86
|
+
initialState.authState = {
|
|
87
|
+
type: AuthStateType.LOGGED_IN,
|
|
88
|
+
token: 'sk-initial-token-st123',
|
|
89
|
+
currentUser: null,
|
|
90
|
+
}
|
|
91
|
+
const state = createStoreState(initialState)
|
|
92
|
+
|
|
93
|
+
const subscription = refreshStampedToken({state, instance, key: null})
|
|
94
|
+
subscriptions.push(subscription)
|
|
164
95
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const visibilityHandler = addEventListenerMock.mock.calls[0][1] as () => void
|
|
184
|
-
|
|
185
|
-
Object.defineProperty(global.document, 'visibilityState', {
|
|
186
|
-
value: 'visible',
|
|
187
|
-
writable: true,
|
|
188
|
-
configurable: true,
|
|
189
|
-
})
|
|
190
|
-
|
|
191
|
-
visibilityHandler()
|
|
192
|
-
await vi.advanceTimersByTimeAsync(100)
|
|
193
|
-
|
|
194
|
-
expect(mockClient.observable.request).not.toHaveBeenCalled()
|
|
195
|
-
const finalAuthState = state.get().authState
|
|
196
|
-
if (finalAuthState.type === AuthStateType.LOGGED_IN) {
|
|
197
|
-
expect(finalAuthState.token).toBe('sk-initial-token-st123')
|
|
198
|
-
}
|
|
96
|
+
await vi.advanceTimersToNextTimerAsync()
|
|
97
|
+
|
|
98
|
+
const finalAuthState = state.get().authState
|
|
99
|
+
expect(finalAuthState.type).toBe(AuthStateType.LOGGED_IN)
|
|
100
|
+
if (finalAuthState.type === AuthStateType.LOGGED_IN) {
|
|
101
|
+
expect(finalAuthState.token).toBe('sk-refreshed-token-st123')
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('writes the refreshed token to storage', async () => {
|
|
106
|
+
const mockClient = {
|
|
107
|
+
observable: {request: vi.fn(() => of({token: 'sk-refreshed-token-st123'}))},
|
|
108
|
+
}
|
|
109
|
+
const mockClientFactory = vi.fn().mockReturnValue(mockClient)
|
|
110
|
+
const instance = createSanityInstance({
|
|
111
|
+
projectId: 'p',
|
|
112
|
+
dataset: 'd',
|
|
113
|
+
auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
|
|
199
114
|
})
|
|
115
|
+
const initialState = authStore.getInitialState(instance, null)
|
|
116
|
+
initialState.authState = {
|
|
117
|
+
type: AuthStateType.LOGGED_IN,
|
|
118
|
+
token: 'sk-initial-token-st123',
|
|
119
|
+
currentUser: null,
|
|
120
|
+
}
|
|
121
|
+
const state = createStoreState(initialState)
|
|
122
|
+
|
|
123
|
+
const subscription = refreshStampedToken({state, instance, key: null})
|
|
124
|
+
subscriptions.push(subscription)
|
|
200
125
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
visibilityHandler()
|
|
235
|
-
await vi.advanceTimersToNextTimerAsync()
|
|
236
|
-
|
|
237
|
-
expect(mockClient.observable.request).toHaveBeenCalled()
|
|
238
|
-
const finalAuthState = state.get().authState
|
|
239
|
-
if (finalAuthState.type === AuthStateType.LOGGED_IN) {
|
|
240
|
-
expect(finalAuthState.token).toBe('sk-refreshed-token-st123')
|
|
241
|
-
expect(finalAuthState.lastTokenRefresh).toBeGreaterThan(staleTimestamp)
|
|
242
|
-
}
|
|
126
|
+
await vi.advanceTimersToNextTimerAsync()
|
|
127
|
+
|
|
128
|
+
expect(mockClient.observable.request).toHaveBeenCalled()
|
|
129
|
+
expect(mockStorage.setItem).toHaveBeenCalledWith(
|
|
130
|
+
initialState.options.storageKey,
|
|
131
|
+
JSON.stringify({token: 'sk-refreshed-token-st123'}),
|
|
132
|
+
)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('does not refresh on visibility change when lastTokenRefresh is recent', async () => {
|
|
136
|
+
const mockClient = {
|
|
137
|
+
observable: {request: vi.fn(() => of({token: 'sk-refreshed-token-st123'}))},
|
|
138
|
+
}
|
|
139
|
+
const mockClientFactory = vi.fn().mockReturnValue(mockClient)
|
|
140
|
+
const instance = createSanityInstance({
|
|
141
|
+
auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
|
|
142
|
+
})
|
|
143
|
+
const initialState = authStore.getInitialState(instance, null)
|
|
144
|
+
initialState.authState = createLoggedInAuthState('sk-initial-token-st123', null)
|
|
145
|
+
const state = createStoreState(initialState)
|
|
146
|
+
|
|
147
|
+
const subscription = refreshStampedToken({state, instance, key: null})
|
|
148
|
+
subscriptions.push(subscription)
|
|
149
|
+
|
|
150
|
+
const addEventListenerMock = global.document.addEventListener as ReturnType<typeof vi.fn>
|
|
151
|
+
expect(addEventListenerMock).toHaveBeenCalledWith('visibilitychange', expect.any(Function))
|
|
152
|
+
const visibilityHandler = addEventListenerMock.mock.calls[0][1] as () => void
|
|
153
|
+
|
|
154
|
+
Object.defineProperty(global.document, 'visibilityState', {
|
|
155
|
+
value: 'visible',
|
|
156
|
+
writable: true,
|
|
157
|
+
configurable: true,
|
|
243
158
|
})
|
|
244
159
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const subscription = refreshStampedToken({state, instance, key: null})
|
|
264
|
-
subscriptions.push(subscription)
|
|
265
|
-
|
|
266
|
-
const addEventListenerMock = global.document.addEventListener as ReturnType<typeof vi.fn>
|
|
267
|
-
const visibilityHandler = addEventListenerMock.mock.calls[0][1] as () => void
|
|
268
|
-
|
|
269
|
-
Object.defineProperty(global.document, 'visibilityState', {
|
|
270
|
-
value: 'visible',
|
|
271
|
-
writable: true,
|
|
272
|
-
configurable: true,
|
|
273
|
-
})
|
|
274
|
-
|
|
275
|
-
visibilityHandler()
|
|
276
|
-
await vi.advanceTimersToNextTimerAsync()
|
|
277
|
-
|
|
278
|
-
// Without lastTokenRefresh, shouldRefreshToken returns true — this is the bug
|
|
279
|
-
expect(mockClient.observable.request).toHaveBeenCalled()
|
|
160
|
+
visibilityHandler()
|
|
161
|
+
await vi.advanceTimersByTimeAsync(100)
|
|
162
|
+
|
|
163
|
+
expect(mockClient.observable.request).not.toHaveBeenCalled()
|
|
164
|
+
const finalAuthState = state.get().authState
|
|
165
|
+
if (finalAuthState.type === AuthStateType.LOGGED_IN) {
|
|
166
|
+
expect(finalAuthState.token).toBe('sk-initial-token-st123')
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('refreshes on visibility change when lastTokenRefresh is stale', async () => {
|
|
171
|
+
const REFRESH_INTERVAL = 12 * 60 * 60 * 1000
|
|
172
|
+
const mockClient = {
|
|
173
|
+
observable: {request: vi.fn(() => of({token: 'sk-refreshed-token-st123'}))},
|
|
174
|
+
}
|
|
175
|
+
const mockClientFactory = vi.fn().mockReturnValue(mockClient)
|
|
176
|
+
const instance = createSanityInstance({
|
|
177
|
+
auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
|
|
280
178
|
})
|
|
179
|
+
const initialState = authStore.getInitialState(instance, null)
|
|
180
|
+
const staleTimestamp = Date.now() - REFRESH_INTERVAL - 1000
|
|
181
|
+
initialState.authState = {
|
|
182
|
+
type: AuthStateType.LOGGED_IN,
|
|
183
|
+
token: 'sk-initial-token-st123',
|
|
184
|
+
currentUser: null,
|
|
185
|
+
lastTokenRefresh: staleTimestamp,
|
|
186
|
+
}
|
|
187
|
+
const state = createStoreState(initialState)
|
|
281
188
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
const mockClient = {
|
|
295
|
-
observable: {request: vi.fn(() => of({token: 'sk-refreshed-token-st123'}))},
|
|
296
|
-
}
|
|
297
|
-
const mockClientFactory = vi.fn().mockReturnValue(mockClient)
|
|
298
|
-
const instance = createSanityInstance({
|
|
299
|
-
projectId: 'p',
|
|
300
|
-
dataset: 'd',
|
|
301
|
-
auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
|
|
302
|
-
})
|
|
303
|
-
const initialState = authStore.getInitialState(instance, null)
|
|
304
|
-
initialState.authState = {
|
|
305
|
-
type: AuthStateType.LOGGED_IN,
|
|
306
|
-
token: 'sk-initial-token-st123',
|
|
307
|
-
currentUser: null,
|
|
308
|
-
}
|
|
309
|
-
initialState.dashboardContext = {mode: 'test'}
|
|
310
|
-
const state = createStoreState(initialState)
|
|
311
|
-
|
|
312
|
-
const subscription = refreshStampedToken({state, instance, key: null})
|
|
313
|
-
subscriptions.push(subscription)
|
|
314
|
-
|
|
315
|
-
await vi.advanceTimersToNextTimerAsync()
|
|
316
|
-
|
|
317
|
-
// Verify that no refresh occurred
|
|
318
|
-
expect(mockClient.observable.request).not.toHaveBeenCalled()
|
|
319
|
-
const finalAuthState = state.get().authState
|
|
320
|
-
if (finalAuthState.type === AuthStateType.LOGGED_IN) {
|
|
321
|
-
expect(finalAuthState.token).toBe('sk-initial-token-st123')
|
|
322
|
-
}
|
|
189
|
+
const subscription = refreshStampedToken({state, instance, key: null})
|
|
190
|
+
subscriptions.push(subscription)
|
|
191
|
+
|
|
192
|
+
const addEventListenerMock = global.document.addEventListener as ReturnType<typeof vi.fn>
|
|
193
|
+
expect(addEventListenerMock).toHaveBeenCalledWith('visibilitychange', expect.any(Function))
|
|
194
|
+
const visibilityHandler = addEventListenerMock.mock.calls[0][1] as () => void
|
|
195
|
+
|
|
196
|
+
Object.defineProperty(global.document, 'visibilityState', {
|
|
197
|
+
value: 'visible',
|
|
198
|
+
writable: true,
|
|
199
|
+
configurable: true,
|
|
323
200
|
})
|
|
201
|
+
|
|
202
|
+
visibilityHandler()
|
|
203
|
+
await vi.advanceTimersToNextTimerAsync()
|
|
204
|
+
|
|
205
|
+
expect(mockClient.observable.request).toHaveBeenCalled()
|
|
206
|
+
const finalAuthState = state.get().authState
|
|
207
|
+
if (finalAuthState.type === AuthStateType.LOGGED_IN) {
|
|
208
|
+
expect(finalAuthState.token).toBe('sk-refreshed-token-st123')
|
|
209
|
+
expect(finalAuthState.lastTokenRefresh).toBeGreaterThan(staleTimestamp)
|
|
210
|
+
}
|
|
324
211
|
})
|
|
325
212
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
projectId: 'p',
|
|
334
|
-
dataset: 'd',
|
|
335
|
-
auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
|
|
336
|
-
})
|
|
337
|
-
const initialState = authStore.getInitialState(instance, null)
|
|
338
|
-
initialState.authState = {
|
|
339
|
-
type: AuthStateType.LOGGED_IN,
|
|
340
|
-
token: 'sk-initial-token-st123',
|
|
341
|
-
currentUser: null,
|
|
342
|
-
}
|
|
343
|
-
const state = createStoreState(initialState)
|
|
344
|
-
|
|
345
|
-
let subscription: Subscription | undefined
|
|
346
|
-
// We expect this NOT to throw, but accept we can't easily test the lock call or outcome
|
|
347
|
-
expect(() => {
|
|
348
|
-
subscription = refreshStampedToken({state, instance, key: null})
|
|
349
|
-
subscriptions.push(subscription!)
|
|
350
|
-
}).not.toThrow()
|
|
351
|
-
|
|
352
|
-
// DO NOT advance timers or yield here - focus on immediate observable logic
|
|
353
|
-
// We cannot reliably test that failingLocksRequest is called due to async/timer issues,
|
|
354
|
-
// but we *can* test the consequence of it resolving to false.
|
|
355
|
-
|
|
356
|
-
// VERIFY THE OUTCOME:
|
|
357
|
-
// Check client request was NOT made (because filter(hasLock => hasLock) receives false)
|
|
358
|
-
expect(mockClient.observable.request).not.toHaveBeenCalled()
|
|
359
|
-
// Check state remains unchanged
|
|
360
|
-
const finalAuthState = state.get().authState
|
|
361
|
-
expect(finalAuthState.type).toBe(AuthStateType.LOGGED_IN)
|
|
362
|
-
if (finalAuthState.type === AuthStateType.LOGGED_IN) {
|
|
363
|
-
expect(finalAuthState.token).toBe('sk-initial-token-st123')
|
|
364
|
-
}
|
|
213
|
+
it('refreshes on visibility change when lastTokenRefresh is undefined (pre-fix behavior)', async () => {
|
|
214
|
+
const mockClient = {
|
|
215
|
+
observable: {request: vi.fn(() => of({token: 'sk-refreshed-token-st123'}))},
|
|
216
|
+
}
|
|
217
|
+
const mockClientFactory = vi.fn().mockReturnValue(mockClient)
|
|
218
|
+
const instance = createSanityInstance({
|
|
219
|
+
auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
|
|
365
220
|
})
|
|
221
|
+
const initialState = authStore.getInitialState(instance, null)
|
|
222
|
+
initialState.authState = {
|
|
223
|
+
type: AuthStateType.LOGGED_IN,
|
|
224
|
+
token: 'sk-initial-token-st123',
|
|
225
|
+
currentUser: null,
|
|
226
|
+
// lastTokenRefresh intentionally omitted to demonstrate the old bug
|
|
227
|
+
}
|
|
228
|
+
const state = createStoreState(initialState)
|
|
229
|
+
|
|
230
|
+
const subscription = refreshStampedToken({state, instance, key: null})
|
|
231
|
+
subscriptions.push(subscription)
|
|
232
|
+
|
|
233
|
+
const addEventListenerMock = global.document.addEventListener as ReturnType<typeof vi.fn>
|
|
234
|
+
const visibilityHandler = addEventListenerMock.mock.calls[0][1] as () => void
|
|
366
235
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
// Use mockResolvedValue: the from(acquireTokenRefreshLock(...)) expects a Promise
|
|
372
|
-
const failingLocksRequest = vi.fn().mockResolvedValue(false)
|
|
373
|
-
Object.defineProperty(global, 'navigator', {
|
|
374
|
-
value: {locks: {...originalLocks, request: failingLocksRequest}},
|
|
375
|
-
writable: true,
|
|
376
|
-
})
|
|
377
|
-
|
|
378
|
-
try {
|
|
379
|
-
const mockClient = {
|
|
380
|
-
observable: {request: vi.fn(() => of({token: 'sk-refreshed-token-st123'}))},
|
|
381
|
-
}
|
|
382
|
-
const mockClientFactory = vi.fn().mockReturnValue(mockClient)
|
|
383
|
-
const instance = createSanityInstance({
|
|
384
|
-
projectId: 'p',
|
|
385
|
-
dataset: 'd',
|
|
386
|
-
auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
|
|
387
|
-
})
|
|
388
|
-
const initialState = authStore.getInitialState(instance, null)
|
|
389
|
-
initialState.authState = {
|
|
390
|
-
type: AuthStateType.LOGGED_IN,
|
|
391
|
-
token: 'sk-initial-token-st123',
|
|
392
|
-
currentUser: null,
|
|
393
|
-
}
|
|
394
|
-
const state = createStoreState(initialState)
|
|
395
|
-
|
|
396
|
-
const subscription = refreshStampedToken({state, instance, key: null})
|
|
397
|
-
subscriptions.push(subscription)
|
|
398
|
-
|
|
399
|
-
// DO NOT advance timers or yield here - focus on immediate observable logic
|
|
400
|
-
// We cannot reliably test that failingLocksRequest is called due to async/timer issues,
|
|
401
|
-
// but we *can* test the consequence of it resolving to false.
|
|
402
|
-
|
|
403
|
-
// VERIFY THE OUTCOME:
|
|
404
|
-
// Check client request was NOT made (because filter(hasLock => hasLock) receives false)
|
|
405
|
-
expect(mockClient.observable.request).not.toHaveBeenCalled()
|
|
406
|
-
// Check state remains unchanged
|
|
407
|
-
const finalAuthState = state.get().authState
|
|
408
|
-
expect(finalAuthState.type).toBe(AuthStateType.LOGGED_IN)
|
|
409
|
-
if (finalAuthState.type === AuthStateType.LOGGED_IN) {
|
|
410
|
-
expect(finalAuthState.token).toBe('sk-initial-token-st123')
|
|
411
|
-
}
|
|
412
|
-
} finally {
|
|
413
|
-
// Restore original navigator.locks
|
|
414
|
-
Object.defineProperty(global, 'navigator', {value: {locks: originalLocks}, writable: true})
|
|
415
|
-
}
|
|
236
|
+
Object.defineProperty(global.document, 'visibilityState', {
|
|
237
|
+
value: 'visible',
|
|
238
|
+
writable: true,
|
|
239
|
+
configurable: true,
|
|
416
240
|
})
|
|
241
|
+
|
|
242
|
+
visibilityHandler()
|
|
243
|
+
await vi.advanceTimersToNextTimerAsync()
|
|
244
|
+
|
|
245
|
+
// Without lastTokenRefresh, shouldRefreshToken returns true — this is the bug
|
|
246
|
+
expect(mockClient.observable.request).toHaveBeenCalled()
|
|
417
247
|
})
|
|
418
248
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
initialState.authState = {
|
|
440
|
-
type: AuthStateType.LOGGED_IN,
|
|
441
|
-
token: 'sk-initial-token-st123',
|
|
442
|
-
currentUser: null,
|
|
443
|
-
}
|
|
444
|
-
const state = createStoreState(initialState)
|
|
445
|
-
|
|
446
|
-
const subscription = refreshStampedToken({state, instance, key: null})
|
|
447
|
-
subscriptions.push(subscription)
|
|
448
|
-
|
|
449
|
-
// Advance timers to allow the async `performRefresh` to execute
|
|
450
|
-
await vi.advanceTimersToNextTimerAsync()
|
|
451
|
-
|
|
452
|
-
// Verify the refresh was performed and state was updated
|
|
453
|
-
expect(mockClient.observable.request).toHaveBeenCalled()
|
|
454
|
-
const finalAuthState = state.get().authState
|
|
455
|
-
expect(finalAuthState.type).toBe(AuthStateType.LOGGED_IN)
|
|
456
|
-
if (finalAuthState.type === AuthStateType.LOGGED_IN) {
|
|
457
|
-
expect(finalAuthState.token).toBe('sk-refreshed-immediately-st123')
|
|
458
|
-
}
|
|
459
|
-
// Verify token was set in storage
|
|
460
|
-
expect(mockStorage.setItem).toHaveBeenCalledWith(
|
|
461
|
-
initialState.options.storageKey,
|
|
462
|
-
JSON.stringify({token: 'sk-refreshed-immediately-st123'}),
|
|
463
|
-
)
|
|
464
|
-
} finally {
|
|
465
|
-
// Restore navigator.locks
|
|
466
|
-
Object.defineProperty(global.navigator, 'locks', {
|
|
467
|
-
value: originalLocks,
|
|
468
|
-
writable: true,
|
|
469
|
-
})
|
|
470
|
-
}
|
|
249
|
+
it('does not refresh when tab is not visible', async () => {
|
|
250
|
+
// Set visibility to hidden
|
|
251
|
+
Object.defineProperty(global, 'document', {
|
|
252
|
+
value: {
|
|
253
|
+
visibilityState: 'hidden',
|
|
254
|
+
addEventListener: vi.fn(),
|
|
255
|
+
removeEventListener: vi.fn(),
|
|
256
|
+
},
|
|
257
|
+
writable: true,
|
|
258
|
+
configurable: true,
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
const mockClient = {
|
|
262
|
+
observable: {request: vi.fn(() => of({token: 'sk-refreshed-token-st123'}))},
|
|
263
|
+
}
|
|
264
|
+
const mockClientFactory = vi.fn().mockReturnValue(mockClient)
|
|
265
|
+
const instance = createSanityInstance({
|
|
266
|
+
projectId: 'p',
|
|
267
|
+
dataset: 'd',
|
|
268
|
+
auth: {clientFactory: mockClientFactory, storageArea: mockStorage},
|
|
471
269
|
})
|
|
270
|
+
const initialState = authStore.getInitialState(instance, null)
|
|
271
|
+
initialState.authState = {
|
|
272
|
+
type: AuthStateType.LOGGED_IN,
|
|
273
|
+
token: 'sk-initial-token-st123',
|
|
274
|
+
currentUser: null,
|
|
275
|
+
}
|
|
276
|
+
const state = createStoreState(initialState)
|
|
277
|
+
|
|
278
|
+
const subscription = refreshStampedToken({state, instance, key: null})
|
|
279
|
+
subscriptions.push(subscription)
|
|
280
|
+
|
|
281
|
+
await vi.advanceTimersToNextTimerAsync()
|
|
282
|
+
|
|
283
|
+
// Verify that no refresh occurred
|
|
284
|
+
expect(mockClient.observable.request).not.toHaveBeenCalled()
|
|
285
|
+
const finalAuthState = state.get().authState
|
|
286
|
+
if (finalAuthState.type === AuthStateType.LOGGED_IN) {
|
|
287
|
+
expect(finalAuthState.token).toBe('sk-initial-token-st123')
|
|
288
|
+
}
|
|
472
289
|
})
|
|
473
290
|
|
|
474
|
-
// Restore other tests to their simpler form
|
|
475
291
|
it('sets an error state when token refresh fails', async () => {
|
|
476
292
|
const error = new Error('Refresh failed')
|
|
477
293
|
const mockClient = {observable: {request: vi.fn(() => throwError(() => error))}}
|
|
@@ -487,7 +303,6 @@ describe('refreshStampedToken', () => {
|
|
|
487
303
|
token: 'sk-initial-token-st123',
|
|
488
304
|
currentUser: null,
|
|
489
305
|
}
|
|
490
|
-
initialState.dashboardContext = {mode: 'test'}
|
|
491
306
|
const state = createStoreState(initialState)
|
|
492
307
|
|
|
493
308
|
const subscription = refreshStampedToken({state, instance, key: null})
|
|
@@ -523,8 +338,6 @@ describe('refreshStampedToken', () => {
|
|
|
523
338
|
await vi.advanceTimersByTimeAsync(0)
|
|
524
339
|
|
|
525
340
|
expect(mockClientFactory).not.toHaveBeenCalled()
|
|
526
|
-
const locksRequest = navigator.locks.request as ReturnType<typeof vi.fn>
|
|
527
|
-
expect(locksRequest).not.toHaveBeenCalled()
|
|
528
341
|
expect(state.get().authState.type).toBe(AuthStateType.LOGGED_OUT)
|
|
529
342
|
})
|
|
530
343
|
|
|
@@ -550,8 +363,6 @@ describe('refreshStampedToken', () => {
|
|
|
550
363
|
await vi.advanceTimersByTimeAsync(0)
|
|
551
364
|
|
|
552
365
|
expect(mockClient.observable.request).not.toHaveBeenCalled()
|
|
553
|
-
const locksRequest = navigator.locks.request as ReturnType<typeof vi.fn>
|
|
554
|
-
expect(locksRequest).not.toHaveBeenCalled()
|
|
555
366
|
// Add type guard before accessing token property
|
|
556
367
|
const finalAuthState = state.get().authState
|
|
557
368
|
expect(finalAuthState.type).toBe(AuthStateType.LOGGED_IN)
|
|
@@ -560,92 +371,3 @@ describe('refreshStampedToken', () => {
|
|
|
560
371
|
}
|
|
561
372
|
})
|
|
562
373
|
})
|
|
563
|
-
|
|
564
|
-
describe('time-based refresh helpers', () => {
|
|
565
|
-
let mockStorage: Storage
|
|
566
|
-
const storageKey = 'my-test-key'
|
|
567
|
-
|
|
568
|
-
beforeEach(() => {
|
|
569
|
-
mockStorage = {
|
|
570
|
-
getItem: vi.fn(),
|
|
571
|
-
setItem: vi.fn(),
|
|
572
|
-
removeItem: vi.fn(),
|
|
573
|
-
clear: vi.fn(),
|
|
574
|
-
key: vi.fn(),
|
|
575
|
-
length: 0,
|
|
576
|
-
}
|
|
577
|
-
vi.useFakeTimers()
|
|
578
|
-
})
|
|
579
|
-
|
|
580
|
-
afterEach(() => {
|
|
581
|
-
vi.useRealTimers()
|
|
582
|
-
})
|
|
583
|
-
|
|
584
|
-
describe('getLastRefreshTime', () => {
|
|
585
|
-
it('returns 0 if storage is undefined', () => {
|
|
586
|
-
expect(getLastRefreshTime(undefined, storageKey)).toBe(0)
|
|
587
|
-
})
|
|
588
|
-
|
|
589
|
-
it('returns 0 if item is not in storage', () => {
|
|
590
|
-
vi.spyOn(mockStorage, 'getItem').mockReturnValue(null)
|
|
591
|
-
expect(getLastRefreshTime(mockStorage, storageKey)).toBe(0)
|
|
592
|
-
expect(mockStorage.getItem).toHaveBeenCalledWith(`${storageKey}_last_refresh`)
|
|
593
|
-
})
|
|
594
|
-
|
|
595
|
-
it('returns the parsed timestamp from storage', () => {
|
|
596
|
-
const now = Date.now()
|
|
597
|
-
vi.spyOn(mockStorage, 'getItem').mockReturnValue(now.toString())
|
|
598
|
-
expect(getLastRefreshTime(mockStorage, storageKey)).toBe(now)
|
|
599
|
-
})
|
|
600
|
-
|
|
601
|
-
it('returns 0 if stored data is malformed', () => {
|
|
602
|
-
vi.spyOn(mockStorage, 'getItem').mockReturnValue('not a number')
|
|
603
|
-
expect(getLastRefreshTime(mockStorage, storageKey)).toBe(0)
|
|
604
|
-
})
|
|
605
|
-
|
|
606
|
-
it('returns 0 on storage access error', () => {
|
|
607
|
-
vi.spyOn(mockStorage, 'getItem').mockImplementation(() => {
|
|
608
|
-
throw new Error('Storage access failed')
|
|
609
|
-
})
|
|
610
|
-
expect(getLastRefreshTime(mockStorage, storageKey)).toBe(0)
|
|
611
|
-
})
|
|
612
|
-
})
|
|
613
|
-
|
|
614
|
-
describe('setLastRefreshTime', () => {
|
|
615
|
-
it('sets the current timestamp in storage', () => {
|
|
616
|
-
const now = Date.now()
|
|
617
|
-
setLastRefreshTime(mockStorage, storageKey)
|
|
618
|
-
expect(mockStorage.setItem).toHaveBeenCalledWith(`${storageKey}_last_refresh`, now.toString())
|
|
619
|
-
})
|
|
620
|
-
|
|
621
|
-
it('does not throw on storage access error', () => {
|
|
622
|
-
vi.spyOn(mockStorage, 'setItem').mockImplementation(() => {
|
|
623
|
-
throw new Error('Storage access failed')
|
|
624
|
-
})
|
|
625
|
-
expect(() => setLastRefreshTime(mockStorage, storageKey)).not.toThrow()
|
|
626
|
-
})
|
|
627
|
-
})
|
|
628
|
-
|
|
629
|
-
describe('getNextRefreshDelay', () => {
|
|
630
|
-
const REFRESH_INTERVAL = 12 * 60 * 60 * 1000
|
|
631
|
-
|
|
632
|
-
it('returns 0 if last refresh time is not available', () => {
|
|
633
|
-
vi.spyOn(mockStorage, 'getItem').mockReturnValue(null)
|
|
634
|
-
expect(getNextRefreshDelay(mockStorage, storageKey)).toBe(0)
|
|
635
|
-
})
|
|
636
|
-
|
|
637
|
-
it('returns the remaining time until the next refresh', () => {
|
|
638
|
-
const lastRefresh = Date.now() - 10000 // 10 seconds ago
|
|
639
|
-
vi.spyOn(mockStorage, 'getItem').mockReturnValue(lastRefresh.toString())
|
|
640
|
-
|
|
641
|
-
const delay = getNextRefreshDelay(mockStorage, storageKey)
|
|
642
|
-
expect(delay).toBeCloseTo(REFRESH_INTERVAL - 10000, -2)
|
|
643
|
-
})
|
|
644
|
-
|
|
645
|
-
it('returns 0 if the refresh interval has passed', () => {
|
|
646
|
-
const lastRefresh = Date.now() - REFRESH_INTERVAL - 5000 // 5 seconds past due
|
|
647
|
-
vi.spyOn(mockStorage, 'getItem').mockReturnValue(lastRefresh.toString())
|
|
648
|
-
expect(getNextRefreshDelay(mockStorage, storageKey)).toBe(0)
|
|
649
|
-
})
|
|
650
|
-
})
|
|
651
|
-
})
|