Files
stats/packages/redis/cachable.test.ts

500 lines
14 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { cacheable, getCache } from './cachable';
import { getRedisCache } from './redis';
describe('cachable', () => {
let redis: any;
beforeEach(async () => {
redis = getRedisCache();
// Clear any existing cache data for clean tests
const keys = [
...(await redis.keys('cachable:*')),
...(await redis.keys('test-key*')),
];
if (keys.length > 0) {
await redis.del(...keys);
}
});
afterEach(async () => {
// Clean up after each test
const keys = [
...(await redis.keys('cachable:*')),
...(await redis.keys('test-key*')),
];
if (keys.length > 0) {
await redis.del(...keys);
}
});
describe('getCache', () => {
it('should return cached data when available', async () => {
const mockData = { id: 1, name: 'test' };
const mockDate = new Date('2023-01-01T00:00:00Z');
const cachedData = { ...mockData, createdAt: mockDate };
// First, cache some data
await redis.setex('test-key', 3600, JSON.stringify(cachedData));
let fnCalled = false;
const fn = async () => {
fnCalled = true;
return mockData;
};
const result = await getCache('test-key', 3600, fn);
expect(result).toEqual(cachedData);
expect(fnCalled).toBe(false);
});
it('should call function and cache result when no cache exists', async () => {
const mockData = { id: 1, name: 'test' };
let fnCalled = false;
const fn = async () => {
fnCalled = true;
return mockData;
};
const result = await getCache('test-key-2', 3600, fn);
expect(result).toEqual(mockData);
expect(fnCalled).toBe(true);
// Verify it was cached
const cached = await redis.get('test-key-2');
expect(cached).toBe(JSON.stringify(mockData));
});
it('should parse Date objects from cached JSON', async () => {
const mockDate = new Date('2023-01-01T00:00:00Z');
const cachedData = { id: 1, createdAt: mockDate };
// Cache the data first
await redis.setex('test-key', 3600, JSON.stringify(cachedData));
let fnCalled = false;
const fn = async () => {
fnCalled = true;
return { id: 1 };
};
const result = await getCache('test-key', 3600, fn);
expect((result as any).createdAt).toBeInstanceOf(Date);
expect((result as any).createdAt.getTime()).toBe(mockDate.getTime());
expect(fnCalled).toBe(false);
});
});
describe('cacheable', () => {
it('should create a cached function with function and expire time', async () => {
const mockData = { id: 1, name: 'test' };
let fnCalled = false;
const fn = async (arg1: string, arg2: string) => {
fnCalled = true;
return mockData;
};
const cachedFn = cacheable(fn, 3600);
const result = await cachedFn('arg1', 'arg2');
expect(result).toEqual(mockData);
expect(fnCalled).toBe(true);
// Verify it was cached
const key = cachedFn.getKey('arg1', 'arg2');
const cached = await redis.get(key);
expect(cached).toBe(JSON.stringify(mockData));
});
it('should create a cached function with name, function and expire time', async () => {
const mockData = { id: 1, name: 'test' };
let fnCalled = false;
const fn = async (arg1: string, arg2: string) => {
fnCalled = true;
return mockData;
};
const cachedFn = cacheable('testFunction', fn, 3600);
const result = await cachedFn('arg1', 'arg2');
expect(result).toEqual(mockData);
expect(fnCalled).toBe(true);
// Verify it was cached
const key = cachedFn.getKey('arg1', 'arg2');
const cached = await redis.get(key);
expect(cached).toBe(JSON.stringify(mockData));
});
it('should return cached result when available', async () => {
const mockData = { id: 1, name: 'test' };
// First cache some data
const cachedFn = cacheable(
'testFunction',
async (arg1: string, arg2: string) => mockData,
3600
);
await cachedFn('arg1', 'arg2');
// Now test that it returns cached data
let fnCalled = false;
const fn = async (arg1: string, arg2: string) => {
fnCalled = true;
return { id: 2, name: 'different' };
};
const newCachedFn = cacheable('testFunction', fn, 3600);
const result = await newCachedFn('arg1', 'arg2');
expect(result).toEqual(mockData);
expect(fnCalled).toBe(false);
});
it('should not cache undefined results', async () => {
let fnCalled = false;
const fn = async (arg1: string) => {
fnCalled = true;
return undefined;
};
const cachedFn = cacheable(fn, 3600);
const result = await cachedFn('arg1');
expect(result).toBeUndefined();
expect(fnCalled).toBe(true);
// Verify nothing was cached
const key = cachedFn.getKey('arg1');
const cached = await redis.get(key);
expect(cached).toBeNull();
});
it('should not cache null results', async () => {
let fnCalled = false;
const fn = async (arg1: string) => {
fnCalled = true;
return null;
};
const cachedFn = cacheable(fn, 3600);
const result = await cachedFn('arg1');
expect(result).toBeNull();
expect(fnCalled).toBe(true);
// Verify nothing was cached
const key = cachedFn.getKey('arg1');
const cached = await redis.get(key);
expect(cached).toBeNull();
});
it('should not cache empty strings', async () => {
let fnCalled = false;
const fn = async (arg1: string) => {
fnCalled = true;
return '';
};
const cachedFn = cacheable(fn, 3600);
const result = await cachedFn('arg1');
expect(result).toBe('');
expect(fnCalled).toBe(true);
// Verify nothing was cached
const key = cachedFn.getKey('arg1');
const cached = await redis.get(key);
expect(cached).toBeNull();
});
it('should not cache empty arrays', async () => {
let fnCalled = false;
const fn = async (arg1: string) => {
fnCalled = true;
return [];
};
const cachedFn = cacheable(fn, 3600);
const result = await cachedFn('arg1');
expect(result).toEqual([]);
expect(fnCalled).toBe(true);
// Verify nothing was cached
const key = cachedFn.getKey('arg1');
const cached = await redis.get(key);
expect(cached).toBeNull();
});
it('should not cache empty objects', async () => {
let fnCalled = false;
const fn = async (arg1: string) => {
fnCalled = true;
return {};
};
const cachedFn = cacheable(fn, 3600);
const result = await cachedFn('arg1');
expect(result).toEqual({});
expect(fnCalled).toBe(true);
// Verify nothing was cached
const key = cachedFn.getKey('arg1');
const cached = await redis.get(key);
expect(cached).toBeNull();
});
it('should cache non-empty strings', async () => {
let fnCalled = false;
const fn = async (arg1: string) => {
fnCalled = true;
return 'hello';
};
const cachedFn = cacheable(fn, 3600);
const result = await cachedFn('arg1');
expect(result).toBe('hello');
expect(fnCalled).toBe(true);
// Verify it was cached
const key = cachedFn.getKey('arg1');
const cached = await redis.get(key);
expect(cached).toBe('"hello"');
});
it('should cache non-empty arrays', async () => {
let fnCalled = false;
const fn = async (arg1: string) => {
fnCalled = true;
return [1, 2, 3];
};
const cachedFn = cacheable(fn, 3600);
const result = await cachedFn('arg1');
expect(result).toEqual([1, 2, 3]);
expect(fnCalled).toBe(true);
// Verify it was cached
const key = cachedFn.getKey('arg1');
const cached = await redis.get(key);
expect(cached).toBe('[1,2,3]');
});
it('should cache non-empty objects', async () => {
let fnCalled = false;
const fn = async (arg1: string) => {
fnCalled = true;
return { id: 1 };
};
const cachedFn = cacheable(fn, 3600);
const result = await cachedFn('arg1');
expect(result).toEqual({ id: 1 });
expect(fnCalled).toBe(true);
// Verify it was cached
const key = cachedFn.getKey('arg1');
const cached = await redis.get(key);
expect(cached).toBe('{"id":1}');
});
it('should cache booleans', async () => {
let fnCalled = false;
const fn = async (arg1: string) => {
fnCalled = true;
return true;
};
const cachedFn = cacheable(fn, 3600);
const result = await cachedFn('arg1');
expect(result).toBe(true);
expect(fnCalled).toBe(true);
// Verify it was cached
const key = cachedFn.getKey('arg1');
const cached = await redis.get(key);
expect(cached).toBe('true');
});
it('should cache numbers', async () => {
let fnCalled = false;
const fn = async (arg1: string) => {
fnCalled = true;
return 42;
};
const cachedFn = cacheable(fn, 3600);
const result = await cachedFn('arg1');
expect(result).toBe(42);
expect(fnCalled).toBe(true);
// Verify it was cached
const key = cachedFn.getKey('arg1');
const cached = await redis.get(key);
expect(cached).toBe('42');
});
it('should handle cache parsing errors gracefully', async () => {
const mockData = { id: 1, name: 'test' };
// First, manually set invalid JSON in cache
const cachedFn = cacheable(
'testFunction',
async (arg1: string) => mockData,
3600
);
const key = cachedFn.getKey('arg1');
await redis.set(key, 'invalid json');
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
let fnCalled = false;
const fn = async (arg1: string) => {
fnCalled = true;
return mockData;
};
const newCachedFn = cacheable('testFunction', fn, 3600);
const result = await newCachedFn('arg1');
expect(result).toEqual(mockData);
expect(fnCalled).toBe(true);
expect(consoleSpy).toHaveBeenCalledWith(
'Failed to parse cache',
expect.any(Error)
);
consoleSpy.mockRestore();
});
it('should parse Date objects from cached JSON', async () => {
const mockDate = new Date('2023-01-01T00:00:00Z');
const cachedData = { id: 1, createdAt: mockDate };
// First cache some data with Date
const cachedFn = cacheable(
'testFunction',
async (arg1: string) => cachedData,
3600
);
await cachedFn('arg1');
// Now test that it returns cached data with proper Date parsing
let fnCalled = false;
const fn = async (arg1: string) => {
fnCalled = true;
return { id: 2 };
};
const newCachedFn = cacheable('testFunction', fn, 3600);
const result = await newCachedFn('arg1');
expect((result as any).createdAt).toBeInstanceOf(Date);
expect((result as any).createdAt.getTime()).toBe(mockDate.getTime());
expect(fnCalled).toBe(false);
});
it('should provide getKey method', () => {
const fn = async (arg1: string, arg2: string) => ({});
const cachedFn = cacheable(fn, 3600);
expect(typeof cachedFn.getKey).toBe('function');
const key = cachedFn.getKey('arg1', 'arg2');
expect(key).toMatch(/^cachable:.*:\[arg1,arg2\]$/);
});
it('should provide clear method', async () => {
const fn = async (arg1: string, arg2: string) => ({ id: 1 });
const cachedFn = cacheable(fn, 3600);
// First cache some data
await cachedFn('arg1', 'arg2');
// Verify it's cached
const key = cachedFn.getKey('arg1', 'arg2');
let cached = await redis.get(key);
expect(cached).not.toBeNull();
// Clear it
const result = await cachedFn.clear('arg1', 'arg2');
expect(result).toBe(1);
// Verify it's cleared
cached = await redis.get(key);
expect(cached).toBeNull();
});
it('should provide set method', async () => {
const fn = async (arg1: string, arg2: string) => ({});
const cachedFn = cacheable(fn, 3600);
const payload = { id: 1, name: 'test' };
await cachedFn.set('arg1', 'arg2')(payload);
// Verify it was set
const key = cachedFn.getKey('arg1', 'arg2');
const cached = await redis.get(key);
expect(cached).toBe(JSON.stringify(payload));
});
it('should throw error when expire time is not provided', () => {
const fn = async (arg1: string, arg2: string) => ({});
expect(() => {
cacheable(fn, undefined as any);
}).toThrow('expireInSec is not a number');
});
it('should generate consistent cache keys for same arguments', () => {
const fn = async (arg1: { a: number; b: number }, arg2: string) => ({});
const cachedFn = cacheable(fn, 3600);
const key1 = cachedFn.getKey({ a: 1, b: 2 }, 'test');
const key2 = cachedFn.getKey({ b: 2, a: 1 }, 'test'); // Different order
expect(key1).toBe(key2);
});
it('should handle complex argument types in cache keys', () => {
const fn = async (
arg1: string,
arg2: number,
arg3: boolean,
arg4: null,
arg5: undefined,
arg6: number[],
arg7: { a: number; b: number },
arg8: Date
) => ({});
const cachedFn = cacheable(fn, 3600);
const key = cachedFn.getKey(
'string',
123,
true,
null,
undefined,
[1, 2, 3],
{ a: 1, b: 2 },
new Date('2023-01-01T00:00:00Z')
);
expect(key).toMatch(/^cachable:.*:/);
});
});
});