import { DateTime } from 'luxon'; import { ApiCache } from './api-cache.ts'; describe('modules/platform/github/api-cache', () => { const now = DateTime.fromISO('2000-01-01T00:00:00.000+00:00'); const t1 = now.plus({ years: 1 }).toISO()!; const t2 = now.plus({ years: 2 }).toISO()!; const t3 = now.plus({ years: 3 }).toISO()!; const t4 = now.plus({ years: 4 }).toISO()!; const t5 = now.plus({ years: 5 }).toISO()!; it('stores and retrieves items', () => { const item1 = { number: 1, updated_at: t1 }; const item2 = { number: 2, updated_at: t2 }; const apiCache = new ApiCache({ items: { 1: item1 }, lastModified: t1, }); expect(apiCache.getItem(1)).toBe(item1); expect(apiCache.getItem(2)).toBeNull(); apiCache.updateItem(item2); expect(apiCache.getItem(2)).toBe(item2); expect(apiCache.getItems()).toEqual([item1, item2]); }); describe('getItems', () => { it('maps items', () => { const item1 = { number: 1, updated_at: t1 }; const item2 = { number: 2, updated_at: t2 }; const item3 = { number: 3, updated_at: t3 }; const apiCache = new ApiCache({ items: { 1: item1, 2: item2, 3: item3, }, }); const res = apiCache.getItems(); expect(res).toMatchObject([{ number: 1 }, { number: 2 }, { number: 3 }]); }); it('resets cache on item update', () => { const item1 = { number: 1, updated_at: t1 }; const item2 = { number: 2, updated_at: t2 }; const item3 = { number: 3, updated_at: t3 }; const apiCache = new ApiCache({ items: { 1: item1, 2: item2, }, }); const numbers1 = apiCache.getItems(); apiCache.updateItem(item3); const numbers2 = apiCache.getItems(); expect(numbers1).toMatchObject([{ number: 1 }, { number: 2 }]); expect(numbers2).toMatchObject([ { number: 1 }, { number: 2 }, { number: 3 }, ]); }); it('resets cache on page reconcile', () => { const item1 = { number: 1, updated_at: t1 }; const item2 = { number: 2, updated_at: t2 }; const item3 = { number: 3, updated_at: t3 }; const apiCache = new ApiCache({ items: { 1: item1, 2: item2, }, }); const numbers1 = apiCache.getItems(); apiCache.reconcile([item3]); const numbers2 = apiCache.getItems(); expect(numbers1).toMatchObject([{ number: 1 }, { number: 2 }]); expect(numbers2).toMatchObject([ { number: 1 }, { number: 2 }, { number: 3 }, ]); }); }); describe('getLastModified', () => { it('returns undefined when no lastModified in cache', () => { const apiCache = new ApiCache({ items: {} }); expect(apiCache.getLastModified()).toBeUndefined(); }); it('returns stored value when present', () => { const apiCache = new ApiCache({ items: {}, lastModified: t2 }); expect(apiCache.getLastModified()).toBe(t2); }); it('returns updated value after reconcile', () => { const apiCache = new ApiCache({ items: {}, lastModified: t1 }); apiCache.reconcile([{ number: 1, updated_at: t3 }]); expect(apiCache.getLastModified()).toBe(t3); }); }); describe('updateLastModified', () => { it('sets lastModified when not present', () => { const apiCache = new ApiCache({ items: {} }); apiCache.updateLastModified(t2); expect(apiCache.getLastModified()).toBe(t2); }); it('advances lastModified to newer timestamp', () => { const apiCache = new ApiCache({ items: {}, lastModified: t1 }); apiCache.updateLastModified(t3); expect(apiCache.getLastModified()).toBe(t3); }); it('does not regress lastModified to older timestamp', () => { const apiCache = new ApiCache({ items: {}, lastModified: t3 }); apiCache.updateLastModified(t1); expect(apiCache.getLastModified()).toBe(t3); }); }); describe('reconcile', () => { it('returns false for empty page', () => { const apiCache = new ApiCache({ items: { 1: { number: 1, updated_at: t1 } }, lastModified: t1, }); expect(apiCache.reconcile([])).toBeFalse(); expect(apiCache.getLastModified()).toBe(t1); }); it('appends new items', () => { const apiCache = new ApiCache({ items: {} }); const res1 = apiCache.reconcile([ { number: 2, updated_at: t2 }, { number: 1, updated_at: t1 }, ]); expect(res1).toBeTrue(); const res2 = apiCache.reconcile([ { number: 4, updated_at: t4 }, { number: 3, updated_at: t3 }, ]); expect(res2).toBeTrue(); expect(apiCache.getItems()).toEqual([ { number: 1, updated_at: t1 }, { number: 2, updated_at: t2 }, { number: 3, updated_at: t3 }, { number: 4, updated_at: t4 }, ]); }); it('handles updated items', () => { const apiCache = new ApiCache({ items: { 1: { number: 1, updated_at: t1 }, 2: { number: 2, updated_at: t2 }, 3: { number: 3, updated_at: t3 }, }, lastModified: t3, }); const needNextPage = apiCache.reconcile([ { number: 1, updated_at: t5 }, { number: 2, updated_at: t4 }, { number: 3, updated_at: t3 }, ]); expect(apiCache.getItems()).toEqual([ { number: 1, updated_at: t5 }, { number: 2, updated_at: t4 }, { number: 3, updated_at: t3 }, ]); expect(needNextPage).toBeFalse(); }); it('ignores page overlap', () => { const apiCache = new ApiCache({ items: {}, }); const res1 = apiCache.reconcile([ { number: 5, updated_at: t5 }, { number: 4, updated_at: t4 }, { number: 3, updated_at: t3 }, ]); const res2 = apiCache.reconcile([ { number: 3, updated_at: t3 }, { number: 2, updated_at: t2 }, { number: 1, updated_at: t1 }, ]); expect(apiCache.getItems()).toEqual([ { number: 1, updated_at: t1 }, { number: 2, updated_at: t2 }, { number: 3, updated_at: t3 }, { number: 4, updated_at: t4 }, { number: 5, updated_at: t5 }, ]); expect(res1).toBeTrue(); expect(res2).toBeTrue(); }); it('does not require new page if all items are old', () => { const apiCache = new ApiCache({ items: { 1: { number: 1, updated_at: t1 }, 2: { number: 2, updated_at: t2 }, 3: { number: 3, updated_at: t3 }, }, lastModified: t3, }); const needNextPage = apiCache.reconcile([ { number: 3, updated_at: t3 }, { number: 2, updated_at: t2 }, { number: 1, updated_at: t1 }, ]); expect(apiCache.getItems()).toEqual([ { number: 1, updated_at: t1 }, { number: 2, updated_at: t2 }, { number: 3, updated_at: t3 }, ]); expect(needNextPage).toBeFalse(); }); }); });