-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathdocker.client.test.ts
More file actions
163 lines (136 loc) · 5.59 KB
/
docker.client.test.ts
File metadata and controls
163 lines (136 loc) · 5.59 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
import { DockerApiClient } from "./docker.client";
import { PassThrough } from "stream";
jest.mock("dockerode", () => {
const listContainers = jest.fn();
const execInspect = jest.fn();
const start = jest.fn((opts: any, cb: any) => {
const stream = new PassThrough();
setImmediate(() => cb(null, stream));
return undefined as unknown as NodeJS.ReadableStream;
});
const exec = jest.fn(async () => ({ start, inspect: execInspect }));
const getContainer = jest.fn(() => ({ exec }));
const __state = { demuxTarget: "stdout" as "stdout" | "stderr" };
class DockerMock {
static __mocks = { listContainers, getContainer, exec, start, execInspect, __state };
modem: any;
constructor() {
this.modem = {
demuxStream: (
combined: NodeJS.ReadableStream,
stdout: PassThrough,
stderr: PassThrough
) => {
combined.on("data", (d) => {
if (__state.demuxTarget === "stdout") stdout.write(d);
else stderr.write(d);
});
combined.on("end", () => {
stdout.end();
stderr.end();
});
},
};
}
listContainers = listContainers;
getContainer = getContainer;
}
return DockerMock as any;
});
const getDockerMocks = () => (require("dockerode") as any).__mocks;
describe("DockerApiClient", () => {
beforeEach(() => {
const mocks = getDockerMocks();
mocks.listContainers.mockReset();
mocks.getContainer.mockReset();
mocks.exec.mockReset();
mocks.start.mockReset();
mocks.execInspect.mockReset();
// Restore default implementations after reset
mocks.getContainer.mockImplementation(() => ({ exec: mocks.exec }));
mocks.exec.mockImplementation(async () => ({ start: mocks.start, inspect: mocks.execInspect }));
mocks.__state.demuxTarget = "stdout";
delete process.env.MAIN_CONTAINER_NAME;
delete process.env.LOCALSTACK_MAIN_CONTAINER_NAME;
});
test("findLocalStackContainer throws when none found", async () => {
const mocks = getDockerMocks();
mocks.listContainers.mockResolvedValueOnce([]);
const client = new DockerApiClient();
await expect(client.findLocalStackContainer()).rejects.toThrow(
/Could not find a running LocalStack container/i
);
});
test("findLocalStackContainer returns id when found", async () => {
const mocks = getDockerMocks();
mocks.listContainers.mockResolvedValueOnce([{ Id: "abc123", Names: ["/localstack-main"] }]);
const client = new DockerApiClient();
await expect(client.findLocalStackContainer()).resolves.toBe("abc123");
});
test("findLocalStackContainer matches MAIN_CONTAINER_NAME when configured", async () => {
process.env.MAIN_CONTAINER_NAME = "my-custom-localstack";
const mocks = getDockerMocks();
mocks.listContainers.mockResolvedValueOnce([
{ Id: "not-this", Names: ["/localstack-main"] },
{ Id: "xyz999", Names: ["/my-custom-localstack"] },
]);
const client = new DockerApiClient();
await expect(client.findLocalStackContainer()).resolves.toBe("xyz999");
});
test("findLocalStackContainer throws when only compose-prefixed name exists without config", async () => {
const mocks = getDockerMocks();
mocks.listContainers.mockResolvedValueOnce([{ Id: "compose123", Names: ["/project-localstack-1"] }]);
const client = new DockerApiClient();
await expect(client.findLocalStackContainer()).rejects.toThrow(
/Could not find a running LocalStack container named "localstack-main"/i
);
});
test("executeInContainer returns stdout on success", async () => {
const mocks = getDockerMocks();
mocks.listContainers.mockResolvedValueOnce([{ Id: "abc123", Names: ["/localstack-main"] }]);
// prepare exec.inspect to return 0
mocks.execInspect.mockResolvedValueOnce({ ExitCode: 0 });
const client = new DockerApiClient();
const containerId = await client.findLocalStackContainer();
// Start call: we must simulate demux writing to stdout, then end the stream
const stream = new PassThrough();
mocks.start.mockImplementationOnce((opts: any, cb: any) => {
setImmediate(() => {
cb(null, stream);
setImmediate(() => {
});
});
});
const execPromise = client.executeInContainer(containerId, ["echo", "hello"]);
// After a tick, feed data to combined stream and end it
setImmediate(() => {
stream.write("hello-world\n");
stream.end();
});
const res = await execPromise;
expect(res.exitCode).toBe(0);
expect(res.stdout).toContain("hello-world");
});
test("executeInContainer returns stderr on failure", async () => {
const mocks = getDockerMocks();
mocks.listContainers.mockResolvedValueOnce([{ Id: "abc123", Names: ["/localstack-main"] }]);
mocks.execInspect.mockResolvedValueOnce({ ExitCode: 2 });
const client = new DockerApiClient();
const containerId = await client.findLocalStackContainer();
const stream = new PassThrough();
mocks.start.mockImplementationOnce((opts: any, cb: any) => {
setImmediate(() => cb(null, stream));
});
// route combined stream to stderr for this test
getDockerMocks().__state.demuxTarget = "stderr";
const execPromise = client.executeInContainer(containerId, ["sh", "-c", "exit 2"]);
setImmediate(() => {
// our default demux pipes to stdout; simulate stderr by writing a marker and expect stderr to capture it
stream.write("something went wrong\n");
stream.end();
});
const res = await execPromise;
expect(res.exitCode).toBe(2);
expect(res.stderr).toContain("something went wrong");
});
});