buster/packages/access-controls/tests/integration/chats-cached.int.test.ts

187 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { canUserAccessChat } from '../../src/chats';
import {
canUserAccessChatCached,
clearCache,
getCacheStats,
resetCacheStats,
} from '../../src/chats-cached';
// We'll use real function but spy on it to count calls
vi.mock('../../src/chats', async () => {
const actual = await vi.importActual<typeof import('../../src/chats')>('../../src/chats');
return {
canUserAccessChat: vi.fn(actual.canUserAccessChat),
};
});
describe('canUserAccessChatCached Integration Tests', () => {
let spiedCanUserAccessChat: any;
beforeEach(async () => {
vi.clearAllMocks();
clearCache();
resetCacheStats();
const chatsModule = await import('../../src/chats');
spiedCanUserAccessChat = vi.mocked(chatsModule.canUserAccessChat);
});
afterEach(() => {
vi.useRealTimers();
});
test('should cache results and reduce database calls', async () => {
// Use non-existent IDs so we get consistent false results
const userId = '00000000-0000-0000-0000-000000000001';
const chatId = '00000000-0000-0000-0000-000000000002';
// First call - should hit database
const result1 = await canUserAccessChatCached({ userId, chatId });
expect(result1).toBe(false);
expect(spiedCanUserAccessChat).toHaveBeenCalledTimes(1);
// Make 10 more calls - all should hit cache
for (let i = 0; i < 10; i++) {
const result = await canUserAccessChatCached({ userId, chatId });
expect(result).toBe(false);
}
// Should still only have called database once
expect(spiedCanUserAccessChat).toHaveBeenCalledTimes(1);
// Check stats
const stats = getCacheStats();
expect(stats.hits).toBe(10);
expect(stats.misses).toBe(1);
expect(stats.hitRate).toBe('90.91%');
});
test('should handle multiple concurrent requests efficiently', async () => {
const userId = '00000000-0000-0000-0000-000000000003';
const chatId = '00000000-0000-0000-0000-000000000004';
// Make 5 concurrent requests
const promises = Array(5)
.fill(null)
.map(() => canUserAccessChatCached({ userId, chatId }));
const results = await Promise.all(promises);
// All should return false
for (const result of results) {
expect(result).toBe(false);
}
// Should only call database once even with concurrent requests
expect(spiedCanUserAccessChat).toHaveBeenCalledTimes(1);
// Stats might vary due to race conditions, but we should have some hits
const stats = getCacheStats();
expect(stats.total).toBe(5);
expect(stats.misses).toBeGreaterThanOrEqual(1);
expect(stats.hits).toBeLessThanOrEqual(4);
});
test('should simulate high-frequency burst scenario', async () => {
// Simulate 100 requests across 3 users and 3 chats
const users = [
'10000000-0000-0000-0000-000000000001',
'20000000-0000-0000-0000-000000000002',
'30000000-0000-0000-0000-000000000003',
];
const chats = [
'40000000-0000-0000-0000-000000000001',
'50000000-0000-0000-0000-000000000002',
'60000000-0000-0000-0000-000000000003',
];
const startTime = Date.now();
let totalRequests = 0;
// Simulate requests
for (let i = 0; i < 100; i++) {
const userId = users[i % users.length];
const chatId = chats[Math.floor(i / 33) % chats.length];
await canUserAccessChatCached({ userId, chatId });
totalRequests++;
}
const duration = Date.now() - startTime;
// Should complete quickly due to caching
expect(duration).toBeLessThan(500); // 500ms for 100 requests
// Check cache effectiveness
const stats = getCacheStats();
expect(stats.total).toBe(100);
// Should have at most 9 unique combinations (3 users × 3 chats)
expect(spiedCanUserAccessChat.mock.calls.length).toBeLessThanOrEqual(9);
// Cache hit rate should be high
const hitRate = Number.parseFloat(stats.hitRate);
expect(hitRate).toBeGreaterThan(80); // At least 80% hit rate
});
test('should expire cache entries after TTL', async () => {
// This test verifies TTL behavior by using fake timers
vi.useFakeTimers();
const userId = '00000000-0000-0000-0000-000000000005';
const chatId = '00000000-0000-0000-0000-000000000006';
// Initial call
await canUserAccessChatCached({ userId, chatId });
expect(spiedCanUserAccessChat).toHaveBeenCalledTimes(1);
// Call again immediately - should use cache
await canUserAccessChatCached({ userId, chatId });
expect(spiedCanUserAccessChat).toHaveBeenCalledTimes(1);
// Advance time by 20 seconds - should still use cache
vi.advanceTimersByTime(20 * 1000);
await canUserAccessChatCached({ userId, chatId });
expect(spiedCanUserAccessChat).toHaveBeenCalledTimes(1);
// Advance time to just over 30 seconds total - cache should expire
vi.advanceTimersByTime(11 * 1000); // Total: 31 seconds
await canUserAccessChatCached({ userId, chatId });
expect(spiedCanUserAccessChat).toHaveBeenCalledTimes(2);
// Next call should use new cache entry
await canUserAccessChatCached({ userId, chatId });
expect(spiedCanUserAccessChat).toHaveBeenCalledTimes(2);
});
test('should refresh TTL on access', async () => {
// This test verifies that TTL is refreshed when updateAgeOnGet is true
vi.useFakeTimers();
const userId = '00000000-0000-0000-0000-000000000007';
const chatId = '00000000-0000-0000-0000-000000000008';
// Initial call
await canUserAccessChatCached({ userId, chatId });
expect(spiedCanUserAccessChat).toHaveBeenCalledTimes(1);
// After 25 seconds, access the cache
vi.advanceTimersByTime(25 * 1000);
await canUserAccessChatCached({ userId, chatId });
expect(spiedCanUserAccessChat).toHaveBeenCalledTimes(1); // Still cached
// After another 25 seconds (total 50 seconds), should still be cached
// because the TTL was refreshed at 25 seconds
vi.advanceTimersByTime(25 * 1000);
await canUserAccessChatCached({ userId, chatId });
expect(spiedCanUserAccessChat).toHaveBeenCalledTimes(1); // Still cached
// After another 31 seconds (total 81 seconds), should expire
// because last access was at 50 seconds, so 50 + 31 > 30 second TTL
vi.advanceTimersByTime(31 * 1000);
await canUserAccessChatCached({ userId, chatId });
expect(spiedCanUserAccessChat).toHaveBeenCalledTimes(2); // Expired, fetched again
});
});