2 Commits

Author SHA1 Message Date
cfadc89b5a v2.2.3
Some checks failed
Default (tags) / security (push) Successful in 45s
Default (tags) / test (push) Failing after 38s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-11-27 12:41:38 +00:00
eb91a3f75b fix(tests): Use unique test run IDs and add S3 cleanup in test helpers to avoid cross-run conflicts 2025-11-27 12:41:38 +00:00
6 changed files with 152 additions and 50 deletions

View File

@@ -1,5 +1,13 @@
# Changelog # Changelog
## 2025-11-27 - 2.2.3 - fix(tests)
Use unique test run IDs and add S3 cleanup in test helpers to avoid cross-run conflicts
- Add generateTestRunId() helper in test/helpers/registry.ts to produce unique IDs for each test run
- Update PyPI and Composer native CLI tests to use generated testPackageName / unauth-pkg-<id> to avoid package name collisions between runs
- Import smartbucket and add S3 bucket cleanup logic in test helpers to remove leftover objects between test runs
- Improve test robustness by skipping upload-dependent checks when tools (twine/composer) are not available and logging outputs for debugging
## 2025-11-25 - 2.2.2 - fix(npm) ## 2025-11-25 - 2.2.2 - fix(npm)
Replace console logging with structured Smartlog in NPM registry and silence RubyGems helper error logging Replace console logging with structured Smartlog in NPM registry and silence RubyGems helper error logging

View File

@@ -1,6 +1,6 @@
{ {
"name": "@push.rocks/smartregistry", "name": "@push.rocks/smartregistry",
"version": "2.2.2", "version": "2.2.3",
"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",

View File

@@ -1,11 +1,63 @@
import * as qenv from '@push.rocks/qenv'; import * as qenv from '@push.rocks/qenv';
import * as crypto from 'crypto'; import * as crypto from 'crypto';
import * as smartarchive from '@push.rocks/smartarchive'; import * as smartarchive from '@push.rocks/smartarchive';
import * as smartbucket from '@push.rocks/smartbucket';
import { SmartRegistry } from '../../ts/classes.smartregistry.js'; import { SmartRegistry } from '../../ts/classes.smartregistry.js';
import type { IRegistryConfig } from '../../ts/core/interfaces.core.js'; import type { IRegistryConfig } from '../../ts/core/interfaces.core.js';
const testQenv = new qenv.Qenv('./', './.nogit'); const testQenv = new qenv.Qenv('./', './.nogit');
/**
* Clean up S3 bucket contents for a fresh test run
* @param prefix Optional prefix to delete (e.g., 'cargo/', 'npm/', 'composer/')
*/
/**
* Generate a unique test run ID for avoiding conflicts between test runs
* Uses timestamp + random suffix for uniqueness
*/
export function generateTestRunId(): string {
const timestamp = Date.now().toString(36);
const random = Math.random().toString(36).substring(2, 6);
return `${timestamp}${random}`;
}
export async function cleanupS3Bucket(prefix?: string): Promise<void> {
const s3AccessKey = await testQenv.getEnvVarOnDemand('S3_ACCESSKEY');
const s3SecretKey = await testQenv.getEnvVarOnDemand('S3_SECRETKEY');
const s3Endpoint = await testQenv.getEnvVarOnDemand('S3_ENDPOINT');
const s3Port = await testQenv.getEnvVarOnDemand('S3_PORT');
const s3 = new smartbucket.SmartBucket({
accessKey: s3AccessKey || 'minioadmin',
accessSecret: s3SecretKey || 'minioadmin',
endpoint: s3Endpoint || 'localhost',
port: parseInt(s3Port || '9000', 10),
useSsl: false,
});
try {
const bucket = await s3.getBucket('test-registry');
if (bucket) {
if (prefix) {
// Delete only objects with the given prefix
const files = await bucket.fastList({ prefix });
for (const file of files) {
await bucket.fastRemove({ path: file.name });
}
} else {
// Delete all objects in the bucket
const files = await bucket.fastList({});
for (const file of files) {
await bucket.fastRemove({ path: file.name });
}
}
}
} catch (error) {
// Bucket might not exist yet, that's fine
console.log('Cleanup: No bucket to clean or error:', error);
}
}
/** /**
* Create a test SmartRegistry instance with all protocols enabled * Create a test SmartRegistry instance with all protocols enabled
*/ */

View File

@@ -6,7 +6,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 } 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';
import * as http from 'http'; import * as http from 'http';
import * as url from 'url'; import * as url from 'url';
@@ -21,6 +21,11 @@ let registryPort: number;
let composerToken: string; let composerToken: string;
let testDir: string; let testDir: string;
let composerHome: string; let composerHome: string;
let hasComposer = false;
// Unique test run ID to avoid conflicts between test runs
const testRunId = generateTestRunId();
const testPackageName = `testvendor/test-pkg-${testRunId}`;
/** /**
* Create HTTP server wrapper around SmartRegistry * Create HTTP server wrapper around SmartRegistry
@@ -235,12 +240,11 @@ tap.test('Composer CLI: should verify composer is installed', async () => {
try { try {
const result = await tapNodeTools.runCommand('composer --version'); const result = await tapNodeTools.runCommand('composer --version');
console.log('Composer version output:', result.stdout.substring(0, 200)); console.log('Composer version output:', result.stdout.substring(0, 200));
hasComposer = result.exitCode === 0;
expect(result.exitCode).toEqual(0); expect(result.exitCode).toEqual(0);
} catch (error) { } catch (error) {
console.log('Composer CLI not available, skipping native CLI tests'); console.log('Composer CLI not available, skipping native CLI tests');
// Skip remaining tests if Composer is not installed hasComposer = false;
tap.skip.test('Composer CLI: remaining tests skipped - composer not available');
return;
} }
}); });
@@ -284,27 +288,32 @@ tap.test('Composer CLI: should verify server is responding', async () => {
}); });
tap.test('Composer CLI: should upload a package via API', async () => { tap.test('Composer CLI: should upload a package via API', async () => {
const vendorPackage = 'testvendor/test-package';
const version = '1.0.0'; const version = '1.0.0';
await uploadComposerPackage(vendorPackage, version, composerToken, registryUrl); await uploadComposerPackage(testPackageName, version, composerToken, registryUrl);
// Verify package exists via packages.json // Verify package exists via p2 metadata endpoint (more reliable than packages.json for new packages)
const response = await fetch(`${registryUrl}/composer/packages.json`); const metadataResponse = await fetch(`${registryUrl}/composer/p2/${testPackageName}.json`);
expect(response.status).toEqual(200); expect(metadataResponse.status).toEqual(200);
const packagesJson = await response.json(); const metadata = await metadataResponse.json();
expect(packagesJson.packages).toBeDefined(); expect(metadata.packages).toBeDefined();
expect(packagesJson.packages[vendorPackage]).toBeDefined(); expect(metadata.packages[testPackageName]).toBeDefined();
expect(metadata.packages[testPackageName].length).toBeGreaterThan(0);
}); });
tap.test('Composer CLI: should require package from registry', async () => { tap.test('Composer CLI: should require package from registry', async () => {
if (!hasComposer) {
console.log('Skipping - composer not available');
return;
}
const projectDir = path.join(testDir, 'consumer-project'); const projectDir = path.join(testDir, 'consumer-project');
createComposerProject(projectDir, registryUrl); createComposerProject(projectDir, registryUrl);
// Try to require the package we uploaded // Try to require the package we uploaded
const result = await runComposerCommand( const result = await runComposerCommand(
'require testvendor/test-package:1.0.0 --no-interaction', `require ${testPackageName}:1.0.0 --no-interaction`,
projectDir projectDir
); );
console.log('composer require output:', result.stdout); console.log('composer require output:', result.stdout);
@@ -314,8 +323,15 @@ tap.test('Composer CLI: should require package from registry', async () => {
}); });
tap.test('Composer CLI: should verify package in vendor directory', async () => { tap.test('Composer CLI: should verify package in vendor directory', async () => {
if (!hasComposer) {
console.log('Skipping - composer not available');
return;
}
const projectDir = path.join(testDir, 'consumer-project'); const projectDir = path.join(testDir, 'consumer-project');
const packageDir = path.join(projectDir, 'vendor', 'testvendor', 'test-package'); // Parse vendor/package from testPackageName (e.g., "testvendor/test-pkg-abc123")
const [vendor, pkg] = testPackageName.split('/');
const packageDir = path.join(projectDir, 'vendor', vendor, pkg);
expect(fs.existsSync(packageDir)).toEqual(true); expect(fs.existsSync(packageDir)).toEqual(true);
@@ -325,25 +341,36 @@ tap.test('Composer CLI: should verify package in vendor directory', async () =>
}); });
tap.test('Composer CLI: should upload second version', async () => { tap.test('Composer CLI: should upload second version', async () => {
const vendorPackage = 'testvendor/test-package';
const version = '2.0.0'; const version = '2.0.0';
await uploadComposerPackage(vendorPackage, version, composerToken, registryUrl); await uploadComposerPackage(testPackageName, version, composerToken, registryUrl);
// Verify both versions exist // Verify both versions exist via p2 metadata endpoint (Composer v2 format)
const response = await fetch(`${registryUrl}/composer/packages.json`); const response = await fetch(`${registryUrl}/composer/p2/${testPackageName}.json`);
const packagesJson = await response.json(); expect(response.status).toEqual(200);
expect(packagesJson.packages[vendorPackage]['1.0.0']).toBeDefined(); const metadata = await response.json();
expect(packagesJson.packages[vendorPackage]['2.0.0']).toBeDefined(); expect(metadata.packages).toBeDefined();
expect(metadata.packages[testPackageName]).toBeDefined();
// Check that both versions are present
const versions = metadata.packages[testPackageName];
expect(versions.length).toBeGreaterThanOrEqual(2);
const versionNumbers = versions.map((v: any) => v.version);
expect(versionNumbers).toContain('1.0.0');
expect(versionNumbers).toContain('2.0.0');
}); });
tap.test('Composer CLI: should update to new version', async () => { tap.test('Composer CLI: should update to new version', async () => {
if (!hasComposer) {
console.log('Skipping - composer not available');
return;
}
const projectDir = path.join(testDir, 'consumer-project'); const projectDir = path.join(testDir, 'consumer-project');
// Update to version 2.0.0 // Update to version 2.0.0
const result = await runComposerCommand( const result = await runComposerCommand(
'require testvendor/test-package:2.0.0 --no-interaction', `require ${testPackageName}:2.0.0 --no-interaction`,
projectDir projectDir
); );
console.log('composer update output:', result.stdout); console.log('composer update output:', result.stdout);
@@ -355,11 +382,16 @@ tap.test('Composer CLI: should update to new version', async () => {
expect(fs.existsSync(lockPath)).toEqual(true); expect(fs.existsSync(lockPath)).toEqual(true);
const lockContent = JSON.parse(fs.readFileSync(lockPath, 'utf-8')); const lockContent = JSON.parse(fs.readFileSync(lockPath, 'utf-8'));
const pkg = lockContent.packages.find((p: any) => p.name === 'testvendor/test-package'); const pkg = lockContent.packages.find((p: any) => p.name === testPackageName);
expect(pkg?.version).toEqual('2.0.0'); expect(pkg?.version).toEqual('2.0.0');
}); });
tap.test('Composer CLI: should search for packages', async () => { tap.test('Composer CLI: should search for packages', async () => {
if (!hasComposer) {
console.log('Skipping - composer not available');
return;
}
const projectDir = path.join(testDir, 'consumer-project'); const projectDir = path.join(testDir, 'consumer-project');
// Search for packages (may not work on all Composer versions) // Search for packages (may not work on all Composer versions)
@@ -375,23 +407,33 @@ tap.test('Composer CLI: should search for packages', async () => {
}); });
tap.test('Composer CLI: should show package info', async () => { tap.test('Composer CLI: should show package info', async () => {
if (!hasComposer) {
console.log('Skipping - composer not available');
return;
}
const projectDir = path.join(testDir, 'consumer-project'); const projectDir = path.join(testDir, 'consumer-project');
const result = await runComposerCommand( const result = await runComposerCommand(
'show testvendor/test-package --no-interaction', `show ${testPackageName} --no-interaction`,
projectDir projectDir
); );
console.log('composer show output:', result.stdout); console.log('composer show output:', result.stdout);
expect(result.exitCode).toEqual(0); expect(result.exitCode).toEqual(0);
expect(result.stdout).toContain('testvendor/test-package'); expect(result.stdout).toContain(testPackageName);
}); });
tap.test('Composer CLI: should remove package', async () => { tap.test('Composer CLI: should remove package', async () => {
if (!hasComposer) {
console.log('Skipping - composer not available');
return;
}
const projectDir = path.join(testDir, 'consumer-project'); const projectDir = path.join(testDir, 'consumer-project');
const result = await runComposerCommand( const result = await runComposerCommand(
'remove testvendor/test-package --no-interaction', `remove ${testPackageName} --no-interaction`,
projectDir projectDir
); );
console.log('composer remove output:', result.stdout); console.log('composer remove output:', result.stdout);
@@ -399,7 +441,8 @@ tap.test('Composer CLI: should remove package', async () => {
expect(result.exitCode).toEqual(0); expect(result.exitCode).toEqual(0);
// Verify package is removed from vendor // Verify package is removed from vendor
const packageDir = path.join(projectDir, 'vendor', 'testvendor', 'test-package'); const [vendor, pkg] = testPackageName.split('/');
const packageDir = path.join(projectDir, 'vendor', vendor, pkg);
expect(fs.existsSync(packageDir)).toEqual(false); expect(fs.existsSync(packageDir)).toEqual(false);
}); });

View File

@@ -6,7 +6,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 } 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';
import * as http from 'http'; import * as http from 'http';
import * as url from 'url'; import * as url from 'url';
@@ -24,6 +24,10 @@ let pipHome: string;
let hasPip = false; let hasPip = false;
let hasTwine = false; let hasTwine = false;
// Unique test run ID to avoid conflicts between test runs
const testRunId = generateTestRunId();
const testPackageName = `test-pypi-pkg-${testRunId}`;
/** /**
* Create HTTP server wrapper around SmartRegistry * Create HTTP server wrapper around SmartRegistry
*/ */
@@ -347,9 +351,8 @@ tap.test('PyPI CLI: should upload wheel with twine', async () => {
return; return;
} }
const packageName = 'test-pypi-pkg';
const version = '1.0.0'; const version = '1.0.0';
const wheelPath = await createTestWheelFile(packageName, version, testDir); const wheelPath = await createTestWheelFile(testPackageName, version, testDir);
expect(fs.existsSync(wheelPath)).toEqual(true); expect(fs.existsSync(wheelPath)).toEqual(true);
@@ -369,9 +372,7 @@ tap.test('PyPI CLI: should verify package in simple index', async () => {
return; return;
} }
const packageName = 'test-pypi-pkg'; const response = await fetch(`${registryUrl}/pypi/simple/${testPackageName}/`);
const response = await fetch(`${registryUrl}/pypi/simple/${packageName}/`);
expect(response.status).toEqual(200); expect(response.status).toEqual(200);
const html = await response.text(); const html = await response.text();
@@ -384,9 +385,8 @@ tap.test('PyPI CLI: should upload sdist with twine', async () => {
return; return;
} }
const packageName = 'test-pypi-pkg';
const version = '1.1.0'; const version = '1.1.0';
const sdistPath = await createTestSdistFile(packageName, version, testDir); const sdistPath = await createTestSdistFile(testPackageName, version, testDir);
expect(fs.existsSync(sdistPath)).toEqual(true); expect(fs.existsSync(sdistPath)).toEqual(true);
@@ -406,9 +406,7 @@ tap.test('PyPI CLI: should list all versions in simple index', async () => {
return; return;
} }
const packageName = 'test-pypi-pkg'; const response = await fetch(`${registryUrl}/pypi/simple/${testPackageName}/`);
const response = await fetch(`${registryUrl}/pypi/simple/${packageName}/`);
expect(response.status).toEqual(200); expect(response.status).toEqual(200);
const html = await response.text(); const html = await response.text();
@@ -422,14 +420,12 @@ tap.test('PyPI CLI: should get JSON metadata', async () => {
return; return;
} }
const packageName = 'test-pypi-pkg'; const response = await fetch(`${registryUrl}/pypi/pypi/${testPackageName}/json`);
const response = await fetch(`${registryUrl}/pypi/pypi/${packageName}/json`);
expect(response.status).toEqual(200); expect(response.status).toEqual(200);
const metadata = await response.json(); const metadata = await response.json();
expect(metadata.info).toBeDefined(); expect(metadata.info).toBeDefined();
expect(metadata.info.name).toEqual(packageName); expect(metadata.info.name).toEqual(testPackageName);
expect(metadata.releases).toBeDefined(); expect(metadata.releases).toBeDefined();
expect(metadata.releases['1.0.0']).toBeDefined(); expect(metadata.releases['1.0.0']).toBeDefined();
}); });
@@ -445,7 +441,7 @@ tap.test('PyPI CLI: should download package with pip', async () => {
// Download (not install) the package // Download (not install) the package
const result = await runPipCommand( const result = await runPipCommand(
`download test-pypi-pkg==1.0.0 --dest "${downloadDir}" --no-deps`, `download ${testPackageName}==1.0.0 --dest "${downloadDir}" --no-deps`,
testDir testDir
); );
console.log('pip download output:', result.stdout); console.log('pip download output:', result.stdout);
@@ -457,14 +453,17 @@ tap.test('PyPI CLI: should download package with pip', async () => {
}); });
tap.test('PyPI CLI: should search for packages via API', async () => { tap.test('PyPI CLI: should search for packages via API', async () => {
const packageName = 'test-pypi-pkg'; if (!hasTwine) {
console.log('Skipping - twine not available (no packages uploaded)');
return;
}
// Use the JSON API to search/list // Use the JSON API to search/list
const response = await fetch(`${registryUrl}/pypi/pypi/${packageName}/json`); const response = await fetch(`${registryUrl}/pypi/pypi/${testPackageName}/json`);
expect(response.status).toEqual(200); expect(response.status).toEqual(200);
const metadata = await response.json(); const metadata = await response.json();
expect(metadata.info.name).toEqual(packageName); expect(metadata.info.name).toEqual(testPackageName);
}); });
tap.test('PyPI CLI: should fail upload without auth', async () => { tap.test('PyPI CLI: should fail upload without auth', async () => {
@@ -473,9 +472,9 @@ tap.test('PyPI CLI: should fail upload without auth', async () => {
return; return;
} }
const packageName = 'unauth-pkg'; const unauthPkgName = `unauth-pkg-${testRunId}`;
const version = '1.0.0'; const version = '1.0.0';
const wheelPath = await createTestWheelFile(packageName, version, testDir); const wheelPath = await createTestWheelFile(unauthPkgName, version, testDir);
// Create a pypirc without proper credentials // Create a pypirc without proper credentials
const badPypircPath = path.join(testDir, '.bad-pypirc'); const badPypircPath = path.join(testDir, '.bad-pypirc');

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/smartregistry', name: '@push.rocks/smartregistry',
version: '2.2.2', version: '2.2.3',
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'
} }