500 lines
14 KiB
TypeScript
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:.*:/);
|
|
});
|
|
});
|
|
});
|