Files
renovate/lib/util/cache/repository/impl/local.spec.ts
2026-02-06 13:40:01 +00:00

261 lines
7.9 KiB
TypeScript

import { fs } from '~test/util.ts';
import { GlobalConfig } from '../../../../config/global.ts';
import { logger } from '../../../../logger/index.ts';
import { compressToBase64 } from '../../../compress.ts';
import { hash } from '../../../hash.ts';
import { CACHE_REVISION } from '../common.ts';
import type { RepoCacheRecord } from '../schema.ts';
import type { RepoCacheData } from '../types.ts';
import { CacheFactory } from './cache-factory.ts';
import { RepoCacheLocal } from './local.ts';
vi.mock('../../../fs/index.ts');
async function createCacheRecord(
data: RepoCacheData,
repository = 'some/repo',
): Promise<RepoCacheRecord> {
const revision = CACHE_REVISION;
const fingerprint = '0123456789abcdef';
const jsonStr = JSON.stringify(data);
const hashedJsonStr = hash(jsonStr);
const payload = await compressToBase64(jsonStr);
return {
revision,
repository,
fingerprint,
payload,
hash: hashedJsonStr,
};
}
describe('util/cache/repository/impl/local', () => {
beforeEach(() => {
GlobalConfig.set({ cacheDir: '/tmp/cache', platform: 'github' });
fs.cachePathExists.mockResolvedValue(true);
});
it('returns empty object before any data load', () => {
const localRepoCache = CacheFactory.get(
'some/repo',
'0123456789abcdef',
'local',
);
expect(localRepoCache.getData()).toBeEmpty();
expect(localRepoCache.isModified()).toBeUndefined();
});
it('skip when receives non-string data', async () => {
const localRepoCache = CacheFactory.get(
'some/repo',
'0123456789abcdef',
'local',
);
await localRepoCache.load(); // readCacheFile is mocked but has no return value set - therefore returns undefined
expect(logger.debug).toHaveBeenCalledWith(
"RepoCacheBase.load() - expecting data of type 'string' received 'undefined' instead - skipping",
);
expect(localRepoCache.isModified()).toBeUndefined();
});
it('should not load empty repository cache files', async () => {
fs.readCacheFile.mockResolvedValue('');
const localRepoCache = CacheFactory.get(
'some/repo',
'0123456789abcdef',
'local',
);
await localRepoCache.load(); // readCacheFile is mocked but has no return value set - therefore returns undefined
expect(logger.debug).toHaveBeenCalledWith(
'RepoCacheBase.load() - cache file is empty - skipping',
);
expect(localRepoCache.isModified()).toBeUndefined();
});
it('skip when not found', async () => {
const localRepoCache = CacheFactory.get(
'some/repo',
'0123456789abcdef',
'local',
);
await localRepoCache.load(); // readCacheFile is mocked but has no return value set - therefore returns undefined
expect(logger.debug).not.toHaveBeenCalledExactlyOnceWith();
expect(localRepoCache.isModified()).toBeUndefined();
});
it('loads previously stored cache from disk', async () => {
const data: RepoCacheData = { semanticCommits: 'enabled' };
const cacheRecord = await createCacheRecord(data);
fs.readCacheFile.mockResolvedValue(JSON.stringify(cacheRecord));
const localRepoCache = CacheFactory.get(
'some/repo',
'0123456789abcdef',
'local',
);
await localRepoCache.load();
expect(localRepoCache.getData()).toEqual(data);
expect(localRepoCache.isModified()).toBeFalse();
});
it('resets if fingerprint does not match', async () => {
const data: RepoCacheData = { semanticCommits: 'enabled' };
const cacheRecord: RepoCacheRecord = {
...(await createCacheRecord(data)),
fingerprint: '111',
};
fs.readCacheFile.mockResolvedValue(JSON.stringify(cacheRecord));
const cache1 = CacheFactory.get('some/repo', '111', 'local');
await cache1.load();
expect(cache1.getData()).toEqual(data);
const cache2 = CacheFactory.get('some/repo', '222', 'local');
await cache2.load();
expect(cache2.getData()).toBeEmpty();
});
it('handles invalid data', async () => {
fs.readCacheFile.mockResolvedValue(JSON.stringify({ foo: 'bar' }));
const localRepoCache = CacheFactory.get(
'some/repo',
'0123456789abcdef',
'local',
);
await localRepoCache.load();
expect(localRepoCache.getData()).toBeEmpty();
expect(localRepoCache.isModified()).toBeUndefined();
});
it('handles file read error', async () => {
fs.readCacheFile.mockRejectedValue(new Error('unknown error'));
const localRepoCache = CacheFactory.get(
'some/repo',
'0123456789abcdef',
'local',
);
await localRepoCache.load();
const data = localRepoCache.getData();
expect(data).toBeEmpty();
expect(localRepoCache.isModified()).toBeUndefined();
});
it('handles invalid json', async () => {
fs.readCacheFile.mockResolvedValue('{1');
const localRepoCache = CacheFactory.get(
'some/repo',
'0123456789abcdef',
'local',
);
await localRepoCache.load();
expect(localRepoCache.getData()).toBeEmpty();
expect(localRepoCache.isModified()).toBeUndefined();
});
it('resets if repository does not match', async () => {
const cacheRecord = createCacheRecord({ semanticCommits: 'enabled' });
fs.readCacheFile.mockResolvedValueOnce(JSON.stringify(cacheRecord));
const localRepoCache = CacheFactory.get(
'some/repo',
'0123456789abcdef',
'local',
);
await localRepoCache.load();
expect(localRepoCache.getData()).toBeEmpty();
expect(localRepoCache.isModified()).toBeUndefined();
});
it('saves modified cache data to file', async () => {
const oldCacheRecord = await createCacheRecord({
semanticCommits: 'enabled',
});
const cacheType = 'protocol://domain/path';
fs.readCacheFile.mockResolvedValueOnce(JSON.stringify(oldCacheRecord));
const localRepoCache = CacheFactory.get(
'some/repo',
'0123456789abcdef',
cacheType,
);
await localRepoCache.load();
const data = localRepoCache.getData();
data.semanticCommits = 'disabled';
await localRepoCache.save();
const newCacheRecord = await createCacheRecord({
semanticCommits: 'disabled',
});
expect(localRepoCache instanceof RepoCacheLocal).toBeTrue();
expect(localRepoCache.isModified()).toBeTrue();
expect(logger.warn).toHaveBeenCalledWith(
{ cacheType },
`Repository cache type not supported using type "local" instead`,
);
expect(fs.outputCacheFile).toHaveBeenCalledExactlyOnceWith(
'renovate/repository/github/some/repo.json',
JSON.stringify(newCacheRecord),
);
});
it('does not write cache that is not changed', async () => {
const oldCacheRecord = await createCacheRecord({
semanticCommits: 'enabled',
});
const cacheType = 'protocol://domain/path';
fs.readCacheFile.mockResolvedValueOnce(JSON.stringify(oldCacheRecord));
const localRepoCache = CacheFactory.get(
'some/repo',
'0123456789abcdef',
cacheType,
);
await localRepoCache.load();
expect(localRepoCache.getData()).toEqual({ semanticCommits: 'enabled' });
expect(localRepoCache.isModified()).toBeFalse();
await localRepoCache.save();
expect(fs.outputCacheFile).not.toHaveBeenCalledExactlyOnceWith();
});
it('does not write cache when only key order has changed', async () => {
const oldCacheRecord = await createCacheRecord({
configFileName: 'renovate.json',
semanticCommits: 'enabled',
});
const cacheType = 'protocol://domain/path';
fs.readCacheFile.mockResolvedValueOnce(JSON.stringify(oldCacheRecord));
const localRepoCache = CacheFactory.get(
'some/repo',
'0123456789abcdef',
cacheType,
);
await localRepoCache.load();
const data = localRepoCache.getData();
delete data.configFileName;
delete data.semanticCommits;
data.semanticCommits = 'enabled';
data.configFileName = 'renovate.json';
expect(localRepoCache.isModified()).toBeFalse();
await localRepoCache.save();
expect(fs.outputCacheFile).not.toHaveBeenCalledExactlyOnceWith();
});
});