mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
This commit introduces several improvements to the security and file handling mechanisms across the application. Key changes include: - Updated the Dockerfile to pin the GitHub CLI version for reproducible builds. - Refactored the secure file system operations to ensure consistent path validation and type handling. - Removed legacy path management functions and streamlined the allowed paths logic in the security module. - Enhanced route handlers to validate path parameters against the ALLOWED_ROOT_DIRECTORY, improving security against unauthorized access. - Updated the settings service to focus solely on the Anthropic API key, removing references to Google and OpenAI keys. These changes aim to enhance security, maintainability, and clarity in the codebase. Tests: All unit tests passing.
617 lines
21 KiB
TypeScript
617 lines
21 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
import fs from "fs/promises";
|
|
import path from "path";
|
|
import os from "os";
|
|
import { SettingsService } from "@/services/settings-service.js";
|
|
import {
|
|
DEFAULT_GLOBAL_SETTINGS,
|
|
DEFAULT_CREDENTIALS,
|
|
DEFAULT_PROJECT_SETTINGS,
|
|
SETTINGS_VERSION,
|
|
CREDENTIALS_VERSION,
|
|
PROJECT_SETTINGS_VERSION,
|
|
type GlobalSettings,
|
|
type Credentials,
|
|
type ProjectSettings,
|
|
} from "@/types/settings.js";
|
|
|
|
describe("settings-service.ts", () => {
|
|
let testDataDir: string;
|
|
let testProjectDir: string;
|
|
let settingsService: SettingsService;
|
|
|
|
beforeEach(async () => {
|
|
testDataDir = path.join(os.tmpdir(), `settings-test-${Date.now()}`);
|
|
testProjectDir = path.join(os.tmpdir(), `project-test-${Date.now()}`);
|
|
await fs.mkdir(testDataDir, { recursive: true });
|
|
await fs.mkdir(testProjectDir, { recursive: true });
|
|
settingsService = new SettingsService(testDataDir);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
try {
|
|
await fs.rm(testDataDir, { recursive: true, force: true });
|
|
await fs.rm(testProjectDir, { recursive: true, force: true });
|
|
} catch {
|
|
// Ignore cleanup errors
|
|
}
|
|
});
|
|
|
|
describe("getGlobalSettings", () => {
|
|
it("should return default settings when file does not exist", async () => {
|
|
const settings = await settingsService.getGlobalSettings();
|
|
expect(settings).toEqual(DEFAULT_GLOBAL_SETTINGS);
|
|
});
|
|
|
|
it("should read and return existing settings", async () => {
|
|
const customSettings: GlobalSettings = {
|
|
...DEFAULT_GLOBAL_SETTINGS,
|
|
theme: "light",
|
|
sidebarOpen: false,
|
|
maxConcurrency: 5,
|
|
};
|
|
const settingsPath = path.join(testDataDir, "settings.json");
|
|
await fs.writeFile(settingsPath, JSON.stringify(customSettings, null, 2));
|
|
|
|
const settings = await settingsService.getGlobalSettings();
|
|
expect(settings.theme).toBe("light");
|
|
expect(settings.sidebarOpen).toBe(false);
|
|
expect(settings.maxConcurrency).toBe(5);
|
|
});
|
|
|
|
it("should merge with defaults for missing properties", async () => {
|
|
const partialSettings = {
|
|
version: SETTINGS_VERSION,
|
|
theme: "dark",
|
|
};
|
|
const settingsPath = path.join(testDataDir, "settings.json");
|
|
await fs.writeFile(settingsPath, JSON.stringify(partialSettings, null, 2));
|
|
|
|
const settings = await settingsService.getGlobalSettings();
|
|
expect(settings.theme).toBe("dark");
|
|
expect(settings.sidebarOpen).toBe(DEFAULT_GLOBAL_SETTINGS.sidebarOpen);
|
|
expect(settings.maxConcurrency).toBe(DEFAULT_GLOBAL_SETTINGS.maxConcurrency);
|
|
});
|
|
|
|
it("should merge keyboard shortcuts deeply", async () => {
|
|
const customSettings: GlobalSettings = {
|
|
...DEFAULT_GLOBAL_SETTINGS,
|
|
keyboardShortcuts: {
|
|
...DEFAULT_GLOBAL_SETTINGS.keyboardShortcuts,
|
|
board: "B",
|
|
},
|
|
};
|
|
const settingsPath = path.join(testDataDir, "settings.json");
|
|
await fs.writeFile(settingsPath, JSON.stringify(customSettings, null, 2));
|
|
|
|
const settings = await settingsService.getGlobalSettings();
|
|
expect(settings.keyboardShortcuts.board).toBe("B");
|
|
expect(settings.keyboardShortcuts.agent).toBe(
|
|
DEFAULT_GLOBAL_SETTINGS.keyboardShortcuts.agent
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("updateGlobalSettings", () => {
|
|
it("should create settings file with updates", async () => {
|
|
const updates: Partial<GlobalSettings> = {
|
|
theme: "light",
|
|
sidebarOpen: false,
|
|
};
|
|
|
|
const updated = await settingsService.updateGlobalSettings(updates);
|
|
|
|
expect(updated.theme).toBe("light");
|
|
expect(updated.sidebarOpen).toBe(false);
|
|
expect(updated.version).toBe(SETTINGS_VERSION);
|
|
|
|
const settingsPath = path.join(testDataDir, "settings.json");
|
|
const fileContent = await fs.readFile(settingsPath, "utf-8");
|
|
const saved = JSON.parse(fileContent);
|
|
expect(saved.theme).toBe("light");
|
|
expect(saved.sidebarOpen).toBe(false);
|
|
});
|
|
|
|
it("should merge updates with existing settings", async () => {
|
|
const initial: GlobalSettings = {
|
|
...DEFAULT_GLOBAL_SETTINGS,
|
|
theme: "dark",
|
|
maxConcurrency: 3,
|
|
};
|
|
const settingsPath = path.join(testDataDir, "settings.json");
|
|
await fs.writeFile(settingsPath, JSON.stringify(initial, null, 2));
|
|
|
|
const updates: Partial<GlobalSettings> = {
|
|
theme: "light",
|
|
};
|
|
|
|
const updated = await settingsService.updateGlobalSettings(updates);
|
|
|
|
expect(updated.theme).toBe("light");
|
|
expect(updated.maxConcurrency).toBe(3); // Preserved from initial
|
|
});
|
|
|
|
it("should deep merge keyboard shortcuts", async () => {
|
|
const updates: Partial<GlobalSettings> = {
|
|
keyboardShortcuts: {
|
|
board: "B",
|
|
},
|
|
};
|
|
|
|
const updated = await settingsService.updateGlobalSettings(updates);
|
|
|
|
expect(updated.keyboardShortcuts.board).toBe("B");
|
|
expect(updated.keyboardShortcuts.agent).toBe(
|
|
DEFAULT_GLOBAL_SETTINGS.keyboardShortcuts.agent
|
|
);
|
|
});
|
|
|
|
it("should create data directory if it does not exist", async () => {
|
|
const newDataDir = path.join(os.tmpdir(), `new-data-dir-${Date.now()}`);
|
|
const newService = new SettingsService(newDataDir);
|
|
|
|
await newService.updateGlobalSettings({ theme: "light" });
|
|
|
|
const stats = await fs.stat(newDataDir);
|
|
expect(stats.isDirectory()).toBe(true);
|
|
|
|
await fs.rm(newDataDir, { recursive: true, force: true });
|
|
});
|
|
});
|
|
|
|
describe("hasGlobalSettings", () => {
|
|
it("should return false when settings file does not exist", async () => {
|
|
const exists = await settingsService.hasGlobalSettings();
|
|
expect(exists).toBe(false);
|
|
});
|
|
|
|
it("should return true when settings file exists", async () => {
|
|
await settingsService.updateGlobalSettings({ theme: "light" });
|
|
const exists = await settingsService.hasGlobalSettings();
|
|
expect(exists).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("getCredentials", () => {
|
|
it("should return default credentials when file does not exist", async () => {
|
|
const credentials = await settingsService.getCredentials();
|
|
expect(credentials).toEqual(DEFAULT_CREDENTIALS);
|
|
});
|
|
|
|
it("should read and return existing credentials", async () => {
|
|
const customCredentials: Credentials = {
|
|
...DEFAULT_CREDENTIALS,
|
|
apiKeys: {
|
|
anthropic: "sk-test-key",
|
|
},
|
|
};
|
|
const credentialsPath = path.join(testDataDir, "credentials.json");
|
|
await fs.writeFile(credentialsPath, JSON.stringify(customCredentials, null, 2));
|
|
|
|
const credentials = await settingsService.getCredentials();
|
|
expect(credentials.apiKeys.anthropic).toBe("sk-test-key");
|
|
});
|
|
|
|
it("should merge with defaults for missing api keys", async () => {
|
|
const partialCredentials = {
|
|
version: CREDENTIALS_VERSION,
|
|
apiKeys: {
|
|
anthropic: "sk-test",
|
|
},
|
|
};
|
|
const credentialsPath = path.join(testDataDir, "credentials.json");
|
|
await fs.writeFile(credentialsPath, JSON.stringify(partialCredentials, null, 2));
|
|
|
|
const credentials = await settingsService.getCredentials();
|
|
expect(credentials.apiKeys.anthropic).toBe("sk-test");
|
|
});
|
|
});
|
|
|
|
describe("updateCredentials", () => {
|
|
it("should create credentials file with updates", async () => {
|
|
const updates: Partial<Credentials> = {
|
|
apiKeys: {
|
|
anthropic: "sk-test-key",
|
|
},
|
|
};
|
|
|
|
const updated = await settingsService.updateCredentials(updates);
|
|
|
|
expect(updated.apiKeys.anthropic).toBe("sk-test-key");
|
|
expect(updated.version).toBe(CREDENTIALS_VERSION);
|
|
|
|
const credentialsPath = path.join(testDataDir, "credentials.json");
|
|
const fileContent = await fs.readFile(credentialsPath, "utf-8");
|
|
const saved = JSON.parse(fileContent);
|
|
expect(saved.apiKeys.anthropic).toBe("sk-test-key");
|
|
});
|
|
|
|
it("should merge updates with existing credentials", async () => {
|
|
const initial: Credentials = {
|
|
...DEFAULT_CREDENTIALS,
|
|
apiKeys: {
|
|
anthropic: "sk-initial",
|
|
},
|
|
};
|
|
const credentialsPath = path.join(testDataDir, "credentials.json");
|
|
await fs.writeFile(credentialsPath, JSON.stringify(initial, null, 2));
|
|
|
|
const updates: Partial<Credentials> = {
|
|
apiKeys: {
|
|
anthropic: "sk-updated",
|
|
},
|
|
};
|
|
|
|
const updated = await settingsService.updateCredentials(updates);
|
|
|
|
expect(updated.apiKeys.anthropic).toBe("sk-updated");
|
|
});
|
|
|
|
it("should deep merge api keys", async () => {
|
|
const initial: Credentials = {
|
|
...DEFAULT_CREDENTIALS,
|
|
apiKeys: {
|
|
anthropic: "sk-anthropic",
|
|
},
|
|
};
|
|
const credentialsPath = path.join(testDataDir, "credentials.json");
|
|
await fs.writeFile(credentialsPath, JSON.stringify(initial, null, 2));
|
|
|
|
const updates: Partial<Credentials> = {
|
|
apiKeys: {
|
|
anthropic: "sk-updated-anthropic",
|
|
},
|
|
};
|
|
|
|
const updated = await settingsService.updateCredentials(updates);
|
|
|
|
expect(updated.apiKeys.anthropic).toBe("sk-updated-anthropic");
|
|
});
|
|
});
|
|
|
|
describe("getMaskedCredentials", () => {
|
|
it("should return masked credentials for empty keys", async () => {
|
|
const masked = await settingsService.getMaskedCredentials();
|
|
expect(masked.anthropic.configured).toBe(false);
|
|
expect(masked.anthropic.masked).toBe("");
|
|
});
|
|
|
|
it("should mask keys correctly", async () => {
|
|
await settingsService.updateCredentials({
|
|
apiKeys: {
|
|
anthropic: "sk-ant-api03-1234567890abcdef",
|
|
},
|
|
});
|
|
|
|
const masked = await settingsService.getMaskedCredentials();
|
|
expect(masked.anthropic.configured).toBe(true);
|
|
expect(masked.anthropic.masked).toBe("sk-a...cdef");
|
|
});
|
|
|
|
it("should handle short keys", async () => {
|
|
await settingsService.updateCredentials({
|
|
apiKeys: {
|
|
anthropic: "short",
|
|
},
|
|
});
|
|
|
|
const masked = await settingsService.getMaskedCredentials();
|
|
expect(masked.anthropic.configured).toBe(true);
|
|
expect(masked.anthropic.masked).toBe("");
|
|
});
|
|
});
|
|
|
|
describe("hasCredentials", () => {
|
|
it("should return false when credentials file does not exist", async () => {
|
|
const exists = await settingsService.hasCredentials();
|
|
expect(exists).toBe(false);
|
|
});
|
|
|
|
it("should return true when credentials file exists", async () => {
|
|
await settingsService.updateCredentials({
|
|
apiKeys: { anthropic: "test" },
|
|
});
|
|
const exists = await settingsService.hasCredentials();
|
|
expect(exists).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("getProjectSettings", () => {
|
|
it("should return default settings when file does not exist", async () => {
|
|
const settings = await settingsService.getProjectSettings(testProjectDir);
|
|
expect(settings).toEqual(DEFAULT_PROJECT_SETTINGS);
|
|
});
|
|
|
|
it("should read and return existing project settings", async () => {
|
|
const customSettings: ProjectSettings = {
|
|
...DEFAULT_PROJECT_SETTINGS,
|
|
theme: "light",
|
|
useWorktrees: true,
|
|
};
|
|
const automakerDir = path.join(testProjectDir, ".automaker");
|
|
await fs.mkdir(automakerDir, { recursive: true });
|
|
const settingsPath = path.join(automakerDir, "settings.json");
|
|
await fs.writeFile(settingsPath, JSON.stringify(customSettings, null, 2));
|
|
|
|
const settings = await settingsService.getProjectSettings(testProjectDir);
|
|
expect(settings.theme).toBe("light");
|
|
expect(settings.useWorktrees).toBe(true);
|
|
});
|
|
|
|
it("should merge with defaults for missing properties", async () => {
|
|
const partialSettings = {
|
|
version: PROJECT_SETTINGS_VERSION,
|
|
theme: "dark",
|
|
};
|
|
const automakerDir = path.join(testProjectDir, ".automaker");
|
|
await fs.mkdir(automakerDir, { recursive: true });
|
|
const settingsPath = path.join(automakerDir, "settings.json");
|
|
await fs.writeFile(settingsPath, JSON.stringify(partialSettings, null, 2));
|
|
|
|
const settings = await settingsService.getProjectSettings(testProjectDir);
|
|
expect(settings.theme).toBe("dark");
|
|
expect(settings.version).toBe(PROJECT_SETTINGS_VERSION);
|
|
});
|
|
});
|
|
|
|
describe("updateProjectSettings", () => {
|
|
it("should create project settings file with updates", async () => {
|
|
const updates: Partial<ProjectSettings> = {
|
|
theme: "light",
|
|
useWorktrees: true,
|
|
};
|
|
|
|
const updated = await settingsService.updateProjectSettings(testProjectDir, updates);
|
|
|
|
expect(updated.theme).toBe("light");
|
|
expect(updated.useWorktrees).toBe(true);
|
|
expect(updated.version).toBe(PROJECT_SETTINGS_VERSION);
|
|
|
|
const automakerDir = path.join(testProjectDir, ".automaker");
|
|
const settingsPath = path.join(automakerDir, "settings.json");
|
|
const fileContent = await fs.readFile(settingsPath, "utf-8");
|
|
const saved = JSON.parse(fileContent);
|
|
expect(saved.theme).toBe("light");
|
|
expect(saved.useWorktrees).toBe(true);
|
|
});
|
|
|
|
it("should merge updates with existing project settings", async () => {
|
|
const initial: ProjectSettings = {
|
|
...DEFAULT_PROJECT_SETTINGS,
|
|
theme: "dark",
|
|
useWorktrees: false,
|
|
};
|
|
const automakerDir = path.join(testProjectDir, ".automaker");
|
|
await fs.mkdir(automakerDir, { recursive: true });
|
|
const settingsPath = path.join(automakerDir, "settings.json");
|
|
await fs.writeFile(settingsPath, JSON.stringify(initial, null, 2));
|
|
|
|
const updates: Partial<ProjectSettings> = {
|
|
theme: "light",
|
|
};
|
|
|
|
const updated = await settingsService.updateProjectSettings(testProjectDir, updates);
|
|
|
|
expect(updated.theme).toBe("light");
|
|
expect(updated.useWorktrees).toBe(false); // Preserved
|
|
});
|
|
|
|
it("should deep merge board background", async () => {
|
|
const initial: ProjectSettings = {
|
|
...DEFAULT_PROJECT_SETTINGS,
|
|
boardBackground: {
|
|
imagePath: "/path/to/image.jpg",
|
|
cardOpacity: 0.8,
|
|
columnOpacity: 0.9,
|
|
columnBorderEnabled: true,
|
|
cardGlassmorphism: false,
|
|
cardBorderEnabled: true,
|
|
cardBorderOpacity: 0.5,
|
|
hideScrollbar: false,
|
|
},
|
|
};
|
|
const automakerDir = path.join(testProjectDir, ".automaker");
|
|
await fs.mkdir(automakerDir, { recursive: true });
|
|
const settingsPath = path.join(automakerDir, "settings.json");
|
|
await fs.writeFile(settingsPath, JSON.stringify(initial, null, 2));
|
|
|
|
const updates: Partial<ProjectSettings> = {
|
|
boardBackground: {
|
|
cardOpacity: 0.9,
|
|
},
|
|
};
|
|
|
|
const updated = await settingsService.updateProjectSettings(testProjectDir, updates);
|
|
|
|
expect(updated.boardBackground?.imagePath).toBe("/path/to/image.jpg");
|
|
expect(updated.boardBackground?.cardOpacity).toBe(0.9);
|
|
expect(updated.boardBackground?.columnOpacity).toBe(0.9);
|
|
});
|
|
|
|
it("should create .automaker directory if it does not exist", async () => {
|
|
const newProjectDir = path.join(os.tmpdir(), `new-project-${Date.now()}`);
|
|
|
|
await settingsService.updateProjectSettings(newProjectDir, { theme: "light" });
|
|
|
|
const automakerDir = path.join(newProjectDir, ".automaker");
|
|
const stats = await fs.stat(automakerDir);
|
|
expect(stats.isDirectory()).toBe(true);
|
|
|
|
await fs.rm(newProjectDir, { recursive: true, force: true });
|
|
});
|
|
});
|
|
|
|
describe("hasProjectSettings", () => {
|
|
it("should return false when project settings file does not exist", async () => {
|
|
const exists = await settingsService.hasProjectSettings(testProjectDir);
|
|
expect(exists).toBe(false);
|
|
});
|
|
|
|
it("should return true when project settings file exists", async () => {
|
|
await settingsService.updateProjectSettings(testProjectDir, { theme: "light" });
|
|
const exists = await settingsService.hasProjectSettings(testProjectDir);
|
|
expect(exists).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("migrateFromLocalStorage", () => {
|
|
it("should migrate global settings from localStorage data", async () => {
|
|
const localStorageData = {
|
|
"automaker-storage": JSON.stringify({
|
|
state: {
|
|
theme: "light",
|
|
sidebarOpen: false,
|
|
maxConcurrency: 5,
|
|
},
|
|
}),
|
|
};
|
|
|
|
const result = await settingsService.migrateFromLocalStorage(localStorageData);
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.migratedGlobalSettings).toBe(true);
|
|
expect(result.migratedCredentials).toBe(false);
|
|
expect(result.migratedProjectCount).toBe(0);
|
|
|
|
const settings = await settingsService.getGlobalSettings();
|
|
expect(settings.theme).toBe("light");
|
|
expect(settings.sidebarOpen).toBe(false);
|
|
expect(settings.maxConcurrency).toBe(5);
|
|
});
|
|
|
|
it("should migrate credentials from localStorage data", async () => {
|
|
const localStorageData = {
|
|
"automaker-storage": JSON.stringify({
|
|
state: {
|
|
apiKeys: {
|
|
anthropic: "sk-test-key",
|
|
},
|
|
},
|
|
}),
|
|
};
|
|
|
|
const result = await settingsService.migrateFromLocalStorage(localStorageData);
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.migratedCredentials).toBe(true);
|
|
|
|
const credentials = await settingsService.getCredentials();
|
|
expect(credentials.apiKeys.anthropic).toBe("sk-test-key");
|
|
});
|
|
|
|
it("should migrate project settings from localStorage data", async () => {
|
|
const localStorageData = {
|
|
"automaker-storage": JSON.stringify({
|
|
state: {
|
|
projects: [
|
|
{
|
|
id: "proj1",
|
|
name: "Project 1",
|
|
path: testProjectDir,
|
|
theme: "light",
|
|
},
|
|
],
|
|
boardBackgroundByProject: {
|
|
[testProjectDir]: {
|
|
imagePath: "/path/to/image.jpg",
|
|
cardOpacity: 0.8,
|
|
columnOpacity: 0.9,
|
|
columnBorderEnabled: true,
|
|
cardGlassmorphism: false,
|
|
cardBorderEnabled: true,
|
|
cardBorderOpacity: 0.5,
|
|
hideScrollbar: false,
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
};
|
|
|
|
const result = await settingsService.migrateFromLocalStorage(localStorageData);
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.migratedProjectCount).toBe(1);
|
|
|
|
const projectSettings = await settingsService.getProjectSettings(testProjectDir);
|
|
expect(projectSettings.theme).toBe("light");
|
|
expect(projectSettings.boardBackground?.imagePath).toBe("/path/to/image.jpg");
|
|
});
|
|
|
|
it("should handle direct localStorage values", async () => {
|
|
const localStorageData = {
|
|
"automaker:lastProjectDir": "/path/to/project",
|
|
"file-browser-recent-folders": JSON.stringify(["/path1", "/path2"]),
|
|
"worktree-panel-collapsed": "true",
|
|
};
|
|
|
|
const result = await settingsService.migrateFromLocalStorage(localStorageData);
|
|
|
|
expect(result.success).toBe(true);
|
|
const settings = await settingsService.getGlobalSettings();
|
|
expect(settings.lastProjectDir).toBe("/path/to/project");
|
|
expect(settings.recentFolders).toEqual(["/path1", "/path2"]);
|
|
expect(settings.worktreePanelCollapsed).toBe(true);
|
|
});
|
|
|
|
it("should handle invalid JSON gracefully", async () => {
|
|
const localStorageData = {
|
|
"automaker-storage": "invalid json",
|
|
"file-browser-recent-folders": "invalid json",
|
|
};
|
|
|
|
const result = await settingsService.migrateFromLocalStorage(localStorageData);
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.errors.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("should handle migration errors gracefully", async () => {
|
|
// Create a read-only directory to cause write errors
|
|
const readOnlyDir = path.join(os.tmpdir(), `readonly-${Date.now()}`);
|
|
await fs.mkdir(readOnlyDir, { recursive: true });
|
|
await fs.chmod(readOnlyDir, 0o444);
|
|
|
|
const readOnlyService = new SettingsService(readOnlyDir);
|
|
const localStorageData = {
|
|
"automaker-storage": JSON.stringify({
|
|
state: { theme: "light" },
|
|
}),
|
|
};
|
|
|
|
const result = await readOnlyService.migrateFromLocalStorage(localStorageData);
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.errors.length).toBeGreaterThan(0);
|
|
|
|
await fs.chmod(readOnlyDir, 0o755);
|
|
await fs.rm(readOnlyDir, { recursive: true, force: true });
|
|
});
|
|
});
|
|
|
|
describe("getDataDir", () => {
|
|
it("should return the data directory path", () => {
|
|
const dataDir = settingsService.getDataDir();
|
|
expect(dataDir).toBe(testDataDir);
|
|
});
|
|
});
|
|
|
|
describe("atomicWriteJson", () => {
|
|
it("should handle write errors and clean up temp file", async () => {
|
|
// Create a read-only directory to cause write errors
|
|
const readOnlyDir = path.join(os.tmpdir(), `readonly-${Date.now()}`);
|
|
await fs.mkdir(readOnlyDir, { recursive: true });
|
|
await fs.chmod(readOnlyDir, 0o444);
|
|
|
|
const readOnlyService = new SettingsService(readOnlyDir);
|
|
|
|
await expect(
|
|
readOnlyService.updateGlobalSettings({ theme: "light" })
|
|
).rejects.toThrow();
|
|
|
|
await fs.chmod(readOnlyDir, 0o755);
|
|
await fs.rm(readOnlyDir, { recursive: true, force: true });
|
|
});
|
|
});
|
|
});
|
|
|