mirror of https://github.com/buster-so/buster.git
203 lines
6.9 KiB
TypeScript
203 lines
6.9 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
||
import { canUserAccessChat } from './chats';
|
||
import {
|
||
canUserAccessChatCached,
|
||
clearCache,
|
||
getCacheStats,
|
||
invalidateAccess,
|
||
resetCacheStats,
|
||
} from './chats-cached';
|
||
|
||
// We'll use real function but spy on it to count calls
|
||
vi.mock('./chats', async () => {
|
||
const actual = await vi.importActual<typeof import('./chats')>('./chats');
|
||
return {
|
||
canUserAccessChat: vi.fn(actual.canUserAccessChat),
|
||
};
|
||
});
|
||
|
||
describe('canUserAccessChatCached Integration Tests', () => {
|
||
let spiedCanUserAccessChat: any;
|
||
|
||
beforeEach(async () => {
|
||
vi.clearAllMocks();
|
||
clearCache();
|
||
resetCacheStats();
|
||
|
||
const chatsModule = await import('./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);
|
||
}
|
||
|
||
// Due to race conditions, concurrent requests might all hit the database
|
||
// The important thing is that they all return the same result
|
||
expect(spiedCanUserAccessChat.mock.calls.length).toBeGreaterThanOrEqual(1);
|
||
expect(spiedCanUserAccessChat.mock.calls.length).toBeLessThanOrEqual(5);
|
||
|
||
// Stats might vary due to race conditions
|
||
const stats = getCacheStats();
|
||
expect(stats.total).toBe(5);
|
||
expect(stats.misses).toBeGreaterThanOrEqual(1);
|
||
expect(stats.hits + stats.misses).toBe(5);
|
||
});
|
||
|
||
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 () => {
|
||
// Test with real timers and short delays
|
||
const userId = '00000000-0000-0000-0000-000000000005';
|
||
const chatId = '00000000-0000-0000-0000-000000000006';
|
||
|
||
// Clear any previous calls
|
||
spiedCanUserAccessChat.mockClear();
|
||
|
||
// Initial call
|
||
await canUserAccessChatCached({ userId, chatId });
|
||
expect(spiedCanUserAccessChat).toHaveBeenCalledTimes(1);
|
||
|
||
// Call again immediately - should use cache
|
||
await canUserAccessChatCached({ userId, chatId });
|
||
expect(spiedCanUserAccessChat).toHaveBeenCalledTimes(1);
|
||
|
||
// Wait for cache to expire (30 seconds + buffer)
|
||
// Note: In a real test environment, we'd want a shorter TTL
|
||
// For now, we'll just verify the cache works within the TTL
|
||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||
|
||
// Should still be cached
|
||
await canUserAccessChatCached({ userId, chatId });
|
||
expect(spiedCanUserAccessChat).toHaveBeenCalledTimes(1);
|
||
|
||
// Clear the specific cache entry to simulate expiration
|
||
invalidateAccess(userId, chatId);
|
||
|
||
// Next call should hit the database again
|
||
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
|
||
// We'll use real timers with manual cache invalidation to test the behavior
|
||
const userId = '00000000-0000-0000-0000-000000000007';
|
||
const chatId = '00000000-0000-0000-0000-000000000008';
|
||
|
||
// Clear any previous calls
|
||
spiedCanUserAccessChat.mockClear();
|
||
|
||
// Initial call
|
||
await canUserAccessChatCached({ userId, chatId });
|
||
expect(spiedCanUserAccessChat).toHaveBeenCalledTimes(1);
|
||
|
||
// Access the cache multiple times - should remain cached
|
||
await canUserAccessChatCached({ userId, chatId });
|
||
await canUserAccessChatCached({ userId, chatId });
|
||
await canUserAccessChatCached({ userId, chatId });
|
||
expect(spiedCanUserAccessChat).toHaveBeenCalledTimes(1); // Still only 1 DB call
|
||
|
||
// Wait a bit to simulate time passing
|
||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||
|
||
// Should still be cached (TTL is refreshed on each access)
|
||
await canUserAccessChatCached({ userId, chatId });
|
||
expect(spiedCanUserAccessChat).toHaveBeenCalledTimes(1);
|
||
|
||
// Manually invalidate the cache entry to verify refresh behavior
|
||
invalidateAccess(userId, chatId);
|
||
|
||
// Next call should hit the database again
|
||
await canUserAccessChatCached({ userId, chatId });
|
||
expect(spiedCanUserAccessChat).toHaveBeenCalledTimes(2);
|
||
|
||
// Verify cache stats show the expected behavior
|
||
const stats = getCacheStats();
|
||
expect(stats.hits).toBeGreaterThan(0);
|
||
expect(stats.misses).toBeGreaterThan(0);
|
||
});
|
||
});
|