110 lines
3.8 KiB
TypeScript
110 lines
3.8 KiB
TypeScript
/**
|
|
* Mutation queue — server-freshness vs response arrival order (same tab).
|
|
*/
|
|
import assert from 'node:assert/strict'
|
|
import test from 'node:test'
|
|
import { createPinia, setActivePinia } from 'pinia'
|
|
import { createMeFixture } from './fixtures/me.fixture'
|
|
import { installFetchMock, resetAppTestState } from './helpers/fetchMock'
|
|
|
|
test.beforeEach(async () => {
|
|
installFetchMock()
|
|
await resetAppTestState()
|
|
setActivePinia(createPinia())
|
|
const { resetUserStores } = await import('../src/api/sync')
|
|
resetUserStores()
|
|
})
|
|
|
|
test('uncoordinated late applyMe simulates stale server overwrite regression', async () => {
|
|
const { applyMeResponse } = await import('../src/api/sync')
|
|
const { useUserStore } = await import('../src/stores/user')
|
|
|
|
applyMeResponse(createMeFixture({ coins: 10 }))
|
|
const newer = createMeFixture({ coins: 200 })
|
|
const older = createMeFixture({ coins: 50 })
|
|
|
|
applyMeResponse(newer)
|
|
applyMeResponse(older)
|
|
assert.equal(useUserStore().coins, 50, 'older server snapshot must not win when applied last')
|
|
})
|
|
|
|
test('serial queue: slower older server response then newer mutation keeps final newer state', async () => {
|
|
const meOlder = createMeFixture({ coins: 50 })
|
|
const meNewer = createMeFixture({ coins: 200 })
|
|
|
|
const { applyMeResponse, runMeMutation } = await import('../src/api/sync')
|
|
const { useUserStore } = await import('../src/stores/user')
|
|
const { api } = await import('../src/api/client')
|
|
|
|
applyMeResponse(createMeFixture({ coins: 10 }))
|
|
|
|
let call = 0
|
|
globalThis.routeHandlers!.set('/store/purchases', () => {
|
|
call += 1
|
|
if (call === 1) {
|
|
return { status: 200, body: meOlder, delayMs: 35 }
|
|
}
|
|
return { status: 200, body: meNewer }
|
|
})
|
|
|
|
const first = runMeMutation(() => api.purchaseProduct('castle'))
|
|
const second = runMeMutation(() => api.purchaseProduct('suit'))
|
|
await Promise.all([first, second])
|
|
|
|
assert.equal(useUserStore().coins, 200)
|
|
})
|
|
|
|
test('failed mutation does not block the next mutation in queue', async () => {
|
|
const meOk = createMeFixture({ coins: 180 })
|
|
const { applyMeResponse, runMeMutation } = await import('../src/api/sync')
|
|
const { useUserStore } = await import('../src/stores/user')
|
|
const { api } = await import('../src/api/client')
|
|
const { ApiClientError } = await import('../src/api/errors')
|
|
|
|
applyMeResponse(createMeFixture({ coins: 10 }))
|
|
|
|
let call = 0
|
|
globalThis.routeHandlers!.set('/store/purchases', () => {
|
|
call += 1
|
|
if (call === 1) {
|
|
return { status: 400, body: { error: 'سکه کافی نداری!' } }
|
|
}
|
|
return { status: 200, body: meOk }
|
|
})
|
|
|
|
await assert.rejects(runMeMutation(() => api.purchaseProduct('castle')), ApiClientError)
|
|
await runMeMutation(() => api.purchaseProduct('suit'))
|
|
assert.equal(useUserStore().coins, 180)
|
|
})
|
|
|
|
test('logout prevents queued mutation from applying or sending after session change', async () => {
|
|
const meStale = createMeFixture({ coins: 999 })
|
|
const meCurrent = createMeFixture({ coins: 40 })
|
|
|
|
const { applyMeResponse, runMeMutation } = await import('../src/api/sync')
|
|
const { invalidateSession } = await import('../src/api/authSession')
|
|
const { useUserStore } = await import('../src/stores/user')
|
|
const { api } = await import('../src/api/client')
|
|
const { StaleSessionError } = await import('../src/api/errors')
|
|
|
|
applyMeResponse(createMeFixture({ coins: 10 }))
|
|
|
|
globalThis.routeHandlers!.set('/store/purchases', () => ({
|
|
status: 200,
|
|
body: meStale,
|
|
delayMs: 40,
|
|
}))
|
|
|
|
const pending = runMeMutation(() => api.purchaseProduct('castle'))
|
|
invalidateSession()
|
|
applyMeResponse(meCurrent)
|
|
|
|
await assert.rejects(pending, StaleSessionError)
|
|
assert.equal(useUserStore().coins, 40)
|
|
})
|
|
|
|
declare global {
|
|
// eslint-disable-next-line no-var
|
|
var routeHandlers: Map<string, (init?: RequestInit) => import('./helpers/fetchMock').MockResponse | Promise<import('./helpers/fetchMock').MockResponse>>
|
|
}
|