Skip to content

Commit 805b3b1

Browse files
committed
Add unit tests for the split modules
1 parent 4bd91ec commit 805b3b1

File tree

7 files changed

+412
-29
lines changed

7 files changed

+412
-29
lines changed

src/__mocks__/testHelpers.ts

Lines changed: 15 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@ import * as vscode from "vscode";
88
export class MockConfigurationProvider {
99
private config = new Map<string, unknown>();
1010

11+
constructor() {
12+
this.setupVSCodeMock();
13+
}
14+
1115
/**
1216
* Set a configuration value that will be returned by vscode.workspace.getConfiguration().get()
1317
*/
1418
set(key: string, value: unknown): void {
1519
this.config.set(key, value);
16-
this.setupVSCodeMock();
1720
}
1821

1922
/**
@@ -31,13 +34,12 @@ export class MockConfigurationProvider {
3134
*/
3235
clear(): void {
3336
this.config.clear();
34-
this.setupVSCodeMock();
3537
}
3638

3739
/**
3840
* Setup the vscode.workspace.getConfiguration mock to return our values
3941
*/
40-
setupVSCodeMock(): void {
42+
private setupVSCodeMock(): void {
4143
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({
4244
get: vi.fn((key: string, defaultValue?: unknown) => {
4345
const value = this.config.get(key);
@@ -58,6 +60,10 @@ export class MockProgressReporter {
5860
private shouldCancel = false;
5961
private progressReports: Array<{ message?: string; increment?: number }> = [];
6062

63+
constructor() {
64+
this.setupVSCodeMock();
65+
}
66+
6167
/**
6268
* Set whether the progress should be cancelled
6369
*/
@@ -82,7 +88,7 @@ export class MockProgressReporter {
8288
/**
8389
* Setup the vscode.window.withProgress mock
8490
*/
85-
setupVSCodeMock(): void {
91+
private setupVSCodeMock(): void {
8692
vi.mocked(vscode.window.withProgress).mockImplementation(
8793
async <T>(
8894
_options: vscode.ProgressOptions,
@@ -121,6 +127,10 @@ export class MockUserInteraction {
121127
private responses = new Map<string, string | undefined>();
122128
private externalUrls: string[] = [];
123129

130+
constructor() {
131+
this.setupVSCodeMock();
132+
}
133+
124134
/**
125135
* Set a response for a specific message or set a default response
126136
*/
@@ -163,7 +173,7 @@ export class MockUserInteraction {
163173
/**
164174
* Setup the vscode.window message dialog mocks
165175
*/
166-
setupVSCodeMock(): void {
176+
private setupVSCodeMock(): void {
167177
const getResponse = (message: string): string | undefined => {
168178
return this.responses.get(message) ?? this.responses.get("default");
169179
};
@@ -200,24 +210,3 @@ export class MockUserInteraction {
200210
);
201211
}
202212
}
203-
204-
/**
205-
* Helper function to setup all VS Code mocks for testing.
206-
* Call this in your test setup to initialize all the mock integrations.
207-
*/
208-
export function setupVSCodeMocks(): {
209-
mockConfig: MockConfigurationProvider;
210-
mockProgress: MockProgressReporter;
211-
mockUI: MockUserInteraction;
212-
} {
213-
const mockConfig = new MockConfigurationProvider();
214-
const mockProgress = new MockProgressReporter();
215-
const mockUI = new MockUserInteraction();
216-
217-
// Setup all the VS Code API mocks
218-
mockConfig.setupVSCodeMock();
219-
mockProgress.setupVSCodeMock();
220-
mockUI.setupVSCodeMock();
221-
222-
return { mockConfig, mockProgress, mockUI };
223-
}

src/core/binaryManager.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
MockConfigurationProvider,
1010
MockProgressReporter,
1111
MockUserInteraction,
12-
setupVSCodeMocks,
1312
} from "../__mocks__/testHelpers";
1413
import * as cli from "../cliManager";
1514
import { Logger } from "../logging/logger";
@@ -45,7 +44,9 @@ describe("BinaryManager", () => {
4544
mockApi = createMockApi(TEST_VERSION, TEST_URL);
4645
mockAxios = mockApi.getAxiosInstance();
4746
vi.mocked(globalAxios.create).mockReturnValue(mockAxios);
48-
({ mockConfig, mockProgress, mockUI } = setupVSCodeMocks());
47+
mockConfig = new MockConfigurationProvider();
48+
mockProgress = new MockProgressReporter();
49+
mockUI = new MockUserInteraction();
4950
manager = new BinaryManager(
5051
mockLogger,
5152
new PathResolver("/path/base", "/code/log"),

src/core/cliConfig.test.ts

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import fs from "fs/promises";
2+
import { describe, it, expect, vi, beforeEach } from "vitest";
3+
import { CliConfigManager } from "./cliConfig";
4+
import { PathResolver } from "./pathResolver";
5+
6+
vi.mock("fs/promises");
7+
8+
describe("CliConfigManager", () => {
9+
let pathResolver: PathResolver;
10+
let cliConfigManager: CliConfigManager;
11+
const mockFs = vi.mocked(fs);
12+
const writtenFiles = new Map<string, string>();
13+
14+
beforeEach(() => {
15+
vi.resetAllMocks();
16+
writtenFiles.clear();
17+
pathResolver = new PathResolver("/test/base", "/test/log");
18+
cliConfigManager = new CliConfigManager(pathResolver);
19+
20+
mockFs.mkdir.mockResolvedValue(undefined);
21+
mockFs.writeFile.mockImplementation(async (path, content) => {
22+
writtenFiles.set(path.toString(), content.toString());
23+
return Promise.resolve();
24+
});
25+
});
26+
27+
describe("configure", () => {
28+
it("should write both url and token to correct paths", async () => {
29+
await cliConfigManager.configure(
30+
"deployment",
31+
"https://coder.example.com",
32+
"test-token",
33+
);
34+
35+
expect([...writtenFiles.entries()]).toEqual([
36+
["/test/base/deployment/url", "https://coder.example.com"],
37+
["/test/base/deployment/session", "test-token"],
38+
]);
39+
});
40+
41+
it("should skip URL when undefined but write token", async () => {
42+
await cliConfigManager.configure("deployment", undefined, "test-token");
43+
44+
// No entry for the url
45+
expect([...writtenFiles.entries()]).toEqual([
46+
["/test/base/deployment/session", "test-token"],
47+
]);
48+
});
49+
50+
it("should skip token when null but write URL", async () => {
51+
await cliConfigManager.configure(
52+
"deployment",
53+
"https://coder.example.com",
54+
null,
55+
);
56+
57+
// No entry for the session
58+
expect([...writtenFiles.entries()]).toEqual([
59+
["/test/base/deployment/url", "https://coder.example.com"],
60+
]);
61+
});
62+
63+
it("should write empty string for token when provided", async () => {
64+
await cliConfigManager.configure(
65+
"deployment",
66+
"https://coder.example.com",
67+
"",
68+
);
69+
70+
expect([...writtenFiles.entries()]).toEqual([
71+
["/test/base/deployment/url", "https://coder.example.com"],
72+
["/test/base/deployment/session", ""],
73+
]);
74+
});
75+
76+
it("should use base path directly when label is empty", async () => {
77+
await cliConfigManager.configure(
78+
"",
79+
"https://coder.example.com",
80+
"token",
81+
);
82+
83+
expect([...writtenFiles.entries()]).toEqual([
84+
["/test/base/url", "https://coder.example.com"],
85+
["/test/base/session", "token"],
86+
]);
87+
});
88+
});
89+
90+
describe("readConfig", () => {
91+
beforeEach(() => {
92+
mockFs.readFile.mockImplementation(async (filePath) => {
93+
const path = filePath.toString();
94+
if (writtenFiles.has(path)) {
95+
return writtenFiles.get(path)!;
96+
}
97+
return Promise.reject(new Error("ENOENT: no such file or directory"));
98+
});
99+
});
100+
101+
it("should read and trim stored configuration", async () => {
102+
writtenFiles.set(
103+
"/test/base/deployment/url",
104+
" https://coder.example.com \n",
105+
);
106+
writtenFiles.set("/test/base/deployment/session", "\t test-token \r\n");
107+
108+
const result = await cliConfigManager.readConfig("deployment");
109+
110+
expect(result).toEqual({
111+
url: "https://coder.example.com",
112+
token: "test-token",
113+
});
114+
});
115+
116+
it("should return empty strings for missing files", async () => {
117+
const result = await cliConfigManager.readConfig("deployment");
118+
119+
expect(result).toEqual({
120+
url: "",
121+
token: "",
122+
});
123+
});
124+
125+
it("should handle partial configuration", async () => {
126+
writtenFiles.set(
127+
"/test/base/deployment/url",
128+
"https://coder.example.com",
129+
);
130+
131+
const result = await cliConfigManager.readConfig("deployment");
132+
133+
expect(result).toEqual({
134+
url: "https://coder.example.com",
135+
token: "",
136+
});
137+
});
138+
});
139+
});

src/core/mementoManager.test.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { describe, it, expect, beforeEach } from "vitest";
2+
import type { Memento } from "vscode";
3+
import { MementoManager } from "./mementoManager";
4+
5+
// Simple in-memory implementation of Memento
6+
class InMemoryMemento implements Memento {
7+
private storage = new Map<string, unknown>();
8+
9+
get<T>(key: string): T | undefined;
10+
get<T>(key: string, defaultValue: T): T;
11+
get<T>(key: string, defaultValue?: T): T | undefined {
12+
return this.storage.has(key) ? (this.storage.get(key) as T) : defaultValue;
13+
}
14+
15+
async update(key: string, value: unknown): Promise<void> {
16+
if (value === undefined) {
17+
this.storage.delete(key);
18+
} else {
19+
this.storage.set(key, value);
20+
}
21+
return Promise.resolve();
22+
}
23+
24+
keys(): readonly string[] {
25+
return Array.from(this.storage.keys());
26+
}
27+
}
28+
29+
describe("MementoManager", () => {
30+
let memento: InMemoryMemento;
31+
let mementoManager: MementoManager;
32+
33+
beforeEach(() => {
34+
memento = new InMemoryMemento();
35+
mementoManager = new MementoManager(memento);
36+
});
37+
38+
describe("setUrl", () => {
39+
it("should store URL and add to history", async () => {
40+
await mementoManager.setUrl("https://coder.example.com");
41+
42+
expect(mementoManager.getUrl()).toBe("https://coder.example.com");
43+
expect(memento.get("urlHistory")).toEqual(["https://coder.example.com"]);
44+
});
45+
46+
it("should not update history for falsy values", async () => {
47+
await mementoManager.setUrl(undefined);
48+
expect(mementoManager.getUrl()).toBeUndefined();
49+
expect(memento.get("urlHistory")).toBeUndefined();
50+
51+
await mementoManager.setUrl("");
52+
expect(mementoManager.getUrl()).toBe("");
53+
expect(memento.get("urlHistory")).toBeUndefined();
54+
});
55+
56+
it("should deduplicate URLs in history", async () => {
57+
await mementoManager.setUrl("url1");
58+
await mementoManager.setUrl("url2");
59+
await mementoManager.setUrl("url1"); // Re-add first URL
60+
61+
expect(memento.get("urlHistory")).toEqual(["url2", "url1"]);
62+
});
63+
});
64+
65+
describe("withUrlHistory", () => {
66+
it("should append URLs and remove duplicates", async () => {
67+
await memento.update("urlHistory", ["existing1", "existing2"]);
68+
69+
const result = mementoManager.withUrlHistory("existing2", "new1");
70+
71+
expect(result).toEqual(["existing1", "existing2", "new1"]);
72+
});
73+
74+
it("should limit to 10 URLs", async () => {
75+
const urls = Array.from({ length: 10 }, (_, i) => `url${i}`);
76+
await memento.update("urlHistory", urls);
77+
78+
const result = mementoManager.withUrlHistory("url20");
79+
80+
expect(result).toHaveLength(10);
81+
expect(result[0]).toBe("url1");
82+
expect(result[9]).toBe("url20");
83+
});
84+
85+
it("should handle non-array storage gracefully", async () => {
86+
await memento.update("urlHistory", "not-an-array");
87+
const result = mementoManager.withUrlHistory("url1");
88+
expect(result).toEqual(["url1"]);
89+
});
90+
});
91+
92+
describe("firstConnect", () => {
93+
it("should return true only once", async () => {
94+
await mementoManager.setFirstConnect();
95+
96+
expect(await mementoManager.getAndClearFirstConnect()).toBe(true);
97+
expect(await mementoManager.getAndClearFirstConnect()).toBe(false);
98+
});
99+
100+
it("should return false for non-boolean values", async () => {
101+
await memento.update("firstConnect", "truthy-string");
102+
expect(await mementoManager.getAndClearFirstConnect()).toBe(false);
103+
});
104+
});
105+
});

src/core/mementoManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export class MementoManager {
3030

3131
/**
3232
* Get the most recently accessed URLs (oldest to newest) with the provided
33-
* values appended. Duplicates will be removed.
33+
* values appended. Duplicates will be removed.
3434
*/
3535
public withUrlHistory(...append: (string | undefined)[]): string[] {
3636
const val = this.memento.get("urlHistory");

0 commit comments

Comments
 (0)