78 lines
3.2 KiB
TypeScript
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();
|
|
});
|
|
});
|