import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { TtlCache, accessibleTasksCache, accessibleTasksKey, invalidateAccessibleTasksForUser, invalidateAccessibleTasksForOrg, } from '../server/utils/cache'; describe('TtlCache', () => { beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); it('возвращает значение в пределах TTL и undefined после истечения', () => { const cache = new TtlCache(30); cache.set('k', new Set([1, 2, 3])); expect(cache.get>('k')?.has(2)).toBe(true); vi.advanceTimersByTime(29_000); expect(cache.get('k')).toBeDefined(); vi.advanceTimersByTime(2_000); expect(cache.get('k')).toBeUndefined(); }); it('кэширует null как значение (отличая от промаха undefined)', () => { const cache = new TtlCache(30); cache.set('admin', null); expect(cache.get('admin')).toBeNull(); expect(cache.get('missing')).toBeUndefined(); }); it('invalidatePrefix удаляет ключи по префиксу', () => { const cache = new TtlCache(30); cache.set('tasks:1:minimal:a', 1); cache.set('tasks:1:minimal:b', 2); cache.set('tasks:2:minimal:a', 3); cache.invalidatePrefix('tasks:1:minimal:'); expect(cache.get('tasks:1:minimal:a')).toBeUndefined(); expect(cache.get('tasks:1:minimal:b')).toBeUndefined(); expect(cache.get('tasks:2:minimal:a')).toBe(3); }); it('invalidateSuffix удаляет ключи по суффиксу', () => { const cache = new TtlCache(30); cache.set('accessible:5:1', 'a'); cache.set('accessible:7:1', 'b'); cache.set('accessible:5:2', 'c'); cache.invalidateSuffix(':1'); expect(cache.get('accessible:5:1')).toBeUndefined(); expect(cache.get('accessible:7:1')).toBeUndefined(); expect(cache.get('accessible:5:2')).toBe('c'); }); it('вытесняет самый старый ключ при переполнении maxSize', () => { const cache = new TtlCache(30, 2); cache.set('a', 1); cache.set('b', 2); cache.set('c', 3); expect(cache.get('a')).toBeUndefined(); expect(cache.get('b')).toBe(2); expect(cache.get('c')).toBe(3); }); }); describe('accessibleTasksCache helpers', () => { beforeEach(() => { accessibleTasksCache.clear(); }); it('ключ имеет формат accessible::', () => { expect(accessibleTasksKey(12, 3)).toBe('accessible:12:3'); }); it('точечная инвалидация сбрасывает только одного пользователя', () => { accessibleTasksCache.set(accessibleTasksKey(1, 1), new Set([10])); accessibleTasksCache.set(accessibleTasksKey(2, 1), new Set([20])); invalidateAccessibleTasksForUser(1, 1); expect(accessibleTasksCache.get(accessibleTasksKey(1, 1))).toBeUndefined(); expect(accessibleTasksCache.get>(accessibleTasksKey(2, 1))?.has(20)).toBe(true); }); it('инвалидация по организации сбрасывает всех её пользователей, не трогая другие организации', () => { accessibleTasksCache.set(accessibleTasksKey(1, 1), new Set([10])); accessibleTasksCache.set(accessibleTasksKey(2, 1), null); accessibleTasksCache.set(accessibleTasksKey(1, 2), new Set([30])); invalidateAccessibleTasksForOrg(1); expect(accessibleTasksCache.get(accessibleTasksKey(1, 1))).toBeUndefined(); expect(accessibleTasksCache.get(accessibleTasksKey(2, 1))).toBeUndefined(); expect(accessibleTasksCache.get>(accessibleTasksKey(1, 2))?.has(30)).toBe(true); }); it('null (admin/view_all) кэшируется и корректно читается', () => { accessibleTasksCache.set(accessibleTasksKey(9, 1), null); expect(accessibleTasksCache.get(accessibleTasksKey(9, 1))).toBeNull(); expect(accessibleTasksCache.get(accessibleTasksKey(9, 2))).toBeUndefined(); }); });