* fix: avoid legacy Windows PasswordVault reads by default * fix: isolate model capability override cache --------- Co-authored-by: OpenClaude Worker 3 <worker-3@openclaude.local>
209 lines
7.3 KiB
TypeScript
209 lines
7.3 KiB
TypeScript
|
|
import { expect, test, mock, describe, beforeEach, afterEach } from "bun:test";
|
|
import { linuxSecretStorage } from "./linuxSecretStorage.js";
|
|
import { windowsCredentialStorage } from "./windowsCredentialStorage.js";
|
|
import { getSecureStorageServiceName, CREDENTIALS_SERVICE_SUFFIX } from "./macOsKeychainHelpers.js";
|
|
|
|
// Mock execaSync
|
|
const mockExecaSync = mock(() => ({ exitCode: 0, stdout: "" }));
|
|
mock.module("execa", () => ({
|
|
execaSync: mockExecaSync,
|
|
}));
|
|
|
|
describe("Secure Storage Platform Implementations", () => {
|
|
const originalEnv = process.env;
|
|
|
|
beforeEach(() => {
|
|
process.env = { ...originalEnv };
|
|
mockExecaSync.mockClear();
|
|
// Default mock behavior
|
|
mockExecaSync.mockImplementation(() => ({ exitCode: 0, stdout: "" }));
|
|
});
|
|
|
|
afterEach(() => {
|
|
process.env = originalEnv;
|
|
});
|
|
|
|
const testData = {
|
|
mcpOAuth: {
|
|
"test-server": {
|
|
accessToken: "secret-token",
|
|
expiresAt: 123456789,
|
|
serverName: "test",
|
|
serverUrl: "http://test"
|
|
}
|
|
}
|
|
};
|
|
|
|
describe("Config-Dir Isolation", () => {
|
|
test("service name changes with CLAUDE_CONFIG_DIR", () => {
|
|
const defaultName = getSecureStorageServiceName(CREDENTIALS_SERVICE_SUFFIX);
|
|
|
|
process.env.CLAUDE_CONFIG_DIR = "/tmp/other-config";
|
|
const otherName = getSecureStorageServiceName(CREDENTIALS_SERVICE_SUFFIX);
|
|
|
|
expect(otherName).not.toBe(defaultName);
|
|
expect(otherName).toContain("Claude Code");
|
|
expect(otherName).toContain(CREDENTIALS_SERVICE_SUFFIX);
|
|
});
|
|
|
|
test("Linux storage uses scoped service name", () => {
|
|
process.env.CLAUDE_CONFIG_DIR = "/tmp/linux-scoped";
|
|
const expectedName = getSecureStorageServiceName(CREDENTIALS_SERVICE_SUFFIX);
|
|
|
|
linuxSecretStorage.update(testData);
|
|
|
|
const args = mockExecaSync.mock.calls[0];
|
|
expect(args[1]).toContain(expectedName);
|
|
});
|
|
|
|
test("Windows storage uses scoped resource name", () => {
|
|
process.env.CLAUDE_CONFIG_DIR = "/tmp/win-scoped";
|
|
const expectedName = getSecureStorageServiceName(CREDENTIALS_SERVICE_SUFFIX);
|
|
|
|
windowsCredentialStorage.update(testData);
|
|
|
|
const script = mockExecaSync.mock.calls[0][1][1];
|
|
const options = mockExecaSync.mock.calls[0][2];
|
|
expect(script).toContain(expectedName);
|
|
expect(script).toContain("ProtectedData");
|
|
expect(options.input).toContain("secret-token");
|
|
});
|
|
});
|
|
|
|
describe("Windows PowerShell Escaping", () => {
|
|
test("escapes single quotes and prevents $ expansion", () => {
|
|
const dataWithDollar = {
|
|
mcpOAuth: {
|
|
"server": {
|
|
accessToken: "token-with-$env:USERNAME",
|
|
expiresAt: 123,
|
|
serverName: "s",
|
|
serverUrl: "u"
|
|
}
|
|
}
|
|
};
|
|
|
|
windowsCredentialStorage.update(dataWithDollar);
|
|
|
|
const script = mockExecaSync.mock.calls[0][1][1];
|
|
const options = mockExecaSync.mock.calls[0][2];
|
|
expect(script).toContain("[Console]::In.ReadToEnd()");
|
|
expect(options.input).toContain("token-with-$env:USERNAME");
|
|
|
|
const dataWithQuote = { mcpOAuth: { "s": { accessToken: "token'quote", expiresAt: 1, serverName: "s", serverUrl: "u" } } };
|
|
windowsCredentialStorage.update(dataWithQuote);
|
|
const options2 = mockExecaSync.mock.calls[1][2];
|
|
expect(options2.input).toContain("token'quote");
|
|
});
|
|
|
|
test("delete() skips legacy PasswordVault by default", () => {
|
|
windowsCredentialStorage.delete();
|
|
expect(mockExecaSync).toHaveBeenCalledTimes(1);
|
|
const script = mockExecaSync.mock.calls[0][1][1];
|
|
expect(script).not.toContain("System.Runtime.WindowsRuntime");
|
|
});
|
|
|
|
test("delete() includes legacy assembly load when explicitly enabled", () => {
|
|
process.env.OPENCLAUDE_ENABLE_LEGACY_WINDOWS_PASSWORDVAULT = "1";
|
|
windowsCredentialStorage.delete();
|
|
const script = mockExecaSync.mock.calls[1][1][1];
|
|
expect(script).toContain("Add-Type -AssemblyName System.Runtime.WindowsRuntime");
|
|
});
|
|
|
|
test("escapes double quotes in username", () => {
|
|
process.env.OPENCLAUDE_ENABLE_LEGACY_WINDOWS_PASSWORDVAULT = "1";
|
|
process.env.USER = 'user"name';
|
|
windowsCredentialStorage.read();
|
|
const script = mockExecaSync.mock.calls[1][1][1];
|
|
expect(script).toContain('user`"name');
|
|
expect(script).not.toContain('user"name');
|
|
});
|
|
|
|
test("read() does not touch legacy PasswordVault by default", () => {
|
|
mockExecaSync.mockImplementationOnce(() => ({ exitCode: 1, stdout: "" }));
|
|
|
|
const result = windowsCredentialStorage.read();
|
|
|
|
expect(result).toBeNull();
|
|
expect(mockExecaSync).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test("read() falls back to legacy PasswordVault when explicitly enabled", () => {
|
|
process.env.OPENCLAUDE_ENABLE_LEGACY_WINDOWS_PASSWORDVAULT = "1";
|
|
mockExecaSync
|
|
.mockImplementationOnce(() => ({ exitCode: 0, stdout: "{not-json" }))
|
|
.mockImplementationOnce(() => ({
|
|
exitCode: 0,
|
|
stdout: JSON.stringify(testData),
|
|
}));
|
|
|
|
const result = windowsCredentialStorage.read();
|
|
|
|
expect(result).toEqual(testData);
|
|
expect(mockExecaSync).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
test("read() fails closed when the legacy PasswordVault payload is invalid JSON", () => {
|
|
process.env.OPENCLAUDE_ENABLE_LEGACY_WINDOWS_PASSWORDVAULT = "1";
|
|
mockExecaSync
|
|
.mockImplementationOnce(() => ({ exitCode: 1, stdout: "" }))
|
|
.mockImplementationOnce(() => ({ exitCode: 0, stdout: "{not-json" }));
|
|
|
|
const result = windowsCredentialStorage.read();
|
|
|
|
expect(result).toBeNull();
|
|
expect(mockExecaSync).toHaveBeenCalledTimes(2);
|
|
});
|
|
});
|
|
|
|
describe("Linux secret-tool Interaction", () => {
|
|
test("update passes payload via stdin", () => {
|
|
linuxSecretStorage.update(testData);
|
|
|
|
const options = mockExecaSync.mock.calls[0][2];
|
|
expect(options.input).toContain("secret-token");
|
|
});
|
|
|
|
test("read parses stdout", () => {
|
|
mockExecaSync.mockReturnValue({ exitCode: 0, stdout: JSON.stringify(testData) });
|
|
const result = linuxSecretStorage.read();
|
|
|
|
expect(result).toEqual(testData);
|
|
});
|
|
});
|
|
|
|
describe("Platform Selection", () => {
|
|
const originalPlatform = process.platform;
|
|
|
|
async function importFreshSecureStorage() {
|
|
return import(`./index.js?ts=${Date.now()}-${Math.random()}`);
|
|
}
|
|
|
|
afterEach(() => {
|
|
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
|
});
|
|
|
|
test("darwin returns keychain with fallback", async () => {
|
|
Object.defineProperty(process, 'platform', { value: 'darwin' });
|
|
const { getSecureStorage } = await importFreshSecureStorage();
|
|
const storage = getSecureStorage();
|
|
expect(storage.name).toContain("keychain");
|
|
});
|
|
|
|
test("linux returns libsecret with fallback", async () => {
|
|
Object.defineProperty(process, 'platform', { value: 'linux' });
|
|
const { getSecureStorage } = await importFreshSecureStorage();
|
|
const storage = getSecureStorage();
|
|
expect(storage.name).toContain("libsecret");
|
|
});
|
|
|
|
test("win32 returns credential-locker with fallback", async () => {
|
|
Object.defineProperty(process, 'platform', { value: 'win32' });
|
|
const { getSecureStorage } = await importFreshSecureStorage();
|
|
const storage = getSecureStorage();
|
|
expect(storage.name).toContain("credential-locker");
|
|
});
|
|
});
|
|
});
|