diff --git a/packages/backend/test/unit/misc/QuantumKVCache.ts b/packages/backend/test/unit/misc/QuantumKVCache.ts index 92792171be..4475bfb97d 100644 --- a/packages/backend/test/unit/misc/QuantumKVCache.ts +++ b/packages/backend/test/unit/misc/QuantumKVCache.ts @@ -4,11 +4,13 @@ */ import { jest } from '@jest/globals'; -import { FakeInternalEventService } from '../../misc/FakeInternalEventService.js'; -import { QuantumKVCache, QuantumKVOpts } from '@/misc/QuantumKVCache.js'; +import { GodOfTimeService } from '../../misc/GodOfTimeService.js'; +import { MockInternalEventService } from '../../misc/MockInternalEventService.js'; +import { QuantumKVCache, QuantumKVOpts, FetchFailedError, KeyNotFoundError } from '@/misc/QuantumKVCache.js'; describe(QuantumKVCache, () => { - let fakeInternalEventService: FakeInternalEventService; + let fakeTimeService: GodOfTimeService; + let fakeInternalEventService: MockInternalEventService; let madeCaches: { dispose: () => void }[]; function makeCache(opts?: Partial> & { name?: string }): QuantumKVCache { @@ -22,14 +24,20 @@ describe(QuantumKVCache, () => { Object.assign(_opts, opts); } - const cache = new QuantumKVCache(fakeInternalEventService, _opts.name, _opts); + const services = { + internalEventService: fakeInternalEventService, + timeService: fakeTimeService, + }; + + const cache = new QuantumKVCache(_opts.name, services, _opts); madeCaches.push(cache); return cache; } beforeEach(() => { madeCaches = []; - fakeInternalEventService = new FakeInternalEventService(); + fakeTimeService = new GodOfTimeService(); + fakeInternalEventService = new MockInternalEventService(); }); afterEach(() => { @@ -333,6 +341,108 @@ describe(QuantumKVCache, () => { }); }); + describe('fetch', () => { + it('should throw FetchFailedError when fetcher throws error', async () => { + const cache = makeCache({ + fetcher: () => { throw new Error('test error'); }, + }); + + await expect(cache.fetch('foo')).rejects.toThrow(FetchFailedError); + }); + + it('should throw KeyNotFoundError when fetcher returns null', async () => { + const cache = makeCache({ + fetcher: () => null, + }); + + await expect(cache.fetch('foo')).rejects.toThrow(KeyNotFoundError); + }); + + it('should throw KeyNotFoundError when fetcher undefined', async () => { + const cache = makeCache({ + fetcher: () => undefined, + }); + + await expect(cache.fetch('foo')).rejects.toThrow(KeyNotFoundError); + }); + }); + + describe('fetchMaybe', () => { + it('should return value when found by fetcher', async () => { + const cache = makeCache({ + fetcher: () => 'bar', + }); + + const result = await cache.fetchMaybe('foo'); + + expect(result).toBe('bar'); + }); + + it('should call onChanged when found by fetcher', async () => { + const fakeOnChanged = jest.fn(() => Promise.resolve()); + const cache = makeCache({ + fetcher: () => 'bar', + onChanged: fakeOnChanged, + }); + + await cache.fetchMaybe('foo'); + + expect(fakeOnChanged).toHaveBeenCalled(); + }); + + it('should return undefined when fetcher returns undefined', async () => { + const cache = makeCache({ + fetcher: () => undefined, + }); + + const result = await cache.fetchMaybe('foo'); + + expect(result).toBe(undefined); + }); + + it('should not call onChanged when fetcher returns undefined', async () => { + const fakeOnChanged = jest.fn(() => Promise.resolve()); + const cache = makeCache({ + fetcher: () => undefined, + onChanged: fakeOnChanged, + }); + + await cache.fetchMaybe('foo'); + + expect(fakeOnChanged).not.toHaveBeenCalled(); + }); + + it('should return undefined when fetcher returns null', async () => { + const cache = makeCache({ + fetcher: () => null, + }); + + const result = await cache.fetchMaybe('foo'); + + expect(result).toBe(undefined); + }); + + it('should not call onChanged when fetcher returns null', async () => { + const fakeOnChanged = jest.fn(() => Promise.resolve()); + const cache = makeCache({ + fetcher: () => null, + onChanged: fakeOnChanged, + }); + + await cache.fetchMaybe('foo'); + + expect(fakeOnChanged).not.toHaveBeenCalled(); + }); + + it('should throw FetchFailedError when fetcher throws error', async () => { + const cache = makeCache({ + fetcher: () => { throw new Error('test error'); }, + }); + + await expect(cache.fetchMaybe('foo')).rejects.toThrow(FetchFailedError); + }); + }); + describe('fetchMany', () => { it('should do nothing for empty input', async () => { const fakeOnChanged = jest.fn(() => Promise.resolve()); @@ -507,6 +617,52 @@ describe(QuantumKVCache, () => { expect(fakeInternalEventService._calls).toContainEqual(['emit', ['quantumCacheUpdated', { name: 'fake', keys: ['foo', 'alpha'] }]]); expect(fakeInternalEventService._calls.filter(c => c[0] === 'emit')).toHaveLength(1); }); + + it('should throw FetchFailedError when bulk fetcher throws error', async () => { + const cache = makeCache({ + bulkFetcher: () => { throw new Error('test error'); }, + }); + + await expect(cache.refreshMany(['foo'])).rejects.toThrow(FetchFailedError); + }); + + it('should throw FetchFailedError when fallback fetcher throws error', async () => { + const cache = makeCache({ + fetcher: () => { throw new Error('test error'); }, + }); + + await expect(cache.refreshMany(['foo'])).rejects.toThrow(FetchFailedError); + }); + + it('should not throw when fallback fetcher returns null', async () => { + const cache = makeCache({ + fetcher: () => null, + }); + + const result = await cache.refreshMany(['foo']); + + expect(result).toHaveLength(0); + }); + + it('should not throw when fallback fetcher returns undefined', async () => { + const cache = makeCache({ + fetcher: () => undefined, + }); + + const result = await cache.refreshMany(['foo']); + + expect(result).toHaveLength(0); + }); + + it('should not throw when bulk fetcher returns empty', async () => { + const cache = makeCache({ + bulkFetcher: () => [], + }); + + const result = await cache.refreshMany(['foo']); + + expect(result).toHaveLength(0); + }); }); describe('deleteMany', () => {