344 lines
11 KiB
TypeScript
344 lines
11 KiB
TypeScript
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
|
import { SmartRegistry } from '../ts/index.js';
|
|
import {
|
|
createTestRegistryWithUpstream,
|
|
createTrackingUpstreamProvider,
|
|
} from './helpers/registry.js';
|
|
import { StaticUpstreamProvider } from '../ts/upstream/interfaces.upstream.js';
|
|
import type {
|
|
IUpstreamProvider,
|
|
IUpstreamResolutionContext,
|
|
IProtocolUpstreamConfig,
|
|
} from '../ts/upstream/interfaces.upstream.js';
|
|
import type { TRegistryProtocol } from '../ts/core/interfaces.core.js';
|
|
|
|
// =============================================================================
|
|
// StaticUpstreamProvider Tests
|
|
// =============================================================================
|
|
|
|
tap.test('StaticUpstreamProvider: should return config for configured protocol', async () => {
|
|
const npmConfig: IProtocolUpstreamConfig = {
|
|
enabled: true,
|
|
upstreams: [{ id: 'npmjs', url: 'https://registry.npmjs.org', priority: 1, enabled: true }],
|
|
};
|
|
|
|
const provider = new StaticUpstreamProvider({
|
|
npm: npmConfig,
|
|
});
|
|
|
|
const result = await provider.resolveUpstreamConfig({
|
|
protocol: 'npm',
|
|
resource: 'lodash',
|
|
scope: null,
|
|
method: 'GET',
|
|
resourceType: 'packument',
|
|
});
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result?.enabled).toEqual(true);
|
|
expect(result?.upstreams[0].id).toEqual('npmjs');
|
|
});
|
|
|
|
tap.test('StaticUpstreamProvider: should return null for unconfigured protocol', async () => {
|
|
const provider = new StaticUpstreamProvider({
|
|
npm: {
|
|
enabled: true,
|
|
upstreams: [{ id: 'npmjs', url: 'https://registry.npmjs.org', priority: 1, enabled: true }],
|
|
},
|
|
});
|
|
|
|
const result = await provider.resolveUpstreamConfig({
|
|
protocol: 'maven',
|
|
resource: 'com.example:lib',
|
|
scope: 'com.example',
|
|
method: 'GET',
|
|
resourceType: 'pom',
|
|
});
|
|
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
tap.test('StaticUpstreamProvider: should support multiple protocols', async () => {
|
|
const provider = new StaticUpstreamProvider({
|
|
npm: {
|
|
enabled: true,
|
|
upstreams: [{ id: 'npmjs', url: 'https://registry.npmjs.org', priority: 1, enabled: true }],
|
|
},
|
|
oci: {
|
|
enabled: true,
|
|
upstreams: [{ id: 'dockerhub', url: 'https://registry-1.docker.io', priority: 1, enabled: true }],
|
|
},
|
|
maven: {
|
|
enabled: true,
|
|
upstreams: [{ id: 'central', url: 'https://repo1.maven.org/maven2', priority: 1, enabled: true }],
|
|
},
|
|
});
|
|
|
|
const npmResult = await provider.resolveUpstreamConfig({
|
|
protocol: 'npm',
|
|
resource: 'lodash',
|
|
scope: null,
|
|
method: 'GET',
|
|
resourceType: 'packument',
|
|
});
|
|
expect(npmResult?.upstreams[0].id).toEqual('npmjs');
|
|
|
|
const ociResult = await provider.resolveUpstreamConfig({
|
|
protocol: 'oci',
|
|
resource: 'library/nginx',
|
|
scope: 'library',
|
|
method: 'GET',
|
|
resourceType: 'manifest',
|
|
});
|
|
expect(ociResult?.upstreams[0].id).toEqual('dockerhub');
|
|
|
|
const mavenResult = await provider.resolveUpstreamConfig({
|
|
protocol: 'maven',
|
|
resource: 'com.example:lib',
|
|
scope: 'com.example',
|
|
method: 'GET',
|
|
resourceType: 'pom',
|
|
});
|
|
expect(mavenResult?.upstreams[0].id).toEqual('central');
|
|
});
|
|
|
|
// =============================================================================
|
|
// Registry with Provider Integration Tests
|
|
// =============================================================================
|
|
|
|
let registry: SmartRegistry;
|
|
let trackingProvider: ReturnType<typeof createTrackingUpstreamProvider>;
|
|
|
|
tap.test('Provider Integration: should create registry with upstream provider', async () => {
|
|
trackingProvider = createTrackingUpstreamProvider({
|
|
npm: {
|
|
enabled: true,
|
|
upstreams: [{ id: 'test-npm', url: 'https://registry.npmjs.org', priority: 1, enabled: true }],
|
|
},
|
|
});
|
|
|
|
registry = await createTestRegistryWithUpstream(trackingProvider.provider);
|
|
|
|
expect(registry).toBeInstanceOf(SmartRegistry);
|
|
expect(registry.isInitialized()).toEqual(true);
|
|
});
|
|
|
|
tap.test('Provider Integration: should call provider when fetching unknown npm package', async () => {
|
|
// Clear previous calls
|
|
trackingProvider.calls.length = 0;
|
|
|
|
// Request a package that doesn't exist locally - should trigger upstream lookup
|
|
const response = await registry.handleRequest({
|
|
method: 'GET',
|
|
path: '/npm/@test-scope/nonexistent-package',
|
|
headers: {},
|
|
query: {},
|
|
});
|
|
|
|
// Provider should have been called for the packument lookup
|
|
const npmCalls = trackingProvider.calls.filter(c => c.protocol === 'npm');
|
|
|
|
// The package doesn't exist locally, so upstream should be consulted
|
|
// Note: actual upstream fetch may fail since the package doesn't exist
|
|
expect(response.status).toBeOneOf([404, 200, 502]); // 404 if not found, 502 if upstream error
|
|
});
|
|
|
|
tap.test('Provider Integration: provider receives correct context for scoped npm package', async () => {
|
|
trackingProvider.calls.length = 0;
|
|
|
|
// Use URL-encoded path for scoped packages as npm client does
|
|
await registry.handleRequest({
|
|
method: 'GET',
|
|
path: '/npm/@myorg%2fmy-package',
|
|
headers: {},
|
|
query: {},
|
|
});
|
|
|
|
// Find any npm call - the exact resource type depends on routing
|
|
const npmCalls = trackingProvider.calls.filter(c => c.protocol === 'npm');
|
|
|
|
// Provider should be called for upstream lookup
|
|
if (npmCalls.length > 0) {
|
|
const call = npmCalls[0];
|
|
expect(call.protocol).toEqual('npm');
|
|
// The resource should include the scoped name
|
|
expect(call.resource).toInclude('myorg');
|
|
expect(call.method).toEqual('GET');
|
|
}
|
|
});
|
|
|
|
tap.test('Provider Integration: provider receives correct context for unscoped npm package', async () => {
|
|
trackingProvider.calls.length = 0;
|
|
|
|
await registry.handleRequest({
|
|
method: 'GET',
|
|
path: '/npm/lodash',
|
|
headers: {},
|
|
query: {},
|
|
});
|
|
|
|
const packumentCall = trackingProvider.calls.find(
|
|
c => c.protocol === 'npm' && c.resourceType === 'packument'
|
|
);
|
|
|
|
if (packumentCall) {
|
|
expect(packumentCall.protocol).toEqual('npm');
|
|
expect(packumentCall.resource).toEqual('lodash');
|
|
expect(packumentCall.scope).toBeNull(); // No scope for unscoped package
|
|
}
|
|
});
|
|
|
|
// =============================================================================
|
|
// Custom Provider Implementation Tests
|
|
// =============================================================================
|
|
|
|
tap.test('Custom Provider: should support dynamic resolution based on context', async () => {
|
|
// Create a provider that returns different configs based on scope
|
|
const dynamicProvider: IUpstreamProvider = {
|
|
async resolveUpstreamConfig(context: IUpstreamResolutionContext) {
|
|
if (context.scope === 'internal') {
|
|
// Internal packages go to private registry
|
|
return {
|
|
enabled: true,
|
|
upstreams: [{ id: 'private', url: 'https://private.registry.com', priority: 1, enabled: true }],
|
|
};
|
|
}
|
|
// Everything else goes to public registry
|
|
return {
|
|
enabled: true,
|
|
upstreams: [{ id: 'public', url: 'https://registry.npmjs.org', priority: 1, enabled: true }],
|
|
};
|
|
},
|
|
};
|
|
|
|
const internalResult = await dynamicProvider.resolveUpstreamConfig({
|
|
protocol: 'npm',
|
|
resource: '@internal/utils',
|
|
scope: 'internal',
|
|
method: 'GET',
|
|
resourceType: 'packument',
|
|
});
|
|
expect(internalResult?.upstreams[0].id).toEqual('private');
|
|
|
|
const publicResult = await dynamicProvider.resolveUpstreamConfig({
|
|
protocol: 'npm',
|
|
resource: '@public/utils',
|
|
scope: 'public',
|
|
method: 'GET',
|
|
resourceType: 'packument',
|
|
});
|
|
expect(publicResult?.upstreams[0].id).toEqual('public');
|
|
});
|
|
|
|
tap.test('Custom Provider: should support actor-based resolution', async () => {
|
|
const actorAwareProvider: IUpstreamProvider = {
|
|
async resolveUpstreamConfig(context: IUpstreamResolutionContext) {
|
|
// Different upstreams based on user's organization
|
|
if (context.actor?.orgId === 'enterprise-org') {
|
|
return {
|
|
enabled: true,
|
|
upstreams: [{ id: 'enterprise', url: 'https://enterprise.registry.com', priority: 1, enabled: true }],
|
|
};
|
|
}
|
|
return {
|
|
enabled: true,
|
|
upstreams: [{ id: 'default', url: 'https://registry.npmjs.org', priority: 1, enabled: true }],
|
|
};
|
|
},
|
|
};
|
|
|
|
const enterpriseResult = await actorAwareProvider.resolveUpstreamConfig({
|
|
protocol: 'npm',
|
|
resource: 'lodash',
|
|
scope: null,
|
|
actor: { orgId: 'enterprise-org', userId: 'user1' },
|
|
method: 'GET',
|
|
resourceType: 'packument',
|
|
});
|
|
expect(enterpriseResult?.upstreams[0].id).toEqual('enterprise');
|
|
|
|
const defaultResult = await actorAwareProvider.resolveUpstreamConfig({
|
|
protocol: 'npm',
|
|
resource: 'lodash',
|
|
scope: null,
|
|
actor: { orgId: 'free-org', userId: 'user2' },
|
|
method: 'GET',
|
|
resourceType: 'packument',
|
|
});
|
|
expect(defaultResult?.upstreams[0].id).toEqual('default');
|
|
});
|
|
|
|
tap.test('Custom Provider: should support disabling upstream for specific resources', async () => {
|
|
const selectiveProvider: IUpstreamProvider = {
|
|
async resolveUpstreamConfig(context: IUpstreamResolutionContext) {
|
|
// Block upstream for internal packages
|
|
if (context.scope === 'internal') {
|
|
return null; // No upstream for internal packages
|
|
}
|
|
return {
|
|
enabled: true,
|
|
upstreams: [{ id: 'public', url: 'https://registry.npmjs.org', priority: 1, enabled: true }],
|
|
};
|
|
},
|
|
};
|
|
|
|
const internalResult = await selectiveProvider.resolveUpstreamConfig({
|
|
protocol: 'npm',
|
|
resource: '@internal/secret',
|
|
scope: 'internal',
|
|
method: 'GET',
|
|
resourceType: 'packument',
|
|
});
|
|
expect(internalResult).toBeNull();
|
|
|
|
const publicResult = await selectiveProvider.resolveUpstreamConfig({
|
|
protocol: 'npm',
|
|
resource: 'lodash',
|
|
scope: null,
|
|
method: 'GET',
|
|
resourceType: 'packument',
|
|
});
|
|
expect(publicResult).not.toBeNull();
|
|
});
|
|
|
|
// =============================================================================
|
|
// Registry without Provider Tests
|
|
// =============================================================================
|
|
|
|
tap.test('No Provider: registry should work without upstream provider', async () => {
|
|
const registryWithoutUpstream = await createTestRegistryWithUpstream(
|
|
// Pass a provider that always returns null
|
|
{
|
|
async resolveUpstreamConfig() {
|
|
return null;
|
|
},
|
|
}
|
|
);
|
|
|
|
expect(registryWithoutUpstream).toBeInstanceOf(SmartRegistry);
|
|
|
|
// Should return 404 for non-existent package (no upstream to check)
|
|
const response = await registryWithoutUpstream.handleRequest({
|
|
method: 'GET',
|
|
path: '/npm/nonexistent-package-xyz',
|
|
headers: {},
|
|
query: {},
|
|
});
|
|
|
|
expect(response.status).toEqual(404);
|
|
|
|
registryWithoutUpstream.destroy();
|
|
});
|
|
|
|
// =============================================================================
|
|
// Cleanup
|
|
// =============================================================================
|
|
|
|
tap.postTask('cleanup registry', async () => {
|
|
if (registry) {
|
|
registry.destroy();
|
|
}
|
|
});
|
|
|
|
export default tap.start();
|