d3ro-voice/apps/mobile-rn/__tests__/account-local-data.test.ts
2026-08-29 18:33:45 +09:00

97 lines
3.5 KiB
TypeScript

const mockClearHistory = jest.fn()
const mockClearEntitlements = jest.fn()
const mockClearPreferences = jest.fn()
const mockCancelRecorder = jest.fn()
const mockClearQueuedAudio = jest.fn()
const mockClearActions = jest.fn()
const mockClearGenerationKeys = jest.fn()
const mockDeletePush = jest.fn()
jest.mock('../src/features/history/history-cache', () => ({
clearAllHistoryCaches: () => mockClearHistory(),
}))
jest.mock('../src/lib/entitlement-context', () => ({
clearAllEntitlementCaches: () => mockClearEntitlements(),
}))
jest.mock('../src/lib/preferences-context', () => ({
clearAllUserPreferenceCaches: () => mockClearPreferences(),
}))
jest.mock('../src/lib/audio-recorder', () => ({
audioRecorder: { cancel: () => mockCancelRecorder() },
}))
jest.mock('../src/features/recording/durable-processing-queue', () => ({
clearAllQueuedAudio: () => mockClearQueuedAudio(),
}))
jest.mock('../src/features/actions/action-service', () => ({
clearAllActionHistories: () => mockClearActions(),
}))
jest.mock('../src/features/templates', () => ({
clearEveryGenerationIdempotencyKey: () => mockClearGenerationKeys(),
}))
jest.mock('../src/features/notifications/notification-native', () => ({
deleteNativePushRegistration: () => mockDeletePush(),
}))
import {
AccountLocalDataPurgeError,
purgeAllAccountLocalData,
} from '../src/lib/account-local-data'
const operations = [
mockClearHistory,
mockClearEntitlements,
mockClearPreferences,
mockCancelRecorder,
mockClearQueuedAudio,
mockClearActions,
mockClearGenerationKeys,
mockDeletePush,
]
describe('account local privacy purge', () => {
beforeEach(() => {
for (const operation of operations) operation.mockReset().mockResolvedValue(undefined)
})
it('awaits every account-scoped cleanup operation', async () => {
await expect(purgeAllAccountLocalData()).resolves.toBeUndefined()
for (const operation of operations) expect(operation).toHaveBeenCalledTimes(1)
})
it('finishes the remaining cleanup work and returns a stable failure', async () => {
let finishQueueCleanup: (() => void) | null = null
mockClearHistory.mockRejectedValue(new Error('private filesystem detail'))
mockClearQueuedAudio.mockReturnValue(new Promise<void>((resolve) => {
finishQueueCleanup = resolve
}))
const purge = purgeAllAccountLocalData()
let settled = false
void purge.finally(() => { settled = true }).catch(() => undefined)
await Promise.resolve()
expect(settled).toBe(false)
finishQueueCleanup?.()
await expect(purge).rejects.toBeInstanceOf(AccountLocalDataPurgeError)
await expect(purge).rejects.not.toThrow('private filesystem detail')
for (const operation of operations) expect(operation).toHaveBeenCalledTimes(1)
})
it('single-flights concurrent privacy boundaries and allows a later retry', async () => {
let finishQueueCleanup: (() => void) | null = null
mockClearQueuedAudio.mockReturnValueOnce(new Promise<void>((resolve) => {
finishQueueCleanup = resolve
}))
const first = purgeAllAccountLocalData()
const concurrent = purgeAllAccountLocalData()
expect(concurrent).toBe(first)
for (const operation of operations) expect(operation).toHaveBeenCalledTimes(1)
finishQueueCleanup?.()
await expect(Promise.all([first, concurrent])).resolves.toEqual([undefined, undefined])
await expect(purgeAllAccountLocalData()).resolves.toBeUndefined()
for (const operation of operations) expect(operation).toHaveBeenCalledTimes(2)
})
})