Add readSettings unit test (#602)
This commit is contained in:
431
src/__tests__/readSettings.test.ts
Normal file
431
src/__tests__/readSettings.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user