Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b3da7dece | |||
| 45e24ecff3 | |||
| 2e2726a4de | |||
| 0e35256062 |
@@ -1,5 +1,19 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-04-30 - 2.9.2 - fix(core,testing)
|
||||||
|
improve type safety and update tests for latest tstest and storage APIs
|
||||||
|
|
||||||
|
- harden header extraction in BaseRegistry to safely handle request contexts with missing or invalid headers
|
||||||
|
- migrate tests to the current tstest tapbundle APIs and assertion methods
|
||||||
|
- adjust binary upload tests to use Uint8Array and add null guards for stricter TypeScript checks
|
||||||
|
- update upstream and S3 cache test helpers to match newer registry and storage interfaces
|
||||||
|
- enable stricter TypeScript compiler settings and refresh related dependency versions
|
||||||
|
|
||||||
|
## 2026-04-16 - 2.9.1 - fix(license)
|
||||||
|
add missing MIT license file to repository
|
||||||
|
|
||||||
|
- Adds the project license file to align the repository contents with the package metadata license declaration.
|
||||||
|
|
||||||
## 2026-04-16 - 2.9.0 - feat(registry)
|
## 2026-04-16 - 2.9.0 - feat(registry)
|
||||||
add declarative protocol routing and request-scoped storage hook context across registries
|
add declarative protocol routing and request-scoped storage hook context across registries
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
Copyright (c) 2025 Task Venture Capital GmbH (hello@task.vc)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
+14
-14
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartregistry",
|
"name": "@push.rocks/smartregistry",
|
||||||
"version": "2.9.0",
|
"version": "2.9.2",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "A composable TypeScript library implementing OCI, NPM, Maven, Cargo, Composer, PyPI, and RubyGems registries for building unified container and package registries",
|
"description": "A composable TypeScript library implementing OCI, NPM, Maven, Cargo, Composer, PyPI, and RubyGems registries for building unified container and package registries",
|
||||||
"main": "dist_ts/index.js",
|
"main": "dist_ts/index.js",
|
||||||
@@ -10,17 +10,19 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "(tstest test/ --verbose --logfile --timeout 240)",
|
"test": "(tstest test/ --verbose --logfile --timeout 240)",
|
||||||
"build": "(tsbuild --allowimplicitany)",
|
"build": "(tsbuild)",
|
||||||
"buildDocs": "(tsdoc)"
|
"buildDocs": "(tsdoc)"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^4.4.0",
|
"@git.zone/tsbuild": "^4.4.0",
|
||||||
"@git.zone/tsbundle": "^2.10.0",
|
"@git.zone/tsbundle": "^2.10.0",
|
||||||
"@git.zone/tsrun": "^2.0.2",
|
"@git.zone/tsrun": "^2.0.2",
|
||||||
"@git.zone/tstest": "^3.6.0",
|
"@git.zone/tstest": "^3.6.3",
|
||||||
"@push.rocks/smartarchive": "^5.2.1",
|
"@push.rocks/smartarchive": "^5.2.1",
|
||||||
"@push.rocks/smartstorage": "^6.3.2",
|
"@push.rocks/smartstorage": "^6.4.1",
|
||||||
"@types/node": "^25.5.0"
|
"@types/adm-zip": "^0.5.8",
|
||||||
|
"@types/lodash.clonedeep": "^4.5.9",
|
||||||
|
"@types/node": "^25.6.0"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -40,20 +42,18 @@
|
|||||||
"assets/**/*",
|
"assets/**/*",
|
||||||
"cli.js",
|
"cli.js",
|
||||||
".smartconfig.json",
|
".smartconfig.json",
|
||||||
|
"license",
|
||||||
"readme.md"
|
"readme.md"
|
||||||
],
|
],
|
||||||
"pnpm": {
|
|
||||||
"overrides": {}
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@push.rocks/qenv": "^6.1.3",
|
"@push.rocks/qenv": "^6.1.3",
|
||||||
"@push.rocks/smartbucket": "^4.5.1",
|
"@push.rocks/smartbucket": "^4.6.1",
|
||||||
"@push.rocks/smartlog": "^3.2.1",
|
"@push.rocks/smartlog": "^3.2.2",
|
||||||
"@push.rocks/smartpath": "^6.0.0",
|
"@push.rocks/smartpath": "^6.0.0",
|
||||||
"@push.rocks/smartrequest": "^5.0.1",
|
"@push.rocks/smartrequest": "^5.0.1",
|
||||||
"@tsclass/tsclass": "^9.5.0",
|
"@tsclass/tsclass": "^9.5.1",
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.17",
|
||||||
"minimatch": "^10.2.4"
|
"minimatch": "^10.2.5"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34"
|
"packageManager": "pnpm@10.28.2"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+1263
-1338
File diff suppressed because it is too large
Load Diff
+31
-31
@@ -1,4 +1,4 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest';
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||||
import { RegistryStorage } from '../ts/core/classes.registrystorage.js';
|
import { RegistryStorage } from '../ts/core/classes.registrystorage.js';
|
||||||
import { CargoRegistry } from '../ts/cargo/classes.cargoregistry.js';
|
import { CargoRegistry } from '../ts/cargo/classes.cargoregistry.js';
|
||||||
import { AuthManager } from '../ts/core/classes.authmanager.js';
|
import { AuthManager } from '../ts/core/classes.authmanager.js';
|
||||||
@@ -17,21 +17,21 @@ tap.test('should calculate correct index paths for different crate names', async
|
|||||||
const getPath = (storage as any).getCargoIndexPath.bind(storage);
|
const getPath = (storage as any).getCargoIndexPath.bind(storage);
|
||||||
|
|
||||||
// 1-character names
|
// 1-character names
|
||||||
expect(getPath('a')).to.equal('cargo/index/1/a');
|
expect(getPath('a')).toEqual('cargo/index/1/a');
|
||||||
expect(getPath('z')).to.equal('cargo/index/1/z');
|
expect(getPath('z')).toEqual('cargo/index/1/z');
|
||||||
|
|
||||||
// 2-character names
|
// 2-character names
|
||||||
expect(getPath('io')).to.equal('cargo/index/2/io');
|
expect(getPath('io')).toEqual('cargo/index/2/io');
|
||||||
expect(getPath('ab')).to.equal('cargo/index/2/ab');
|
expect(getPath('ab')).toEqual('cargo/index/2/ab');
|
||||||
|
|
||||||
// 3-character names
|
// 3-character names
|
||||||
expect(getPath('axo')).to.equal('cargo/index/3/a/axo');
|
expect(getPath('axo')).toEqual('cargo/index/3/a/axo');
|
||||||
expect(getPath('foo')).to.equal('cargo/index/3/f/foo');
|
expect(getPath('foo')).toEqual('cargo/index/3/f/foo');
|
||||||
|
|
||||||
// 4+ character names
|
// 4+ character names
|
||||||
expect(getPath('serde')).to.equal('cargo/index/se/rd/serde');
|
expect(getPath('serde')).toEqual('cargo/index/se/rd/serde');
|
||||||
expect(getPath('tokio')).to.equal('cargo/index/to/ki/tokio');
|
expect(getPath('tokio')).toEqual('cargo/index/to/ki/tokio');
|
||||||
expect(getPath('my-crate')).to.equal('cargo/index/my/--/my-crate');
|
expect(getPath('my-crate')).toEqual('cargo/index/my/--/my-crate');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Test crate file path calculation
|
// Test crate file path calculation
|
||||||
@@ -46,9 +46,9 @@ tap.test('should calculate correct crate file paths', async () => {
|
|||||||
// Access private method for testing
|
// Access private method for testing
|
||||||
const getPath = (storage as any).getCargoCratePath.bind(storage);
|
const getPath = (storage as any).getCargoCratePath.bind(storage);
|
||||||
|
|
||||||
expect(getPath('serde', '1.0.0')).to.equal('cargo/crates/serde/serde-1.0.0.crate');
|
expect(getPath('serde', '1.0.0')).toEqual('cargo/crates/serde/serde-1.0.0.crate');
|
||||||
expect(getPath('tokio', '1.28.0')).to.equal('cargo/crates/tokio/tokio-1.28.0.crate');
|
expect(getPath('tokio', '1.28.0')).toEqual('cargo/crates/tokio/tokio-1.28.0.crate');
|
||||||
expect(getPath('my-crate', '0.1.0')).to.equal('cargo/crates/my-crate/my-crate-0.1.0.crate');
|
expect(getPath('my-crate', '0.1.0')).toEqual('cargo/crates/my-crate/my-crate-0.1.0.crate');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Test crate name validation
|
// Test crate name validation
|
||||||
@@ -73,28 +73,28 @@ tap.test('should validate crate names correctly', async () => {
|
|||||||
const validate = (registry as any).validateCrateName.bind(registry);
|
const validate = (registry as any).validateCrateName.bind(registry);
|
||||||
|
|
||||||
// Valid names
|
// Valid names
|
||||||
expect(validate('serde')).to.be.true;
|
expect(validate('serde')).toBeTrue();
|
||||||
expect(validate('tokio')).to.be.true;
|
expect(validate('tokio')).toBeTrue();
|
||||||
expect(validate('my-crate')).to.be.true;
|
expect(validate('my-crate')).toBeTrue();
|
||||||
expect(validate('my_crate')).to.be.true;
|
expect(validate('my_crate')).toBeTrue();
|
||||||
expect(validate('crate123')).to.be.true;
|
expect(validate('crate123')).toBeTrue();
|
||||||
expect(validate('a')).to.be.true;
|
expect(validate('a')).toBeTrue();
|
||||||
|
|
||||||
// Invalid names (uppercase not allowed)
|
// Invalid names (uppercase not allowed)
|
||||||
expect(validate('Serde')).to.be.false;
|
expect(validate('Serde')).toBeFalse();
|
||||||
expect(validate('MyCreate')).to.be.false;
|
expect(validate('MyCreate')).toBeFalse();
|
||||||
|
|
||||||
// Invalid names (special characters)
|
// Invalid names (special characters)
|
||||||
expect(validate('my.crate')).to.be.false;
|
expect(validate('my.crate')).toBeFalse();
|
||||||
expect(validate('my@crate')).to.be.false;
|
expect(validate('my@crate')).toBeFalse();
|
||||||
expect(validate('my crate')).to.be.false;
|
expect(validate('my crate')).toBeFalse();
|
||||||
|
|
||||||
// Invalid names (too long)
|
// Invalid names (too long)
|
||||||
const longName = 'a'.repeat(65);
|
const longName = 'a'.repeat(65);
|
||||||
expect(validate(longName)).to.be.false;
|
expect(validate(longName)).toBeFalse();
|
||||||
|
|
||||||
// Invalid names (empty)
|
// Invalid names (empty)
|
||||||
expect(validate('')).to.be.false;
|
expect(validate('')).toBeFalse();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Test config.json response
|
// Test config.json response
|
||||||
@@ -122,12 +122,12 @@ tap.test('should return valid config.json', async () => {
|
|||||||
query: {},
|
query: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(response.status).to.equal(200);
|
expect(response.status).toEqual(200);
|
||||||
expect(response.headers['Content-Type']).to.equal('application/json');
|
expect(response.headers['Content-Type']).toEqual('application/json');
|
||||||
const body = await streamToJson(response.body);
|
const body = await streamToJson(response.body);
|
||||||
expect(body).to.be.an('object');
|
expect(body).toBeTypeOf('object');
|
||||||
expect(body.dl).to.include('/api/v1/crates/{crate}/{version}/download');
|
expect(body.dl).toInclude('/api/v1/crates/{crate}/{version}/download');
|
||||||
expect(body.api).to.equal('http://localhost:5000/cargo');
|
expect(body.api).toEqual('http://localhost:5000/cargo');
|
||||||
});
|
});
|
||||||
|
|
||||||
export default tap.start();
|
export default tap.start();
|
||||||
|
|||||||
@@ -26,9 +26,6 @@ tap.test('setup: should create DefaultAuthProvider', async () => {
|
|||||||
realm: 'https://auth.example.com/token',
|
realm: 'https://auth.example.com/token',
|
||||||
service: 'test-registry',
|
service: 'test-registry',
|
||||||
},
|
},
|
||||||
mavenTokens: { enabled: true },
|
|
||||||
cargoTokens: { enabled: true },
|
|
||||||
composerTokens: { enabled: true },
|
|
||||||
pypiTokens: { enabled: true },
|
pypiTokens: { enabled: true },
|
||||||
rubygemsTokens: { enabled: true },
|
rubygemsTokens: { enabled: true },
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
|
import { TapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
|
||||||
import { SmartRegistry } from '../ts/index.js';
|
import { SmartRegistry } from '../ts/index.js';
|
||||||
import { createTestRegistry, createTestTokens } from './helpers/registry.js';
|
import { createTestRegistry, createTestTokens } from './helpers/registry.js';
|
||||||
import type { IRequestContext, IResponse } from '../ts/core/interfaces.core.js';
|
import type { IRequestContext, IResponse } from '../ts/core/interfaces.core.js';
|
||||||
@@ -13,6 +13,8 @@ import * as url from 'url';
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
|
const tapNodeTools = new TapNodeTools(tap);
|
||||||
|
|
||||||
// Test context
|
// Test context
|
||||||
let registry: SmartRegistry;
|
let registry: SmartRegistry;
|
||||||
let server: http.Server;
|
let server: http.Server;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
|
import { TapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
|
||||||
import { SmartRegistry } from '../ts/index.js';
|
import { SmartRegistry } from '../ts/index.js';
|
||||||
import { createTestRegistry, createTestTokens, createComposerZip, generateTestRunId } from './helpers/registry.js';
|
import { createTestRegistry, createTestTokens, createComposerZip, generateTestRunId } from './helpers/registry.js';
|
||||||
import type { IRequestContext, IResponse } from '../ts/core/interfaces.core.js';
|
import type { IRequestContext, IResponse } from '../ts/core/interfaces.core.js';
|
||||||
@@ -13,6 +13,8 @@ import * as url from 'url';
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
|
const tapNodeTools = new TapNodeTools(tap);
|
||||||
|
|
||||||
// Test context
|
// Test context
|
||||||
let registry: SmartRegistry;
|
let registry: SmartRegistry;
|
||||||
let server: http.Server;
|
let server: http.Server;
|
||||||
@@ -208,7 +210,7 @@ async function uploadComposerPackage(
|
|||||||
'Content-Type': 'application/zip',
|
'Content-Type': 'application/zip',
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
body: zipData,
|
body: Uint8Array.from(zipData),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|||||||
@@ -97,6 +97,9 @@ tap.test('NPM: should publish package to smarts3', async () => {
|
|||||||
username: 'testuser',
|
username: 'testuser',
|
||||||
password: 'testpass',
|
password: 'testpass',
|
||||||
});
|
});
|
||||||
|
if (!userId) {
|
||||||
|
throw new Error('Expected test user authentication to succeed');
|
||||||
|
}
|
||||||
const token = await authManager.createNpmToken(userId, false);
|
const token = await authManager.createNpmToken(userId, false);
|
||||||
|
|
||||||
const packageData = {
|
const packageData = {
|
||||||
@@ -157,6 +160,9 @@ tap.test('OCI: should store blob in smarts3', async () => {
|
|||||||
username: 'testuser',
|
username: 'testuser',
|
||||||
password: 'testpass',
|
password: 'testpass',
|
||||||
});
|
});
|
||||||
|
if (!userId) {
|
||||||
|
throw new Error('Expected test user authentication to succeed');
|
||||||
|
}
|
||||||
const token = await authManager.createOciToken(
|
const token = await authManager.createOciToken(
|
||||||
userId,
|
userId,
|
||||||
['oci:repository:test-image:push'],
|
['oci:repository:test-image:push'],
|
||||||
@@ -260,8 +266,11 @@ tap.test('Cargo: should store crate in smarts3', async () => {
|
|||||||
// Verify stored
|
// Verify stored
|
||||||
const retrievedIndex = await storage.getCargoIndex('test-crate');
|
const retrievedIndex = await storage.getCargoIndex('test-crate');
|
||||||
expect(retrievedIndex).toBeDefined();
|
expect(retrievedIndex).toBeDefined();
|
||||||
|
if (!retrievedIndex) {
|
||||||
|
throw new Error('Expected Cargo index to be stored');
|
||||||
|
}
|
||||||
expect(retrievedIndex.length).toEqual(1);
|
expect(retrievedIndex.length).toEqual(1);
|
||||||
expect(retrievedIndex[0].name).toEqual('test-crate');
|
expect(retrievedIndex[0]?.name).toEqual('test-crate');
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
|
import { TapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
|
||||||
import { SmartRegistry } from '../ts/index.js';
|
import { SmartRegistry } from '../ts/index.js';
|
||||||
import { createTestRegistry, createTestTokens, createTestPom, createTestJar } from './helpers/registry.js';
|
import { createTestRegistry, createTestTokens, createTestPom, createTestJar } from './helpers/registry.js';
|
||||||
import type { IRequestContext, IResponse } from '../ts/core/interfaces.core.js';
|
import type { IRequestContext, IResponse } from '../ts/core/interfaces.core.js';
|
||||||
@@ -13,6 +13,8 @@ import * as url from 'url';
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
|
const tapNodeTools = new TapNodeTools(tap);
|
||||||
|
|
||||||
// Test context
|
// Test context
|
||||||
let registry: SmartRegistry;
|
let registry: SmartRegistry;
|
||||||
let server: http.Server;
|
let server: http.Server;
|
||||||
@@ -270,7 +272,7 @@ tap.test('Maven CLI: should verify mvn is installed', async () => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Maven CLI not available, skipping native CLI tests');
|
console.log('Maven CLI not available, skipping native CLI tests');
|
||||||
// Skip remaining tests if Maven is not installed
|
// Skip remaining tests if Maven is not installed
|
||||||
tap.skip.test('Maven CLI: remaining tests skipped - mvn not available');
|
tap.skip.test('Maven CLI: remaining tests skipped - mvn not available', async () => {});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
|
|
||||||
import { SmartRegistry } from '../ts/index.js';
|
import { SmartRegistry } from '../ts/index.js';
|
||||||
import { createTestRegistry, createTestTokens, cleanupS3Bucket } from './helpers/registry.js';
|
import { createTestRegistry, createTestTokens, cleanupS3Bucket } from './helpers/registry.js';
|
||||||
import type { IRequestContext, IResponse } from '../ts/core/interfaces.core.js';
|
import type { IRequestContext, IResponse } from '../ts/core/interfaces.core.js';
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
|
import { TapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
|
||||||
import { SmartRegistry } from '../ts/index.js';
|
import { SmartRegistry } from '../ts/index.js';
|
||||||
import type { IRequestContext, IResponse, IRegistryConfig } from '../ts/core/interfaces.core.js';
|
import type { IRequestContext, IResponse, IRegistryConfig } from '../ts/core/interfaces.core.js';
|
||||||
import * as qenv from '@push.rocks/qenv';
|
import * as qenv from '@push.rocks/qenv';
|
||||||
@@ -13,6 +13,8 @@ import * as url from 'url';
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
|
const tapNodeTools = new TapNodeTools(tap);
|
||||||
|
|
||||||
const testQenv = new qenv.Qenv('./', './.nogit');
|
const testQenv = new qenv.Qenv('./', './.nogit');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
|
import { TapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
|
||||||
import { SmartRegistry } from '../ts/index.js';
|
import { SmartRegistry } from '../ts/index.js';
|
||||||
import { createTestRegistry, createTestTokens, createPythonWheel, createPythonSdist, generateTestRunId } from './helpers/registry.js';
|
import { createTestRegistry, createTestTokens, createPythonWheel, createPythonSdist, generateTestRunId } from './helpers/registry.js';
|
||||||
import type { IRequestContext, IResponse } from '../ts/core/interfaces.core.js';
|
import type { IRequestContext, IResponse } from '../ts/core/interfaces.core.js';
|
||||||
@@ -13,6 +13,8 @@ import * as url from 'url';
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
|
const tapNodeTools = new TapNodeTools(tap);
|
||||||
|
|
||||||
// Test context
|
// Test context
|
||||||
let registry: SmartRegistry;
|
let registry: SmartRegistry;
|
||||||
let server: http.Server;
|
let server: http.Server;
|
||||||
@@ -291,7 +293,7 @@ tap.test('PyPI CLI: should verify twine is installed', async () => {
|
|||||||
|
|
||||||
if (!hasPip && !hasTwine) {
|
if (!hasPip && !hasTwine) {
|
||||||
console.log('Neither pip nor twine available, skipping native CLI tests');
|
console.log('Neither pip nor twine available, skipping native CLI tests');
|
||||||
tap.skip.test('PyPI CLI: remaining tests skipped - no CLI tools available');
|
tap.skip.test('PyPI CLI: remaining tests skipped - no CLI tools available', async () => {});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-1
@@ -62,7 +62,7 @@ tap.test('PyPI: should upload wheel file (POST /pypi/)', async () => {
|
|||||||
formData.append('pyversion', 'py3');
|
formData.append('pyversion', 'py3');
|
||||||
formData.append('metadata_version', '2.1');
|
formData.append('metadata_version', '2.1');
|
||||||
formData.append('sha256_digest', hashes.sha256);
|
formData.append('sha256_digest', hashes.sha256);
|
||||||
formData.append('content', new Blob([testWheelData]), filename);
|
formData.append('content', new Blob([Uint8Array.from(testWheelData)]), filename);
|
||||||
|
|
||||||
const response = await registry.handleRequest({
|
const response = await registry.handleRequest({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
|
import { TapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
|
||||||
import { SmartRegistry } from '../ts/index.js';
|
import { SmartRegistry } from '../ts/index.js';
|
||||||
import { createTestRegistry, createTestTokens, createRubyGem } from './helpers/registry.js';
|
import { createTestRegistry, createTestTokens, createRubyGem } from './helpers/registry.js';
|
||||||
import type { IRequestContext, IResponse } from '../ts/core/interfaces.core.js';
|
import type { IRequestContext, IResponse } from '../ts/core/interfaces.core.js';
|
||||||
@@ -13,6 +13,8 @@ import * as url from 'url';
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
|
const tapNodeTools = new TapNodeTools(tap);
|
||||||
|
|
||||||
// Test context
|
// Test context
|
||||||
let registry: SmartRegistry;
|
let registry: SmartRegistry;
|
||||||
let server: http.Server;
|
let server: http.Server;
|
||||||
|
|||||||
@@ -89,6 +89,17 @@ tap.test('setup: should create S3 storage backend', async () => {
|
|||||||
}
|
}
|
||||||
return paths;
|
return paths;
|
||||||
},
|
},
|
||||||
|
objectExists: async (key: string): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
const data = await s3Bucket.fastGet({ path: key });
|
||||||
|
return !!data;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getMetadata: async (_key: string): Promise<Record<string, string> | null> => {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(storageBackend).toBeTruthy();
|
expect(storageBackend).toBeTruthy();
|
||||||
@@ -584,9 +595,8 @@ tap.test('cleanup: should stop cache and clean up bucket', async () => {
|
|||||||
// Clean up test bucket
|
// Clean up test bucket
|
||||||
if (s3Bucket) {
|
if (s3Bucket) {
|
||||||
try {
|
try {
|
||||||
const files = await s3Bucket.fastList({});
|
for await (const filePath of s3Bucket.listAllObjects('')) {
|
||||||
for (const file of files) {
|
await s3Bucket.fastRemove({ path: filePath });
|
||||||
await s3Bucket.fastRemove({ path: file.name });
|
|
||||||
}
|
}
|
||||||
await smartBucket.removeBucket(bucketName);
|
await smartBucket.removeBucket(bucketName);
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -9,9 +9,19 @@ import type {
|
|||||||
IUpstreamProvider,
|
IUpstreamProvider,
|
||||||
IUpstreamResolutionContext,
|
IUpstreamResolutionContext,
|
||||||
IProtocolUpstreamConfig,
|
IProtocolUpstreamConfig,
|
||||||
|
IUpstreamRegistryConfig,
|
||||||
} from '../ts/upstream/interfaces.upstream.js';
|
} from '../ts/upstream/interfaces.upstream.js';
|
||||||
import type { TRegistryProtocol } from '../ts/core/interfaces.core.js';
|
import type { TRegistryProtocol } from '../ts/core/interfaces.core.js';
|
||||||
|
|
||||||
|
const createUpstream = (id: string, url: string): IUpstreamRegistryConfig => ({
|
||||||
|
id,
|
||||||
|
name: id,
|
||||||
|
url,
|
||||||
|
priority: 1,
|
||||||
|
enabled: true,
|
||||||
|
auth: { type: 'none' },
|
||||||
|
});
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// StaticUpstreamProvider Tests
|
// StaticUpstreamProvider Tests
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -19,7 +29,7 @@ import type { TRegistryProtocol } from '../ts/core/interfaces.core.js';
|
|||||||
tap.test('StaticUpstreamProvider: should return config for configured protocol', async () => {
|
tap.test('StaticUpstreamProvider: should return config for configured protocol', async () => {
|
||||||
const npmConfig: IProtocolUpstreamConfig = {
|
const npmConfig: IProtocolUpstreamConfig = {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
upstreams: [{ id: 'npmjs', url: 'https://registry.npmjs.org', priority: 1, enabled: true }],
|
upstreams: [createUpstream('npmjs', 'https://registry.npmjs.org')],
|
||||||
};
|
};
|
||||||
|
|
||||||
const provider = new StaticUpstreamProvider({
|
const provider = new StaticUpstreamProvider({
|
||||||
@@ -43,7 +53,7 @@ tap.test('StaticUpstreamProvider: should return null for unconfigured protocol',
|
|||||||
const provider = new StaticUpstreamProvider({
|
const provider = new StaticUpstreamProvider({
|
||||||
npm: {
|
npm: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
upstreams: [{ id: 'npmjs', url: 'https://registry.npmjs.org', priority: 1, enabled: true }],
|
upstreams: [createUpstream('npmjs', 'https://registry.npmjs.org')],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -62,15 +72,15 @@ tap.test('StaticUpstreamProvider: should support multiple protocols', async () =
|
|||||||
const provider = new StaticUpstreamProvider({
|
const provider = new StaticUpstreamProvider({
|
||||||
npm: {
|
npm: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
upstreams: [{ id: 'npmjs', url: 'https://registry.npmjs.org', priority: 1, enabled: true }],
|
upstreams: [createUpstream('npmjs', 'https://registry.npmjs.org')],
|
||||||
},
|
},
|
||||||
oci: {
|
oci: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
upstreams: [{ id: 'dockerhub', url: 'https://registry-1.docker.io', priority: 1, enabled: true }],
|
upstreams: [createUpstream('dockerhub', 'https://registry-1.docker.io')],
|
||||||
},
|
},
|
||||||
maven: {
|
maven: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
upstreams: [{ id: 'central', url: 'https://repo1.maven.org/maven2', priority: 1, enabled: true }],
|
upstreams: [createUpstream('central', 'https://repo1.maven.org/maven2')],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -113,7 +123,7 @@ tap.test('Provider Integration: should create registry with upstream provider',
|
|||||||
trackingProvider = createTrackingUpstreamProvider({
|
trackingProvider = createTrackingUpstreamProvider({
|
||||||
npm: {
|
npm: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
upstreams: [{ id: 'test-npm', url: 'https://registry.npmjs.org', priority: 1, enabled: true }],
|
upstreams: [createUpstream('test-npm', 'https://registry.npmjs.org')],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -200,13 +210,13 @@ tap.test('Custom Provider: should support dynamic resolution based on context',
|
|||||||
// Internal packages go to private registry
|
// Internal packages go to private registry
|
||||||
return {
|
return {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
upstreams: [{ id: 'private', url: 'https://private.registry.com', priority: 1, enabled: true }],
|
upstreams: [createUpstream('private', 'https://private.registry.com')],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// Everything else goes to public registry
|
// Everything else goes to public registry
|
||||||
return {
|
return {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
upstreams: [{ id: 'public', url: 'https://registry.npmjs.org', priority: 1, enabled: true }],
|
upstreams: [createUpstream('public', 'https://registry.npmjs.org')],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -237,12 +247,12 @@ tap.test('Custom Provider: should support actor-based resolution', async () => {
|
|||||||
if (context.actor?.orgId === 'enterprise-org') {
|
if (context.actor?.orgId === 'enterprise-org') {
|
||||||
return {
|
return {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
upstreams: [{ id: 'enterprise', url: 'https://enterprise.registry.com', priority: 1, enabled: true }],
|
upstreams: [createUpstream('enterprise', 'https://enterprise.registry.com')],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
upstreams: [{ id: 'default', url: 'https://registry.npmjs.org', priority: 1, enabled: true }],
|
upstreams: [createUpstream('default', 'https://registry.npmjs.org')],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -277,7 +287,7 @@ tap.test('Custom Provider: should support disabling upstream for specific resour
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
upstreams: [{ id: 'public', url: 'https://registry.npmjs.org', priority: 1, enabled: true }],
|
upstreams: [createUpstream('public', 'https://registry.npmjs.org')],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartregistry',
|
name: '@push.rocks/smartregistry',
|
||||||
version: '2.9.0',
|
version: '2.9.2',
|
||||||
description: 'A composable TypeScript library implementing OCI, NPM, Maven, Cargo, Composer, PyPI, and RubyGems registries for building unified container and package registries'
|
description: 'A composable TypeScript library implementing OCI, NPM, Maven, Cargo, Composer, PyPI, and RubyGems registries for building unified container and package registries'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ import type { IRequestContext, IResponse, IAuthToken, IRequestActor } from './in
|
|||||||
*/
|
*/
|
||||||
export abstract class BaseRegistry {
|
export abstract class BaseRegistry {
|
||||||
protected getHeader(contextOrHeaders: IRequestContext | Record<string, string>, name: string): string | undefined {
|
protected getHeader(contextOrHeaders: IRequestContext | Record<string, string>, name: string): string | undefined {
|
||||||
const headers = 'headers' in contextOrHeaders ? contextOrHeaders.headers : contextOrHeaders;
|
const headers: Record<string, string> =
|
||||||
|
'headers' in contextOrHeaders &&
|
||||||
|
typeof contextOrHeaders.headers === 'object' &&
|
||||||
|
contextOrHeaders.headers !== null
|
||||||
|
? contextOrHeaders.headers
|
||||||
|
: contextOrHeaders as Record<string, string>;
|
||||||
if (headers[name] !== undefined) {
|
if (headers[name] !== undefined) {
|
||||||
return headers[name];
|
return headers[name];
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -4,7 +4,11 @@
|
|||||||
"module": "NodeNext",
|
"module": "NodeNext",
|
||||||
"moduleResolution": "NodeNext",
|
"moduleResolution": "NodeNext",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"verbatimModuleSyntax": true
|
"verbatimModuleSyntax": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"types": [
|
||||||
|
"node"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"exclude": ["dist_*/**/*.d.ts"]
|
"exclude": ["dist_*/**/*.d.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user