Files

78 lines
3.2 KiB
TypeScript

/**
* buildPinnedRef: turn a repo:tag + RepoDigests into a supply-chain-hardened
* `repo:tag@sha256:...` reference for the "copy pinned reference" button.
*/
import { describe, it, expect } from 'bun:test';
import { buildPinnedRef, shortDigest, repoBaseOf } from '../src/lib/utils/pinned-ref';
describe('repoBaseOf (#1437: digest-pinned running image)', () => {
it('strips both tag and digest from a digest-pinned running image', () => {
// The bug: the running image is repo:tag@sha256:..., and cutting at the last
// colon lands inside @sha256:, leaving repo:tag@sha256 and duplicating the tag.
expect(
repoBaseOf(
'ghcr.io/github/github-mcp-server:v1.10.0@sha256:1817b57d43916532dc002bdc5f344d639bd9fb54a9148d42168458f7c3280567'
)
).toBe('ghcr.io/github/github-mcp-server');
expect(
repoBaseOf(
'ghcr.io/searxng/searxng:2026.8.19-5ffd32ca2@sha256:3cb8eba87bb347613fab9dfe87d448c21300b8f0648295c93b85f4246e93ae73'
)
).toBe('ghcr.io/searxng/searxng');
});
it('handles a plain repo:tag, a bare repo, and a host:port registry', () => {
expect(repoBaseOf('ghcr.io/owner/app:1.2')).toBe('ghcr.io/owner/app');
expect(repoBaseOf('nginx:1.25')).toBe('nginx');
expect(repoBaseOf('nginx')).toBe('nginx');
expect(repoBaseOf('registry.local:5000/app:1.2')).toBe('registry.local:5000/app');
expect(repoBaseOf('registry.local:5000/app:1.2@sha256:abc')).toBe('registry.local:5000/app');
});
});
describe('buildPinnedRef', () => {
it('keeps the tag and pins the matching digest', () => {
expect(buildPinnedRef('nginx:1.25', ['nginx@sha256:abc'])).toBe('nginx:1.25@sha256:abc');
});
it('matches the digest by repo when several are present', () => {
const digests = ['other/app@sha256:zzz', 'nginx@sha256:abc'];
expect(buildPinnedRef('nginx:1.25', digests)).toBe('nginx:1.25@sha256:abc');
});
it('handles a registry host:port in the repo', () => {
expect(buildPinnedRef('registry.example.com:5000/app:v2', ['registry.example.com:5000/app@sha256:def']))
.toBe('registry.example.com:5000/app:v2@sha256:def');
});
it('falls back to the sole digest when the repo does not string-match', () => {
// Docker normalizes library images; the tag may read `nginx` while the digest
// reads `docker.io/library/nginx`. With a single digest, use it.
expect(buildPinnedRef('nginx:latest', ['docker.io/library/nginx@sha256:abc']))
.toBe('nginx:latest@sha256:abc');
});
it('produces repo@digest when there is no tag', () => {
expect(buildPinnedRef('nginx', ['nginx@sha256:abc'])).toBe('nginx@sha256:abc');
});
it('returns null when the image has no RepoDigests (local-only image)', () => {
expect(buildPinnedRef('myapp:dev', [])).toBeNull();
expect(buildPinnedRef('myapp:dev', undefined)).toBeNull();
});
it('returns null when multiple digests exist and none match the repo', () => {
expect(buildPinnedRef('nginx:1.25', ['a/b@sha256:1', 'c/d@sha256:2'])).toBeNull();
});
});
describe('shortDigest', () => {
it('keeps the sha256: prefix and shortens the hex', () => {
expect(shortDigest('nginx:1.25', ['nginx@sha256:abcdef0123456789abcdef'], 12)).toBe('sha256:abcdef012345');
});
it('returns null when there is no usable digest', () => {
expect(shortDigest('myapp:dev', [])).toBeNull();
});
});