Add readSettings unit test (#602)

This commit is contained in:
Will Chen
2025-07-08 17:29:34 -07:00
committed by GitHub
parent 4d7d9ec2a6
commit d57c6e24a0

View File

@@ -0,0 +1,431 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import fs from "node:fs";
import path from "node:path";
import { safeStorage } from "electron";
import { readSettings, getSettingsFilePath } from "@/main/settings";
import { getUserDataPath } from "@/paths/paths";
// Mock dependencies
vi.mock("node:fs");
vi.mock("node:path");
vi.mock("electron", () => ({
safeStorage: {
isEncryptionAvailable: vi.fn(),
decryptString: vi.fn(),
},
}));
vi.mock("@/paths/paths", () => ({
getUserDataPath: vi.fn(),
}));
const mockFs = vi.mocked(fs);
const mockPath = vi.mocked(path);
const mockSafeStorage = vi.mocked(safeStorage);
const mockGetUserDataPath = vi.mocked(getUserDataPath);
describe("readSettings", () => {
const mockUserDataPath = "/mock/user/data";
const mockSettingsPath = "/mock/user/data/user-settings.json";
beforeEach(() => {
vi.clearAllMocks();
mockGetUserDataPath.mockReturnValue(mockUserDataPath);
mockPath.join.mockReturnValue(mockSettingsPath);
mockSafeStorage.isEncryptionAvailable.mockReturnValue(true);
});
afterEach(() => {
vi.restoreAllMocks();
});
describe("when settings file does not exist", () => {
it("should create default settings file and return default settings", () => {
mockFs.existsSync.mockReturnValue(false);
mockFs.writeFileSync.mockImplementation(() => {});
const result = readSettings();
expect(mockFs.existsSync).toHaveBeenCalledWith(mockSettingsPath);
expect(mockFs.writeFileSync).toHaveBeenCalledWith(
mockSettingsPath,
expect.stringContaining('"selectedModel"'),
);
expect(result).toEqual({
selectedModel: {
name: "auto",
provider: "auto",
},
providerSettings: {},
telemetryConsent: "unset",
telemetryUserId: expect.any(String),
hasRunBefore: false,
experiments: {},
enableProLazyEditsMode: true,
enableProSmartFilesContextMode: true,
selectedChatMode: "build",
enableAutoFixProblems: false,
enableAutoUpdate: true,
releaseChannel: "stable",
});
});
});
describe("when settings file exists", () => {
it("should read and merge settings with defaults", () => {
const mockFileContent = {
selectedModel: {
name: "gpt-4",
provider: "openai",
},
telemetryConsent: "opted_in",
hasRunBefore: true,
};
mockFs.existsSync.mockReturnValue(true);
mockFs.readFileSync.mockReturnValue(JSON.stringify(mockFileContent));
const result = readSettings();
expect(mockFs.readFileSync).toHaveBeenCalledWith(
mockSettingsPath,
"utf-8",
);
expect(result.selectedModel).toEqual({
name: "gpt-4",
provider: "openai",
});
expect(result.telemetryConsent).toBe("opted_in");
expect(result.hasRunBefore).toBe(true);
// Should still have defaults for missing properties
expect(result.enableAutoUpdate).toBe(true);
expect(result.releaseChannel).toBe("stable");
});
it("should decrypt encrypted provider API keys", () => {
const mockFileContent = {
providerSettings: {
openai: {
apiKey: {
value: "encrypted-api-key",
encryptionType: "electron-safe-storage",
},
},
},
};
mockFs.existsSync.mockReturnValue(true);
mockFs.readFileSync.mockReturnValue(JSON.stringify(mockFileContent));
mockSafeStorage.decryptString.mockReturnValue("decrypted-api-key");
const result = readSettings();
expect(mockSafeStorage.decryptString).toHaveBeenCalledWith(
Buffer.from("encrypted-api-key", "base64"),
);
expect(result.providerSettings.openai.apiKey).toEqual({
value: "decrypted-api-key",
encryptionType: "electron-safe-storage",
});
});
it("should decrypt encrypted GitHub access token", () => {
const mockFileContent = {
githubAccessToken: {
value: "encrypted-github-token",
encryptionType: "electron-safe-storage",
},
};
mockFs.existsSync.mockReturnValue(true);
mockFs.readFileSync.mockReturnValue(JSON.stringify(mockFileContent));
mockSafeStorage.decryptString.mockReturnValue("decrypted-github-token");
const result = readSettings();
expect(mockSafeStorage.decryptString).toHaveBeenCalledWith(
Buffer.from("encrypted-github-token", "base64"),
);
expect(result.githubAccessToken).toEqual({
value: "decrypted-github-token",
encryptionType: "electron-safe-storage",
});
});
it("should decrypt encrypted Supabase tokens", () => {
const mockFileContent = {
supabase: {
accessToken: {
value: "encrypted-access-token",
encryptionType: "electron-safe-storage",
},
refreshToken: {
value: "encrypted-refresh-token",
encryptionType: "electron-safe-storage",
},
},
};
mockFs.existsSync.mockReturnValue(true);
mockFs.readFileSync.mockReturnValue(JSON.stringify(mockFileContent));
mockSafeStorage.decryptString
.mockReturnValueOnce("decrypted-refresh-token")
.mockReturnValueOnce("decrypted-access-token");
const result = readSettings();
expect(mockSafeStorage.decryptString).toHaveBeenCalledTimes(2);
expect(result.supabase?.refreshToken).toEqual({
value: "decrypted-refresh-token",
encryptionType: "electron-safe-storage",
});
expect(result.supabase?.accessToken).toEqual({
value: "decrypted-access-token",
encryptionType: "electron-safe-storage",
});
});
it("should handle plaintext secrets without decryption", () => {
const mockFileContent = {
githubAccessToken: {
value: "plaintext-token",
encryptionType: "plaintext",
},
providerSettings: {
openai: {
apiKey: {
value: "plaintext-api-key",
encryptionType: "plaintext",
},
},
},
};
mockFs.existsSync.mockReturnValue(true);
mockFs.readFileSync.mockReturnValue(JSON.stringify(mockFileContent));
const result = readSettings();
expect(mockSafeStorage.decryptString).not.toHaveBeenCalled();
expect(result.githubAccessToken?.value).toBe("plaintext-token");
expect(result.providerSettings.openai.apiKey?.value).toBe(
"plaintext-api-key",
);
});
it("should handle secrets without encryptionType", () => {
const mockFileContent = {
githubAccessToken: {
value: "token-without-encryption-type",
},
providerSettings: {
openai: {
apiKey: {
value: "api-key-without-encryption-type",
},
},
},
};
mockFs.existsSync.mockReturnValue(true);
mockFs.readFileSync.mockReturnValue(JSON.stringify(mockFileContent));
const result = readSettings();
expect(mockSafeStorage.decryptString).not.toHaveBeenCalled();
expect(result.githubAccessToken?.value).toBe(
"token-without-encryption-type",
);
expect(result.providerSettings.openai.apiKey?.value).toBe(
"api-key-without-encryption-type",
);
});
it("should strip extra fields not recognized by the schema", () => {
const mockFileContent = {
selectedModel: {
name: "gpt-4",
provider: "openai",
},
telemetryConsent: "opted_in",
hasRunBefore: true,
// Extra fields that are not in the schema
unknownField: "should be removed",
deprecatedSetting: true,
extraConfig: {
someValue: 123,
anotherValue: "test",
},
};
mockFs.existsSync.mockReturnValue(true);
mockFs.readFileSync.mockReturnValue(JSON.stringify(mockFileContent));
const result = readSettings();
expect(mockFs.readFileSync).toHaveBeenCalledWith(
mockSettingsPath,
"utf-8",
);
expect(result.selectedModel).toEqual({
name: "gpt-4",
provider: "openai",
});
expect(result.telemetryConsent).toBe("opted_in");
expect(result.hasRunBefore).toBe(true);
// Extra fields should be stripped by schema validation
expect(result).not.toHaveProperty("unknownField");
expect(result).not.toHaveProperty("deprecatedSetting");
expect(result).not.toHaveProperty("extraConfig");
// Should still have defaults for missing properties
expect(result.enableAutoUpdate).toBe(true);
expect(result.releaseChannel).toBe("stable");
});
});
describe("error handling", () => {
it("should return default settings when file read fails", () => {
mockFs.existsSync.mockReturnValue(true);
mockFs.readFileSync.mockImplementation(() => {
throw new Error("File read error");
});
const consoleSpy = vi
.spyOn(console, "error")
.mockImplementation(() => {});
const result = readSettings();
expect(consoleSpy).toHaveBeenCalledWith(
"Error reading settings:",
expect.any(Error),
);
expect(result).toEqual({
selectedModel: {
name: "auto",
provider: "auto",
},
providerSettings: {},
telemetryConsent: "unset",
telemetryUserId: expect.any(String),
hasRunBefore: false,
experiments: {},
enableProLazyEditsMode: true,
enableProSmartFilesContextMode: true,
selectedChatMode: "build",
enableAutoFixProblems: false,
enableAutoUpdate: true,
releaseChannel: "stable",
});
consoleSpy.mockRestore();
});
it("should return default settings when JSON parsing fails", () => {
mockFs.existsSync.mockReturnValue(true);
mockFs.readFileSync.mockReturnValue("invalid json");
const consoleSpy = vi
.spyOn(console, "error")
.mockImplementation(() => {});
const result = readSettings();
expect(consoleSpy).toHaveBeenCalledWith(
"Error reading settings:",
expect.any(Error),
);
expect(result).toMatchObject({
selectedModel: {
name: "auto",
provider: "auto",
},
releaseChannel: "stable",
});
consoleSpy.mockRestore();
});
it("should return default settings when schema validation fails", () => {
const mockFileContent = {
selectedModel: {
name: "gpt-4",
// Missing required 'provider' field
},
releaseChannel: "invalid-channel", // Invalid enum value
};
mockFs.existsSync.mockReturnValue(true);
mockFs.readFileSync.mockReturnValue(JSON.stringify(mockFileContent));
const consoleSpy = vi
.spyOn(console, "error")
.mockImplementation(() => {});
const result = readSettings();
expect(consoleSpy).toHaveBeenCalledWith(
"Error reading settings:",
expect.any(Error),
);
expect(result).toMatchObject({
selectedModel: {
name: "auto",
provider: "auto",
},
releaseChannel: "stable",
});
consoleSpy.mockRestore();
});
it("should handle decryption errors gracefully", () => {
const mockFileContent = {
githubAccessToken: {
value: "corrupted-encrypted-data",
encryptionType: "electron-safe-storage",
},
};
mockFs.existsSync.mockReturnValue(true);
mockFs.readFileSync.mockReturnValue(JSON.stringify(mockFileContent));
mockSafeStorage.decryptString.mockImplementation(() => {
throw new Error("Decryption failed");
});
const consoleSpy = vi
.spyOn(console, "error")
.mockImplementation(() => {});
const result = readSettings();
expect(consoleSpy).toHaveBeenCalledWith(
"Error reading settings:",
expect.any(Error),
);
expect(result).toMatchObject({
selectedModel: {
name: "auto",
provider: "auto",
},
releaseChannel: "stable",
});
consoleSpy.mockRestore();
});
});
describe("getSettingsFilePath", () => {
it("should return correct settings file path", () => {
const result = getSettingsFilePath();
expect(mockGetUserDataPath).toHaveBeenCalled();
expect(mockPath.join).toHaveBeenCalledWith(
mockUserDataPath,
"user-settings.json",
);
expect(result).toBe(mockSettingsPath);
});
});
});