Files
Tony 3bc1cc248d chore: bump to msw v3 (#23463)
* chore: bump to msw v3

* refactor: anti-slop

* test: use vi.stubEnv for REDIS_URL in redis error test

* refactor: move remote debugging registration into a conditional block

* chore: add code coverage upload for github
2026-10-03 06:36:19 +08:00

161 lines
5.9 KiB
TypeScript

import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
import wait from '@/utils/wait';
beforeAll(() => {
process.env.CACHE_EXPIRE = '1';
process.env.CACHE_CONTENT_EXPIRE = '3';
});
afterEach(() => {
delete process.env.CACHE_TYPE;
vi.resetModules();
});
afterAll(() => {
delete process.env.CACHE_EXPIRE;
});
describe('cache', () => {
it('memory', async () => {
process.env.CACHE_TYPE = 'memory';
const cache = (await import('@/utils/cache')).default;
if (!cache.clients.memoryCache || !cache.status.available) {
throw new Error('Memory cache client error');
}
expect(cache.globalCache.supportsAtomicClaims).toBe(true);
await cache.set('mock', undefined);
expect(await cache.get('mock')).toBe('');
expect(await cache.has('mock')).toBe(true);
await cache.globalCache.set('mock', undefined);
expect(await cache.globalCache.get('mock')).toBe('');
expect(await cache.globalCache.has('mock')).toBe(true);
await cache.globalCache.set('mock', {
mock: 1,
});
expect(await cache.globalCache.get('mock')).toBe('{"mock":1}');
}, 10000);
it('memory get returns null before init', async () => {
const memory = (await import('@/utils/cache/memory')).default;
expect(await memory.get('missing')).toBeNull();
expect(await memory.has('missing')).toBe(false);
});
it('tryGet preserves snowflake ID precision', async () => {
process.env.CACHE_TYPE = 'memory';
const cache = (await import('@/utils/cache')).default;
if (!cache.clients.memoryCache || !cache.status.available) {
throw new Error('Memory cache client error');
}
const snowflakeId = '1234567890123456789';
const fetcher = vi.fn(() => Promise.resolve(snowflakeId));
const fresh = await cache.tryGet('snowflake', fetcher);
expect(fresh).toBe(snowflakeId);
const cached = await cache.tryGet('snowflake', fetcher);
expect(cached).toBeTypeOf('string');
expect(cached).toBe(snowflakeId);
expect(fetcher).toHaveBeenCalledTimes(1);
});
it('redis', async () => {
process.env.CACHE_TYPE = 'redis';
const cache = (await import('@/utils/cache')).default;
await wait(500);
if (!cache.clients.redisClient || !cache.status.available) {
throw new Error('Redis client error');
}
expect(cache.globalCache.supportsAtomicClaims).toBe(true);
await cache.set('mock1', undefined);
expect(await cache.get('mock1')).toBe('');
await cache.set('mock2', '2');
await cache.set('mock2', '2');
expect(await cache.get('mock2')).toBe('2');
expect(await cache.has('mock2')).toBe(true);
await cache.clients.redisClient?.quit();
}, 10000);
it('redis with quit', async () => {
process.env.CACHE_TYPE = 'redis';
const cache = (await import('@/utils/cache')).default;
if (cache.clients.redisClient) {
await cache.clients.redisClient.quit();
} else {
throw new Error('No redis client');
}
await cache.set('mock2', '2');
expect(await cache.get('mock2')).toBe(null);
expect(await cache.has('mock2')).toBe(false);
});
it('redis with error', async () => {
process.env.CACHE_TYPE = 'redis';
vi.stubEnv('REDIS_URL', 'redis://wrongpath:6379');
const cache = (await import('@/utils/cache')).default;
await cache.set('mock2', '2');
expect(await cache.get('mock2')).toBe(null);
expect(await cache.has('mock2')).toBe(false);
cache.clients.redisClient?.disconnect();
vi.unstubAllEnvs();
});
it('no cache', async () => {
process.env.CACHE_TYPE = 'NO';
const cache = (await import('@/utils/cache')).default;
await cache.init();
await cache.set('mock2', '2');
expect(await cache.get('mock2')).toBe(null);
expect(await cache.has('mock2')).toBe(false);
expect(await cache.globalCache.get('mock2')).toBeNull();
expect(await cache.globalCache.has('mock2')).toBe(false);
expect(cache.globalCache.set('mock2', '2')).toBeNull();
});
it('throws TTL key', async () => {
process.env.CACHE_TYPE = 'redis';
const cache = (await import('@/utils/cache')).default;
try {
await cache.get('rsshub:cacheTtl:mock');
} catch (error: any) {
expect(error.message).toContain('reserved for the internal usage');
} finally {
await cache.clients.redisClient?.quit();
}
});
});
describe('atomic cache backends', () => {
it.each(['memory', 'redis'])('retains atomic claims and release for %s', async (backend) => {
process.env.CACHE_TYPE = backend;
const cache = (await import('@/utils/cache')).default;
const key = `rsshub-test:atomic-claim:${backend}`;
const { redisClient } = cache.clients;
if (redisClient) {
await vi.waitFor(() => expect(cache.status.available).toBe(true));
await redisClient.del(key);
}
const evalSpy = redisClient && vi.spyOn(redisClient, 'eval');
try {
expect(cache.globalCache.supportsAtomicClaims).toBe(true);
expect(await Promise.all([cache.globalCache.claim(key, 60), cache.globalCache.claim(key, 60)])).toEqual([true, false]);
await cache.globalCache.set(key, '0', 60);
expect(await cache.globalCache.claim(key, 60)).toBe(true);
if (!evalSpy) {
return;
}
expect(evalSpy).toHaveBeenCalledTimes(3);
expect(evalSpy).toHaveBeenCalledWith(expect.stringContaining("redis.call('GET', KEYS[1])"), 1, key, 60);
} finally {
await redisClient?.del(key);
await redisClient?.quit();
}
});
});