import { expect, tap } from '@git.zone/tstest/tapbundle'; import { Qenv } from '@push.rocks/qenv'; const testQenv = new Qenv('./', './.nogit/'); import * as plugins from '../ts/plugins.js'; import * as paths from '../ts/paths.js'; import * as docker from '../ts/index.js'; let testDockerHost: docker.DockerHost; tap.test('should create a new Dockersock instance', async () => { testDockerHost = new docker.DockerHost({}); await testDockerHost.start(); return expect(testDockerHost).toBeInstanceOf(docker.DockerHost); }); tap.test('should create a docker swarm', async () => { await testDockerHost.activateSwarm(); }); // Containers tap.test('should list containers', async () => { const containers = await testDockerHost.listContainers(); console.log(containers); }); // Networks tap.test('should list networks', async () => { const networks = await testDockerHost.listNetworks(); console.log(networks); }); tap.test('should create a network', async () => { const newNetwork = await testDockerHost.createNetwork({ Name: 'webgateway', }); expect(newNetwork).toBeInstanceOf(docker.DockerNetwork); expect(newNetwork.Name).toEqual('webgateway'); }); tap.test('should remove a network', async () => { const webgateway = await testDockerHost.getNetworkByName('webgateway'); await webgateway.remove(); }); // Images tap.test('should pull an image from imagetag', async () => { const image = await testDockerHost.createImageFromRegistry({ imageUrl: 'hosttoday/ht-docker-node', imageTag: 'alpine', }); expect(image).toBeInstanceOf(docker.DockerImage); console.log(image); }); tap.test('should return a change Observable', async (tools) => { const testObservable = await testDockerHost.getEventObservable(); const subscription = testObservable.subscribe((changeObject) => { console.log(changeObject); }); await tools.delayFor(2000); subscription.unsubscribe(); }); // SECRETS tap.test('should create a secret', async () => { const mySecret = await testDockerHost.createSecret({ name: 'testSecret', version: '1.0.3', contentArg: `{ "hi": "wow"}`, labels: {}, }); console.log(mySecret); }); tap.test('should remove a secret by name', async () => { const mySecret = await testDockerHost.getSecretByName('testSecret'); await mySecret.remove(); }); // SERVICES tap.test('should activate swarm mode', async () => { await testDockerHost.activateSwarm(); }); tap.test('should list all services', async (tools) => { const services = await testDockerHost.listServices(); console.log(services); }); tap.test('should create a service', async () => { const testNetwork = await testDockerHost.createNetwork({ Name: 'testNetwork', }); const testSecret = await testDockerHost.createSecret({ name: 'testSecret', version: '0.0.1', labels: {}, contentArg: '{"hi": "wow"}', }); const testImage = await testDockerHost.createImageFromRegistry({ imageUrl: 'code.foss.global/host.today/ht-docker-node:latest', }); const testService = await testDockerHost.createService({ image: testImage, labels: {}, name: 'testService', networks: [testNetwork], networkAlias: 'testService', secrets: [testSecret], ports: ['3000:80'], }); await testService.remove(); await testNetwork.remove(); await testSecret.remove(); }); tap.test('should export images', async (toolsArg) => { const done = toolsArg.defer(); const testImage = await testDockerHost.createImageFromRegistry({ imageUrl: 'code.foss.global/host.today/ht-docker-node:latest', }); const fsWriteStream = plugins.smartfile.fsStream.createWriteStream( plugins.path.join(paths.nogitDir, 'testimage.tar'), ); const exportStream = await testImage.exportToTarStream(); exportStream.pipe(fsWriteStream).on('finish', () => { done.resolve(); }); await done.promise; }); tap.test('should import images', async () => { const fsReadStream = plugins.smartfile.fsStream.createReadStream( plugins.path.join(paths.nogitDir, 'testimage.tar'), ); const importedImage = await testDockerHost.createImageFromTarStream( fsReadStream, { imageUrl: 'code.foss.global/host.today/ht-docker-node:latest', }, ); expect(importedImage).toBeInstanceOf(docker.DockerImage); }); tap.test('should expose a working DockerImageStore', async () => { // lets first add am s3 target const s3Descriptor = { endpoint: await testQenv.getEnvVarOnDemand('S3_ENDPOINT'), accessKey: await testQenv.getEnvVarOnDemand('S3_ACCESSKEY'), accessSecret: await testQenv.getEnvVarOnDemand('S3_ACCESSSECRET'), bucketName: await testQenv.getEnvVarOnDemand('S3_BUCKET'), }; await testDockerHost.addS3Storage(s3Descriptor); // Use the new public API instead of direct imageStore access await testDockerHost.storeImage( 'hello2', plugins.smartfile.fsStream.createReadStream( plugins.path.join(paths.nogitDir, 'testimage.tar'), ), ); }); // CONTAINER GETTERS tap.test('should return undefined for non-existent container', async () => { const container = await testDockerHost.getContainerById('invalid-container-id-12345'); expect(container).toEqual(undefined); }); tap.test('should return container for valid container ID', async () => { const containers = await testDockerHost.listContainers(); if (containers.length > 0) { const validId = containers[0].Id; const container = await testDockerHost.getContainerById(validId); expect(container).toBeInstanceOf(docker.DockerContainer); expect(container?.Id).toEqual(validId); } }); // CONTAINER STREAMING FEATURES let testContainer: docker.DockerContainer; tap.test('should get an existing container for streaming tests', async () => { const containers = await testDockerHost.listContainers(); // Use the first running container we find testContainer = containers.find((c) => c.State === 'running'); if (!testContainer) { throw new Error('No running containers found for streaming tests'); } expect(testContainer).toBeInstanceOf(docker.DockerContainer); console.log('Using existing container for tests:', testContainer.Names[0], testContainer.Id); }); tap.test('should stream container logs', async (tools) => { const done = tools.defer(); const logStream = await testContainer.streamLogs({ stdout: true, stderr: true, timestamps: true, }); let receivedData = false; logStream.on('data', (chunk) => { console.log('Received log chunk:', chunk.toString().slice(0, 100)); receivedData = true; }); logStream.on('error', (error) => { console.error('Stream error:', error); done.resolve(); }); // Wait for 2 seconds to collect logs, then close await tools.delayFor(2000); logStream.destroy(); done.resolve(); await done.promise; console.log('Log streaming test completed. Received data:', receivedData); }); tap.test('should get container logs (one-shot)', async () => { const logs = await testContainer.logs({ stdout: true, stderr: true, tail: 10, }); expect(typeof logs).toEqual('string'); console.log('Container logs (last 10 lines):', logs.slice(0, 200)); }); tap.test('should execute command in container', async (tools) => { const done = tools.defer(); const { stream, close } = await testContainer.exec('echo "Hello from exec"', { tty: false, attachStdout: true, attachStderr: true, }); let output = ''; stream.on('data', (chunk) => { output += chunk.toString(); console.log('Exec output:', chunk.toString()); }); stream.on('end', async () => { await close(); console.log('Exec completed. Full output:', output); done.resolve(); }); stream.on('error', async (error) => { console.error('Exec error:', error); await close(); done.resolve(); }); await done.promise; expect(output.length).toBeGreaterThan(0); }); tap.test('should attach to container', async (tools) => { const done = tools.defer(); const { stream, close } = await testContainer.attach({ stream: true, stdout: true, stderr: true, stdin: false, }); let receivedData = false; stream.on('data', (chunk) => { console.log('Attach received:', chunk.toString().slice(0, 100)); receivedData = true; }); stream.on('error', async (error) => { console.error('Attach error:', error); await close(); done.resolve(); }); // Monitor for 2 seconds then detach await tools.delayFor(2000); await close(); done.resolve(); await done.promise; console.log('Attach test completed. Received data:', receivedData); }); tap.test('should get container stats', async () => { const stats = await testContainer.stats({ stream: false }); expect(stats).toBeInstanceOf(Object); console.log('Container stats keys:', Object.keys(stats)); }); tap.test('should inspect container', async () => { const inspection = await testContainer.inspect(); expect(inspection).toBeInstanceOf(Object); expect(inspection.Id).toEqual(testContainer.Id); console.log('Container state:', inspection.State?.Status); }); tap.test('should complete container tests', async () => { // Using existing container, no cleanup needed console.log('Container streaming tests completed'); }); // NEW FEATURES TESTS (v5.1.0) // Test 1: Network creation with custom driver and IPAM tap.test('should create bridge network with custom IPAM config', async () => { const network = await testDockerHost.createNetwork({ Name: 'test-bridge-network', Driver: 'bridge', IPAM: { Config: [{ Subnet: '172.20.0.0/16', Gateway: '172.20.0.1', }] }, Labels: { testLabel: 'v5.1.0' }, }); expect(network).toBeInstanceOf(docker.DockerNetwork); expect(network.Name).toEqual('test-bridge-network'); expect(network.Driver).toEqual('bridge'); console.log('Created bridge network:', network.Name, 'with driver:', network.Driver); await network.remove(); }); // Test 2: getVersion() returns proper Docker daemon info tap.test('should get Docker daemon version information', async () => { const version = await testDockerHost.getVersion(); expect(version).toBeInstanceOf(Object); expect(typeof version.Version).toEqual('string'); expect(typeof version.ApiVersion).toEqual('string'); expect(typeof version.Os).toEqual('string'); expect(typeof version.Arch).toEqual('string'); console.log('Docker version:', version.Version, 'API version:', version.ApiVersion); }); // Test 3: pruneImages() functionality tap.test('should prune dangling images', async () => { const result = await testDockerHost.pruneImages({ dangling: true }); expect(result).toBeInstanceOf(Object); expect(result).toHaveProperty('ImagesDeleted'); expect(result).toHaveProperty('SpaceReclaimed'); expect(Array.isArray(result.ImagesDeleted)).toEqual(true); expect(typeof result.SpaceReclaimed).toEqual('number'); console.log('Pruned images. Space reclaimed:', result.SpaceReclaimed, 'bytes'); }); // Test 4: exec() inspect() returns exit codes tap.test('should get exit code from exec command', async (tools) => { const done = tools.defer(); // Execute a successful command (exit code 0) const { stream, close, inspect } = await testContainer.exec('echo "test successful"', { tty: false, attachStdout: true, attachStderr: true, }); stream.on('end', async () => { // Give Docker a moment to finalize the exec state await tools.delayFor(500); const info = await inspect(); expect(info).toBeInstanceOf(Object); expect(typeof info.ExitCode).toEqual('number'); expect(info.ExitCode).toEqual(0); // Success expect(typeof info.Running).toEqual('boolean'); expect(info.Running).toEqual(false); // Should be done expect(typeof info.ContainerID).toEqual('string'); console.log('Exec inspect - ExitCode:', info.ExitCode, 'Running:', info.Running); await close(); done.resolve(); }); stream.on('error', async (error) => { console.error('Exec error:', error); await close(); done.resolve(); }); await done.promise; }); tap.test('should get non-zero exit code from failed exec command', async (tools) => { const done = tools.defer(); // Execute a command that fails (exit code 1) const { stream, close, inspect } = await testContainer.exec('exit 1', { tty: false, attachStdout: true, attachStderr: true, }); stream.on('end', async () => { // Give Docker a moment to finalize the exec state await tools.delayFor(500); const info = await inspect(); expect(info.ExitCode).toEqual(1); // Failure expect(info.Running).toEqual(false); console.log('Exec inspect (failed command) - ExitCode:', info.ExitCode); await close(); done.resolve(); }); stream.on('error', async (error) => { console.error('Exec error:', error); await close(); done.resolve(); }); await done.promise; }); tap.test('cleanup', async () => { await testDockerHost.stop(); }); export default tap.start();