Files
renovate/lib/util/cache/package/with-cache.spec.ts
Sebastian Poxhofer 05e1271376 test: use expect().resolves instead of awaiting inside expect (#45672)
Enables the vitest/prefer-expect-resolves oxlint rule and applies its
autofix across the spec suite.
2026-09-04 10:40:46 +00:00

573 lines
14 KiB
TypeScript

import { dir as tmpDir } from 'tmp-promise';
import type { MockInstance } from 'vitest';
import { GlobalConfig } from '../../../config/global.ts';
import * as memCache from '../memory/index.ts';
import * as packageCache from './index.ts';
import { withCache } from './with-cache.ts';
describe('util/cache/package/with-cache', () => {
let setCache: MockInstance<typeof packageCache.setWithRawTtl>;
let dirResult: Awaited<ReturnType<typeof tmpDir>>;
const getValue = vi.fn();
let count = 1;
beforeEach(async () => {
vi.useRealTimers();
GlobalConfig.reset();
memCache.init();
dirResult = await tmpDir({ unsafeCleanup: true });
setCache = vi.spyOn(packageCache, 'setWithRawTtl');
await packageCache.init({ cacheDir: dirResult.path });
count = 1;
getValue.mockImplementation(() => {
const res = String(100 * count + 10 * count + count);
count += 1;
return Promise.resolve(res);
});
});
afterEach(async () => {
setCache.mockRestore();
await packageCache.cleanup({});
await dirResult.cleanup();
});
it('caches string result', async () => {
function fn() {
return getValue();
}
await expect(
withCache({ namespace: '_test-namespace', key: 'some-key' }, fn),
).resolves.toBe('111');
await expect(
withCache({ namespace: '_test-namespace', key: 'some-key' }, fn),
).resolves.toBe('111');
await expect(
withCache({ namespace: '_test-namespace', key: 'some-key' }, fn),
).resolves.toBe('111');
expect(getValue).toHaveBeenCalledTimes(1);
expect(setCache).toHaveBeenCalledExactlyOnceWith(
'_test-namespace',
'cache-decorator:some-key',
{ cachedAt: expect.any(String), value: '111' },
30,
);
});
it('disables cache if cacheable is false', async () => {
function fn() {
return getValue();
}
await expect(
withCache(
{ namespace: '_test-namespace', key: 'key', cacheable: false },
fn,
),
).resolves.toBe('111');
await expect(
withCache(
{ namespace: '_test-namespace', key: 'key', cacheable: false },
fn,
),
).resolves.toBe('222');
await expect(
withCache(
{ namespace: '_test-namespace', key: 'key', cacheable: false },
fn,
),
).resolves.toBe('333');
expect(getValue).toHaveBeenCalledTimes(3);
expect(setCache).not.toHaveBeenCalled();
});
it('forces cache if cachePrivatePackages=true', async () => {
GlobalConfig.set({ cachePrivatePackages: true });
function fn() {
return getValue();
}
await expect(
withCache(
{ namespace: '_test-namespace', key: 'key', cacheable: false },
fn,
),
).resolves.toBe('111');
await expect(
withCache(
{ namespace: '_test-namespace', key: 'key', cacheable: false },
fn,
),
).resolves.toBe('111');
await expect(
withCache(
{ namespace: '_test-namespace', key: 'key', cacheable: false },
fn,
),
).resolves.toBe('111');
expect(getValue).toHaveBeenCalledTimes(1);
expect(setCache).toHaveBeenCalledExactlyOnceWith(
'_test-namespace',
'cache-decorator:key',
{ cachedAt: expect.any(String), value: '111' },
30,
);
});
it('caches null values', async () => {
async function fn(): Promise<string | null> {
await getValue();
return null;
}
await expect(
withCache({ namespace: '_test-namespace', key: 'key' }, fn),
).resolves.toBeNull();
await expect(
withCache({ namespace: '_test-namespace', key: 'key' }, fn),
).resolves.toBeNull();
await expect(
withCache({ namespace: '_test-namespace', key: 'key' }, fn),
).resolves.toBeNull();
expect(getValue).toHaveBeenCalledTimes(1);
expect(setCache).toHaveBeenCalledExactlyOnceWith(
'_test-namespace',
'cache-decorator:key',
{ cachedAt: expect.any(String), value: null },
30,
);
});
it('does not cache values rejected by cacheResult predicate', async () => {
async function fn(): Promise<string | null> {
await getValue();
return null;
}
function shouldCacheResult(value: unknown): boolean {
return value !== null;
}
await expect(
withCache(
{ namespace: '_test-namespace', key: 'key', shouldCacheResult },
fn,
),
).resolves.toBeNull();
await expect(
withCache(
{ namespace: '_test-namespace', key: 'key', shouldCacheResult },
fn,
),
).resolves.toBeNull();
await expect(
withCache(
{ namespace: '_test-namespace', key: 'key', shouldCacheResult },
fn,
),
).resolves.toBeNull();
expect(getValue).toHaveBeenCalledTimes(3);
expect(setCache).not.toHaveBeenCalled();
});
it('ignores cached values rejected by cacheResult predicate', async () => {
async function nullFn(): Promise<string | null> {
await getValue();
return null;
}
function fn() {
return getValue();
}
function cacheResult(value: unknown): boolean {
return value !== null;
}
await expect(
withCache({ namespace: '_test-namespace', key: 'key' }, nullFn),
).resolves.toBeNull();
await expect(
withCache(
{
namespace: '_test-namespace',
key: 'key',
shouldCacheResult: cacheResult,
},
fn,
),
).resolves.toBe('222');
await expect(
withCache(
{
namespace: '_test-namespace',
key: 'key',
shouldCacheResult: cacheResult,
},
fn,
),
).resolves.toBe('222');
expect(getValue).toHaveBeenCalledTimes(2);
expect(setCache).toHaveBeenCalledTimes(2);
expect(setCache).toHaveBeenLastCalledWith(
'_test-namespace',
'cache-decorator:key',
{ cachedAt: expect.any(String), value: '222' },
30,
);
});
it('does not cache undefined', async () => {
async function fn(): Promise<string | undefined> {
await getValue();
return undefined;
}
await expect(
withCache({ namespace: '_test-namespace', key: 'key' }, fn),
).resolves.toBeUndefined();
await expect(
withCache({ namespace: '_test-namespace', key: 'key' }, fn),
).resolves.toBeUndefined();
await expect(
withCache({ namespace: '_test-namespace', key: 'key' }, fn),
).resolves.toBeUndefined();
expect(getValue).toHaveBeenCalledTimes(3);
expect(setCache).not.toHaveBeenCalled();
});
it('uses custom ttlMinutes', async () => {
function fn() {
return getValue();
}
await expect(
withCache(
{ namespace: '_test-namespace', key: 'key', ttlMinutes: 60 },
fn,
),
).resolves.toBe('111');
expect(setCache).toHaveBeenCalledExactlyOnceWith(
'_test-namespace',
'cache-decorator:key',
{ cachedAt: expect.any(String), value: '111' },
60,
);
});
describe('fallback with hard TTL', () => {
beforeEach(() => {
vi.useFakeTimers();
GlobalConfig.set({ cacheHardTtlMinutes: 2 });
});
it('updates cached result after soft TTL expires', async () => {
function fn() {
return getValue();
}
await expect(
withCache(
{
namespace: '_test-namespace',
key: 'key',
ttlMinutes: 1,
fallback: true,
},
fn,
),
).resolves.toBe('111');
expect(getValue).toHaveBeenCalledTimes(1);
vi.advanceTimersByTime(60 * 1000 - 1);
await expect(
withCache(
{
namespace: '_test-namespace',
key: 'key',
ttlMinutes: 1,
fallback: true,
},
fn,
),
).resolves.toBe('111');
expect(getValue).toHaveBeenCalledTimes(1);
expect(setCache).toHaveBeenLastCalledWith(
'_test-namespace',
'cache-decorator:key',
{ cachedAt: expect.any(String), value: '111' },
2,
);
vi.advanceTimersByTime(1);
await expect(
withCache(
{
namespace: '_test-namespace',
key: 'key',
ttlMinutes: 1,
fallback: true,
},
fn,
),
).resolves.toBe('222');
expect(getValue).toHaveBeenCalledTimes(2);
expect(setCache).toHaveBeenLastCalledWith(
'_test-namespace',
'cache-decorator:key',
{ cachedAt: expect.any(String), value: '222' },
2,
);
});
it('overrides soft ttl and updates result', async () => {
GlobalConfig.set({
cacheTtlOverride: { '_test-namespace': 2 },
cacheHardTtlMinutes: 3,
});
function fn() {
return getValue();
}
await expect(
withCache(
{
namespace: '_test-namespace',
key: 'key',
ttlMinutes: 1,
fallback: true,
},
fn,
),
).resolves.toBe('111');
expect(getValue).toHaveBeenCalledTimes(1);
expect(setCache).toHaveBeenLastCalledWith(
'_test-namespace',
'cache-decorator:key',
{ cachedAt: expect.any(String), value: '111' },
3,
);
vi.advanceTimersByTime(120 * 1000 - 1);
await expect(
withCache(
{
namespace: '_test-namespace',
key: 'key',
ttlMinutes: 1,
fallback: true,
},
fn,
),
).resolves.toBe('111');
expect(getValue).toHaveBeenCalledTimes(1);
expect(setCache).toHaveBeenCalledTimes(1);
vi.advanceTimersByTime(1);
await expect(
withCache(
{
namespace: '_test-namespace',
key: 'key',
ttlMinutes: 1,
fallback: true,
},
fn,
),
).resolves.toBe('222');
expect(getValue).toHaveBeenCalledTimes(2);
expect(setCache).toHaveBeenLastCalledWith(
'_test-namespace',
'cache-decorator:key',
{ cachedAt: expect.any(String), value: '222' },
3,
);
});
it('returns stale result on error', async () => {
function fn() {
return getValue();
}
await expect(
withCache(
{
namespace: '_test-namespace',
key: 'key',
ttlMinutes: 1,
fallback: true,
},
fn,
),
).resolves.toBe('111');
expect(getValue).toHaveBeenCalledTimes(1);
expect(setCache).toHaveBeenLastCalledWith(
'_test-namespace',
'cache-decorator:key',
{ cachedAt: expect.any(String), value: '111' },
2,
);
vi.advanceTimersByTime(60 * 1000);
getValue.mockRejectedValueOnce(new Error('test'));
await expect(
withCache(
{
namespace: '_test-namespace',
key: 'key',
ttlMinutes: 1,
fallback: true,
},
fn,
),
).resolves.toBe('111');
expect(getValue).toHaveBeenCalledTimes(2);
expect(setCache).toHaveBeenCalledTimes(1);
});
it('does not return stale values rejected by cacheResult predicate', async () => {
async function nullFn(): Promise<string | null> {
await getValue();
return null;
}
function fn() {
return getValue();
}
function shouldCacheResult(value: unknown): boolean {
return value !== null;
}
await expect(
withCache(
{
namespace: '_test-namespace',
key: 'key',
ttlMinutes: 1,
fallback: true,
},
nullFn,
),
).resolves.toBeNull();
expect(getValue).toHaveBeenCalledTimes(1);
expect(setCache).toHaveBeenCalledTimes(1);
vi.advanceTimersByTime(60 * 1000);
getValue.mockRejectedValueOnce(new Error('test'));
await expect(
withCache(
{
namespace: '_test-namespace',
key: 'key',
ttlMinutes: 1,
fallback: true,
shouldCacheResult,
},
fn,
),
).rejects.toThrow('test');
expect(getValue).toHaveBeenCalledTimes(2);
expect(setCache).toHaveBeenCalledTimes(1);
});
it('drops stale value after hard TTL expires', async () => {
function fn() {
return getValue();
}
await expect(
withCache(
{
namespace: '_test-namespace',
key: 'key',
ttlMinutes: 1,
fallback: true,
},
fn,
),
).resolves.toBe('111');
expect(getValue).toHaveBeenCalledTimes(1);
expect(setCache).toHaveBeenLastCalledWith(
'_test-namespace',
'cache-decorator:key',
{ cachedAt: expect.any(String), value: '111' },
2,
);
vi.advanceTimersByTime(2 * 60 * 1000 - 1);
getValue.mockRejectedValueOnce(new Error('test'));
await expect(
withCache(
{
namespace: '_test-namespace',
key: 'key',
ttlMinutes: 1,
fallback: true,
},
fn,
),
).resolves.toBe('111');
vi.advanceTimersByTime(1);
getValue.mockRejectedValueOnce(new Error('test'));
await expect(
withCache(
{
namespace: '_test-namespace',
key: 'key',
ttlMinutes: 1,
fallback: true,
},
fn,
),
).rejects.toThrow('test');
});
it('does not use fallback when fallback=false', async () => {
function fn() {
return getValue();
}
await expect(
withCache(
{
namespace: '_test-namespace',
key: 'key',
ttlMinutes: 1,
fallback: false,
},
fn,
),
).resolves.toBe('111');
expect(getValue).toHaveBeenCalledTimes(1);
// Without fallback, hard TTL equals soft TTL
expect(setCache).toHaveBeenLastCalledWith(
'_test-namespace',
'cache-decorator:key',
{ cachedAt: expect.any(String), value: '111' },
1,
);
vi.advanceTimersByTime(60 * 1000);
getValue.mockRejectedValueOnce(new Error('test'));
// Error should propagate since fallback is disabled
await expect(
withCache(
{
namespace: '_test-namespace',
key: 'key',
ttlMinutes: 1,
fallback: false,
},
fn,
),
).rejects.toThrow('test');
});
});
});