feat(tapbundle_serverside): add network port discovery utilities and migrate file I/O to smartfs; refactor runtimes to use Node fs and SmartFs, update server APIs and bump dependencies

This commit is contained in:
2026-03-03 20:15:59 +00:00
parent 4d1896bdf9
commit f23c902658
24 changed files with 2562 additions and 2094 deletions

View File

@@ -1,5 +1,16 @@
# Changelog
## 2026-03-03 - 3.2.0 - feat(tapbundle_serverside)
add network port discovery utilities and migrate file I/O to smartfs; refactor runtimes to use Node fs and SmartFs, update server APIs and bump dependencies
- Add tapNodeTools.findFreePort, findFreePorts and findFreePortRange to provide network port discovery and ranges for tests
- Integrate @push.rocks/smartfs (smartfsInstance) and replace many @push.rocks/smartfile.fs usages with smartfsInstance file/directory APIs
- Switch several internals to Node's fs (exported via plugins) and introduce SmartFileFactory.nodeFs() for file handling
- Replace smartchok with smartwatch for file watching and update watch/start/stop flows
- Update server instantiation to TypedServer and change addRoute usage to the new handler signature; serve bundled test directory
- Add tests for network tools and update migration/test code to use smartfsInstance
- Bump multiple dependencies (e.g. @api.global/typedserver, @push.rocks/smartfile/smartfs/smartnetwork/smarts3/smartwatch) and @git.zone/tsbuild
## 2026-01-25 - 3.1.8 - fix(tapbundle)
treat tests that call tools.allowFailure() as passing and update tests to use tools parameter

View File

@@ -25,35 +25,36 @@
"buildDocs": "tsdoc"
},
"devDependencies": {
"@git.zone/tsbuild": "^3.1.0",
"@git.zone/tsbuild": "^4.1.2",
"@types/node": "^22.15.21"
},
"dependencies": {
"@api.global/typedserver": "^3.0.80",
"@git.zone/tsbundle": "^2.5.2",
"@git.zone/tsrun": "^2.0.0",
"@api.global/typedserver": "^8.4.0",
"@git.zone/tsbundle": "^2.9.0",
"@git.zone/tsrun": "^2.0.1",
"@push.rocks/consolecolor": "^2.0.3",
"@push.rocks/qenv": "^6.1.3",
"@push.rocks/smartbrowser": "^2.0.8",
"@push.rocks/smartchok": "^1.1.1",
"@push.rocks/smartcrypto": "^2.0.4",
"@push.rocks/smartdelay": "^3.0.5",
"@push.rocks/smartenv": "^6.0.0",
"@push.rocks/smartexpect": "^2.5.0",
"@push.rocks/smartfile": "^11.2.7",
"@push.rocks/smartjson": "^5.2.0",
"@push.rocks/smartlog": "^3.1.10",
"@push.rocks/smartmongo": "^2.0.14",
"@push.rocks/smartfile": "^13.1.2",
"@push.rocks/smartfs": "^1.3.1",
"@push.rocks/smartjson": "^6.0.0",
"@push.rocks/smartlog": "^3.2.1",
"@push.rocks/smartmongo": "^5.1.0",
"@push.rocks/smartnetwork": "^4.4.0",
"@push.rocks/smartpath": "^6.0.0",
"@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartrequest": "^5.0.1",
"@push.rocks/smarts3": "^3.0.0",
"@push.rocks/smarts3": "^5.3.0",
"@push.rocks/smartshell": "^3.3.0",
"@push.rocks/smarttime": "^4.1.1",
"@push.rocks/smartwatch": "^6.3.0",
"@push.rocks/smarttime": "^4.2.3",
"@types/ws": "^8.18.1",
"figures": "^6.1.0",
"ws": "^8.18.3"
"ws": "^8.19.0"
},
"files": [
"ts/**/*",

2519
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@ This project integrates tstest with tapbundle through a modular architecture:
1. **tstest** (`/ts/`) - The test runner that discovers and executes test files
2. **tapbundle** (`/ts_tapbundle/`) - The TAP testing framework for writing tests
3. **tapbundle_serverside** (`/ts_tapbundle_serverside/`) - Server-side testing utilities (runCommand, env vars, HTTPS certs, MongoDB, S3, test assets)
3. **tapbundle_serverside** (`/ts_tapbundle_serverside/`) - Server-side testing utilities (network port finding, runCommand, env vars, HTTPS certs, MongoDB, S3, test assets)
## How Components Work Together

1193
readme.md

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,55 @@
import { tap, expect } from '../../ts_tapbundle/index.js';
import { tapNodeTools } from '../../ts_tapbundle_serverside/index.js';
tap.test('should find a single free port', async () => {
const port = await tapNodeTools.findFreePort();
expect(port).toBeTypeOf('number');
expect(port).toBeGreaterThanOrEqual(3000);
expect(port).toBeLessThanOrEqual(60000);
});
tap.test('should find a free port in a specific range', async () => {
const port = await tapNodeTools.findFreePort({
startPort: 8000,
endPort: 9000,
});
expect(port).toBeGreaterThanOrEqual(8000);
expect(port).toBeLessThanOrEqual(9000);
});
tap.test('should find a free port with exclusions', async () => {
const port = await tapNodeTools.findFreePort({
startPort: 10000,
endPort: 10100,
exclude: [10000, 10001, 10002],
});
expect(port).toBeGreaterThanOrEqual(10000);
expect(port).toBeLessThanOrEqual(10100);
expect(port).not.toEqual(10000);
expect(port).not.toEqual(10001);
expect(port).not.toEqual(10002);
});
tap.test('should find multiple free ports', async () => {
const ports = await tapNodeTools.findFreePorts(3);
expect(ports).toHaveLength(3);
// All ports should be distinct
const uniquePorts = new Set(ports);
expect(uniquePorts.size).toEqual(3);
for (const port of ports) {
expect(port).toBeGreaterThanOrEqual(3000);
expect(port).toBeLessThanOrEqual(60000);
}
});
tap.test('should find a consecutive port range', async () => {
const ports = await tapNodeTools.findFreePortRange(3, {
startPort: 20000,
endPort: 30000,
});
expect(ports).toHaveLength(3);
expect(ports[1]).toEqual(ports[0] + 1);
expect(ports[2]).toEqual(ports[0] + 2);
});
tap.start();

View File

@@ -37,10 +37,11 @@ tap.test('Migration - generateReport works', async () => {
tap.test('Migration - detects legacy files when they exist', async () => {
// Create a temporary legacy test file
const tempDir = plugins.path.join(process.cwd(), '.nogit', 'test_migration');
await plugins.smartfile.fs.ensureEmptyDir(tempDir);
try { await plugins.smartfsInstance.directory(tempDir).recursive().delete(); } catch (e) { /* may not exist */ }
await plugins.smartfsInstance.directory(tempDir).recursive().create();
const legacyFile = plugins.path.join(tempDir, 'test.browser.ts');
await plugins.smartfile.memory.toFs('// Legacy test file\nexport default Promise.resolve();', legacyFile);
await plugins.smartfsInstance.file(legacyFile).write('// Legacy test file\nexport default Promise.resolve();');
const migration = new Migration({
baseDir: tempDir,
@@ -53,18 +54,19 @@ tap.test('Migration - detects legacy files when they exist', async () => {
expect(legacyFiles[0]).toContain('test.browser.ts');
// Clean up
await plugins.smartfile.fs.removeSync(tempDir);
plugins.fs.rmSync(tempDir, { recursive: true, force: true });
});
tap.test('Migration - detects both legacy pattern', async () => {
// Create temporary legacy files
const tempDir = plugins.path.join(process.cwd(), '.nogit', 'test_migration_both');
await plugins.smartfile.fs.ensureEmptyDir(tempDir);
try { await plugins.smartfsInstance.directory(tempDir).recursive().delete(); } catch (e) { /* may not exist */ }
await plugins.smartfsInstance.directory(tempDir).recursive().create();
const browserFile = plugins.path.join(tempDir, 'test.browser.ts');
const bothFile = plugins.path.join(tempDir, 'test.both.ts');
await plugins.smartfile.memory.toFs('// Browser test\nexport default Promise.resolve();', browserFile);
await plugins.smartfile.memory.toFs('// Both test\nexport default Promise.resolve();', bothFile);
await plugins.smartfsInstance.file(browserFile).write('// Browser test\nexport default Promise.resolve();');
await plugins.smartfsInstance.file(bothFile).write('// Both test\nexport default Promise.resolve();');
const migration = new Migration({
baseDir: tempDir,
@@ -76,16 +78,17 @@ tap.test('Migration - detects both legacy pattern', async () => {
expect(legacyFiles.length).toEqual(2);
// Clean up
await plugins.smartfile.fs.removeSync(tempDir);
plugins.fs.rmSync(tempDir, { recursive: true, force: true });
});
tap.test('Migration - dry run does not modify files', async () => {
// Create a temporary legacy test file
const tempDir = plugins.path.join(process.cwd(), '.nogit', 'test_migration_dryrun');
await plugins.smartfile.fs.ensureEmptyDir(tempDir);
try { await plugins.smartfsInstance.directory(tempDir).recursive().delete(); } catch (e) { /* may not exist */ }
await plugins.smartfsInstance.directory(tempDir).recursive().create();
const legacyFile = plugins.path.join(tempDir, 'test.browser.ts');
await plugins.smartfile.memory.toFs('// Legacy test file\nexport default Promise.resolve();', legacyFile);
await plugins.smartfsInstance.file(legacyFile).write('// Legacy test file\nexport default Promise.resolve();');
const migration = new Migration({
baseDir: tempDir,
@@ -101,11 +104,11 @@ tap.test('Migration - dry run does not modify files', async () => {
expect(summary.migratedCount).toEqual(1); // Dry run still counts as "would migrate"
// Verify original file still exists
const fileExists = await plugins.smartfile.fs.fileExists(legacyFile);
const fileExists = await plugins.smartfsInstance.file(legacyFile).exists();
expect(fileExists).toEqual(true);
// Clean up
await plugins.smartfile.fs.removeSync(tempDir);
plugins.fs.rmSync(tempDir, { recursive: true, force: true });
});
export default tap.start();

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@git.zone/tstest',
version: '3.1.8',
version: '3.2.0',
description: 'a test utility to run tests that match test/**/*.ts'
}

View File

@@ -111,10 +111,7 @@ export class Migration {
* Find all legacy test files in the base directory
*/
async findLegacyFiles(): Promise<string[]> {
const files = await plugins.smartfile.fs.listFileTree(
this.options.baseDir,
this.options.pattern
);
const files = plugins.fs.globSync(this.options.pattern, { cwd: this.options.baseDir }) as string[];
const legacyFiles: string[] = [];
@@ -154,7 +151,7 @@ export class Migration {
const newPath = plugins.path.join(dirName, newFileName);
// Check if target file already exists
if (await plugins.smartfile.fs.fileExists(newPath)) {
if (await plugins.smartfsInstance.file(newPath).exists()) {
return {
oldPath: filePath,
newPath,
@@ -206,7 +203,7 @@ export class Migration {
private async isGitRepository(dir: string): Promise<boolean> {
try {
const gitDir = plugins.path.join(dir, '.git');
return await plugins.smartfile.fs.isDirectory(gitDir);
return await plugins.smartfsInstance.directory(gitDir).exists();
} catch {
return false;
}

View File

@@ -121,7 +121,7 @@ export class BunRuntimeAdapter extends RuntimeAdapter {
// Check for 00init.ts file in test directory
const testDir = plugins.path.dirname(testFile);
const initFile = plugins.path.join(testDir, '00init.ts');
const initFileExists = await plugins.smartfile.fs.fileExists(initFile);
const initFileExists = await plugins.smartfsInstance.file(initFile).exists();
let runCommand = fullCommand;
let loaderPath: string | null = null;
@@ -135,7 +135,7 @@ import '${absoluteInitFile.replace(/\\/g, '/')}';
import '${absoluteTestFile.replace(/\\/g, '/')}';
`;
loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(testFile)}`);
await plugins.smartfile.memory.toFs(loaderContent, loaderPath);
await plugins.smartfsInstance.file(loaderPath).write(loaderContent);
// Rebuild command with loader file
const loaderCommand = this.createCommand(loaderPath, mergedOptions);
@@ -148,8 +148,8 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
if (loaderPath) {
const cleanup = () => {
try {
if (plugins.smartfile.fs.fileExistsSync(loaderPath)) {
plugins.smartfile.fs.removeSync(loaderPath);
if (plugins.fs.existsSync(loaderPath)) {
plugins.fs.rmSync(loaderPath, { force: true });
}
} catch (e) {
// Ignore cleanup errors

View File

@@ -107,7 +107,8 @@ export class ChromiumRuntimeAdapter extends RuntimeAdapter {
const bundleFilePath = plugins.path.join(tsbundleCacheDirPath, bundleFileName);
// lets bundle the test
await plugins.smartfile.fs.ensureEmptyDir(tsbundleCacheDirPath);
try { await plugins.smartfsInstance.directory(tsbundleCacheDirPath).recursive().delete(); } catch (e) { /* may not exist */ }
await plugins.smartfsInstance.directory(tsbundleCacheDirPath).recursive().create();
await this.tsbundleInstance.build(process.cwd(), testFile, bundleFilePath, {
bundler: 'esbuild',
});
@@ -116,15 +117,13 @@ export class ChromiumRuntimeAdapter extends RuntimeAdapter {
const { httpPort, wsPort } = await this.findFreePorts();
// lets create a server
const server = new plugins.typedserver.servertools.Server({
const server = new plugins.typedserver.TypedServer({
cors: true,
port: httpPort,
serveDir: tsbundleCacheDirPath,
});
server.addRoute(
'/test',
new plugins.typedserver.servertools.Handler('GET', async (_req, res) => {
res.type('.html');
res.write(`
server.addRoute('/test', 'GET', async () => {
return new Response(`
<html>
<head>
<script>
@@ -134,11 +133,8 @@ export class ChromiumRuntimeAdapter extends RuntimeAdapter {
</head>
<body></body>
</html>
`);
res.end();
})
);
server.addRoute('/*splat', new plugins.typedserver.servertools.HandlerStatic(tsbundleCacheDirPath));
`, { headers: { 'Content-Type': 'text/html' } });
});
await server.start();
// lets handle realtime comms

View File

@@ -36,9 +36,9 @@ export class DenoRuntimeAdapter extends RuntimeAdapter {
const denoJsonPath = plugins.path.join(process.cwd(), 'deno.json');
const denoJsoncPath = plugins.path.join(process.cwd(), 'deno.jsonc');
if (plugins.smartfile.fs.fileExistsSync(denoJsonPath)) {
if (plugins.fs.existsSync(denoJsonPath)) {
configPath = denoJsonPath;
} else if (plugins.smartfile.fs.fileExistsSync(denoJsoncPath)) {
} else if (plugins.fs.existsSync(denoJsoncPath)) {
configPath = denoJsoncPath;
}
@@ -173,7 +173,7 @@ export class DenoRuntimeAdapter extends RuntimeAdapter {
// Check for 00init.ts file in test directory
const testDir = plugins.path.dirname(testFile);
const initFile = plugins.path.join(testDir, '00init.ts');
const initFileExists = await plugins.smartfile.fs.fileExists(initFile);
const initFileExists = await plugins.smartfsInstance.file(initFile).exists();
let runCommand = fullCommand;
let loaderPath: string | null = null;
@@ -187,7 +187,7 @@ import '${absoluteInitFile.replace(/\\/g, '/')}';
import '${absoluteTestFile.replace(/\\/g, '/')}';
`;
loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(testFile)}`);
await plugins.smartfile.memory.toFs(loaderContent, loaderPath);
await plugins.smartfsInstance.file(loaderPath).write(loaderContent);
// Rebuild command with loader file
const loaderCommand = this.createCommand(loaderPath, mergedOptions);
@@ -200,8 +200,8 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
if (loaderPath) {
const cleanup = () => {
try {
if (plugins.smartfile.fs.fileExistsSync(loaderPath)) {
plugins.smartfile.fs.removeSync(loaderPath);
if (plugins.fs.existsSync(loaderPath)) {
plugins.fs.rmSync(loaderPath, { force: true });
}
} catch (e) {
// Ignore cleanup errors

View File

@@ -102,7 +102,7 @@ export class DockerRuntimeAdapter extends RuntimeAdapter {
}
// Check if Dockerfile exists
if (!await plugins.smartfile.fs.fileExists(dockerfilePath)) {
if (!await plugins.smartfsInstance.file(dockerfilePath).exists()) {
throw new Error(
`Dockerfile not found: ${dockerfilePath}\n` +
`Expected Dockerfile for Docker test variant.`

View File

@@ -123,7 +123,7 @@ export class NodeRuntimeAdapter extends RuntimeAdapter {
// Check for 00init.ts file in test directory
const testDir = plugins.path.dirname(testFile);
const initFile = plugins.path.join(testDir, '00init.ts');
const initFileExists = await plugins.smartfile.fs.fileExists(initFile);
const initFileExists = await plugins.smartfsInstance.file(initFile).exists();
// Determine which file to run
let fileToRun = testFile;
@@ -138,7 +138,7 @@ import '${absoluteInitFile.replace(/\\/g, '/')}';
import '${absoluteTestFile.replace(/\\/g, '/')}';
`;
loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(testFile)}`);
await plugins.smartfile.memory.toFs(loaderContent, loaderPath);
await plugins.smartfsInstance.file(loaderPath).write(loaderContent);
fileToRun = loaderPath;
}
@@ -150,8 +150,8 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
if (loaderPath) {
const cleanup = () => {
try {
if (plugins.smartfile.fs.fileExistsSync(loaderPath)) {
plugins.smartfile.fs.removeSync(loaderPath);
if (plugins.fs.existsSync(loaderPath)) {
plugins.fs.rmSync(loaderPath, { force: true });
}
} catch (e) {
// Ignore cleanup errors

View File

@@ -497,12 +497,10 @@ export class TapParser {
*/
private async handleSnapshot(snapshotData: { path: string; content: string; action: string }) {
try {
const smartfile = await import('@push.rocks/smartfile');
if (snapshotData.action === 'compare') {
// Try to read existing snapshot
try {
const existingSnapshot = await smartfile.fs.toStringSync(snapshotData.path);
const existingSnapshot = await plugins.smartfsInstance.file(snapshotData.path).encoding('utf8').read() as string;
if (existingSnapshot !== snapshotData.content) {
// Snapshot mismatch
if (this.logger) {
@@ -520,8 +518,8 @@ export class TapParser {
if (error.code === 'ENOENT') {
// Snapshot doesn't exist, create it
const dirPath = snapshotData.path.substring(0, snapshotData.path.lastIndexOf('/'));
await smartfile.fs.ensureDir(dirPath);
await smartfile.memory.toFs(snapshotData.content, snapshotData.path);
await plugins.smartfsInstance.directory(dirPath).recursive().create();
await plugins.smartfsInstance.file(snapshotData.path).write(snapshotData.content);
if (this.logger) {
this.logger.testConsoleOutput(`Snapshot created: ${snapshotData.path}`);
}
@@ -532,8 +530,8 @@ export class TapParser {
} else if (snapshotData.action === 'update') {
// Update snapshot
const dirPath = snapshotData.path.substring(0, snapshotData.path.lastIndexOf('/'));
await smartfile.fs.ensureDir(dirPath);
await smartfile.memory.toFs(snapshotData.content, snapshotData.path);
await plugins.smartfsInstance.directory(dirPath).recursive().create();
await plugins.smartfsInstance.file(snapshotData.path).write(snapshotData.content);
if (this.logger) {
this.logger.testConsoleOutput(`Snapshot updated: ${snapshotData.path}`);
}

View File

@@ -1,8 +1,10 @@
import * as plugins from './tstest.plugins.js';
import * as paths from './tstest.paths.js';
import { SmartFile } from '@push.rocks/smartfile';
import { type SmartFile, SmartFileFactory } from '@push.rocks/smartfile';
import { TestExecutionMode } from './index.js';
const smartFileFactory = SmartFileFactory.nodeFs();
// tap related stuff
import { TapCombinator } from './tstest.classes.tap.combinator.js';
import { TapParser } from './tstest.classes.tap.parser.js';
@@ -45,28 +47,28 @@ export class TestDirectory {
switch (this.executionMode) {
case TestExecutionMode.FILE:
// Single file mode
const filePath = plugins.path.isAbsolute(this.testPath)
? this.testPath
const filePath = plugins.path.isAbsolute(this.testPath)
? this.testPath
: plugins.path.join(this.cwd, this.testPath);
if (await plugins.smartfile.fs.fileExists(filePath)) {
this.testfileArray = [await plugins.smartfile.SmartFile.fromFilePath(filePath)];
if (await plugins.smartfsInstance.file(filePath).exists()) {
this.testfileArray = [await smartFileFactory.fromFilePath(filePath)];
} else {
throw new Error(`Test file not found: ${filePath}`);
}
break;
case TestExecutionMode.GLOB:
// Glob pattern mode - use listFileTree which supports glob patterns
// Glob pattern mode - use Node.js fs.globSync for full glob support
const globPattern = this.testPath;
const matchedFiles = await plugins.smartfile.fs.listFileTree(this.cwd, globPattern);
const matchedFiles = plugins.fs.globSync(globPattern, { cwd: this.cwd });
this.testfileArray = await Promise.all(
matchedFiles.map(async (filePath) => {
const absolutePath = plugins.path.isAbsolute(filePath)
? filePath
matchedFiles.map(async (filePath: string) => {
const absolutePath = plugins.path.isAbsolute(filePath)
? filePath
: plugins.path.join(this.cwd, filePath);
return await plugins.smartfile.SmartFile.fromFilePath(absolutePath);
return await smartFileFactory.fromFilePath(absolutePath);
})
);
break;
@@ -79,19 +81,16 @@ export class TestDirectory {
const tsPattern = '**/test*.ts';
const dockerPattern = '**/*.docker.sh';
const [tsFiles, dockerFiles] = await Promise.all([
plugins.smartfile.fs.listFileTree(dirPath, tsPattern),
plugins.smartfile.fs.listFileTree(dirPath, dockerPattern),
]);
const allTestFiles = [...tsFiles, ...dockerFiles];
const tsFiles = plugins.fs.globSync(tsPattern, { cwd: dirPath });
const dockerFiles = plugins.fs.globSync(dockerPattern, { cwd: dirPath });
const allTestFiles = [...tsFiles, ...dockerFiles] as string[];
this.testfileArray = await Promise.all(
allTestFiles.map(async (filePath) => {
const absolutePath = plugins.path.isAbsolute(filePath)
? filePath
: plugins.path.join(dirPath, filePath);
return await plugins.smartfile.SmartFile.fromFilePath(absolutePath);
return await smartFileFactory.fromFilePath(absolutePath);
})
);
break;

View File

@@ -132,7 +132,7 @@ export class TsTest {
}
public async runWatch(ignorePatterns: string[] = []) {
const smartchokInstance = new plugins.smartchok.Smartchok([this.testDir.cwd]);
const smartwatchInstance = new plugins.smartwatch.Smartwatch([this.testDir.cwd]);
console.clear();
this.logger.watchModeStart();
@@ -155,12 +155,12 @@ export class TsTest {
};
// Start watching before subscribing to events
await smartchokInstance.start();
await smartwatchInstance.start();
// Subscribe to file change events
const changeObservable = await smartchokInstance.getObservableFor('change');
const addObservable = await smartchokInstance.getObservableFor('add');
const unlinkObservable = await smartchokInstance.getObservableFor('unlink');
const changeObservable = await smartwatchInstance.getObservableFor('change');
const addObservable = await smartwatchInstance.getObservableFor('add');
const unlinkObservable = await smartwatchInstance.getObservableFor('unlink');
const handleFileChange = (changedPath: string) => {
// Skip if path matches ignore patterns
@@ -194,7 +194,7 @@ export class TsTest {
// Handle Ctrl+C to exit gracefully
process.on('SIGINT', async () => {
this.logger.watchModeStop();
await smartchokInstance.stop();
await smartwatchInstance.stop();
process.exit(0);
});
@@ -316,8 +316,8 @@ export class TsTest {
const initFile = plugins.path.join(testDir, '00init.ts');
let runCommand = `tsrun ${fileNameArg}${tsrunOptions}`;
const initFileExists = await plugins.smartfile.fs.fileExists(initFile);
const initFileExists = await plugins.smartfsInstance.file(initFile).exists();
// If 00init.ts exists, run it first
if (initFileExists) {
// Create a temporary loader file that imports both 00init.ts and the test file
@@ -328,7 +328,7 @@ import '${absoluteInitFile.replace(/\\/g, '/')}';
import '${absoluteTestFile.replace(/\\/g, '/')}';
`;
const loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(fileNameArg)}`);
await plugins.smartfile.memory.toFs(loaderContent, loaderPath);
await plugins.smartfsInstance.file(loaderPath).write(loaderContent);
runCommand = `tsrun ${loaderPath}${tsrunOptions}`;
}
@@ -339,14 +339,14 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
const loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(fileNameArg)}`);
const cleanup = () => {
try {
if (plugins.smartfile.fs.fileExistsSync(loaderPath)) {
plugins.smartfile.fs.removeSync(loaderPath);
if (plugins.fs.existsSync(loaderPath)) {
plugins.fs.rmSync(loaderPath, { force: true });
}
} catch (e) {
// Ignore cleanup errors
}
};
execResultStreaming.childProcess.on('exit', cleanup);
execResultStreaming.childProcess.on('error', cleanup);
}
@@ -445,7 +445,8 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
const bundleFilePath = plugins.path.join(tsbundleCacheDirPath, bundleFileName);
// lets bundle the test
await plugins.smartfile.fs.ensureEmptyDir(tsbundleCacheDirPath);
try { await plugins.smartfsInstance.directory(tsbundleCacheDirPath).recursive().delete(); } catch (e) { /* may not exist */ }
await plugins.smartfsInstance.directory(tsbundleCacheDirPath).recursive().create();
await this.tsbundleInstance.build(process.cwd(), fileNameArg, bundleFilePath, {
bundler: 'esbuild',
});
@@ -454,15 +455,13 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
const { httpPort, wsPort } = await this.findFreePorts();
// lets create a server
const server = new plugins.typedserver.servertools.Server({
const server = new plugins.typedserver.TypedServer({
cors: true,
port: httpPort,
serveDir: tsbundleCacheDirPath,
});
server.addRoute(
'/test',
new plugins.typedserver.servertools.Handler('GET', async (_req, res) => {
res.type('.html');
res.write(`
server.addRoute('/test', 'GET', async () => {
return new Response(`
<html>
<head>
<script>
@@ -472,11 +471,8 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
</head>
<body></body>
</html>
`);
res.end();
})
);
server.addRoute('/*splat', new plugins.typedserver.servertools.HandlerStatic(tsbundleCacheDirPath));
`, { headers: { 'Content-Type': 'text/html' } });
});
await server.start();
// lets handle realtime comms
@@ -640,34 +636,33 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
try {
// Delete 00err and 00diff directories if they exist
if (plugins.smartfile.fs.isDirectorySync(errDir)) {
plugins.smartfile.fs.removeSync(errDir);
if (plugins.fs.existsSync(errDir) && plugins.fs.statSync(errDir).isDirectory()) {
plugins.fs.rmSync(errDir, { recursive: true, force: true });
}
if (plugins.smartfile.fs.isDirectorySync(diffDir)) {
plugins.smartfile.fs.removeSync(diffDir);
if (plugins.fs.existsSync(diffDir) && plugins.fs.statSync(diffDir).isDirectory()) {
plugins.fs.rmSync(diffDir, { recursive: true, force: true });
}
// Get all .log files in log directory (not in subdirectories)
const files = await plugins.smartfile.fs.listFileTree(logDir, '*.log');
const logFiles = files.filter((file: string) => !file.includes('/'));
const entries = await plugins.smartfsInstance.directory(logDir).filter('*.log').list();
const logFiles = entries.filter((entry) => entry.isFile).map((entry) => entry.name);
if (logFiles.length === 0) {
return;
}
// Ensure previous directory exists
await plugins.smartfile.fs.ensureDir(previousDir);
await plugins.smartfsInstance.directory(previousDir).recursive().create();
// Move each log file to previous directory
for (const file of logFiles) {
const filename = plugins.path.basename(file);
for (const filename of logFiles) {
const sourcePath = plugins.path.join(logDir, filename);
const destPath = plugins.path.join(previousDir, filename);
try {
// Copy file to new location and remove original
await plugins.smartfile.fs.copy(sourcePath, destPath);
await plugins.smartfile.fs.remove(sourcePath);
await plugins.smartfsInstance.file(sourcePath).copy(destPath);
await plugins.smartfsInstance.file(sourcePath).delete();
} catch (error) {
// Silently continue if a file can't be moved
}

View File

@@ -1,7 +1,8 @@
// node native
import * as fs from 'fs';
import * as path from 'path';
export { path };
export { fs, path };
// @apiglobal scope
import * as typedserver from '@api.global/typedserver';
@@ -13,25 +14,29 @@ export {
// @push.rocks scope
import * as consolecolor from '@push.rocks/consolecolor';
import * as smartbrowser from '@push.rocks/smartbrowser';
import * as smartchok from '@push.rocks/smartchok';
import * as smartdelay from '@push.rocks/smartdelay';
import * as smartfile from '@push.rocks/smartfile';
import * as smartfs from '@push.rocks/smartfs';
const smartfsInstance = new smartfs.SmartFs(new smartfs.SmartFsProviderNode());
import * as smartlog from '@push.rocks/smartlog';
import * as smartnetwork from '@push.rocks/smartnetwork';
import * as smartpromise from '@push.rocks/smartpromise';
import * as smartshell from '@push.rocks/smartshell';
import * as smartwatch from '@push.rocks/smartwatch';
import * as tapbundle from '../dist_ts_tapbundle/index.js';
export {
consolecolor,
smartbrowser,
smartchok,
smartdelay,
smartfile,
smartfs,
smartfsInstance,
smartlog,
smartnetwork,
smartpromise,
smartshell,
smartwatch,
tapbundle,
};

View File

@@ -11,7 +11,7 @@ pnpm install --save-dev @git.zone/tstest
## Issue Reporting and Security
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who want to sign a contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
## Overview
@@ -615,19 +615,21 @@ tap.test('should use context', async (tapTools) => {
## License and Legal Information
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](../license) file within this repository.
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](../LICENSE) file.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
### Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
### Company Information
Task Venture Capital GmbH
Registered at District court Bremen HRB 35230 HB, Germany
Registered at District Court Bremen HRB 35230 HB, Germany
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
For any legal inquiries or further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

View File

@@ -11,7 +11,7 @@ pnpm install --save-dev @git.zone/tstest
## Issue Reporting and Security
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who want to sign a contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
## Overview
@@ -586,19 +586,21 @@ No runtime-specific APIs are used, making it truly portable.
## License and Legal Information
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](../license) file within this repository.
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](../LICENSE) file.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
### Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
### Company Information
Task Venture Capital GmbH
Registered at District court Bremen HRB 35230 HB, Germany
Registered at District Court Bremen HRB 35230 HB, Germany
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
For any legal inquiries or further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

View File

@@ -3,6 +3,7 @@ import * as plugins from './plugins.js';
class TapNodeTools {
private smartshellInstance: plugins.smartshell.Smartshell;
private smartnetworkInstance: plugins.smartnetwork.SmartNetwork;
public testFileProvider = new TestFileProvider();
constructor() {}
@@ -87,12 +88,136 @@ class TapNodeTools {
public async createSmarts3() {
const smarts3Mod = await import('@push.rocks/smarts3');
const smarts3Instance = new smarts3Mod.Smarts3({
port: 3003,
cleanSlate: true,
server: { port: 3003 },
storage: { cleanSlate: true },
});
await smarts3Instance.start();
return smarts3Instance;
}
// ============
// Network Tools
// ============
private getSmartNetwork(): plugins.smartnetwork.SmartNetwork {
if (!this.smartnetworkInstance) {
this.smartnetworkInstance = new plugins.smartnetwork.SmartNetwork();
}
return this.smartnetworkInstance;
}
/**
* Find a single free port on the local machine.
*/
public async findFreePort(optionsArg?: {
startPort?: number;
endPort?: number;
randomize?: boolean;
exclude?: number[];
}): Promise<number> {
const options = {
startPort: 3000,
endPort: 60000,
randomize: true,
exclude: [] as number[],
...optionsArg,
};
const smartnetwork = this.getSmartNetwork();
const port = await smartnetwork.findFreePort(options.startPort, options.endPort, {
randomize: options.randomize,
exclude: options.exclude,
});
if (!port) {
throw new Error(
`Could not find a free port in range ${options.startPort}-${options.endPort}`
);
}
return port;
}
/**
* Find multiple distinct free ports on the local machine.
* Each found port is automatically excluded from subsequent searches.
*/
public async findFreePorts(countArg: number, optionsArg?: {
startPort?: number;
endPort?: number;
randomize?: boolean;
exclude?: number[];
}): Promise<number[]> {
const options = {
startPort: 3000,
endPort: 60000,
randomize: true,
exclude: [] as number[],
...optionsArg,
};
const smartnetwork = this.getSmartNetwork();
const ports: number[] = [];
const excluded = new Set(options.exclude);
for (let i = 0; i < countArg; i++) {
const port = await smartnetwork.findFreePort(options.startPort, options.endPort, {
randomize: options.randomize,
exclude: [...excluded],
});
if (!port) {
throw new Error(
`Could only find ${ports.length} of ${countArg} free ports in range ${options.startPort}-${options.endPort}`
);
}
ports.push(port);
excluded.add(port);
}
return ports;
}
/**
* Find a range of consecutive free ports on the local machine.
* All returned ports are sequential (e.g., [4000, 4001, 4002]).
*/
public async findFreePortRange(countArg: number, optionsArg?: {
startPort?: number;
endPort?: number;
exclude?: number[];
}): Promise<number[]> {
const options = {
startPort: 3000,
endPort: 60000,
exclude: [] as number[],
...optionsArg,
};
const smartnetwork = this.getSmartNetwork();
const excludeSet = new Set(options.exclude);
for (let start = options.startPort; start <= options.endPort - countArg + 1; start++) {
let allFree = true;
for (let offset = 0; offset < countArg; offset++) {
const port = start + offset;
if (excludeSet.has(port)) {
allFree = false;
start = port; // skip ahead past excluded port
break;
}
const isFree = await smartnetwork.isLocalPortUnused(port);
if (!isFree) {
allFree = false;
start = port; // skip ahead past occupied port
break;
}
}
if (allFree) {
const ports: number[] = [];
for (let offset = 0; offset < countArg; offset++) {
ports.push(start + offset);
}
return ports;
}
}
throw new Error(
`Could not find ${countArg} consecutive free ports in range ${options.startPort}-${options.endPort}`
);
}
}
export const tapNodeTools = new TapNodeTools();

View File

@@ -12,9 +12,9 @@ export class TestFileProvider {
const response = await plugins.smartrequest.SmartRequest.create()
.url(fileUrls.dockerAlpineImage)
.get();
await plugins.smartfile.fs.ensureDir(paths.testFilesDir);
await plugins.smartfsInstance.directory(paths.testFilesDir).recursive().create();
const buffer = Buffer.from(await response.arrayBuffer());
await plugins.smartfile.memory.toFs(buffer, filePath);
await plugins.smartfsInstance.file(filePath).write(buffer);
return filePath;
}
}

View File

@@ -9,8 +9,11 @@ export { crypto,fs, path, };
import * as qenv from '@push.rocks/qenv';
import * as smartcrypto from '@push.rocks/smartcrypto';
import * as smartfile from '@push.rocks/smartfile';
import * as smartfs from '@push.rocks/smartfs';
const smartfsInstance = new smartfs.SmartFs(new smartfs.SmartFsProviderNode());
import * as smartnetwork from '@push.rocks/smartnetwork';
import * as smartpath from '@push.rocks/smartpath';
import * as smartrequest from '@push.rocks/smartrequest';
import * as smartshell from '@push.rocks/smartshell';
export { qenv, smartcrypto, smartfile, smartpath, smartrequest, smartshell, };
export { qenv, smartcrypto, smartfile, smartfs, smartfsInstance, smartnetwork, smartpath, smartrequest, smartshell, };

View File

@@ -11,31 +11,31 @@ pnpm install --save-dev @git.zone/tstest
## Issue Reporting and Security
For reporting bugs, issues, or security vulnerabilities, please visit https://community.foss.global/. This is the central community hub for all issue reporting. Developers who want to sign a contribution agreement and go through identification can also get a code.foss.global account to submit Pull Requests directly.
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
## Overview
`@git.zone/tstest/tapbundle_serverside` provides server-side testing utilities exclusively for Node.js runtime. These tools enable shell command execution, environment variable management, HTTPS certificate generation, database testing, object storage testing, and test asset management - all functionality that only makes sense on the server-side.
`@git.zone/tstest/tapbundle_serverside` provides server-side testing utilities exclusively for Node.js runtime. These tools make it trivial to spin up test infrastructure — free ports, HTTPS certificates, ephemeral databases, S3 storage, shell commands, and environment variable management.
## Key Features
- 🔐 **Environment Variables** - On-demand environment variable loading with qenv
- 💻 **Shell Commands** - Execute bash commands during tests
- 🔒 **HTTPS Certificates** - Generate self-signed certificates for testing
- 🗄️ **MongoDB Testing** - Create ephemeral MongoDB instances
- 📦 **S3 Storage Testing** - Create local S3-compatible storage for tests
- 📁 **Test File Management** - Download and manage test assets
- 🌐 **Network Utilities** — Find free ports and port ranges for test servers
- 🔒 **HTTPS Certificates** — Generate self-signed certificates for testing
- 💻 **Shell Commands** — Execute bash commands during tests
- 🔐 **Environment Variables** — On-demand environment variable loading with qenv
- 🗄️ **MongoDB Testing** Create ephemeral MongoDB instances
- 📦 **S3 Storage Testing** — Create local S3-compatible storage
- 📁 **Test File Management** — Download and manage test assets
## Basic Usage
```typescript
import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
import { tap } from '@git.zone/tstest/tapbundle';
import { tap, expect } from '@git.zone/tstest/tapbundle';
tap.test('should use server-side tools', async () => {
// Execute shell commands on the server-side
const result = await tapNodeTools.runCommand('echo "hello"');
console.log(result);
tap.test('should start server on free port', async () => {
const port = await tapNodeTools.findFreePort();
// start your server on `port`
});
export default tap.start();
@@ -47,340 +47,224 @@ export default tap.start();
The main singleton instance providing all Node.js-specific utilities.
#### Environment Variables
---
##### `getQenv()`
### 🌐 Network Utilities
#### `findFreePort(options?)`
Find a single free port on the local machine.
```typescript
// Default: random free port in range 300060000
const port = await tapNodeTools.findFreePort();
// Custom range
const port = await tapNodeTools.findFreePort({
startPort: 8000,
endPort: 9000,
});
// With exclusions and sequential scan
const port = await tapNodeTools.findFreePort({
startPort: 3000,
endPort: 60000,
randomize: false, // default: true
exclude: [8080, 8443],
});
```
**Options:**
| Option | Type | Default | Description |
|---|---|---|---|
| `startPort` | `number` | `3000` | Start of port range (inclusive) |
| `endPort` | `number` | `60000` | End of port range (inclusive) |
| `randomize` | `boolean` | `true` | Pick a random port vs first available |
| `exclude` | `number[]` | `[]` | Ports to skip |
**Returns:** `Promise<number>` — Throws if no free port is found.
#### `findFreePorts(count, options?)`
Find multiple distinct free ports. Each found port is automatically excluded from subsequent searches, guaranteeing all returned ports are unique.
```typescript
const [httpPort, wsPort, adminPort] = await tapNodeTools.findFreePorts(3);
// With custom range
const ports = await tapNodeTools.findFreePorts(2, {
startPort: 10000,
endPort: 20000,
});
```
**Parameters:**
- `count` — Number of ports to find
- `options` — Same as `findFreePort()`
**Returns:** `Promise<number[]>` — Array of distinct free ports.
#### `findFreePortRange(count, options?)`
Find consecutive free ports (e.g., `[4000, 4001, 4002]`). Useful when you need a contiguous block.
```typescript
const ports = await tapNodeTools.findFreePortRange(3, {
startPort: 20000,
endPort: 30000,
});
// ports = [N, N+1, N+2] where all three are free
```
**Options:**
| Option | Type | Default | Description |
|---|---|---|---|
| `startPort` | `number` | `3000` | Start of search range |
| `endPort` | `number` | `60000` | End of search range |
| `exclude` | `number[]` | `[]` | Ports to skip |
**Returns:** `Promise<number[]>` — Array of consecutive free ports.
---
### 🔒 HTTPS Certificates
#### `createHttpsCert(commonName?, allowSelfSigned?)`
Generate a self-signed HTTPS certificate for testing secure connections.
```typescript
const { key, cert } = await tapNodeTools.createHttpsCert('localhost', true);
// Use with Node.js https module
const server = https.createServer({ key, cert }, handler);
server.listen(port);
```
**Parameters:**
- `commonName` (default: `'localhost'`) — Certificate common name
- `allowSelfSigned` (default: `true`) — Sets `NODE_TLS_REJECT_UNAUTHORIZED=0`
**Returns:** `Promise<{ key: string; cert: string }>` — PEM-encoded key and certificate.
---
### 💻 Shell Commands
#### `runCommand(command)`
Execute a bash command and return the result.
```typescript
const result = await tapNodeTools.runCommand('ls -la');
console.log(result.exitCode);
console.log(result.stdout);
```
---
### 🔐 Environment Variables
#### `getQenv()`
Get the qenv instance for managing environment variables from `.nogit/` directory.
```typescript
const qenv = await tapNodeTools.getQenv();
// qenv will load from .env files in .nogit/ directory
```
##### `getEnvVarOnDemand(envVarName)`
#### `getEnvVarOnDemand(envVarName)`
Request an environment variable. If not available, qenv will prompt for it and store it securely.
```typescript
tap.test('should get API key', async () => {
const apiKey = await tapNodeTools.getEnvVarOnDemand('GITHUB_API_KEY');
// If GITHUB_API_KEY is not set, qenv will prompt for it
// The value is stored in .nogit/.env for future use
});
const apiKey = await tapNodeTools.getEnvVarOnDemand('GITHUB_API_KEY');
// If not set, prompts interactively and stores in .nogit/.env
```
**Use Cases:**
- API keys for integration tests
- Database credentials
- Service endpoints
- Any sensitive configuration needed for testing
---
#### Shell Commands
### 🗄️ Database Testing
##### `runCommand(command)`
#### `createSmartmongo()`
Execute a bash command and return the result.
Create an ephemeral MongoDB instance. Automatically started and ready to use.
```typescript
tap.test('should execute shell commands', async () => {
const result = await tapNodeTools.runCommand('ls -la');
console.log(result.stdout);
});
const mongo = await tapNodeTools.createSmartmongo();
// ... run database tests ...
await mongo.stop();
```
**Use Cases:**
- Setup test environment
- Execute CLI tools
- File system operations
- Process management
Uses [@push.rocks/smartmongo](https://code.foss.global/push.rocks/smartmongo).
#### HTTPS Certificates
---
##### `createHttpsCert(commonName?, allowSelfSigned?)`
### 📦 Storage Testing
Generate a self-signed HTTPS certificate for testing secure connections.
#### `createSmarts3()`
Create a local S3-compatible storage instance for testing.
```typescript
tap.test('should create HTTPS server', async () => {
const { key, cert } = await tapNodeTools.createHttpsCert('localhost', true);
// Use with Node.js https module
const server = https.createServer({ key, cert }, (req, res) => {
res.end('Hello Secure World');
});
server.listen(3000);
});
const s3 = await tapNodeTools.createSmarts3();
// ... run storage tests ...
await s3.stop();
```
**Parameters:**
- `commonName` (optional): Certificate common name, default: 'localhost'
- `allowSelfSigned` (optional): Allow self-signed certificates by setting `NODE_TLS_REJECT_UNAUTHORIZED=0`, default: true
Default config: port 3003, clean slate enabled. Uses [@push.rocks/smarts3](https://code.foss.global/push.rocks/smarts3).
**Returns:**
- `key`: PEM-encoded private key
- `cert`: PEM-encoded certificate
---
**Use Cases:**
- Testing HTTPS servers
- Testing secure WebSocket connections
- Testing certificate validation logic
- Mocking secure external services
### 📁 Test File Provider
#### Database Testing
##### `createSmartmongo()`
Create an ephemeral MongoDB instance for testing. Automatically started and ready to use.
```typescript
import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
tap.test('should use MongoDB', async () => {
const mongoInstance = await tapNodeTools.createSmartmongo();
// Use the MongoDB instance
const db = await mongoInstance.getDatabase('testdb');
const collection = await db.getCollection('users');
await collection.insertOne({ name: 'Alice', age: 30 });
const user = await collection.findOne({ name: 'Alice' });
expect(user.age).toEqual(30);
// Cleanup (optional - instance will be cleaned up automatically)
await mongoInstance.stop();
});
export default tap.start();
```
**Features:**
- Ephemeral instance (starts fresh)
- Automatic cleanup
- Full MongoDB API via [@push.rocks/smartmongo](https://code.foss.global/push.rocks/smartmongo)
**Use Cases:**
- Testing database operations
- Integration tests with MongoDB
- Testing data models
- Schema validation tests
#### Storage Testing
##### `createSmarts3()`
Create a local S3-compatible storage instance for testing object storage operations.
```typescript
import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
tap.test('should use S3 storage', async () => {
const s3Instance = await tapNodeTools.createSmarts3();
// Use the S3 instance (MinIO-compatible API)
const bucket = await s3Instance.createBucket('test-bucket');
await bucket.putObject('file.txt', Buffer.from('Hello S3'));
const file = await bucket.getObject('file.txt');
expect(file.toString()).toEqual('Hello S3');
// Cleanup
await s3Instance.stop();
});
export default tap.start();
```
**Configuration:**
- Port: 3003 (default)
- Clean slate: true (starts fresh each time)
- Full S3-compatible API via [@push.rocks/smarts3](https://code.foss.global/push.rocks/smarts3)
**Use Cases:**
- Testing file uploads/downloads
- Testing object storage operations
- Testing backup/restore logic
- Mocking cloud storage
### TestFileProvider
Utility for downloading and managing test assets.
#### `getDockerAlpineImageAsLocalTarball()`
#### `testFileProvider.getDockerAlpineImageAsLocalTarball()`
Download the Alpine Linux Docker image as a tarball for testing.
```typescript
import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
tap.test('should provide docker image', async () => {
const tarballPath = await tapNodeTools.testFileProvider.getDockerAlpineImageAsLocalTarball();
// Use the tarball path
// Path: ./.nogit/testfiles/alpine.tar
expect(tarballPath).toMatch(/alpine\.tar$/);
});
export default tap.start();
```
**Features:**
- Downloads from https://code.foss.global/testassets/docker
- Caches in `.nogit/testfiles/` directory
- Returns local file path
**Use Cases:**
- Testing Docker operations
- Testing container deployment
- Testing image handling logic
### Path Utilities
The module exports useful path constants:
```typescript
import * as paths from '@git.zone/tstest/tapbundle_serverside/paths';
console.log(paths.cwd); // Current working directory
console.log(paths.testFilesDir); // ./.nogit/testfiles/
```
## Patterns and Best Practices
### Testing with External Services
```typescript
import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
import { tap, expect } from '@git.zone/tstest/tapbundle';
tap.describe('User Service Integration', () => {
let mongoInstance;
let db;
tap.beforeEach(async () => {
mongoInstance = await tapNodeTools.createSmartmongo();
db = await mongoInstance.getDatabase('testdb');
});
tap.test('should create user', async () => {
const users = await db.getCollection('users');
await users.insertOne({ name: 'Bob', email: 'bob@example.com' });
const user = await users.findOne({ name: 'Bob' });
expect(user.email).toEqual('bob@example.com');
});
tap.afterEach(async () => {
await mongoInstance.stop();
});
});
export default tap.start();
```
### Testing HTTPS Servers
```typescript
import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as https from 'https';
tap.test('should serve over HTTPS', async () => {
const { key, cert } = await tapNodeTools.createHttpsCert();
const server = https.createServer({ key, cert }, (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Secure Response');
});
await new Promise((resolve) => {
server.listen(8443, () => resolve(undefined));
});
// Test the server
const response = await fetch('https://localhost:8443');
const text = await response.text();
expect(text).toEqual('Secure Response');
// Cleanup
server.close();
});
export default tap.start();
```
### Environment-Dependent Tests
```typescript
import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
import { tap, expect } from '@git.zone/tstest/tapbundle';
tap.test('should authenticate with GitHub', async () => {
const githubToken = await tapNodeTools.getEnvVarOnDemand('GITHUB_TOKEN');
// Use the token for API calls
const response = await fetch('https://api.github.com/user', {
headers: {
Authorization: `Bearer ${githubToken}`
}
});
expect(response.ok).toBeTruthy();
});
export default tap.start();
const tarballPath = await tapNodeTools.testFileProvider.getDockerAlpineImageAsLocalTarball();
// Path: ./.nogit/testfiles/alpine.tar
```
## Runtime Requirements
⚠️ **Server-Side Only (Node.js)**: All utilities in this module are designed exclusively for server-side testing in Node.js runtime. They provide functionality like shell command execution, file system operations, and process management that only make sense on the server.
**NOT available in:**
- Browser environments
- Deno runtime
- Bun runtime
**Important:** Import tapbundle_serverside only in tests that run exclusively on the server-side (`.node.ts` test files). For cross-runtime tests, these utilities will fail in non-Node environments.
## File Naming
Use Node.js-specific file naming when using these utilities:
⚠️ **Node.js only.** All utilities in this module require Node.js. Import only in `.node.ts` test files.
```
test/mytest.node.ts ✅ Node.js only
test/mytest.node+deno.ts ❌ Will fail in Deno
test/mytest.browser+node.ts ⚠️ Browser won't have access to these tools
test/mytest.node.ts ✅ Correct — Node.js only
test/mytest.ts ✅ Correct — defaults to Node.js
test/mytest.all.ts ❌ Will fail in Deno/Bun/Chromium
```
## Dependencies
This module uses the following packages:
- [@push.rocks/qenv](https://code.foss.global/push.rocks/qenv) - Environment variable management
- [@push.rocks/smartshell](https://code.foss.global/push.rocks/smartshell) - Shell command execution
- [@push.rocks/smartcrypto](https://code.foss.global/push.rocks/smartcrypto) - Certificate generation
- [@push.rocks/smartmongo](https://code.foss.global/push.rocks/smartmongo) - MongoDB testing
- [@push.rocks/smarts3](https://code.foss.global/push.rocks/smarts3) - S3 storage testing
- [@push.rocks/smartfile](https://code.foss.global/push.rocks/smartfile) - File operations
- [@push.rocks/smartrequest](https://code.foss.global/push.rocks/smartrequest) - HTTP requests
- [@push.rocks/smartnetwork](https://code.foss.global/push.rocks/smartnetwork) — Port discovery
- [@push.rocks/qenv](https://code.foss.global/push.rocks/qenv) Environment variable management
- [@push.rocks/smartshell](https://code.foss.global/push.rocks/smartshell) Shell command execution
- [@push.rocks/smartcrypto](https://code.foss.global/push.rocks/smartcrypto) Certificate generation
- [@push.rocks/smartmongo](https://code.foss.global/push.rocks/smartmongo) MongoDB testing
- [@push.rocks/smarts3](https://code.foss.global/push.rocks/smarts3) S3 storage testing
- [@push.rocks/smartfile](https://code.foss.global/push.rocks/smartfile) File operations
- [@push.rocks/smartrequest](https://code.foss.global/push.rocks/smartrequest) HTTP requests
## License and Legal Information
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](../license) file within this repository.
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](../LICENSE) file.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
### Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
### Company Information
Task Venture Capital GmbH
Registered at District court Bremen HRB 35230 HB, Germany
Registered at District Court Bremen HRB 35230 HB, Germany
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
For any legal inquiries or further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.