Skip to content

Commit eb5d3b4

Browse files
committed
Add network and disk error tests
1 parent f6d76c8 commit eb5d3b4

File tree

3 files changed

+65
-22
lines changed

3 files changed

+65
-22
lines changed

src/core/downloadProgress.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { promises as fs } from "node:fs";
1+
import * as fs from "node:fs/promises";
22
import * as path from "node:path";
33

44
export interface DownloadProgress {
@@ -36,5 +36,9 @@ export async function readProgress(
3636
}
3737

3838
export async function clearProgress(logPath: string): Promise<void> {
39-
await fs.rm(logPath, { force: true });
39+
try {
40+
await fs.rm(logPath, { force: true });
41+
} catch {
42+
// If we cannot remove it now then we'll do it in the next startup
43+
}
4044
}

test/mocks/testHelpers.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -302,13 +302,19 @@ export function createMockLogger(): Logger {
302302

303303
export function createMockStream(
304304
content: string,
305-
options: { chunkSize?: number; delay?: number } = {},
305+
options: {
306+
chunkSize?: number;
307+
delay?: number;
308+
// If defined will throw an error instead of closing normally
309+
error?: NodeJS.ErrnoException;
310+
} = {},
306311
): IncomingMessage {
307-
const { chunkSize = 8, delay = 1 } = options;
312+
const { chunkSize = 8, delay = 1, error } = options;
308313

309314
const buffer = Buffer.from(content);
310315
let position = 0;
311316
let closeCallback: ((...args: unknown[]) => void) | null = null;
317+
let errorCallback: ((error: Error) => void) | null = null;
312318

313319
return {
314320
on: vi.fn((event: string, callback: (...args: unknown[]) => void) => {
@@ -325,14 +331,18 @@ export function createMockStream(
325331
setTimeout(sendChunk, delay);
326332
} else {
327333
setImmediate(() => {
328-
if (closeCallback) {
334+
if (error && errorCallback) {
335+
errorCallback(error);
336+
} else if (closeCallback) {
329337
closeCallback();
330338
}
331339
});
332340
}
333341
}
334342
};
335343
setTimeout(sendChunk, delay);
344+
} else if (event === "error") {
345+
errorCallback = callback;
336346
} else if (event === "close") {
337347
closeCallback = callback;
338348
}

test/unit/core/cliManager.concurrent.test.ts

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77
import { type AxiosInstance } from "axios";
88
import { type Api } from "coder/site/src/api/api";
9-
import * as fsPromises from "node:fs/promises";
9+
import * as fs from "node:fs/promises";
1010
import * as os from "node:os";
1111
import * as path from "node:path";
1212
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
@@ -46,7 +46,11 @@ function setupCliUtilsMocks(version: string) {
4646

4747
function createMockApi(
4848
version: string,
49-
options: { chunkSize?: number; delay?: number } = {},
49+
options: {
50+
chunkSize?: number;
51+
delay?: number;
52+
error?: NodeJS.ErrnoException;
53+
} = {},
5054
): Api {
5155
const mockAxios = {
5256
get: vi.fn().mockImplementation(() =>
@@ -66,7 +70,7 @@ function createMockApi(
6670
}
6771

6872
function setupManager(testDir: string): CliManager {
69-
const _progressReporter = new MockProgressReporter();
73+
const _mockProgress = new MockProgressReporter();
7074
const mockConfig = new MockConfigurationProvider();
7175
mockConfig.set("coder.disableSignatureVerification", true);
7276

@@ -81,13 +85,13 @@ describe("CliManager Concurrent Downloads", () => {
8185
let testDir: string;
8286

8387
beforeEach(async () => {
84-
testDir = await fsPromises.mkdtemp(
88+
testDir = await fs.mkdtemp(
8589
path.join(os.tmpdir(), "climanager-concurrent-"),
8690
);
8791
});
8892

8993
afterEach(async () => {
90-
await fsPromises.rm(testDir, { recursive: true, force: true });
94+
await fs.rm(testDir, { recursive: true, force: true });
9195
});
9296

9397
it("handles multiple concurrent downloads without race conditions", async () => {
@@ -110,18 +114,16 @@ describe("CliManager Concurrent Downloads", () => {
110114
}
111115

112116
// Verify binary exists and lock/progress files are cleaned up
113-
await expect(fsPromises.access(binaryPath)).resolves.toBeUndefined();
114-
await expect(fsPromises.access(binaryPath + ".lock")).rejects.toThrow();
115-
await expect(
116-
fsPromises.access(binaryPath + ".progress.log"),
117-
).rejects.toThrow();
117+
await expect(fs.access(binaryPath)).resolves.toBeUndefined();
118+
await expect(fs.access(binaryPath + ".lock")).rejects.toThrow();
119+
await expect(fs.access(binaryPath + ".progress.log")).rejects.toThrow();
118120
});
119121

120122
it("redownloads when version mismatch is detected concurrently", async () => {
121123
const manager = setupManager(testDir);
122124
setupCliUtilsMocks("1.2.3");
123125
vi.mocked(cliUtils.version).mockImplementation(async (binPath) => {
124-
const fileContent = await fsPromises.readFile(binPath, {
126+
const fileContent = await fs.readFile(binPath, {
125127
encoding: "utf-8",
126128
});
127129
return fileContent.includes("1.2.3") ? "1.2.3" : "2.0.0";
@@ -151,12 +153,39 @@ describe("CliManager Concurrent Downloads", () => {
151153
}
152154

153155
// Binary should be updated to 2.0.0, lock/progress files cleaned up
154-
await expect(fsPromises.access(binaryPath)).resolves.toBeUndefined();
155-
const finalContent = await fsPromises.readFile(binaryPath, "utf8");
156+
await expect(fs.access(binaryPath)).resolves.toBeUndefined();
157+
const finalContent = await fs.readFile(binaryPath, "utf8");
156158
expect(finalContent).toContain("v2.0.0");
157-
await expect(fsPromises.access(binaryPath + ".lock")).rejects.toThrow();
158-
await expect(
159-
fsPromises.access(binaryPath + ".progress.log"),
160-
).rejects.toThrow();
159+
await expect(fs.access(binaryPath + ".lock")).rejects.toThrow();
160+
await expect(fs.access(binaryPath + ".progress.log")).rejects.toThrow();
161+
});
162+
163+
it.each([
164+
{
165+
name: "disk storage insufficient",
166+
code: "ENOSPC",
167+
message: "no space left on device",
168+
},
169+
{
170+
name: "connection timeout",
171+
code: "ETIMEDOUT",
172+
message: "connection timed out",
173+
},
174+
])("handles $name error during download", async ({ code, message }) => {
175+
const manager = setupManager(testDir);
176+
setupCliUtilsMocks("1.2.3");
177+
178+
const error = new Error(`${code}: ${message}`);
179+
(error as NodeJS.ErrnoException).code = code;
180+
const mockApi = createMockApi("1.2.3", { error });
181+
182+
const label = "test.coder.com";
183+
const binaryPath = path.join(testDir, label, "bin", "coder-linux-amd64");
184+
185+
await expect(manager.fetchBinary(mockApi, label)).rejects.toThrow(
186+
`Unable to download binary: ${code}: ${message}`,
187+
);
188+
189+
await expect(fs.access(binaryPath + ".lock")).rejects.toThrow();
161190
});
162191
});

0 commit comments

Comments
 (0)