RSSHub/lib/utils/cache/index.worker.test.ts

78 lines
3.1 KiB
TypeScript

/// <reference types="@cloudflare/vitest-pool-workers" />
import { env } from 'cloudflare:test';
import { describe, expect, it, vi } from 'vitest';
import cache, { setKVNamespace } from '@/utils/cache/index.worker';
import kv from '@/utils/cache/kv';
describe('worker cache before KV binding', () => {
it('is unavailable and always calls the value function', async () => {
expect(cache.status.available).toBe(false);
const getValue = vi.fn().mockResolvedValue({ fresh: 1 });
const first = await cache.tryGet('worker:fallback', getValue);
const second = await cache.tryGet('worker:fallback', getValue);
expect(first).toEqual({ fresh: 1 });
expect(second).toEqual({ fresh: 1 });
expect(getValue).toHaveBeenCalledTimes(2);
expect(await cache.globalCache.get('worker:fallback')).toBeNull();
});
});
// The binding is module-global state, so these run after the unavailable tests above
describe('worker cache with KV binding', () => {
it('becomes available once the KV namespace is bound', () => {
setKVNamespace(env.CACHE);
expect(cache.status.available).toBe(true);
});
it('round-trips strings and serializes objects', async () => {
await kv.set('worker:string', 'value');
expect(await kv.get('worker:string')).toBe('value');
await kv.set('worker:object', { a: 1 });
expect(await kv.get('worker:object')).toBe('{"a":1}');
expect(await kv.has('worker:object')).toBe(true);
expect(await kv.has('worker:missing')).toBe(false);
});
it('tryGet caches the computed value and parses JSON on the next hit', async () => {
const getValue = vi.fn().mockResolvedValue({ cached: true });
const miss = await cache.tryGet('worker:tryget', getValue);
expect(miss).toEqual({ cached: true });
// tryGet writes the cache without awaiting the KV put
await vi.waitFor(async () => {
expect(await kv.has('worker:tryget')).toBe(true);
});
const hit = await cache.tryGet('worker:tryget', getValue);
expect(hit).toEqual({ cached: true });
expect(getValue).toHaveBeenCalledTimes(1);
});
it('stores a ttl sidecar key for non-default expiry', async () => {
await kv.set('worker:custom-ttl', 'v', 120);
expect(await env.CACHE.get('rsshub:cacheTtl:worker:custom-ttl')).toBe('120');
await kv.set('worker:default-ttl', 'v');
expect(await env.CACHE.get('rsshub:cacheTtl:worker:default-ttl')).toBeNull();
});
it('rejects keys using the reserved ttl prefix', async () => {
await expect(kv.get('rsshub:cacheTtl:foo')).rejects.toThrow('reserved');
});
it('claims a key only once', async () => {
expect(await cache.globalCache.claim('worker:claim', 60)).toBe(true);
expect(await cache.globalCache.claim('worker:claim', 60)).toBe(false);
});
it('exposes get/set through globalCache', async () => {
await cache.globalCache.set('worker:global', { b: 2 }, 60);
expect(await cache.globalCache.get('worker:global')).toBe('{"b":2}');
});
});