Skip to content

Commit 89e3934

Browse files
authored
Add extensive end-to-end tests of IContainerOrchestratorClient (#252)
1 parent 2cd1deb commit 89e3934

File tree

3 files changed

+389
-96
lines changed

3 files changed

+389
-96
lines changed
Lines changed: 375 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,375 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See LICENSE in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { expect } from 'chai';
7+
import * as fs from 'fs/promises';
8+
import * as path from 'path';
9+
import { DockerComposeClient } from '../clients/DockerComposeClient/DockerComposeClient';
10+
import { ShellStreamCommandRunnerFactory, ShellStreamCommandRunnerOptions } from '../commandRunners/shellStream';
11+
import { WslShellCommandRunnerFactory, WslShellCommandRunnerOptions } from '../commandRunners/wslStream';
12+
import { IContainerOrchestratorClient } from '../contracts/ContainerOrchestratorClient';
13+
import { ICommandRunnerFactory } from '../contracts/CommandRunner';
14+
import { wslifyPath } from '../utils/wslifyPath';
15+
import { DockerClient } from '../clients/DockerClient/DockerClient';
16+
import { validateContainerExists } from './ContainersClientE2E.test';
17+
18+
// Modify the below options to configure the tests
19+
const runInWsl: boolean = !!process.env.RUN_IN_WSL || false; // Set to true if running in WSL
20+
21+
// No need to modify below this
22+
describe('(integration) ContainerOrchestratorClientE2E', function () {
23+
24+
// #region Test Setup
25+
26+
let client: IContainerOrchestratorClient;
27+
let containerClient: DockerClient;
28+
let defaultRunner: ICommandRunnerFactory;
29+
let defaultRunnerFactory: (options: ShellStreamCommandRunnerOptions) => ICommandRunnerFactory;
30+
let composeFilePath: string;
31+
let composeDir: string;
32+
let composeProfileFilePath: string;
33+
34+
this.timeout(10000); // Set a longer timeout for integration tests
35+
36+
before(async function () {
37+
client = new DockerComposeClient();
38+
containerClient = new DockerClient(); // Used for validating that the containers are created and removed correctly
39+
40+
if (!runInWsl) {
41+
defaultRunnerFactory = (options: ShellStreamCommandRunnerOptions) => new ShellStreamCommandRunnerFactory(options);
42+
} else {
43+
defaultRunnerFactory = (options: WslShellCommandRunnerOptions) => new WslShellCommandRunnerFactory(options);
44+
}
45+
46+
defaultRunner = defaultRunnerFactory({ strict: true, });
47+
48+
// Set up paths for test docker-compose file
49+
composeDir = path.resolve(__dirname, 'buildContext');
50+
composeFilePath = path.resolve(composeDir, 'docker-compose.yml');
51+
composeProfileFilePath = path.resolve(composeDir, 'docker-compose.profiles.yml');
52+
53+
// Create the test docker-compose.yml file
54+
await fs.mkdir(composeDir, { recursive: true });
55+
await fs.writeFile(composeFilePath, TestComposeFileContent);
56+
await fs.writeFile(composeProfileFilePath, TestComposeFileContentWithProfiles);
57+
58+
if (runInWsl) {
59+
composeDir = wslifyPath(composeDir);
60+
composeFilePath = wslifyPath(composeFilePath);
61+
composeProfileFilePath = wslifyPath(composeProfileFilePath);
62+
}
63+
});
64+
65+
after(async function () {
66+
// Ensure all services are down after tests complete
67+
try {
68+
await defaultRunner.getCommandRunner()(
69+
client.down({
70+
files: [composeFilePath],
71+
removeVolumes: true
72+
})
73+
);
74+
} catch (error) {
75+
// Ignore errors during cleanup
76+
}
77+
});
78+
79+
// #endregion
80+
81+
// #region Client Identity
82+
83+
describe('Client Identity', function () {
84+
it('ClientIdentity', function () {
85+
expect(client.id).to.be.a('string');
86+
expect(client.displayName).to.be.a('string');
87+
expect(client.description).to.be.a('string');
88+
expect(client.commandName).to.be.a('string');
89+
});
90+
});
91+
92+
// #endregion
93+
94+
// #region Up Command
95+
96+
describe('Up', function () {
97+
it('UpCommand', async function () {
98+
// Bring up the services
99+
await defaultRunner.getCommandRunner()(
100+
client.up({
101+
files: [composeFilePath],
102+
detached: true,
103+
})
104+
);
105+
106+
// Verify the containers are created
107+
expect(await validateContainerExists(containerClient, defaultRunner, { containerName: 'buildcontext-frontend-1' })).to.be.ok;
108+
expect(await validateContainerExists(containerClient, defaultRunner, { containerName: 'buildcontext-backend-1' })).to.be.ok;
109+
});
110+
});
111+
112+
// #endregion
113+
114+
// #region Down Command
115+
116+
describe('Down', function () {
117+
before('Down', async function () {
118+
// Ensure services are up before down test
119+
await defaultRunner.getCommandRunner()(
120+
client.up({
121+
files: [composeFilePath],
122+
detached: true,
123+
})
124+
);
125+
});
126+
127+
it('DownCommand', async function () {
128+
// Bring down the services
129+
await defaultRunner.getCommandRunner()(
130+
client.down({
131+
files: [composeFilePath],
132+
removeVolumes: true,
133+
})
134+
);
135+
136+
expect(await validateContainerExists(containerClient, defaultRunner, { containerName: 'buildcontext-frontend-1' })).to.be.not.ok;
137+
expect(await validateContainerExists(containerClient, defaultRunner, { containerName: 'buildcontext-backend-1' })).to.be.not.ok;
138+
});
139+
});
140+
141+
// #endregion
142+
143+
// #region Start Command
144+
145+
describe('Start', function () {
146+
before('Start', async function () {
147+
// Create services but make sure they're stopped
148+
await defaultRunner.getCommandRunner()(
149+
client.up({
150+
files: [composeFilePath],
151+
detached: true,
152+
})
153+
);
154+
155+
await defaultRunner.getCommandRunner()(
156+
client.stop({
157+
files: [composeFilePath],
158+
})
159+
);
160+
});
161+
162+
it('StartCommand', async function () {
163+
// Start the services
164+
await defaultRunner.getCommandRunner()(
165+
client.start({
166+
files: [composeFilePath],
167+
})
168+
);
169+
170+
// Verify the containers are running
171+
const service1 = await validateContainerExists(containerClient, defaultRunner, { containerName: 'buildcontext-frontend-1' });
172+
const service2 = await validateContainerExists(containerClient, defaultRunner, { containerName: 'buildcontext-backend-1' });
173+
174+
expect(service1).to.be.ok;
175+
expect(service2).to.be.ok;
176+
177+
// Validate that they are specifically running
178+
expect(service1?.state).to.equal('running');
179+
expect(service2?.state).to.equal('running');
180+
});
181+
});
182+
183+
// #endregion
184+
185+
// #region Stop Command
186+
187+
describe('Stop', function () {
188+
before('Stop', async function () {
189+
// Ensure services are up before stop test
190+
await defaultRunner.getCommandRunner()(
191+
client.up({
192+
files: [composeFilePath],
193+
detached: true,
194+
})
195+
);
196+
});
197+
198+
it('StopCommand', async function () {
199+
// Stop the services
200+
await defaultRunner.getCommandRunner()(
201+
client.stop({
202+
files: [composeFilePath],
203+
})
204+
);
205+
206+
// Verify the containers are stopped
207+
const service1 = await validateContainerExists(containerClient, defaultRunner, { containerName: 'buildcontext-frontend-1' });
208+
const service2 = await validateContainerExists(containerClient, defaultRunner, { containerName: 'buildcontext-backend-1' });
209+
210+
expect(service1).to.be.ok;
211+
expect(service2).to.be.ok;
212+
213+
// Validate that they are specifically stopped
214+
expect(service1?.state).to.equal('exited');
215+
expect(service2?.state).to.equal('exited');
216+
});
217+
});
218+
219+
// #endregion
220+
221+
// #region Restart Command
222+
223+
describe('Restart', function () {
224+
before('Restart', async function () {
225+
// Ensure services are up before restart test
226+
await defaultRunner.getCommandRunner()(
227+
client.up({
228+
files: [composeFilePath],
229+
detached: true,
230+
})
231+
);
232+
});
233+
234+
it('RestartCommand', async function () {
235+
// Restart the services
236+
await defaultRunner.getCommandRunner()(
237+
client.restart({
238+
files: [composeFilePath],
239+
})
240+
);
241+
242+
// Verify the containers are running
243+
const service1 = await validateContainerExists(containerClient, defaultRunner, { containerName: 'buildcontext-frontend-1' });
244+
const service2 = await validateContainerExists(containerClient, defaultRunner, { containerName: 'buildcontext-backend-1' });
245+
246+
expect(service1).to.be.ok;
247+
expect(service2).to.be.ok;
248+
249+
// Validate that they are specifically running
250+
expect(service1?.state).to.equal('running');
251+
expect(service2?.state).to.equal('running');
252+
});
253+
});
254+
255+
// #endregion
256+
257+
// #region Logs Command
258+
259+
describe('Logs', function () {
260+
before('Logs', async function () {
261+
// Ensure services are up before logs test
262+
await defaultRunner.getCommandRunner()(
263+
client.up({
264+
files: [composeFilePath],
265+
detached: true,
266+
})
267+
);
268+
});
269+
270+
it('LogsCommand', async function () {
271+
// Get logs without following
272+
const logsStream = defaultRunner.getStreamingCommandRunner()(
273+
client.logs({
274+
files: [composeFilePath],
275+
follow: false,
276+
tail: 10,
277+
})
278+
);
279+
280+
let logsOutput = '';
281+
for await (const chunk of logsStream) {
282+
expect(chunk).to.be.a('string');
283+
logsOutput += chunk;
284+
}
285+
286+
// Check if the logs contain the expected output
287+
expect(logsOutput).to.include('Log entry for testing');
288+
});
289+
});
290+
291+
// #endregion
292+
293+
// #region Config Command
294+
295+
describe('Config', function () {
296+
it('ConfigServices', async function () {
297+
const services = await defaultRunner.getCommandRunner()(
298+
client.config({
299+
files: [composeFilePath],
300+
configType: 'services',
301+
})
302+
);
303+
304+
expect(services).to.be.an('array');
305+
expect(services).to.include('frontend');
306+
expect(services).to.include('backend');
307+
});
308+
309+
it('ConfigImages', async function () {
310+
const images = await defaultRunner.getCommandRunner()(
311+
client.config({
312+
files: [composeFilePath],
313+
configType: 'images',
314+
})
315+
);
316+
317+
expect(images).to.be.an('array');
318+
expect(images).to.include('alpine:latest');
319+
});
320+
321+
it('ConfigProfiles', async function () {
322+
// The test docker-compose.yml doesn't define profiles, but the command should still work
323+
const profiles = await defaultRunner.getCommandRunner()(
324+
client.config({
325+
files: [composeFilePath, composeProfileFilePath],
326+
configType: 'profiles',
327+
})
328+
);
329+
330+
expect(profiles).to.be.an('array');
331+
expect(profiles).to.include('test-profile');
332+
});
333+
334+
it('ConfigVolumes', async function () {
335+
// The test docker-compose.yml doesn't define volumes, but the command should still work
336+
const volumes = await defaultRunner.getCommandRunner()(
337+
client.config({
338+
files: [composeFilePath],
339+
configType: 'volumes',
340+
})
341+
);
342+
343+
expect(volumes).to.be.an('array');
344+
expect(volumes).to.include('test-volume');
345+
});
346+
});
347+
348+
// #endregion
349+
});
350+
351+
const TestComposeFileContent = `
352+
services:
353+
frontend:
354+
image: alpine:latest
355+
volumes:
356+
- test-volume:/test-volume
357+
358+
backend:
359+
image: alpine:latest
360+
entrypoint: ["echo", "Log entry for testing"]
361+
362+
volumes:
363+
test-volume:
364+
`;
365+
366+
const TestComposeFileContentWithProfiles = `
367+
services:
368+
frontend:
369+
profiles:
370+
- test-profile
371+
372+
backend:
373+
profiles:
374+
- test-profile
375+
`;

0 commit comments

Comments
 (0)