The OCI handler had /v2/ baked into all regex patterns and Location headers. When basePath was set to /v2 (as in stack.gallery), stripping it removed the prefix that patterns expected, causing all OCI endpoints to 404. Now patterns match on bare paths after basePath stripping, working correctly regardless of the basePath value. Also adds configurable apiPrefix to OCI upstream class (default /v2) for registries behind reverse proxies with custom path prefixes.
304 lines
9.0 KiB
TypeScript
304 lines
9.0 KiB
TypeScript
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
|
import { SmartRegistry } from '../ts/index.js';
|
|
import { createTestRegistry, createTestTokens, calculateDigest, createTestManifest } from './helpers/registry.js';
|
|
|
|
let registry: SmartRegistry;
|
|
let ociToken: string;
|
|
let testBlobDigest: string;
|
|
let testConfigDigest: string;
|
|
let testManifestDigest: string;
|
|
|
|
// Test data
|
|
const testBlobData = Buffer.from('Hello from OCI test blob!', 'utf-8');
|
|
const testConfigData = Buffer.from(JSON.stringify({ arch: 'amd64', os: 'linux' }), 'utf-8');
|
|
|
|
tap.test('OCI: should create registry instance', async () => {
|
|
registry = await createTestRegistry();
|
|
const tokens = await createTestTokens(registry);
|
|
ociToken = tokens.ociToken;
|
|
|
|
expect(registry).toBeInstanceOf(SmartRegistry);
|
|
expect(ociToken).toBeTypeOf('string');
|
|
});
|
|
|
|
tap.test('OCI: should handle version check (GET /v2/)', async () => {
|
|
const response = await registry.handleRequest({
|
|
method: 'GET',
|
|
path: '/oci/',
|
|
headers: {},
|
|
query: {},
|
|
});
|
|
|
|
expect(response.status).toEqual(200);
|
|
expect(response.headers['Docker-Distribution-API-Version']).toEqual('registry/2.0');
|
|
});
|
|
|
|
tap.test('OCI: should initiate blob upload (POST /v2/{name}/blobs/uploads/)', async () => {
|
|
const response = await registry.handleRequest({
|
|
method: 'POST',
|
|
path: '/oci/test-repo/blobs/uploads/',
|
|
headers: {
|
|
Authorization: `Bearer ${ociToken}`,
|
|
},
|
|
query: {},
|
|
});
|
|
|
|
expect(response.status).toEqual(202);
|
|
expect(response.headers).toHaveProperty('Location');
|
|
expect(response.headers).toHaveProperty('Docker-Upload-UUID');
|
|
});
|
|
|
|
tap.test('OCI: should upload blob in single PUT', async () => {
|
|
testBlobDigest = calculateDigest(testBlobData);
|
|
|
|
const response = await registry.handleRequest({
|
|
method: 'POST',
|
|
path: '/oci/test-repo/blobs/uploads/',
|
|
headers: {
|
|
Authorization: `Bearer ${ociToken}`,
|
|
},
|
|
query: {
|
|
digest: testBlobDigest,
|
|
},
|
|
body: testBlobData,
|
|
});
|
|
|
|
expect(response.status).toEqual(201);
|
|
expect(response.headers).toHaveProperty('Location');
|
|
expect(response.headers['Docker-Content-Digest']).toEqual(testBlobDigest);
|
|
});
|
|
|
|
tap.test('OCI: should upload config blob', async () => {
|
|
testConfigDigest = calculateDigest(testConfigData);
|
|
|
|
const response = await registry.handleRequest({
|
|
method: 'POST',
|
|
path: '/oci/test-repo/blobs/uploads/',
|
|
headers: {
|
|
Authorization: `Bearer ${ociToken}`,
|
|
},
|
|
query: {
|
|
digest: testConfigDigest,
|
|
},
|
|
body: testConfigData,
|
|
});
|
|
|
|
expect(response.status).toEqual(201);
|
|
expect(response.headers['Docker-Content-Digest']).toEqual(testConfigDigest);
|
|
});
|
|
|
|
tap.test('OCI: should check if blob exists (HEAD /v2/{name}/blobs/{digest})', async () => {
|
|
const response = await registry.handleRequest({
|
|
method: 'HEAD',
|
|
path: `/oci/test-repo/blobs/${testBlobDigest}`,
|
|
headers: {
|
|
Authorization: `Bearer ${ociToken}`,
|
|
},
|
|
query: {},
|
|
});
|
|
|
|
expect(response.status).toEqual(200);
|
|
expect(response.headers['Content-Length']).toEqual(testBlobData.length.toString());
|
|
expect(response.headers['Docker-Content-Digest']).toEqual(testBlobDigest);
|
|
});
|
|
|
|
tap.test('OCI: should retrieve blob (GET /v2/{name}/blobs/{digest})', async () => {
|
|
const response = await registry.handleRequest({
|
|
method: 'GET',
|
|
path: `/oci/test-repo/blobs/${testBlobDigest}`,
|
|
headers: {
|
|
Authorization: `Bearer ${ociToken}`,
|
|
},
|
|
query: {},
|
|
});
|
|
|
|
expect(response.status).toEqual(200);
|
|
expect(response.body).toBeInstanceOf(Buffer);
|
|
expect((response.body as Buffer).toString('utf-8')).toEqual('Hello from OCI test blob!');
|
|
expect(response.headers['Docker-Content-Digest']).toEqual(testBlobDigest);
|
|
});
|
|
|
|
tap.test('OCI: should upload manifest (PUT /v2/{name}/manifests/{reference})', async () => {
|
|
const manifest = createTestManifest(testConfigDigest, testBlobDigest);
|
|
const manifestJson = JSON.stringify(manifest);
|
|
const manifestBuffer = Buffer.from(manifestJson, 'utf-8');
|
|
testManifestDigest = calculateDigest(manifestBuffer);
|
|
|
|
const response = await registry.handleRequest({
|
|
method: 'PUT',
|
|
path: '/oci/test-repo/manifests/v1.0.0',
|
|
headers: {
|
|
Authorization: `Bearer ${ociToken}`,
|
|
'Content-Type': 'application/vnd.oci.image.manifest.v1+json',
|
|
},
|
|
query: {},
|
|
body: manifestBuffer,
|
|
});
|
|
|
|
expect(response.status).toEqual(201);
|
|
expect(response.headers).toHaveProperty('Location');
|
|
expect(response.headers['Docker-Content-Digest']).toMatch(/^sha256:[a-f0-9]{64}$/);
|
|
});
|
|
|
|
tap.test('OCI: should retrieve manifest by tag (GET /v2/{name}/manifests/{reference})', async () => {
|
|
const response = await registry.handleRequest({
|
|
method: 'GET',
|
|
path: '/oci/test-repo/manifests/v1.0.0',
|
|
headers: {
|
|
Authorization: `Bearer ${ociToken}`,
|
|
Accept: 'application/vnd.oci.image.manifest.v1+json',
|
|
},
|
|
query: {},
|
|
});
|
|
|
|
expect(response.status).toEqual(200);
|
|
expect(response.body).toBeInstanceOf(Buffer);
|
|
|
|
const manifest = JSON.parse((response.body as Buffer).toString('utf-8'));
|
|
expect(manifest.schemaVersion).toEqual(2);
|
|
expect(manifest.config.digest).toEqual(testConfigDigest);
|
|
expect(manifest.layers[0].digest).toEqual(testBlobDigest);
|
|
});
|
|
|
|
tap.test('OCI: should retrieve manifest by digest (GET /v2/{name}/manifests/{digest})', async () => {
|
|
const response = await registry.handleRequest({
|
|
method: 'GET',
|
|
path: `/oci/test-repo/manifests/${testManifestDigest}`,
|
|
headers: {
|
|
Authorization: `Bearer ${ociToken}`,
|
|
Accept: 'application/vnd.oci.image.manifest.v1+json',
|
|
},
|
|
query: {},
|
|
});
|
|
|
|
expect(response.status).toEqual(200);
|
|
expect(response.headers['Docker-Content-Digest']).toEqual(testManifestDigest);
|
|
});
|
|
|
|
tap.test('OCI: should check if manifest exists (HEAD /v2/{name}/manifests/{reference})', async () => {
|
|
const response = await registry.handleRequest({
|
|
method: 'HEAD',
|
|
path: '/oci/test-repo/manifests/v1.0.0',
|
|
headers: {
|
|
Authorization: `Bearer ${ociToken}`,
|
|
Accept: 'application/vnd.oci.image.manifest.v1+json',
|
|
},
|
|
query: {},
|
|
});
|
|
|
|
expect(response.status).toEqual(200);
|
|
expect(response.headers['Docker-Content-Digest']).toMatch(/^sha256:[a-f0-9]{64}$/);
|
|
});
|
|
|
|
tap.test('OCI: should list tags (GET /v2/{name}/tags/list)', async () => {
|
|
const response = await registry.handleRequest({
|
|
method: 'GET',
|
|
path: '/oci/test-repo/tags/list',
|
|
headers: {
|
|
Authorization: `Bearer ${ociToken}`,
|
|
},
|
|
query: {},
|
|
});
|
|
|
|
expect(response.status).toEqual(200);
|
|
expect(response.body).toHaveProperty('tags');
|
|
|
|
const tagList = response.body as any;
|
|
expect(tagList.name).toEqual('test-repo');
|
|
expect(tagList.tags).toBeInstanceOf(Array);
|
|
expect(tagList.tags).toContain('v1.0.0');
|
|
});
|
|
|
|
tap.test('OCI: should handle pagination for tag list', async () => {
|
|
const response = await registry.handleRequest({
|
|
method: 'GET',
|
|
path: '/oci/test-repo/tags/list',
|
|
headers: {
|
|
Authorization: `Bearer ${ociToken}`,
|
|
},
|
|
query: {
|
|
n: '10',
|
|
},
|
|
});
|
|
|
|
expect(response.status).toEqual(200);
|
|
expect(response.body).toHaveProperty('tags');
|
|
});
|
|
|
|
tap.test('OCI: should return 404 for non-existent blob', async () => {
|
|
const response = await registry.handleRequest({
|
|
method: 'GET',
|
|
path: '/oci/test-repo/blobs/sha256:0000000000000000000000000000000000000000000000000000000000000000',
|
|
headers: {
|
|
Authorization: `Bearer ${ociToken}`,
|
|
},
|
|
query: {},
|
|
});
|
|
|
|
expect(response.status).toEqual(404);
|
|
expect(response.body).toHaveProperty('errors');
|
|
});
|
|
|
|
tap.test('OCI: should return 404 for non-existent manifest', async () => {
|
|
const response = await registry.handleRequest({
|
|
method: 'GET',
|
|
path: '/oci/test-repo/manifests/non-existent-tag',
|
|
headers: {
|
|
Authorization: `Bearer ${ociToken}`,
|
|
Accept: 'application/vnd.oci.image.manifest.v1+json',
|
|
},
|
|
query: {},
|
|
});
|
|
|
|
expect(response.status).toEqual(404);
|
|
expect(response.body).toHaveProperty('errors');
|
|
});
|
|
|
|
tap.test('OCI: should delete manifest (DELETE /v2/{name}/manifests/{digest})', async () => {
|
|
const response = await registry.handleRequest({
|
|
method: 'DELETE',
|
|
path: `/oci/test-repo/manifests/${testManifestDigest}`,
|
|
headers: {
|
|
Authorization: `Bearer ${ociToken}`,
|
|
},
|
|
query: {},
|
|
});
|
|
|
|
expect(response.status).toEqual(202);
|
|
});
|
|
|
|
tap.test('OCI: should delete blob (DELETE /v2/{name}/blobs/{digest})', async () => {
|
|
const response = await registry.handleRequest({
|
|
method: 'DELETE',
|
|
path: `/oci/test-repo/blobs/${testBlobDigest}`,
|
|
headers: {
|
|
Authorization: `Bearer ${ociToken}`,
|
|
},
|
|
query: {},
|
|
});
|
|
|
|
expect(response.status).toEqual(202);
|
|
});
|
|
|
|
tap.test('OCI: should handle unauthorized requests', async () => {
|
|
const response = await registry.handleRequest({
|
|
method: 'GET',
|
|
path: '/oci/test-repo/manifests/v1.0.0',
|
|
headers: {
|
|
// No authorization header
|
|
},
|
|
query: {},
|
|
});
|
|
|
|
expect(response.status).toEqual(401);
|
|
expect(response.headers['WWW-Authenticate']).toInclude('Bearer');
|
|
});
|
|
|
|
tap.postTask('cleanup registry', async () => {
|
|
if (registry) {
|
|
registry.destroy();
|
|
}
|
|
});
|
|
|
|
export default tap.start();
|