108 lines
3.5 KiB
JavaScript
108 lines
3.5 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import test from 'node:test'
|
|
import { coordinateCrossTabRefresh, CrossTabRefreshTimeoutError } from './crossTabRefresh.ts'
|
|
|
|
test('reuses tokens rotated by another browser tab while waiting for the refresh lock', async () => {
|
|
let currentRefreshToken = 'refresh-old'
|
|
let refreshCalls = 0
|
|
const originalNavigator = globalThis.navigator
|
|
Object.defineProperty(globalThis, 'navigator', {
|
|
configurable: true,
|
|
value: {
|
|
locks: {
|
|
async request(_name, _options, callback) {
|
|
currentRefreshToken = 'refresh-new'
|
|
return callback()
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
try {
|
|
const result = await coordinateCrossTabRefresh({
|
|
lockName: 'client-refresh',
|
|
observedRefreshToken: 'refresh-old',
|
|
currentRefreshToken: () => currentRefreshToken,
|
|
reuseCurrentSession: async () => 'access-new',
|
|
refresh: async () => {
|
|
refreshCalls += 1
|
|
return 'unexpected'
|
|
},
|
|
})
|
|
|
|
assert.equal(result, 'access-new')
|
|
assert.equal(refreshCalls, 0)
|
|
} finally {
|
|
Object.defineProperty(globalThis, 'navigator', { configurable: true, value: originalNavigator })
|
|
}
|
|
})
|
|
|
|
test('refreshes normally when this tab owns the current refresh token', async () => {
|
|
const originalNavigator = globalThis.navigator
|
|
Object.defineProperty(globalThis, 'navigator', {
|
|
configurable: true,
|
|
value: { locks: { request: async (_name, _options, callback) => callback() } },
|
|
})
|
|
|
|
try {
|
|
const result = await coordinateCrossTabRefresh({
|
|
lockName: 'client-refresh',
|
|
observedRefreshToken: 'refresh-old',
|
|
currentRefreshToken: () => 'refresh-old',
|
|
reuseCurrentSession: async () => 'unexpected',
|
|
refresh: async (refreshToken) => `access-for-${refreshToken}`,
|
|
})
|
|
|
|
assert.equal(result, 'access-for-refresh-old')
|
|
} finally {
|
|
Object.defineProperty(globalThis, 'navigator', { configurable: true, value: originalNavigator })
|
|
}
|
|
})
|
|
|
|
test('does not wait for rotation after the shared session was removed', async () => {
|
|
const originalNavigator = globalThis.navigator
|
|
const refreshError = new Error('session removed')
|
|
Object.defineProperty(globalThis, 'navigator', { configurable: true, value: {} })
|
|
|
|
try {
|
|
await assert.rejects(coordinateCrossTabRefresh({
|
|
lockName: 'client-refresh',
|
|
observedRefreshToken: 'refresh-old',
|
|
currentRefreshToken: () => '',
|
|
reuseCurrentSession: async () => 'unexpected',
|
|
refresh: async () => { throw refreshError },
|
|
}), refreshError)
|
|
} finally {
|
|
Object.defineProperty(globalThis, 'navigator', { configurable: true, value: originalNavigator })
|
|
}
|
|
})
|
|
|
|
test('aborts a refresh lock that cannot be acquired', async () => {
|
|
const originalNavigator = globalThis.navigator
|
|
Object.defineProperty(globalThis, 'navigator', {
|
|
configurable: true,
|
|
value: {
|
|
locks: {
|
|
request(_name, options) {
|
|
return new Promise((_resolve, reject) => {
|
|
options.signal.addEventListener('abort', () => reject(options.signal.reason), { once: true })
|
|
})
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
try {
|
|
await assert.rejects(coordinateCrossTabRefresh({
|
|
lockName: 'client-refresh',
|
|
lockTimeoutMs: 5,
|
|
observedRefreshToken: 'refresh-old',
|
|
currentRefreshToken: () => 'refresh-old',
|
|
reuseCurrentSession: async () => 'unexpected',
|
|
refresh: async () => 'unexpected',
|
|
}), CrossTabRefreshTimeoutError)
|
|
} finally {
|
|
Object.defineProperty(globalThis, 'navigator', { configurable: true, value: originalNavigator })
|
|
}
|
|
})
|