Files
renovate/lib/modules/datasource/docker/dockerhub-cache.spec.ts
Sebastian Poxhofer e0cec3dc95 refactor(lint): add types-location rule and enable it for lib/modules (#45497)
feat(lint): add `types-location` rule and enable it for `lib/modules`

Reviewers consistently ask for exported types to be moved into a
colocated `types.ts`. Add a `renovate/types-location` oxlint rule that
reports `export interface` / `export type` declarations outside such a
file, and turn it on across `lib/modules`.

Re-exports (`export type { X }`, `export type *`), file-local types, and
values (enums, classes, functions) are left alone. `schema.ts` files and
`schema/` directories are excluded via config, matching how
`zod-schema-location` is configured — inferred types belong next to the
schema they are derived from.

Relocating the 87 existing violations means:

- new `types.ts` files for the modules that had none
- `terragrunt/common.ts` folded into its `types.ts`, as it held nothing
  else
- `RangeBound` in `versioning/gradle/compare.ts` un-exported, since only
  a file-local interface referenced it
- `bazel-module/parser/fragments.ts` split from its inferred types, which
  now live in `parser/types.ts` and infer through a namespace import so
  each schema keeps its merged name
2026-08-27 19:52:09 +00:00

245 lines
6.4 KiB
TypeScript

import * as _packageCache from '../../../util/cache/package/index.ts';
import { DockerHubCache } from './dockerhub-cache.ts';
import type { DockerHubTag } from './schema.ts';
import type { DockerHubCacheData } from './types.ts';
vi.mock('../../../util/cache/package/index.ts');
const packageCache = vi.mocked(_packageCache);
function oldCacheData(): DockerHubCacheData {
return {
items: {
1: {
id: 1,
last_updated: '2022-01-01',
name: '1',
tag_last_pushed: '2022-01-01',
digest: 'sha256:111',
images: [],
},
2: {
id: 2,
last_updated: '2022-01-02',
name: '2',
tag_last_pushed: '2022-01-02',
digest: 'sha256:222',
images: [
{ architecture: 'amd64', digest: 'sha256:222-amd64' },
{ architecture: 'arm64', digest: 'sha256:222-arm64' },
],
},
3: {
id: 3,
last_updated: '2022-01-03',
name: '3',
tag_last_pushed: '2022-01-03',
digest: 'sha256:333',
images: [],
},
},
updatedAt: '2022-01-01',
};
}
function newItem(): DockerHubTag {
return {
id: 4,
last_updated: '2022-01-04',
name: '4',
tag_last_pushed: '2022-01-04',
digest: 'sha256:444',
images: [],
};
}
function newCacheData(): DockerHubCacheData {
const { items } = oldCacheData();
const item = newItem();
return {
items: {
...items,
[item.id]: item,
},
updatedAt: '2022-01-04',
};
}
describe('modules/datasource/docker/dockerhub-cache', () => {
const dockerRepository = 'foo/bar';
it('initializes empty cache', async () => {
packageCache.get.mockResolvedValue(undefined);
const res = await DockerHubCache.init(dockerRepository);
expect(res).toEqual({
dockerRepository,
cache: {
items: {},
updatedAt: null,
},
isChanged: false,
reconciledIds: new Set(),
});
});
it('initializes cache with data', async () => {
const oldCache = oldCacheData();
packageCache.get.mockResolvedValue(oldCache);
const res = await DockerHubCache.init(dockerRepository);
expect(res).toEqual({
dockerRepository,
cache: oldCache,
isChanged: false,
reconciledIds: new Set(),
});
});
it('reconciles new items', async () => {
const oldCache = oldCacheData();
const newCache = newCacheData();
packageCache.get.mockResolvedValue(oldCache);
const cache = await DockerHubCache.init(dockerRepository);
const newItems: DockerHubTag[] = [newItem()];
const needNextPage = cache.reconcile(newItems, 4);
expect(needNextPage).toBe(true);
expect(cache).toEqual({
cache: newCache,
dockerRepository: 'foo/bar',
isChanged: true,
reconciledIds: new Set([4]),
});
const res = cache.getItems();
expect(res).toEqual(Object.values(newCache.items));
await cache.save();
expect(packageCache.set).toHaveBeenCalledExactlyOnceWith(
'datasource-docker-hub-cache',
'foo/bar',
newCache,
3 * 60 * 24 * 30,
);
});
it('reconciles existing items', async () => {
const oldCache = oldCacheData();
packageCache.get.mockResolvedValue(oldCache);
const cache = await DockerHubCache.init(dockerRepository);
const items: DockerHubTag[] = Object.values(oldCache.items);
const needNextPage = cache.reconcile(items, 3);
expect(needNextPage).toBe(false);
expect(cache).toEqual({
cache: oldCache,
dockerRepository: 'foo/bar',
isChanged: false,
reconciledIds: new Set([1, 2, 3]),
});
const res = cache.getItems();
expect(res).toEqual(items);
await cache.save();
expect(packageCache.set).not.toHaveBeenCalled();
});
it('asks for the next page if the expected count does not match cached items', async () => {
const oldCache = oldCacheData();
packageCache.get.mockResolvedValue(oldCache);
const cache = await DockerHubCache.init(dockerRepository);
const items: DockerHubTag[] = Object.values(oldCache.items);
const needNextPage = cache.reconcile(items, 0);
expect(needNextPage).toBe(true);
expect(cache).toEqual({
cache: oldCache,
dockerRepository: 'foo/bar',
isChanged: false,
reconciledIds: new Set([1, 2, 3]),
});
const res = cache.getItems();
expect(res).toEqual(items);
await cache.save();
expect(packageCache.set).not.toHaveBeenCalled();
});
it('reconciles deleted items', async () => {
const oldCache = oldCacheData();
packageCache.get.mockResolvedValue(oldCache);
const cache = await DockerHubCache.init(dockerRepository);
const items: DockerHubTag[] = [oldCache.items[1], oldCache.items[3]];
const needNextPage = cache.reconcile(items, 2);
expect(needNextPage).toBe(false);
const res = cache.getItems();
expect(res).toEqual(items);
await cache.save();
expect(packageCache.set).toHaveBeenCalled();
});
it('returns cached digest for a known tag', async () => {
packageCache.get.mockResolvedValue(oldCacheData());
const cache = await DockerHubCache.init(dockerRepository);
expect(cache.getDigestForTag('2')).toBe('sha256:222');
expect(cache.getDigestForTag('nope')).toBeNull();
});
it('returns cached arch-specific digest', async () => {
packageCache.get.mockResolvedValue(oldCacheData());
const cache = await DockerHubCache.init(dockerRepository);
expect(cache.getArchDigestForTag('2', 'amd64')).toBe('sha256:222-amd64');
expect(cache.getArchDigestForTag('2', 'arm64')).toBe('sha256:222-arm64');
expect(cache.getArchDigestForTag('2', 'unknown')).toBeNull();
expect(cache.getArchDigestForTag('nope', 'amd64')).toBeNull();
});
it('reconciles from empty cache', async () => {
const item = newItem();
const expectedCache = {
items: {
[item.id]: item,
},
updatedAt: item.last_updated,
};
const cache = await DockerHubCache.init(dockerRepository);
const needNextPage = cache.reconcile([item], 1);
expect(needNextPage).toBe(true);
expect(cache).toEqual({
cache: expectedCache,
dockerRepository: 'foo/bar',
isChanged: true,
reconciledIds: new Set([4]),
});
const res = cache.getItems();
expect(res).toEqual([item]);
await cache.save();
expect(packageCache.set).toHaveBeenCalledExactlyOnceWith(
'datasource-docker-hub-cache',
'foo/bar',
expectedCache,
3 * 60 * 24 * 30,
);
});
});