feat(auth): Implement HMAC-SHA256 OCI JWTs; enhance PyPI & RubyGems uploads and normalize responses
This commit is contained in:
288
test/test.integration.crossprotocol.ts
Normal file
288
test/test.integration.crossprotocol.ts
Normal file
@@ -0,0 +1,288 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { SmartRegistry } from '../ts/index.js';
|
||||
import {
|
||||
createTestRegistry,
|
||||
createTestTokens,
|
||||
createPythonWheel,
|
||||
createRubyGem,
|
||||
} from './helpers/registry.js';
|
||||
|
||||
let registry: SmartRegistry;
|
||||
let pypiToken: string;
|
||||
let rubygemsToken: string;
|
||||
|
||||
tap.test('Integration: should initialize registry with all protocols', async () => {
|
||||
registry = await createTestRegistry();
|
||||
const tokens = await createTestTokens(registry);
|
||||
pypiToken = tokens.pypiToken;
|
||||
rubygemsToken = tokens.rubygemsToken;
|
||||
|
||||
expect(registry).toBeInstanceOf(SmartRegistry);
|
||||
expect(registry.isInitialized()).toEqual(true);
|
||||
expect(pypiToken).toBeTypeOf('string');
|
||||
expect(rubygemsToken).toBeTypeOf('string');
|
||||
});
|
||||
|
||||
tap.test('Integration: should correctly route PyPI requests', async () => {
|
||||
const wheelData = await createPythonWheel('integration-test-py', '1.0.0');
|
||||
|
||||
const response = await registry.handleRequest({
|
||||
method: 'POST',
|
||||
path: '/pypi/',
|
||||
headers: {
|
||||
Authorization: `Bearer ${pypiToken}`,
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
query: {},
|
||||
body: {
|
||||
':action': 'file_upload',
|
||||
protocol_version: '1',
|
||||
name: 'integration-test-py',
|
||||
version: '1.0.0',
|
||||
filetype: 'bdist_wheel',
|
||||
pyversion: 'py3',
|
||||
metadata_version: '2.1',
|
||||
content: wheelData,
|
||||
filename: 'integration_test_py-1.0.0-py3-none-any.whl',
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).toEqual(201);
|
||||
});
|
||||
|
||||
tap.test('Integration: should correctly route RubyGems requests', async () => {
|
||||
const gemData = await createRubyGem('integration-test-gem', '1.0.0');
|
||||
|
||||
const response = await registry.handleRequest({
|
||||
method: 'POST',
|
||||
path: '/rubygems/api/v1/gems',
|
||||
headers: {
|
||||
Authorization: rubygemsToken,
|
||||
'Content-Type': 'application/octet-stream',
|
||||
},
|
||||
query: {},
|
||||
body: gemData,
|
||||
});
|
||||
|
||||
expect(response.status).toEqual(201);
|
||||
});
|
||||
|
||||
tap.test('Integration: should handle /simple path for PyPI', async () => {
|
||||
const response = await registry.handleRequest({
|
||||
method: 'GET',
|
||||
path: '/simple/',
|
||||
headers: {
|
||||
Accept: 'text/html',
|
||||
},
|
||||
query: {},
|
||||
});
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.headers['Content-Type']).toStartWith('text/html');
|
||||
expect(response.body).toContain('integration-test-py');
|
||||
});
|
||||
|
||||
tap.test('Integration: should reject PyPI token for RubyGems endpoint', async () => {
|
||||
const gemData = await createRubyGem('unauthorized-gem', '1.0.0');
|
||||
|
||||
const response = await registry.handleRequest({
|
||||
method: 'POST',
|
||||
path: '/rubygems/api/v1/gems',
|
||||
headers: {
|
||||
Authorization: pypiToken, // Using PyPI token for RubyGems endpoint
|
||||
'Content-Type': 'application/octet-stream',
|
||||
},
|
||||
query: {},
|
||||
body: gemData,
|
||||
});
|
||||
|
||||
expect(response.status).toEqual(401);
|
||||
});
|
||||
|
||||
tap.test('Integration: should reject RubyGems token for PyPI endpoint', async () => {
|
||||
const wheelData = await createPythonWheel('unauthorized-py', '1.0.0');
|
||||
|
||||
const response = await registry.handleRequest({
|
||||
method: 'POST',
|
||||
path: '/pypi/',
|
||||
headers: {
|
||||
Authorization: `Bearer ${rubygemsToken}`, // Using RubyGems token for PyPI endpoint
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
query: {},
|
||||
body: {
|
||||
':action': 'file_upload',
|
||||
protocol_version: '1',
|
||||
name: 'unauthorized-py',
|
||||
version: '1.0.0',
|
||||
filetype: 'bdist_wheel',
|
||||
pyversion: 'py3',
|
||||
metadata_version: '2.1',
|
||||
content: wheelData,
|
||||
filename: 'unauthorized_py-1.0.0-py3-none-any.whl',
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).toEqual(401);
|
||||
});
|
||||
|
||||
tap.test('Integration: should return 404 for unknown paths', async () => {
|
||||
const response = await registry.handleRequest({
|
||||
method: 'GET',
|
||||
path: '/unknown-protocol/endpoint',
|
||||
headers: {},
|
||||
query: {},
|
||||
});
|
||||
|
||||
expect(response.status).toEqual(404);
|
||||
expect(response.body).toHaveProperty('error');
|
||||
expect((response.body as any).error).toEqual('NOT_FOUND');
|
||||
});
|
||||
|
||||
tap.test('Integration: should retrieve PyPI registry instance', async () => {
|
||||
const pypiRegistry = registry.getRegistry('pypi');
|
||||
|
||||
expect(pypiRegistry).toBeDefined();
|
||||
expect(pypiRegistry).not.toBeNull();
|
||||
});
|
||||
|
||||
tap.test('Integration: should retrieve RubyGems registry instance', async () => {
|
||||
const rubygemsRegistry = registry.getRegistry('rubygems');
|
||||
|
||||
expect(rubygemsRegistry).toBeDefined();
|
||||
expect(rubygemsRegistry).not.toBeNull();
|
||||
});
|
||||
|
||||
tap.test('Integration: should retrieve all other protocol instances', async () => {
|
||||
const ociRegistry = registry.getRegistry('oci');
|
||||
const npmRegistry = registry.getRegistry('npm');
|
||||
const mavenRegistry = registry.getRegistry('maven');
|
||||
const composerRegistry = registry.getRegistry('composer');
|
||||
const cargoRegistry = registry.getRegistry('cargo');
|
||||
|
||||
expect(ociRegistry).toBeDefined();
|
||||
expect(npmRegistry).toBeDefined();
|
||||
expect(mavenRegistry).toBeDefined();
|
||||
expect(composerRegistry).toBeDefined();
|
||||
expect(cargoRegistry).toBeDefined();
|
||||
});
|
||||
|
||||
tap.test('Integration: should share storage across protocols', async () => {
|
||||
const storage = registry.getStorage();
|
||||
|
||||
expect(storage).toBeDefined();
|
||||
|
||||
// Verify storage has methods for all protocols
|
||||
expect(typeof storage.getPypiPackageMetadata).toEqual('function');
|
||||
expect(typeof storage.getRubyGemsVersions).toEqual('function');
|
||||
expect(typeof storage.getNpmPackument).toEqual('function');
|
||||
expect(typeof storage.getOciBlob).toEqual('function');
|
||||
});
|
||||
|
||||
tap.test('Integration: should share auth manager across protocols', async () => {
|
||||
const authManager = registry.getAuthManager();
|
||||
|
||||
expect(authManager).toBeDefined();
|
||||
|
||||
// Verify auth manager has methods for all protocols
|
||||
expect(typeof authManager.createPypiToken).toEqual('function');
|
||||
expect(typeof authManager.createRubyGemsToken).toEqual('function');
|
||||
expect(typeof authManager.createNpmToken).toEqual('function');
|
||||
expect(typeof authManager.createOciToken).toEqual('function');
|
||||
});
|
||||
|
||||
tap.test('Integration: should handle concurrent requests to different protocols', async () => {
|
||||
const pypiRequest = registry.handleRequest({
|
||||
method: 'GET',
|
||||
path: '/simple/',
|
||||
headers: {},
|
||||
query: {},
|
||||
});
|
||||
|
||||
const rubygemsRequest = registry.handleRequest({
|
||||
method: 'GET',
|
||||
path: '/rubygems/versions',
|
||||
headers: {},
|
||||
query: {},
|
||||
});
|
||||
|
||||
const [pypiResponse, rubygemsResponse] = await Promise.all([pypiRequest, rubygemsRequest]);
|
||||
|
||||
expect(pypiResponse.status).toEqual(200);
|
||||
expect(rubygemsResponse.status).toEqual(200);
|
||||
});
|
||||
|
||||
tap.test('Integration: should handle package name conflicts across protocols', async () => {
|
||||
const packageName = 'conflict-test';
|
||||
|
||||
// Upload PyPI package
|
||||
const wheelData = await createPythonWheel(packageName, '1.0.0');
|
||||
const pypiResponse = await registry.handleRequest({
|
||||
method: 'POST',
|
||||
path: '/pypi/',
|
||||
headers: {
|
||||
Authorization: `Bearer ${pypiToken}`,
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
query: {},
|
||||
body: {
|
||||
':action': 'file_upload',
|
||||
protocol_version: '1',
|
||||
name: packageName,
|
||||
version: '1.0.0',
|
||||
filetype: 'bdist_wheel',
|
||||
pyversion: 'py3',
|
||||
metadata_version: '2.1',
|
||||
content: wheelData,
|
||||
filename: `${packageName.replace(/-/g, '_')}-1.0.0-py3-none-any.whl`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(pypiResponse.status).toEqual(201);
|
||||
|
||||
// Upload RubyGems package with same name
|
||||
const gemData = await createRubyGem(packageName, '1.0.0');
|
||||
const rubygemsResponse = await registry.handleRequest({
|
||||
method: 'POST',
|
||||
path: '/rubygems/api/v1/gems',
|
||||
headers: {
|
||||
Authorization: rubygemsToken,
|
||||
'Content-Type': 'application/octet-stream',
|
||||
},
|
||||
query: {},
|
||||
body: gemData,
|
||||
});
|
||||
|
||||
expect(rubygemsResponse.status).toEqual(201);
|
||||
|
||||
// Both should exist independently
|
||||
const pypiGetResponse = await registry.handleRequest({
|
||||
method: 'GET',
|
||||
path: `/simple/${packageName}/`,
|
||||
headers: {},
|
||||
query: {},
|
||||
});
|
||||
|
||||
const rubygemsGetResponse = await registry.handleRequest({
|
||||
method: 'GET',
|
||||
path: `/rubygems/gems/${packageName}-1.0.0.gem`,
|
||||
headers: {},
|
||||
query: {},
|
||||
});
|
||||
|
||||
expect(pypiGetResponse.status).toEqual(200);
|
||||
expect(rubygemsGetResponse.status).toEqual(200);
|
||||
});
|
||||
|
||||
tap.test('Integration: should properly clean up resources on destroy', async () => {
|
||||
// Destroy should clean up all registries
|
||||
expect(() => registry.destroy()).not.toThrow();
|
||||
});
|
||||
|
||||
tap.postTask('cleanup registry', async () => {
|
||||
if (registry && registry.isInitialized()) {
|
||||
registry.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
Reference in New Issue
Block a user