@milaboratories/pl-deployments 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/common/os_and_arch.d.ts +9 -0
- package/dist/common/os_and_arch.d.ts.map +1 -0
- package/dist/common/pl_binary.d.ts +14 -0
- package/dist/common/pl_binary.d.ts.map +1 -0
- package/dist/common/pl_binary_download.d.ts +30 -0
- package/dist/common/pl_binary_download.d.ts.map +1 -0
- package/dist/common/pl_version.d.ts +2 -0
- package/dist/common/pl_version.d.ts.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1028 -0
- package/dist/index.mjs.map +1 -0
- package/dist/local/options.d.ts +31 -0
- package/dist/local/options.d.ts.map +1 -0
- package/dist/local/pid.d.ts +4 -0
- package/dist/local/pid.d.ts.map +1 -0
- package/dist/local/pl.d.ts +66 -0
- package/dist/local/pl.d.ts.map +1 -0
- package/dist/local/process.d.ts +12 -0
- package/dist/local/process.d.ts.map +1 -0
- package/dist/local/trace.d.ts +10 -0
- package/dist/local/trace.d.ts.map +1 -0
- package/dist/ssh/__tests__/common-utils.d.ts +18 -0
- package/dist/ssh/__tests__/common-utils.d.ts.map +1 -0
- package/dist/ssh/pl.d.ts +101 -0
- package/dist/ssh/pl.d.ts.map +1 -0
- package/dist/ssh/pl_paths.d.ts +17 -0
- package/dist/ssh/pl_paths.d.ts.map +1 -0
- package/dist/ssh/ssh.d.ts +128 -0
- package/dist/ssh/ssh.d.ts.map +1 -0
- package/dist/ssh/supervisord.d.ts +8 -0
- package/dist/ssh/supervisord.d.ts.map +1 -0
- package/package.json +64 -0
- package/src/common/os_and_arch.ts +44 -0
- package/src/common/pl_binary.ts +39 -0
- package/src/common/pl_binary_download.ts +258 -0
- package/src/common/pl_version.ts +5 -0
- package/src/index.ts +4 -0
- package/src/local/config.test.yaml +60 -0
- package/src/local/options.ts +34 -0
- package/src/local/pid.ts +21 -0
- package/src/local/pl.test.ts +129 -0
- package/src/local/pl.ts +220 -0
- package/src/local/process.ts +44 -0
- package/src/local/trace.ts +30 -0
- package/src/ssh/Dockerfile +29 -0
- package/src/ssh/__tests__/common-utils.ts +131 -0
- package/src/ssh/__tests__/pl-docker.test.ts +192 -0
- package/src/ssh/__tests__/ssh-docker.test.ts +175 -0
- package/src/ssh/pl.ts +451 -0
- package/src/ssh/pl_paths.ts +57 -0
- package/src/ssh/ssh.ts +512 -0
- package/src/ssh/supervisord.ts +137 -0
- package/src/ssh/test-assets/simple-server.js +10 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { describe, it, beforeAll, expect, afterAll } from 'vitest';
|
|
2
|
+
import { initContainer, getConnectionForSsh, cleanUp as cleanUpT } from './common-utils';
|
|
3
|
+
import { SshPl } from '../pl';
|
|
4
|
+
import upath from 'upath';
|
|
5
|
+
import { getDefaultPlVersion } from '../../common/pl_version';
|
|
6
|
+
import { existsSync, unlinkSync, rmSync } from 'fs';
|
|
7
|
+
import { newArch } from '../../common/os_and_arch';
|
|
8
|
+
import { downloadBinary, downloadPlBinary } from '../../common/pl_binary_download';
|
|
9
|
+
import { ConsoleLoggerAdapter } from '@milaboratories/ts-helpers';
|
|
10
|
+
import * as plpath from '../pl_paths';
|
|
11
|
+
|
|
12
|
+
let sshPl: SshPl;
|
|
13
|
+
const testContainer = await initContainer('pl');
|
|
14
|
+
|
|
15
|
+
const downloadDestination = upath.resolve(__dirname, '..', 'test-assets', 'downloads');
|
|
16
|
+
|
|
17
|
+
async function cleanUp() {
|
|
18
|
+
const version = getDefaultPlVersion();
|
|
19
|
+
const arch = await sshPl.getArch();
|
|
20
|
+
const tgzName = 'supervisord-0.7.3';
|
|
21
|
+
|
|
22
|
+
unlinkSync(`${downloadDestination}/pl-${version}-${newArch(arch!.arch)}.tgz`);
|
|
23
|
+
unlinkSync(`${downloadDestination}/${tgzName}-${newArch(arch!.arch)}.tgz`);
|
|
24
|
+
rmSync(downloadDestination, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
beforeAll(async () => {
|
|
27
|
+
const logger = new ConsoleLoggerAdapter();
|
|
28
|
+
sshPl = await SshPl.init(logger, getConnectionForSsh(testContainer));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('SshPl', async () => {
|
|
32
|
+
it('User home direcory', async () => {
|
|
33
|
+
const home = await sshPl.getUserHomeDirectory();
|
|
34
|
+
expect(home).toBe('/home/pl-doctor');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('Get OS arch', async () => {
|
|
38
|
+
const platformInfo = await sshPl.getArch();
|
|
39
|
+
expect(platformInfo).toHaveProperty('platform');
|
|
40
|
+
expect(platformInfo).toHaveProperty('arch');
|
|
41
|
+
|
|
42
|
+
expect(platformInfo?.arch).toBe('x86_64');
|
|
43
|
+
expect(platformInfo?.platform).toBe('Linux');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('Check start/stop cmd', async () => {
|
|
47
|
+
await sshPl.platformaInit(downloadDestination);
|
|
48
|
+
expect(await sshPl.isAlive()).toBe(true);
|
|
49
|
+
|
|
50
|
+
await sshPl.stop();
|
|
51
|
+
expect(await sshPl.isAlive()).toBe(false);
|
|
52
|
+
|
|
53
|
+
// FIXME: it doesn't work in CI
|
|
54
|
+
// await sshPl.start();
|
|
55
|
+
// expect(await sshPl.isAlive()).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('downloadBinariesAndUploadToServer', async () => {
|
|
59
|
+
await sshPl.stop();
|
|
60
|
+
|
|
61
|
+
const arch = await sshPl.getArch();
|
|
62
|
+
const remoteHome = await sshPl.getUserHomeDirectory();
|
|
63
|
+
await sshPl.stop(); // ensure stopped
|
|
64
|
+
await sshPl.downloadBinariesAndUploadToTheServer(downloadDestination, remoteHome, arch);
|
|
65
|
+
|
|
66
|
+
const pathSupervisor = `${plpath.supervisorBinDir(remoteHome, arch.arch)}/supervisord`;
|
|
67
|
+
const pathMinio = `${plpath.minioDir(remoteHome, arch.arch)}/minio`;
|
|
68
|
+
|
|
69
|
+
expect((await sshPl?.sshClient.checkPathExists(pathSupervisor))?.exists).toBe(true);
|
|
70
|
+
expect((await sshPl?.sshClient.checkPathExists(pathMinio))?.exists).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('platformaInit', async () => {
|
|
74
|
+
const result = await sshPl.platformaInit(downloadDestination);
|
|
75
|
+
|
|
76
|
+
const remoteHome = await sshPl.getUserHomeDirectory();
|
|
77
|
+
|
|
78
|
+
expect(await sshPl?.sshClient.checkFileExists(`${plpath.workDir(remoteHome)}/config.yaml`)).toBe(true);
|
|
79
|
+
expect(typeof result?.ports).toBe('object');
|
|
80
|
+
expect(result?.plPassword).toBeTruthy();
|
|
81
|
+
expect(result?.plUser).toBeTruthy();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('Transfer Platforma to server', async () => {
|
|
85
|
+
const arch = await sshPl.getArch();
|
|
86
|
+
|
|
87
|
+
const plPath = await downloadPlBinary(
|
|
88
|
+
new ConsoleLoggerAdapter(),
|
|
89
|
+
downloadDestination,
|
|
90
|
+
getDefaultPlVersion(),
|
|
91
|
+
arch.arch,
|
|
92
|
+
arch.platform,
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const plFolderName = upath.basename(plPath.targetFolder);
|
|
96
|
+
const dirPath = upath.resolve(downloadDestination, plFolderName);
|
|
97
|
+
await sshPl.sshClient.uploadDirectory(dirPath, `/home/pl-doctor/${plFolderName}`);
|
|
98
|
+
|
|
99
|
+
console.log(plPath, dirPath);
|
|
100
|
+
|
|
101
|
+
const execResult2 = await testContainer!.exec(['cat', `/home/pl-doctor/${plFolderName}/.ok`]);
|
|
102
|
+
const output2 = execResult2.output.trim();
|
|
103
|
+
expect(output2).toBe('ok');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('Get free port', async () => {
|
|
107
|
+
await sshPl?.platformaInit(downloadDestination);
|
|
108
|
+
const isAlive = await sshPl?.isAlive();
|
|
109
|
+
expect(isAlive).toBe(true);
|
|
110
|
+
|
|
111
|
+
const arch = await sshPl.getArch();
|
|
112
|
+
const remoteHome = await sshPl.getUserHomeDirectory();
|
|
113
|
+
const port = await sshPl.getFreePortForPlatformaOnServer(remoteHome, arch);
|
|
114
|
+
|
|
115
|
+
expect(typeof port).toBe('number');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('fetchPorts', async () => {
|
|
119
|
+
const arch = await sshPl.getArch();
|
|
120
|
+
const remoteHome = await sshPl.getUserHomeDirectory();
|
|
121
|
+
const ports = await sshPl.fetchPorts(remoteHome, arch);
|
|
122
|
+
|
|
123
|
+
if (ports) {
|
|
124
|
+
Object.entries(ports).forEach(([, port]) => {
|
|
125
|
+
expect(typeof port.local).toBe('number');
|
|
126
|
+
expect(typeof port.remote).toBe('number');
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
expect(ports?.grpc).toMatchObject({
|
|
131
|
+
local: expect.anything(),
|
|
132
|
+
remote: expect.anything(),
|
|
133
|
+
});
|
|
134
|
+
expect(ports?.monitoring).toMatchObject({
|
|
135
|
+
local: expect.anything(),
|
|
136
|
+
remote: expect.anything(),
|
|
137
|
+
});
|
|
138
|
+
expect(ports?.debug).toMatchObject({
|
|
139
|
+
local: expect.anything(),
|
|
140
|
+
remote: expect.anything(),
|
|
141
|
+
});
|
|
142
|
+
expect(ports?.minioPort).toMatchObject({
|
|
143
|
+
local: expect.anything(),
|
|
144
|
+
remote: expect.anything(),
|
|
145
|
+
});
|
|
146
|
+
expect(ports?.minioConsolePort).toMatchObject({
|
|
147
|
+
local: expect.anything(),
|
|
148
|
+
remote: expect.anything(),
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('SshPl download binaries', async () => {
|
|
154
|
+
it('Download pl. We have archive and extracted data', async () => {
|
|
155
|
+
const arch = await sshPl.getArch();
|
|
156
|
+
|
|
157
|
+
const result = await downloadPlBinary(
|
|
158
|
+
new ConsoleLoggerAdapter(),
|
|
159
|
+
downloadDestination,
|
|
160
|
+
getDefaultPlVersion(),
|
|
161
|
+
arch.arch,
|
|
162
|
+
arch.platform,
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const version = getDefaultPlVersion();
|
|
166
|
+
expect(!!result).toBe(true);
|
|
167
|
+
expect(existsSync(`${downloadDestination}/pl-${version}-${newArch(arch.arch)}.tgz`)).toBe(true);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('Download other software', async () => {
|
|
171
|
+
const arch = await sshPl.getArch();
|
|
172
|
+
|
|
173
|
+
const softwareName = 'supervisord';
|
|
174
|
+
const tgzName = 'supervisord-0.7.3';
|
|
175
|
+
|
|
176
|
+
const result = await downloadBinary(
|
|
177
|
+
new ConsoleLoggerAdapter(),
|
|
178
|
+
downloadDestination,
|
|
179
|
+
softwareName, tgzName,
|
|
180
|
+
arch.arch,
|
|
181
|
+
arch.platform,
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
expect(!!result).toBe(true);
|
|
185
|
+
expect(existsSync(`${downloadDestination}/${tgzName}-${newArch(arch.arch)}.tgz`)).toBe(true);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
afterAll(async () => {
|
|
190
|
+
await cleanUp();
|
|
191
|
+
await cleanUpT(testContainer);
|
|
192
|
+
});
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { describe, it, beforeAll, afterAll, expect } from 'vitest';
|
|
2
|
+
import { writeFileSync, readFileSync } from 'fs';
|
|
3
|
+
import { SshClient } from '../ssh';
|
|
4
|
+
import ssh from 'ssh2';
|
|
5
|
+
import { downloadsFolder, cleanUp, getConnectionForSsh, getContainerHostAndPort, initContainer, localFileDownload, localFileUpload } from './common-utils';
|
|
6
|
+
import { ConsoleLoggerAdapter } from '@milaboratories/ts-helpers';
|
|
7
|
+
|
|
8
|
+
let client: SshClient;
|
|
9
|
+
const testContainer = await initContainer('ssh');
|
|
10
|
+
|
|
11
|
+
beforeAll(async () => {
|
|
12
|
+
client = await SshClient.init(new ConsoleLoggerAdapter(), getConnectionForSsh(testContainer));
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('SSH Tests', () => {
|
|
16
|
+
it('isPassphraseRequiredForKey', async () => {
|
|
17
|
+
const k1 = ssh.utils.generateKeyPairSync('rsa', {
|
|
18
|
+
passphrase: 'password',
|
|
19
|
+
cipher: 'aes256-cbc',
|
|
20
|
+
bits: 2048,
|
|
21
|
+
});
|
|
22
|
+
expect(await SshClient.isPassphraseRequiredForKey(k1.private)).toBe(true);
|
|
23
|
+
|
|
24
|
+
const k2 = ssh.utils.generateKeyPairSync('rsa', {
|
|
25
|
+
cipher: 'aes256-cbc',
|
|
26
|
+
bits: 2048,
|
|
27
|
+
});
|
|
28
|
+
expect(await SshClient.isPassphraseRequiredForKey(k2.private)).toBe(false);
|
|
29
|
+
|
|
30
|
+
const k3 = ssh.utils.generateKeyPairSync('ecdsa', { bits: 256, comment: 'node.js rules!', passphrase: 'password', cipher: 'aes256-cbc' });
|
|
31
|
+
expect(await SshClient.isPassphraseRequiredForKey(k3.private)).toBe(true);
|
|
32
|
+
|
|
33
|
+
const k4 = ssh.utils.generateKeyPairSync('ecdsa', { bits: 256, comment: 'node.js rules!', cipher: 'aes256-cbc' });
|
|
34
|
+
expect(await SshClient.isPassphraseRequiredForKey(k4.private)).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
it('should create file from string', async () => {
|
|
37
|
+
expect(await client.writeFileOnTheServer('/home/pl-doctor/from-string.txt', 'hello'));
|
|
38
|
+
|
|
39
|
+
const execResult = await testContainer.exec(['cat', `/home/pl-doctor/from-string.txt`]);
|
|
40
|
+
const output = execResult.output.trim();
|
|
41
|
+
expect(output).toBe('hello');
|
|
42
|
+
});
|
|
43
|
+
it('should create all directories if none exist', async () => {
|
|
44
|
+
const remotePath = '/home/pl-doctor/upload/nested/directory';
|
|
45
|
+
await expect(client.createRemoteDirectory(remotePath)).resolves.not.toThrow();
|
|
46
|
+
|
|
47
|
+
// Additional check to ensure the directory exists
|
|
48
|
+
await client.withSftp(async (sftp) => {
|
|
49
|
+
const stat = await new Promise((resolve, reject) => {
|
|
50
|
+
sftp.stat(remotePath, (err, stats) => {
|
|
51
|
+
if (err) return reject(err);
|
|
52
|
+
resolve(stats);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
expect(stat).toBeDefined();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should handle existing directories gracefully', async () => {
|
|
60
|
+
const remotePath = '/home/pl-doctor/upload/nested';
|
|
61
|
+
|
|
62
|
+
await expect(client.createRemoteDirectory(remotePath)).resolves.not.toThrow();
|
|
63
|
+
await expect(client.createRemoteDirectory(remotePath)).resolves.not.toThrow();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should throw an error if an invalid path is provided', async () => {
|
|
67
|
+
const remotePath = '/invalid_path/nested';
|
|
68
|
+
|
|
69
|
+
await expect(client.createRemoteDirectory(remotePath)).rejects.toThrow();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('Upload directory', async () => {
|
|
73
|
+
const remoteDir = '/home/pl-doctor';
|
|
74
|
+
await client.uploadDirectory(`${downloadsFolder}/rec-upload`, '/home/pl-doctor/rec-upload');
|
|
75
|
+
|
|
76
|
+
const execResult = await testContainer.exec(['cat', `${remoteDir}/rec-upload/sub-1/test.txt`]);
|
|
77
|
+
const output = execResult.output.trim();
|
|
78
|
+
expect(output).toBe('test-1');
|
|
79
|
+
|
|
80
|
+
const execResult1 = await testContainer.exec(['cat', `${remoteDir}/rec-upload/sub-1/sub-1-1/test-5.txt`]);
|
|
81
|
+
const output1 = execResult1.output.trim();
|
|
82
|
+
expect(output1).toBe('test-5');
|
|
83
|
+
|
|
84
|
+
const execResult2 = await testContainer.exec(['cat', `${remoteDir}/rec-upload/sub-1/sub-1-18/test-2.txt`]);
|
|
85
|
+
const output2 = execResult2.output.trim();
|
|
86
|
+
expect(output2).toBe('test-2');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should upload a file to the SSH server', async () => {
|
|
90
|
+
const data = `Test data ${new Date().getTime()}`;
|
|
91
|
+
const remoteFile = '/home/pl-doctor/uploaded-file.txt';
|
|
92
|
+
writeFileSync(localFileUpload, data);
|
|
93
|
+
const result = await client.uploadFile(localFileUpload, remoteFile);
|
|
94
|
+
expect(result).toBe(true);
|
|
95
|
+
const execResult = await testContainer.exec(['cat', remoteFile]);
|
|
96
|
+
const output = execResult.output.trim();
|
|
97
|
+
expect(output).toBe(data);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should download file from SSH server', async () => {
|
|
101
|
+
const data = `Test data ${new Date().getTime()}`;
|
|
102
|
+
// const localFile = './test-file-upload.txt';
|
|
103
|
+
const remoteFile = '/home/pl-doctor/uploaded-file.txt';
|
|
104
|
+
|
|
105
|
+
writeFileSync(localFileDownload, data);
|
|
106
|
+
|
|
107
|
+
const uploadResult = await client.uploadFile(localFileDownload, remoteFile);
|
|
108
|
+
expect(uploadResult).toBe(true);
|
|
109
|
+
|
|
110
|
+
const downloadResult = await client.downloadFile(remoteFile, localFileDownload);
|
|
111
|
+
expect(downloadResult).toBe(true);
|
|
112
|
+
expect(readFileSync(localFileDownload, { encoding: 'utf-8' })).toBe(data);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('Simple server should forward remote SSH port to a local port', async () => {
|
|
116
|
+
const localPort = 3001;
|
|
117
|
+
|
|
118
|
+
const resFailed = await fetch(`http://127.0.0.1:${localPort}`).catch((err) => console.log('Must fail'));
|
|
119
|
+
expect(resFailed).toBe(undefined);
|
|
120
|
+
|
|
121
|
+
const { server } = await client.forwardPort({
|
|
122
|
+
remotePort: localPort,
|
|
123
|
+
localPort,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const res = await fetch(`http://127.0.0.1:${localPort}`);
|
|
127
|
+
expect(await res.text()).toBe('Hello, this is a simple Node.js server!');
|
|
128
|
+
|
|
129
|
+
server.close();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('Auth types', async () => {
|
|
133
|
+
const hostData = getContainerHostAndPort(testContainer);
|
|
134
|
+
const types = await SshClient.getAuthTypes(hostData.host, hostData.port);
|
|
135
|
+
expect(types[0]).toBe('publickey');
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('sshConnect', () => {
|
|
140
|
+
it('should successfully connect to the SSH server', async () => {
|
|
141
|
+
const client = await SshClient.init(new ConsoleLoggerAdapter(), getConnectionForSsh(testContainer));
|
|
142
|
+
client.close();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should fail with invalid credentials', async () => {
|
|
146
|
+
await expect(SshClient.init(new ConsoleLoggerAdapter(), { ...getConnectionForSsh(testContainer), privateKey: '123' })).rejects.toThrow();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should timeout if the server is unreachable', async () => {
|
|
150
|
+
await expect(SshClient.init(new ConsoleLoggerAdapter(), { ...getConnectionForSsh(testContainer), port: 3233 })).rejects.toThrow('ssh.connect: error occurred: AggregateError');
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('sshExec', () => {
|
|
155
|
+
it('should execute a valid command and return stdout', async () => {
|
|
156
|
+
const { stdout, stderr } = await client.exec('echo "Hello, SSH"');
|
|
157
|
+
expect(stdout.trim()).toBe('Hello, SSH');
|
|
158
|
+
expect(stderr).toBe('');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should capture stderr for an invalid command', async () => {
|
|
162
|
+
const command = 'nonexistentcommand';
|
|
163
|
+
await expect(client.exec(command)).rejects.toThrow();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should handle a command with both stdout and stderr', async () => {
|
|
167
|
+
const { stdout, stderr } = await client.exec('sh -c "echo stdout && echo stderr >&2"');
|
|
168
|
+
expect(stdout.trim()).toBe('stdout');
|
|
169
|
+
expect(stderr.trim()).toBe('stderr');
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
afterAll(async () => {
|
|
174
|
+
await cleanUp(testContainer);
|
|
175
|
+
});
|