This commit is contained in:
2025-11-19 20:45:37 +00:00
parent 754ec7b7db
commit cf891cf275
7 changed files with 683 additions and 199 deletions

View File

@@ -1,214 +1,197 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as smartregistry from '../ts/index.js';
import * as qenv from '@push.rocks/qenv';
import { SmartRegistry } from '../ts/index.js';
import { createTestRegistry, createTestTokens } from './helpers/registry.js';
const testQenv = new qenv.Qenv('./', './.nogit');
let registry: SmartRegistry;
let registry: smartregistry.SmartRegistry;
let testToken: string;
tap.test('should create SmartRegistry instance', async () => {
// Create mock callbacks for testing
const loginCallback: smartregistry.TLoginCallback = async (credentials) => {
// Simple mock: return a fake JWT token
const tokenPayload = {
iss: 'test-registry',
sub: credentials.username,
aud: 'test-service',
exp: Math.floor(Date.now() / 1000) + 3600,
nbf: Math.floor(Date.now() / 1000),
iat: Math.floor(Date.now() / 1000),
access: [
{
type: 'repository' as const,
name: 'test/repo',
actions: ['*'] as smartregistry.TRegistryAction[],
},
],
};
// In production, this would be a real JWT
return JSON.stringify(tokenPayload);
};
const authCallback: smartregistry.TAuthCallback = async (token, repository, action) => {
// Simple mock: allow all actions for testing
try {
const payload = JSON.parse(token);
// Check if token has access to the repository
const hasAccess = payload.access.some(
(acc: any) =>
acc.name === repository &&
(acc.actions.includes(action) || acc.actions.includes('*'))
);
return hasAccess;
} catch {
return false;
}
};
// 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: smartregistry.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',
},
serviceName: 'test-registry',
tokenRealm: 'https://auth.example.com/token',
loginCallback,
authCallback,
};
registry = new smartregistry.SmartRegistry(config);
await registry.init();
expect(registry).toBeInstanceOf(smartregistry.SmartRegistry);
tap.test('Integration: should create SmartRegistry instance with both protocols', async () => {
registry = await createTestRegistry();
expect(registry).toBeInstanceOf(SmartRegistry);
expect(registry.isInitialized()).toEqual(true);
});
tap.test('should login and get token', async () => {
testToken = await registry.login({
username: 'testuser',
password: 'testpass',
tap.test('Integration: should have both OCI and NPM registries enabled', async () => {
const ociRegistry = registry.getRegistry('oci');
const npmRegistry = registry.getRegistry('npm');
expect(ociRegistry).toBeDefined();
expect(npmRegistry).toBeDefined();
expect(ociRegistry?.getBasePath()).toEqual('/oci');
expect(npmRegistry?.getBasePath()).toEqual('/npm');
});
tap.test('Integration: should route OCI requests correctly', async () => {
const response = await registry.handleRequest({
method: 'GET',
path: '/oci/v2/',
headers: {},
query: {},
});
expect(testToken).toBeTypeOf('string');
expect(testToken.length).toBeGreaterThan(0);
expect(response.status).toEqual(200);
expect(response.headers['Docker-Distribution-API-Version']).toEqual('registry/2.0');
});
tap.test('should upload a blob via chunked upload', async () => {
const testData = Buffer.from('Hello, OCI Registry!', 'utf-8');
const crypto = await import('crypto');
const digest = `sha256:${crypto.createHash('sha256').update(testData).digest('hex')}`;
tap.test('Integration: should route NPM requests correctly', async () => {
const response = await registry.handleRequest({
method: 'GET',
path: '/npm/some-package',
headers: {},
query: {},
});
// Initiate upload
const initResult = await registry.initiateUpload('test/repo', testToken);
expect(initResult).toHaveProperty('uploadId');
if ('uploadId' in initResult) {
const uploadId = initResult.uploadId;
// Upload chunk
const chunkResult = await registry.uploadChunk(
uploadId,
testData,
`0-${testData.length - 1}`,
testToken
);
expect(chunkResult).toHaveProperty('location');
// Complete upload
const completeResult = await registry.completeUpload(uploadId, digest, testToken);
expect(completeResult).toHaveProperty('digest');
if ('digest' in completeResult) {
expect(completeResult.digest).toEqual(digest);
}
}
// Will return 404 since package doesn't exist, but should route correctly
expect(response.status).toEqual(404);
expect(response.headers['Content-Type']).toEqual('application/json');
});
tap.test('should retrieve a blob', async () => {
const testData = Buffer.from('Hello, OCI Registry!', 'utf-8');
const crypto = await import('crypto');
const digest = `sha256:${crypto.createHash('sha256').update(testData).digest('hex')}`;
tap.test('Integration: should return 404 for unknown paths', async () => {
const response = await registry.handleRequest({
method: 'GET',
path: '/unknown/path',
headers: {},
query: {},
});
const result = await registry.getBlob('test/repo', digest, testToken);
expect(result).toHaveProperty('data');
if ('data' in result) {
expect(result.data.toString('utf-8')).toEqual('Hello, OCI Registry!');
}
expect(response.status).toEqual(404);
expect(response.body).toHaveProperty('error');
expect((response.body as any).error).toEqual('NOT_FOUND');
});
tap.test('should check if blob exists (HEAD)', async () => {
const testData = Buffer.from('Hello, OCI Registry!', 'utf-8');
const crypto = await import('crypto');
const digest = `sha256:${crypto.createHash('sha256').update(testData).digest('hex')}`;
tap.test('Integration: should create and validate tokens', async () => {
const tokens = await createTestTokens(registry);
const result = await registry.headBlob('test/repo', digest, testToken);
expect(result).toHaveProperty('exists');
expect(tokens.npmToken).toBeTypeOf('string');
expect(tokens.ociToken).toBeTypeOf('string');
expect(tokens.userId).toBeTypeOf('string');
if ('exists' in result) {
expect(result.exists).toEqual(true);
expect(result.size).toEqual(testData.length);
}
// Validate NPM token
const authManager = registry.getAuthManager();
const npmTokenObj = await authManager.validateToken(tokens.npmToken, 'npm');
expect(npmTokenObj).toBeDefined();
expect(npmTokenObj?.type).toEqual('npm');
expect(npmTokenObj?.userId).toEqual(tokens.userId);
// Validate OCI token
const ociTokenObj = await authManager.validateToken(tokens.ociToken, 'oci');
expect(ociTokenObj).toBeDefined();
expect(ociTokenObj?.type).toEqual('oci');
expect(ociTokenObj?.userId).toEqual(tokens.userId);
});
tap.test('should upload a manifest', async () => {
const testManifest: smartregistry.IOciManifest = {
schemaVersion: 2,
mediaType: 'application/vnd.oci.image.manifest.v1+json',
config: {
mediaType: 'application/vnd.oci.image.config.v1+json',
size: 123,
digest: 'sha256:' + '0'.repeat(64),
},
layers: [
{
mediaType: 'application/vnd.oci.image.layer.v1.tar+gzip',
size: 456,
digest: 'sha256:' + '1'.repeat(64),
},
],
};
tap.test('Integration: should handle authentication properly', async () => {
const authManager = registry.getAuthManager();
const result = await registry.putManifest(
'test/repo',
'latest',
testManifest,
'application/vnd.oci.image.manifest.v1+json',
testToken
// Create a new user
const userId = await authManager.authenticate({
username: 'newuser',
password: 'newpass',
});
expect(userId).toBeTypeOf('string');
expect(userId).toEqual('newuser');
// Verify login with correct credentials
const userId2 = await authManager.authenticate({
username: 'newuser',
password: 'newpass',
});
expect(userId2).toEqual('newuser');
// Verify login fails with wrong credentials
const userId3 = await authManager.authenticate({
username: 'newuser',
password: 'wrongpass',
});
expect(userId3).toBeNull();
});
tap.test('Integration: should handle scoped permissions correctly', async () => {
const authManager = registry.getAuthManager();
// Create user and token with specific scopes
const userId = await authManager.authenticate({
username: 'scopeduser',
password: 'pass',
});
const npmToken = await authManager.createNpmToken(userId!, false);
const tokenObj = await authManager.validateToken(npmToken, 'npm');
// Check authorization for different resources
const canWrite = await authManager.authorize(
tokenObj,
'npm:package:test-package',
'write'
);
expect(canWrite).toEqual(true);
expect(result).toHaveProperty('digest');
if ('digest' in result) {
expect(result.digest).toMatch(/^sha256:[a-f0-9]{64}$/);
}
const canRead = await authManager.authorize(
tokenObj,
'npm:package:test-package',
'read'
);
expect(canRead).toEqual(true);
});
tap.test('should retrieve a manifest by tag', async () => {
const result = await registry.getManifest('test/repo', 'latest', testToken);
expect(result).toHaveProperty('data');
tap.test('Integration: should respect readonly token restrictions', async () => {
const authManager = registry.getAuthManager();
if ('data' in result) {
const manifest = JSON.parse(result.data.toString('utf-8'));
expect(manifest).toHaveProperty('schemaVersion');
expect(manifest.schemaVersion).toEqual(2);
}
const userId = await authManager.authenticate({
username: 'readonlyuser',
password: 'pass',
});
const readonlyToken = await authManager.createNpmToken(userId!, true);
const tokenObj = await authManager.validateToken(readonlyToken, 'npm');
// Readonly token should allow read
const canRead = await authManager.authorize(
tokenObj,
'npm:package:test-package',
'read'
);
expect(canRead).toEqual(true);
// Readonly token should deny write
const canWrite = await authManager.authorize(
tokenObj,
'npm:package:test-package',
'write'
);
expect(canWrite).toEqual(false);
// Readonly token should deny push
const canPush = await authManager.authorize(
tokenObj,
'oci:repository:test-repo',
'push'
);
expect(canPush).toEqual(false);
});
tap.test('should list tags', async () => {
const result = await registry.listTags('test/repo', testToken);
expect(result).toHaveProperty('tags');
tap.test('Integration: should access storage backend', async () => {
const storage = registry.getStorage();
expect(storage).toBeDefined();
if ('tags' in result) {
expect(result.tags).toBeInstanceOf(Array);
expect(result.tags).toContain('latest');
}
});
// Test basic storage operations
const testKey = 'test/storage/key';
const testData = Buffer.from('test data', 'utf-8');
tap.test('should generate auth challenge', async () => {
const challenge = registry.getAuthChallenge('test/repo', ['pull', 'push']);
expect(challenge).toInclude('Bearer');
expect(challenge).toInclude('realm=');
expect(challenge).toInclude('service=');
expect(challenge).toInclude('scope=');
});
await storage.putObject(testKey, testData);
const retrieved = await storage.getObject(testKey);
tap.test('should handle unauthorized access', async () => {
const result = await registry.getBlob('test/repo', 'sha256:invalid', 'invalid-token');
expect(result).toHaveProperty('errors');
expect(retrieved).toBeInstanceOf(Buffer);
expect(retrieved?.toString('utf-8')).toEqual('test data');
if ('errors' in result) {
expect(result.errors[0].code).toEqual('DENIED');
}
const exists = await storage.objectExists(testKey);
expect(exists).toEqual(true);
await storage.deleteObject(testKey);
const existsAfterDelete = await storage.objectExists(testKey);
expect(existsAfterDelete).toEqual(false);
});
export default tap.start();