210 lines
6.9 KiB
TypeScript
210 lines
6.9 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import { createHash } from "node:crypto";
|
|
import {
|
|
parseImageReference,
|
|
resolveRegistryDigest,
|
|
} from "../../server/registry";
|
|
|
|
describe("OCI registry digest resolution", () => {
|
|
test("parses tags, ports, defaults, and pinned references", () => {
|
|
expect(parseImageReference("localhost:5000/team/image:v1")).toMatchObject({
|
|
registry: "localhost:5000",
|
|
repository: "team/image",
|
|
reference: "v1",
|
|
});
|
|
expect(
|
|
parseImageReference("registry.example.com/team/image").reference,
|
|
).toBe("latest");
|
|
const pinned = `sha256:${"a".repeat(64)}` as const;
|
|
expect(
|
|
parseImageReference(`registry.example.com/team/image@${pinned}`).digest,
|
|
).toBe(pinned);
|
|
expect(() => parseImageReference("image:latest")).toThrow("registry host");
|
|
expect(() =>
|
|
parseImageReference(`registry.example.com/../image@${pinned}`),
|
|
).toThrow("Invalid image reference");
|
|
});
|
|
|
|
test("resolves an anonymous manifest using the advertised digest", async () => {
|
|
const expected = `sha256:${"b".repeat(64)}` as const;
|
|
const calls: Array<{ url: string; authorization: string | null }> = [];
|
|
const fetcher = async (
|
|
input: string | URL | Request,
|
|
init?: RequestInit,
|
|
) => {
|
|
const headers = new Headers(init?.headers);
|
|
calls.push({
|
|
url: String(input),
|
|
authorization: headers.get("authorization"),
|
|
});
|
|
return new Response("manifest", {
|
|
headers: { "docker-content-digest": expected },
|
|
});
|
|
};
|
|
expect(
|
|
await resolveRegistryDigest("registry.example.com/team/image:v1", {
|
|
fetch: fetcher,
|
|
}),
|
|
).toBe(expected);
|
|
expect(calls).toEqual([
|
|
{
|
|
url: "https://registry.example.com/v2/team/image/manifests/v1",
|
|
authorization: null,
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("caches successful digest resolutions with deterministic expiry and bounds", async () => {
|
|
let now = 0;
|
|
let calls = 0;
|
|
const fetcher = async () => {
|
|
calls += 1;
|
|
return new Response("manifest", {
|
|
headers: { "docker-content-digest": `sha256:${"d".repeat(64)}` },
|
|
});
|
|
};
|
|
const options = {
|
|
fetch: fetcher,
|
|
cacheTtlMs: 100,
|
|
cacheMaxEntries: 1,
|
|
clock: () => now,
|
|
};
|
|
|
|
await resolveRegistryDigest("cache.example.com/team/first:v1", options);
|
|
await resolveRegistryDigest("cache.example.com/team/first:v1", options);
|
|
expect(calls).toBe(1);
|
|
|
|
now = 100;
|
|
await resolveRegistryDigest("cache.example.com/team/first:v1", options);
|
|
expect(calls).toBe(2);
|
|
|
|
await resolveRegistryDigest("cache.example.com/team/second:v1", options);
|
|
await resolveRegistryDigest("cache.example.com/team/first:v1", options);
|
|
expect(calls).toBe(4);
|
|
});
|
|
|
|
test("does not cache failed resolutions or share entries across credentials", async () => {
|
|
let calls = 0;
|
|
const failing = async () => {
|
|
calls += 1;
|
|
return new Response("unavailable", { status: 503 });
|
|
};
|
|
const options = { fetch: failing, cacheTtlMs: 1_000 };
|
|
await expect(
|
|
resolveRegistryDigest("failure.example.com/team/image:v1", options),
|
|
).rejects.toThrow("503");
|
|
await expect(
|
|
resolveRegistryDigest("failure.example.com/team/image:v1", options),
|
|
).rejects.toThrow("503");
|
|
expect(calls).toBe(2);
|
|
|
|
const authorizedCalls: string[] = [];
|
|
const authorized = async (
|
|
_input: string | URL | Request,
|
|
init?: RequestInit,
|
|
) => {
|
|
authorizedCalls.push(new Headers(init?.headers).get("authorization")!);
|
|
return new Response("manifest", {
|
|
headers: { "docker-content-digest": `sha256:${"e".repeat(64)}` },
|
|
});
|
|
};
|
|
for (const username of ["one", "two"]) {
|
|
await resolveRegistryDigest("credentials.example.com/team/image:v1", {
|
|
fetch: authorized,
|
|
credentials: { username, password: "password" },
|
|
});
|
|
}
|
|
expect(authorizedCalls).toHaveLength(2);
|
|
});
|
|
|
|
test("resolves through a trusted internal registry origin", async () => {
|
|
const expected = `sha256:${"c".repeat(64)}` as const;
|
|
let requested = "";
|
|
await expect(
|
|
resolveRegistryDigest("registry.example.com/team/image:v1", {
|
|
origin: "http://registry.registry.svc.cluster.local:5000/",
|
|
insecure: true,
|
|
fetch: async (input) => {
|
|
requested = String(input);
|
|
return new Response("manifest", {
|
|
headers: { "docker-content-digest": expected },
|
|
});
|
|
},
|
|
}),
|
|
).resolves.toBe(expected);
|
|
expect(requested).toBe(
|
|
"http://registry.registry.svc.cluster.local:5000/v2/team/image/manifests/v1",
|
|
);
|
|
});
|
|
|
|
test("follows a standard bearer challenge and hashes a digest-less response", async () => {
|
|
const body = '{"schemaVersion":2}';
|
|
const expected =
|
|
`sha256:${createHash("sha256").update(body).digest("hex")}` as const;
|
|
const calls: Array<{ url: string; authorization: string | null }> = [];
|
|
const fetcher = async (
|
|
input: string | URL | Request,
|
|
init?: RequestInit,
|
|
) => {
|
|
const url = String(input);
|
|
const authorization = new Headers(init?.headers).get("authorization");
|
|
calls.push({ url, authorization });
|
|
if (url.startsWith("https://auth.example/token")) {
|
|
expect(new URL(url).searchParams.get("scope")).toBe(
|
|
"repository:team/image:pull",
|
|
);
|
|
expect(authorization).toBe(
|
|
`Basic ${Buffer.from("user:pass").toString("base64")}`,
|
|
);
|
|
return Response.json({ access_token: "registry-token" });
|
|
}
|
|
if (authorization !== "Bearer registry-token") {
|
|
return new Response("unauthorized", {
|
|
status: 401,
|
|
headers: {
|
|
"www-authenticate":
|
|
'Bearer realm="https://auth.example/token",service="registry.example.com"',
|
|
},
|
|
});
|
|
}
|
|
return new Response(body, {
|
|
headers: {
|
|
"content-type": "application/vnd.oci.image.manifest.v1+json",
|
|
},
|
|
});
|
|
};
|
|
|
|
expect(
|
|
await resolveRegistryDigest("registry.example.com/team/image:v2", {
|
|
fetch: fetcher,
|
|
credentials: { username: "user", password: "pass" },
|
|
}),
|
|
).toBe(expected);
|
|
expect(calls).toHaveLength(3);
|
|
expect(calls[2]?.authorization).toBe("Bearer registry-token");
|
|
});
|
|
|
|
test("rejects unsupported challenges and malformed advertised digests", async () => {
|
|
const unauthorized = async () =>
|
|
new Response("no", {
|
|
status: 401,
|
|
headers: { "www-authenticate": 'Basic realm="registry"' },
|
|
});
|
|
await expect(
|
|
resolveRegistryDigest("registry.example.com/team/image", {
|
|
fetch: unauthorized,
|
|
}),
|
|
).rejects.toThrow("401");
|
|
|
|
const malformed = async () =>
|
|
new Response("body", {
|
|
headers: { "docker-content-digest": "sha256:bad" },
|
|
});
|
|
await expect(
|
|
resolveRegistryDigest("registry.example.com/team/image", {
|
|
fetch: malformed,
|
|
}),
|
|
).rejects.toThrow("Invalid SHA-256");
|
|
});
|
|
});
|