Files
iistwin/tests/accessible-tasks-cache.test.ts
Ильяс Султанов 23c49adcd1 perf(access): TTL-кэш getAccessibleTaskIds с точечной инвалидацией
Шаг 0.12 плана production-готовности:
- accessibleTasksCache (45с) per userId:orgId внутри storage-метода
- инвалидация в storage-слое: роли, делегирования, form access,
  assignees, createTask/updateTask, appRole пользователя
- 9 новых юнит-тестов (65/65 зелёные)
2026-09-07 22:27:33 +03:00

104 lines
4.0 KiB
TypeScript

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<Set<number>>('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:<userId>:<organizationId>', () => {
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<Set<number>>(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<Set<number>>(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();
});
});