Files
RSSHub/lib/utils/cache/http.test.ts
T
2026-04-24 11:35:51 +08:00

119 lines
3.8 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest';
const errorSpy = vi.fn();
const infoSpy = vi.fn();
vi.mock('@/utils/logger', () => ({
default: {
error: errorSpy,
info: infoSpy,
},
}));
const setHttpCacheEnv = () => {
process.env.CACHE_HTTP_URL = 'https://cache.example.com/';
process.env.CACHE_HTTP_TOKEN = 'token';
};
const clearHttpCacheEnv = () => {
delete process.env.CACHE_TYPE;
delete process.env.CACHE_HTTP_URL;
delete process.env.CACHE_HTTP_TOKEN;
};
describe('http cache module', () => {
afterEach(() => {
clearHttpCacheEnv();
vi.unstubAllGlobals();
vi.resetModules();
vi.clearAllMocks();
});
it('requires endpoint and token', async () => {
const cache = (await import('@/utils/cache/http')).default;
cache.init();
expect(cache.status.available).toBe(false);
expect(errorSpy).toHaveBeenCalledWith('HTTP cache requires CACHE_HTTP_URL and CACHE_HTTP_TOKEN.');
});
it('sets, gets, refreshes, and checks hashed keys', async () => {
setHttpCacheEnv();
const requests: Array<{ body?: string; init?: RequestInit; url: string }> = [];
const fetchMock = vi.fn((input: RequestInfo | URL, init?: RequestInit) => {
requests.push({
body: init?.body?.toString(),
init,
url: input.toString(),
});
if (init?.method === 'PUT') {
return new Response(null, { status: 204 });
}
if (init?.method === 'HEAD') {
return new Response(null, { status: 204 });
}
return Response.json({ hit: true, value: 'cached' });
});
vi.stubGlobal('fetch', fetchMock);
const cache = (await import('@/utils/cache/http')).default;
cache.init();
await cache.set('mock/key', { ok: true }, 30);
await expect(cache.get('mock/key')).resolves.toBe('cached');
await expect(cache.get('mock/key', false)).resolves.toBe('cached');
await expect(cache.has('mock/key')).resolves.toBe(true);
expect(cache.status.available).toBe(true);
expect(requests[0].url).toMatch(/^https:\/\/cache\.example\.com\/v1\/cache\/rsshub%3Ahttp-cache%3A[a-f0-9]{32}$/);
expect(requests[0].init?.headers).toMatchObject({
authorization: 'Bearer token',
'content-type': 'application/json',
});
expect(JSON.parse(requests[0].body || '{}')).toEqual({
ttl: 30,
value: '{"ok":true}',
});
expect(requests[1].url).toMatch(/\?refresh=1$/);
expect(requests[2].url).not.toContain('?refresh=1');
expect(requests[3].init?.method).toBe('HEAD');
});
it('treats a 404 response as a miss', async () => {
setHttpCacheEnv();
vi.stubGlobal(
'fetch',
vi.fn(() => Response.json({ hit: false }, { status: 404 }))
);
const cache = (await import('@/utils/cache/http')).default;
cache.init();
await expect(cache.get('missing')).resolves.toBe('');
await expect(cache.has('missing')).resolves.toBe(false);
});
it('uses non-refreshing reads for global cache', async () => {
process.env.CACHE_TYPE = 'http';
setHttpCacheEnv();
const urls: string[] = [];
vi.stubGlobal(
'fetch',
vi.fn((input: RequestInfo | URL) => {
urls.push(input.toString());
return Response.json({ hit: true, value: 'route-cache' });
})
);
const cache = (await import('@/utils/cache')).default;
await expect(cache.globalCache.get('route/key')).resolves.toBe('route-cache');
expect(cache.status.available).toBe(true);
expect(urls[0]).not.toContain('?refresh=1');
});
});