fix(auth): keep cleared compliance state isolated from pending requests

This commit is contained in:
chihao.ou
2026-09-30 19:11:19 +08:00
parent 42bc7f6cff
commit 21d90fb5e5
2 changed files with 91 additions and 2 deletions
@@ -0,0 +1,83 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createPinia, setActivePinia } from 'pinia'
import type { AdminComplianceStatus } from '@/api/admin/compliance'
import { useAdminComplianceStore } from '../adminCompliance'
const api = vi.hoisted(() => ({ getStatus: vi.fn(), accept: vi.fn() }))
vi.mock('@/api/admin/compliance', () => ({ default: api }))
vi.mock('@/i18n', () => ({ getLocale: () => 'en' }))
function status(required: boolean, version = 'current'): AdminComplianceStatus {
return {
required, version, document_path_zh: '', document_path_en: '',
document_url_zh: '', document_url_en: '', ack_phrase_zh: '测试', ack_phrase_en: 'test'
}
}
function deferred() {
let resolve!: (value: AdminComplianceStatus) => void
let reject!: (error: Error) => void
const promise = new Promise<AdminComplianceStatus>((yes, no) => { resolve = yes; reject = no })
return { promise, resolve, reject }
}
beforeEach(() => {
setActivePinia(createPinia())
vi.resetAllMocks()
})
describe.each(['fetch', 'accept'] as const)('compliance %s across logout', (action) => {
const mock = () => action === 'fetch' ? api.getStatus : api.accept
const run = (store: ReturnType<typeof useAdminComplianceStore>) =>
action === 'fetch' ? store.fetchStatus() : store.accept('test')
const busy = (store: ReturnType<typeof useAdminComplianceStore>) =>
action === 'fetch' ? store.loading : store.submitting
it('does not repopulate cleared state when an old request succeeds', async () => {
const pending = deferred()
mock().mockReturnValueOnce(pending.promise)
const store = useAdminComplianceStore()
const request = run(store)
store.reset()
const response = status(true, 'old')
pending.resolve(response)
expect(await request).toEqual(response)
expect(store.status).toBeNull()
expect(store.initialized).toBe(false)
expect(store.shouldShow).toBe(false)
expect(busy(store)).toBe(false)
})
it.each(['resolve', 'reject'] as const)('keeps a new session pending when the old request %s', async (outcome) => {
const old = deferred()
const current = deferred()
mock().mockReturnValueOnce(old.promise).mockReturnValueOnce(current.promise)
const store = useAdminComplianceStore()
const first = run(store).catch(error => error)
store.reset()
const second = run(store)
if (outcome === 'resolve') old.resolve(status(false, 'old'))
else old.reject(new Error('old failure'))
await first
expect(busy(store)).toBe(true)
expect(store.status).toBeNull()
current.resolve(status(true))
await second
expect(busy(store)).toBe(false)
expect(store.status).toEqual(status(true))
expect(store.shouldShow).toBe(true)
})
})
it('still updates compliance status during a normal fetch and acceptance', async () => {
api.getStatus.mockResolvedValue(status(true))
api.accept.mockResolvedValue(status(false))
const store = useAdminComplianceStore()
await store.fetchStatus()
expect(store.initialized).toBe(true)
expect(store.shouldShow).toBe(true)
await store.accept('test')
expect(api.accept).toHaveBeenCalledWith({ phrase: 'test', language: 'en' })
expect(store.status).toEqual(status(false))
expect(store.shouldShow).toBe(false)
})
+8 -2
View File
@@ -12,6 +12,7 @@ export const useAdminComplianceStore = defineStore('adminCompliance', () => {
const submitting = ref(false)
const initialized = ref(false)
const forceVisible = ref(false)
let stateVersion = 0
const required = computed(() => status.value?.required === true)
const shouldShow = computed(() => required.value || forceVisible.value)
@@ -24,30 +25,34 @@ export const useAdminComplianceStore = defineStore('adminCompliance', () => {
})
async function fetchStatus(): Promise<AdminComplianceStatus> {
const version = stateVersion
loading.value = true
try {
const nextStatus = await adminComplianceAPI.getStatus()
if (version !== stateVersion) return nextStatus
status.value = nextStatus
initialized.value = true
forceVisible.value = nextStatus.required
return nextStatus
} finally {
loading.value = false
if (version === stateVersion) loading.value = false
}
}
async function accept(phrase: string): Promise<AdminComplianceStatus> {
const version = stateVersion
submitting.value = true
try {
const nextStatus = await adminComplianceAPI.accept({
phrase,
language: currentLocale.value
})
if (version !== stateVersion) return nextStatus
status.value = nextStatus
forceVisible.value = nextStatus.required
return nextStatus
} finally {
submitting.value = false
if (version === stateVersion) submitting.value = false
}
}
@@ -68,6 +73,7 @@ export const useAdminComplianceStore = defineStore('adminCompliance', () => {
}
function reset(): void {
stateVersion++
status.value = null
loading.value = false
submitting.value = false