fix(tests): Use unique test run IDs and add S3 cleanup in test helpers to avoid cross-run conflicts
This commit is contained in:
@@ -1,11 +1,63 @@
|
||||
import * as qenv from '@push.rocks/qenv';
|
||||
import * as crypto from 'crypto';
|
||||
import * as smartarchive from '@push.rocks/smartarchive';
|
||||
import * as smartbucket from '@push.rocks/smartbucket';
|
||||
import { SmartRegistry } from '../../ts/classes.smartregistry.js';
|
||||
import type { IRegistryConfig } from '../../ts/core/interfaces.core.js';
|
||||
|
||||
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
|
||||
*/
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
|
||||
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 * as http from 'http';
|
||||
import * as url from 'url';
|
||||
@@ -21,6 +21,11 @@ let registryPort: number;
|
||||
let composerToken: string;
|
||||
let testDir: 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
|
||||
@@ -235,12 +240,11 @@ tap.test('Composer CLI: should verify composer is installed', async () => {
|
||||
try {
|
||||
const result = await tapNodeTools.runCommand('composer --version');
|
||||
console.log('Composer version output:', result.stdout.substring(0, 200));
|
||||
hasComposer = result.exitCode === 0;
|
||||
expect(result.exitCode).toEqual(0);
|
||||
} catch (error) {
|
||||
console.log('Composer CLI not available, skipping native CLI tests');
|
||||
// Skip remaining tests if Composer is not installed
|
||||
tap.skip.test('Composer CLI: remaining tests skipped - composer not available');
|
||||
return;
|
||||
hasComposer = false;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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 () => {
|
||||
const vendorPackage = 'testvendor/test-package';
|
||||
const version = '1.0.0';
|
||||
|
||||
await uploadComposerPackage(vendorPackage, version, composerToken, registryUrl);
|
||||
await uploadComposerPackage(testPackageName, version, composerToken, registryUrl);
|
||||
|
||||
// Verify package exists via packages.json
|
||||
const response = await fetch(`${registryUrl}/composer/packages.json`);
|
||||
expect(response.status).toEqual(200);
|
||||
// Verify package exists via p2 metadata endpoint (more reliable than packages.json for new packages)
|
||||
const metadataResponse = await fetch(`${registryUrl}/composer/p2/${testPackageName}.json`);
|
||||
expect(metadataResponse.status).toEqual(200);
|
||||
|
||||
const packagesJson = await response.json();
|
||||
expect(packagesJson.packages).toBeDefined();
|
||||
expect(packagesJson.packages[vendorPackage]).toBeDefined();
|
||||
const metadata = await metadataResponse.json();
|
||||
expect(metadata.packages).toBeDefined();
|
||||
expect(metadata.packages[testPackageName]).toBeDefined();
|
||||
expect(metadata.packages[testPackageName].length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
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');
|
||||
createComposerProject(projectDir, registryUrl);
|
||||
|
||||
// Try to require the package we uploaded
|
||||
const result = await runComposerCommand(
|
||||
'require testvendor/test-package:1.0.0 --no-interaction',
|
||||
`require ${testPackageName}:1.0.0 --no-interaction`,
|
||||
projectDir
|
||||
);
|
||||
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 () => {
|
||||
if (!hasComposer) {
|
||||
console.log('Skipping - composer not available');
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -325,25 +341,36 @@ tap.test('Composer CLI: should verify package in vendor directory', async () =>
|
||||
});
|
||||
|
||||
tap.test('Composer CLI: should upload second version', async () => {
|
||||
const vendorPackage = 'testvendor/test-package';
|
||||
const version = '2.0.0';
|
||||
|
||||
await uploadComposerPackage(vendorPackage, version, composerToken, registryUrl);
|
||||
await uploadComposerPackage(testPackageName, version, composerToken, registryUrl);
|
||||
|
||||
// Verify both versions exist
|
||||
const response = await fetch(`${registryUrl}/composer/packages.json`);
|
||||
const packagesJson = await response.json();
|
||||
// Verify both versions exist via p2 metadata endpoint (Composer v2 format)
|
||||
const response = await fetch(`${registryUrl}/composer/p2/${testPackageName}.json`);
|
||||
expect(response.status).toEqual(200);
|
||||
|
||||
expect(packagesJson.packages[vendorPackage]['1.0.0']).toBeDefined();
|
||||
expect(packagesJson.packages[vendorPackage]['2.0.0']).toBeDefined();
|
||||
const metadata = await response.json();
|
||||
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 () => {
|
||||
if (!hasComposer) {
|
||||
console.log('Skipping - composer not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const projectDir = path.join(testDir, 'consumer-project');
|
||||
|
||||
// Update to version 2.0.0
|
||||
const result = await runComposerCommand(
|
||||
'require testvendor/test-package:2.0.0 --no-interaction',
|
||||
`require ${testPackageName}:2.0.0 --no-interaction`,
|
||||
projectDir
|
||||
);
|
||||
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);
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
// 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 () => {
|
||||
if (!hasComposer) {
|
||||
console.log('Skipping - composer not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const projectDir = path.join(testDir, 'consumer-project');
|
||||
|
||||
const result = await runComposerCommand(
|
||||
'show testvendor/test-package --no-interaction',
|
||||
`show ${testPackageName} --no-interaction`,
|
||||
projectDir
|
||||
);
|
||||
console.log('composer show output:', result.stdout);
|
||||
|
||||
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 () => {
|
||||
if (!hasComposer) {
|
||||
console.log('Skipping - composer not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const projectDir = path.join(testDir, 'consumer-project');
|
||||
|
||||
const result = await runComposerCommand(
|
||||
'remove testvendor/test-package --no-interaction',
|
||||
`remove ${testPackageName} --no-interaction`,
|
||||
projectDir
|
||||
);
|
||||
console.log('composer remove output:', result.stdout);
|
||||
@@ -399,7 +441,8 @@ tap.test('Composer CLI: should remove package', async () => {
|
||||
expect(result.exitCode).toEqual(0);
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
|
||||
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 * as http from 'http';
|
||||
import * as url from 'url';
|
||||
@@ -24,6 +24,10 @@ let pipHome: string;
|
||||
let hasPip = 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
|
||||
*/
|
||||
@@ -347,9 +351,8 @@ tap.test('PyPI CLI: should upload wheel with twine', async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const packageName = 'test-pypi-pkg';
|
||||
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);
|
||||
|
||||
@@ -369,9 +372,7 @@ tap.test('PyPI CLI: should verify package in simple index', async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const packageName = 'test-pypi-pkg';
|
||||
|
||||
const response = await fetch(`${registryUrl}/pypi/simple/${packageName}/`);
|
||||
const response = await fetch(`${registryUrl}/pypi/simple/${testPackageName}/`);
|
||||
expect(response.status).toEqual(200);
|
||||
|
||||
const html = await response.text();
|
||||
@@ -384,9 +385,8 @@ tap.test('PyPI CLI: should upload sdist with twine', async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const packageName = 'test-pypi-pkg';
|
||||
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);
|
||||
|
||||
@@ -406,9 +406,7 @@ tap.test('PyPI CLI: should list all versions in simple index', async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const packageName = 'test-pypi-pkg';
|
||||
|
||||
const response = await fetch(`${registryUrl}/pypi/simple/${packageName}/`);
|
||||
const response = await fetch(`${registryUrl}/pypi/simple/${testPackageName}/`);
|
||||
expect(response.status).toEqual(200);
|
||||
|
||||
const html = await response.text();
|
||||
@@ -422,14 +420,12 @@ tap.test('PyPI CLI: should get JSON metadata', async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const packageName = 'test-pypi-pkg';
|
||||
|
||||
const response = await fetch(`${registryUrl}/pypi/pypi/${packageName}/json`);
|
||||
const response = await fetch(`${registryUrl}/pypi/pypi/${testPackageName}/json`);
|
||||
expect(response.status).toEqual(200);
|
||||
|
||||
const metadata = await response.json();
|
||||
expect(metadata.info).toBeDefined();
|
||||
expect(metadata.info.name).toEqual(packageName);
|
||||
expect(metadata.info.name).toEqual(testPackageName);
|
||||
expect(metadata.releases).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
|
||||
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
|
||||
);
|
||||
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 () => {
|
||||
const packageName = 'test-pypi-pkg';
|
||||
if (!hasTwine) {
|
||||
console.log('Skipping - twine not available (no packages uploaded)');
|
||||
return;
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
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 () => {
|
||||
@@ -473,9 +472,9 @@ tap.test('PyPI CLI: should fail upload without auth', async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const packageName = 'unauth-pkg';
|
||||
const unauthPkgName = `unauth-pkg-${testRunId}`;
|
||||
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
|
||||
const badPypircPath = path.join(testDir, '.bad-pypirc');
|
||||
|
||||
Reference in New Issue
Block a user