multi registry support v3

This commit is contained in:
2025-11-19 15:32:00 +00:00
parent e4480bff5d
commit 754ec7b7db
19 changed files with 1661 additions and 1740 deletions

149
test/test.helper.ts Normal file
View File

@@ -0,0 +1,149 @@
import * as qenv from '@push.rocks/qenv';
import { SmartRegistry, IRegistryConfig } from '../ts/index.js';
const testQenv = new qenv.Qenv('./', './.nogit');
/**
* Create a test SmartRegistry instance with both OCI and NPM enabled
*/
export async function createTestRegistry(): Promise<SmartRegistry> {
// Read S3 config from env.json
const s3AccessKey = await testQenv.getEnvVarOnDemand('S3_ACCESS_KEY');
const s3SecretKey = await testQenv.getEnvVarOnDemand('S3_SECRET_KEY');
const s3Endpoint = await testQenv.getEnvVarOnDemand('S3_ENDPOINT');
const s3Port = await testQenv.getEnvVarOnDemand('S3_PORT');
const config: IRegistryConfig = {
storage: {
accessKey: s3AccessKey || 'minioadmin',
accessSecret: s3SecretKey || 'minioadmin',
endpoint: s3Endpoint || 'localhost',
port: parseInt(s3Port || '9000', 10),
useSsl: false,
region: 'us-east-1',
bucketName: 'test-registry',
},
auth: {
jwtSecret: 'test-secret-key',
tokenStore: 'memory',
npmTokens: {
enabled: true,
},
ociTokens: {
enabled: true,
realm: 'https://auth.example.com/token',
service: 'test-registry',
},
},
oci: {
enabled: true,
basePath: '/oci',
},
npm: {
enabled: true,
basePath: '/npm',
},
};
const registry = new SmartRegistry(config);
await registry.init();
return registry;
}
/**
* Helper to create test authentication tokens
*/
export async function createTestTokens(registry: SmartRegistry) {
const authManager = registry.getAuthManager();
// Authenticate and create tokens
const userId = await authManager.authenticate({
username: 'testuser',
password: 'testpass',
});
if (!userId) {
throw new Error('Failed to authenticate test user');
}
// Create NPM token
const npmToken = await authManager.createNpmToken(userId, false);
// Create OCI token with full access
const ociToken = await authManager.createOciToken(
userId,
['oci:repository:*:*'],
3600
);
return { npmToken, ociToken, userId };
}
/**
* Helper to calculate SHA-256 digest in OCI format
*/
export function calculateDigest(data: Buffer): string {
const crypto = require('crypto');
const hash = crypto.createHash('sha256').update(data).digest('hex');
return `sha256:${hash}`;
}
/**
* Helper to create a minimal valid OCI manifest
*/
export function createTestManifest(configDigest: string, layerDigest: string) {
return {
schemaVersion: 2,
mediaType: 'application/vnd.oci.image.manifest.v1+json',
config: {
mediaType: 'application/vnd.oci.image.config.v1+json',
size: 123,
digest: configDigest,
},
layers: [
{
mediaType: 'application/vnd.oci.image.layer.v1.tar+gzip',
size: 456,
digest: layerDigest,
},
],
};
}
/**
* Helper to create a minimal valid NPM packument
*/
export function createTestPackument(packageName: string, version: string, tarballData: Buffer) {
const crypto = require('crypto');
const shasum = crypto.createHash('sha1').update(tarballData).digest('hex');
const integrity = `sha512-${crypto.createHash('sha512').update(tarballData).digest('base64')}`;
return {
name: packageName,
versions: {
[version]: {
name: packageName,
version: version,
description: 'Test package',
main: 'index.js',
scripts: {},
dist: {
shasum: shasum,
integrity: integrity,
tarball: `http://localhost:5000/npm/${packageName}/-/${packageName}-${version}.tgz`,
},
},
},
'dist-tags': {
latest: version,
},
_attachments: {
[`${packageName}-${version}.tgz`]: {
content_type: 'application/octet-stream',
data: tarballData.toString('base64'),
length: tarballData.length,
},
},
};
}

0
test/test.npm.ts Normal file
View File

297
test/test.oci.ts Normal file
View File

@@ -0,0 +1,297 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { SmartRegistry } from '../ts/index.js';
import { createTestRegistry, createTestTokens, calculateDigest, createTestManifest } from './test.helper.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/v2/',
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/v2/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/v2/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/v2/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/v2/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/v2/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/v2/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/v2/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/v2/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/v2/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/v2/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/v2/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/v2/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/v2/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/v2/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/v2/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/v2/test-repo/manifests/v1.0.0',
headers: {
// No authorization header
},
query: {},
});
expect(response.status).toEqual(401);
expect(response.headers['WWW-Authenticate']).toInclude('Bearer');
});
export default tap.start();