fix
This commit is contained in:
+90
-8
@@ -48,6 +48,7 @@ async function fixture(t: TestContext, secure = true) {
|
||||
upload: null as { filename: string; data: Buffer } | null,
|
||||
bindings: [] as Record<string, unknown>[],
|
||||
unbindError: 0,
|
||||
unbindResponse: null as ((signal?: AbortSignal) => Promise<Response>) | null,
|
||||
createScreenError: 0,
|
||||
offline: false,
|
||||
server: 'https://server.test',
|
||||
@@ -62,6 +63,7 @@ async function fixture(t: TestContext, secure = true) {
|
||||
accessExpired: false,
|
||||
refreshed: false,
|
||||
refreshCalls: 0,
|
||||
refreshResponse: null as (() => Promise<Response>) | null,
|
||||
advanceCalls: 0,
|
||||
resultError: false,
|
||||
title: 'Дело',
|
||||
@@ -110,6 +112,7 @@ async function fixture(t: TestContext, secure = true) {
|
||||
return json({ accessToken: 'ACCESS-SECRET', refreshToken: 'REFRESH-SECRET' })
|
||||
if (path === '/api/auth/refresh') {
|
||||
model.refreshCalls++
|
||||
if (model.refreshResponse) return model.refreshResponse()
|
||||
await new Promise((resolve) => setTimeout(resolve, 5))
|
||||
if (model.refreshExpired) return json({}, 401)
|
||||
model.refreshed = true
|
||||
@@ -121,6 +124,7 @@ async function fixture(t: TestContext, secure = true) {
|
||||
: json({ screenID: '12', accessToken: 'SCREEN-SECRET' })
|
||||
if (path.endsWith('/unbind')) {
|
||||
assert.equal(auth, 'Bearer ACCESS-SECRET')
|
||||
if (model.unbindResponse) return model.unbindResponse(init?.signal ?? undefined)
|
||||
return json({}, model.unbindError || 200)
|
||||
}
|
||||
if (path === '/api/screens/state') {
|
||||
@@ -607,6 +611,44 @@ for (const gameId of [7, 8]) {
|
||||
})
|
||||
}
|
||||
|
||||
test('отмена подготовки не ждёт общий refresh и не отменяет обновление для другого запроса', async (t) => {
|
||||
const f = await fixture(t)
|
||||
await f.prepare()
|
||||
f.model.accessExpired = true
|
||||
let started!: () => void
|
||||
let release!: () => void
|
||||
const refreshStarted = new Promise<void>((resolve) => { started = resolve })
|
||||
const refreshReleased = new Promise<void>((resolve) => { release = resolve })
|
||||
f.model.refreshResponse = async () => {
|
||||
started()
|
||||
await refreshReleased
|
||||
f.model.refreshed = true
|
||||
return Response.json({ accessToken: 'ACCESS-ROTATED', refreshToken: 'REFRESH-ROTATED' })
|
||||
}
|
||||
const cancelled = f.online.act({ action: 'check', gameId: 7 }).then(
|
||||
() => 'completed',
|
||||
(error: Error) => { assert.match(error.message, /отмен|ABORTED/); return 'cancelled' },
|
||||
)
|
||||
await refreshStarted
|
||||
const background = f.online.refreshPlan()
|
||||
await new Promise<void>((resolve) => setImmediate(resolve))
|
||||
await f.online.act({ action: 'cancel' })
|
||||
try {
|
||||
assert.equal(await Promise.race([
|
||||
cancelled,
|
||||
new Promise<string>((resolve) => { const timer = setTimeout(() => resolve('still waiting'), 1000); timer.unref() }),
|
||||
]), 'cancelled')
|
||||
assert.equal(f.model.refreshed, false)
|
||||
} finally {
|
||||
release()
|
||||
await background
|
||||
await cancelled
|
||||
}
|
||||
assert.equal(f.model.refreshCalls, 1)
|
||||
assert.equal(f.online.snapshot().authenticated, true)
|
||||
assert.equal(f.online.planStatus().notice, '')
|
||||
})
|
||||
|
||||
for (const status of [200, 404]) {
|
||||
test(`отключение отвязывает экран, сохраняет игру и переживает перезапуск: ${status}`, async (t) => {
|
||||
const f = await fixture(t)
|
||||
@@ -634,23 +676,63 @@ for (const status of [200, 404]) {
|
||||
}
|
||||
|
||||
for (const failure of ['server', 'network'] as const) {
|
||||
test(`ошибка отключения сохраняет сессию для повтора: ${failure}`, async (t) => {
|
||||
test(`ошибка отключения прекращает локальную сессию и не восстанавливает её: ${failure}`, async (t) => {
|
||||
const f = await fixture(t)
|
||||
await f.prepare()
|
||||
const saved = await readFile(join(f.directory, 'connection', 'credentials.bin'))
|
||||
f.model.unbindError = failure === 'server' ? 500 : 0
|
||||
f.model.offline = failure === 'network'
|
||||
await assert.rejects(f.online.act({ action: 'disconnect' }))
|
||||
assert.equal(f.online.snapshot().screenId, '12')
|
||||
assert.equal(f.online.snapshot().connection, 'waiting')
|
||||
assert.deepEqual(await readFile(join(f.directory, 'connection', 'credentials.bin')), saved)
|
||||
f.model.unbindError = 0
|
||||
f.model.offline = false
|
||||
await f.online.act({ action: 'disconnect' })
|
||||
assert.equal(f.online.snapshot().screenId, null)
|
||||
assert.equal(f.online.snapshot().connection, 'disabled')
|
||||
assert.match(f.online.snapshot().notice, /отключён локально/)
|
||||
const saved = JSON.parse(f.secrets.decrypt(await readFile(join(f.directory, 'connection', 'credentials.bin'))))
|
||||
assert.equal(saved.screen, null)
|
||||
f.model.unbindError = 0
|
||||
f.model.offline = false
|
||||
const count = f.requests.length
|
||||
await f.online.poll()
|
||||
assert.equal(f.requests.length, count)
|
||||
assert.equal(f.online.snapshot().screenId, null)
|
||||
const restored = new Online(join(f.directory, 'connection'), f.store, new Controller(() => {}), f.secrets, f.fetchAPI)
|
||||
t.after(() => restored.close())
|
||||
await restored.initialize()
|
||||
assert.equal(restored.snapshot().screenId, null)
|
||||
})
|
||||
}
|
||||
|
||||
test('локальное отключение сохраняется до ответа сервера, отвязка ограничена пятью секундами', async (t) => {
|
||||
const f = await fixture(t)
|
||||
await f.prepare()
|
||||
const timeout = AbortSignal.timeout
|
||||
let bounded = false
|
||||
t.mock.method(AbortSignal, 'timeout', (milliseconds: number) => {
|
||||
if (milliseconds === 5000) bounded = true
|
||||
return timeout(milliseconds === 5000 ? 20 : milliseconds)
|
||||
})
|
||||
let started!: () => void
|
||||
const requested = new Promise<void>((resolve) => { started = resolve })
|
||||
f.model.unbindResponse = async (signal) => {
|
||||
started()
|
||||
return new Promise((_resolve, reject) => {
|
||||
signal!.addEventListener('abort', () => reject(signal!.reason), { once: true })
|
||||
})
|
||||
}
|
||||
const disconnect = f.online.act({ action: 'disconnect' })
|
||||
await requested
|
||||
assert.equal(f.online.snapshot().screenId, null)
|
||||
const saved = JSON.parse(f.secrets.decrypt(await readFile(join(f.directory, 'connection', 'credentials.bin'))))
|
||||
assert.equal(saved.screen, null)
|
||||
// AbortSignal timers are unref'ed; keep the test alive until the simulated timeout.
|
||||
const keepAlive = setTimeout(() => {}, 1000)
|
||||
try {
|
||||
await disconnect
|
||||
} finally {
|
||||
clearTimeout(keepAlive)
|
||||
}
|
||||
assert.equal(bounded, true)
|
||||
assert.match(f.online.snapshot().notice, /не подтвердил отвязку/)
|
||||
})
|
||||
|
||||
test('ошибка отвязки сохраняет прежнюю сессию и не создаёт новую', async (t) => {
|
||||
const f = await fixture(t)
|
||||
await f.prepare()
|
||||
|
||||
Reference in New Issue
Block a user