332 lines
10 KiB
TypeScript
332 lines
10 KiB
TypeScript
// Mock safeStorage implementation
|
|
const mockSafeStorage = {
|
|
isEncryptionAvailable: jest.fn(() => true),
|
|
encryptString: jest.fn((data: string) => Buffer.from(`encrypted:${data}`)),
|
|
decryptString: jest.fn((buffer: Buffer) => buffer.toString().replace('encrypted:', ''))
|
|
};
|
|
|
|
// Setup window.require mock before importing the module
|
|
const mockWindowRequire = jest.fn((module: string) => {
|
|
if (module === 'electron') {
|
|
return { safeStorage: mockSafeStorage };
|
|
}
|
|
throw new Error(`Module not found: ${module}`);
|
|
});
|
|
|
|
// Create mock window object for Node environment
|
|
const mockWindow: Window & { require?: unknown } = {
|
|
require: mockWindowRequire
|
|
} as unknown as Window & { require?: unknown };
|
|
|
|
// Store original global window
|
|
const originalWindow = (globalThis as unknown as { window?: unknown }).window;
|
|
|
|
// Set up window.require before tests run
|
|
beforeAll(() => {
|
|
(globalThis as unknown as { window: typeof mockWindow }).window = mockWindow;
|
|
});
|
|
|
|
// Clean up after all tests
|
|
afterAll(() => {
|
|
if (originalWindow === undefined) {
|
|
delete (globalThis as unknown as { window?: unknown }).window;
|
|
} else {
|
|
(globalThis as unknown as { window: typeof originalWindow }).window = originalWindow;
|
|
}
|
|
});
|
|
|
|
// Import after mock is set up - use require to ensure module loads after mock
|
|
let encryptApiKey: typeof import('../src/utils/encryption-utils').encryptApiKey;
|
|
let decryptApiKey: typeof import('../src/utils/encryption-utils').decryptApiKey;
|
|
let isEncryptionAvailable: typeof import('../src/utils/encryption-utils').isEncryptionAvailable;
|
|
|
|
beforeAll(() => {
|
|
// Reset modules to ensure fresh load with mock
|
|
jest.resetModules();
|
|
const encryptionUtils = require('../src/utils/encryption-utils');
|
|
encryptApiKey = encryptionUtils.encryptApiKey;
|
|
decryptApiKey = encryptionUtils.decryptApiKey;
|
|
isEncryptionAvailable = encryptionUtils.isEncryptionAvailable;
|
|
});
|
|
|
|
describe('Encryption Utils', () => {
|
|
beforeEach(() => {
|
|
// Reset mock implementations before each test
|
|
mockSafeStorage.isEncryptionAvailable.mockReturnValue(true);
|
|
mockSafeStorage.encryptString.mockImplementation((data: string) => Buffer.from(`encrypted:${data}`));
|
|
mockSafeStorage.decryptString.mockImplementation((buffer: Buffer) => buffer.toString().replace('encrypted:', ''));
|
|
mockWindowRequire.mockClear();
|
|
});
|
|
|
|
describe('encryptApiKey', () => {
|
|
it('should encrypt API key when encryption is available', () => {
|
|
const apiKey = 'test-api-key-12345';
|
|
const encrypted = encryptApiKey(apiKey);
|
|
|
|
expect(encrypted).toMatch(/^encrypted:/);
|
|
expect(encrypted).not.toContain('test-api-key-12345');
|
|
});
|
|
|
|
it('should return plaintext when encryption is not available', () => {
|
|
// Need to reload module with different mock behavior
|
|
jest.resetModules();
|
|
const mockStorage = {
|
|
isEncryptionAvailable: jest.fn(() => false),
|
|
encryptString: jest.fn(),
|
|
decryptString: jest.fn()
|
|
};
|
|
mockWindow.require = jest.fn(() => ({ safeStorage: mockStorage }));
|
|
|
|
const { encryptApiKey: encrypt } = require('../src/utils/encryption-utils');
|
|
const apiKey = 'test-api-key-12345';
|
|
const result = encrypt(apiKey);
|
|
|
|
expect(result).toBe(apiKey);
|
|
|
|
// Restore original mock
|
|
mockWindow.require = mockWindowRequire;
|
|
});
|
|
|
|
it('should handle empty string', () => {
|
|
const result = encryptApiKey('');
|
|
expect(result).toBe('');
|
|
});
|
|
});
|
|
|
|
describe('decryptApiKey', () => {
|
|
it('should decrypt encrypted API key', () => {
|
|
const apiKey = 'test-api-key-12345';
|
|
const encrypted = encryptApiKey(apiKey);
|
|
const decrypted = decryptApiKey(encrypted);
|
|
|
|
expect(decrypted).toBe(apiKey);
|
|
});
|
|
|
|
it('should return plaintext if not encrypted format', () => {
|
|
const plaintext = 'plain-api-key';
|
|
const result = decryptApiKey(plaintext);
|
|
|
|
expect(result).toBe(plaintext);
|
|
});
|
|
|
|
it('should handle empty string', () => {
|
|
const result = decryptApiKey('');
|
|
expect(result).toBe('');
|
|
});
|
|
});
|
|
|
|
describe('round-trip encryption', () => {
|
|
it('should successfully encrypt and decrypt', () => {
|
|
const original = 'my-secret-api-key-abc123';
|
|
const encrypted = encryptApiKey(original);
|
|
const decrypted = decryptApiKey(encrypted);
|
|
|
|
expect(decrypted).toBe(original);
|
|
expect(encrypted).not.toBe(original);
|
|
});
|
|
});
|
|
|
|
describe('error handling', () => {
|
|
it('should handle encryption errors and fallback to plaintext', () => {
|
|
// Reload module with error-throwing mock
|
|
jest.resetModules();
|
|
const mockStorage = {
|
|
isEncryptionAvailable: jest.fn(() => true),
|
|
encryptString: jest.fn(() => {
|
|
throw new Error('Encryption failed');
|
|
}),
|
|
decryptString: jest.fn()
|
|
};
|
|
mockWindow.require = jest.fn(() => ({ safeStorage: mockStorage }));
|
|
|
|
const { encryptApiKey: encrypt } = require('../src/utils/encryption-utils');
|
|
const apiKey = 'test-api-key-12345';
|
|
const result = encrypt(apiKey);
|
|
|
|
expect(result).toBe(apiKey); // Should return plaintext on error
|
|
|
|
// Restore original mock
|
|
mockWindow.require = mockWindowRequire;
|
|
});
|
|
|
|
it('should throw error when decryption fails', () => {
|
|
// Reload module with error-throwing mock
|
|
jest.resetModules();
|
|
const mockStorage = {
|
|
isEncryptionAvailable: jest.fn(() => true),
|
|
encryptString: jest.fn((data: string) => Buffer.from(`encrypted:${data}`)),
|
|
decryptString: jest.fn(() => {
|
|
throw new Error('Decryption failed');
|
|
})
|
|
};
|
|
mockWindow.require = jest.fn(() => ({ safeStorage: mockStorage }));
|
|
|
|
const { decryptApiKey: decrypt } = require('../src/utils/encryption-utils');
|
|
const encrypted = 'encrypted:aW52YWxpZA=='; // Invalid encrypted data
|
|
|
|
expect(() => decrypt(encrypted)).toThrow('Failed to decrypt API key');
|
|
|
|
// Restore original mock
|
|
mockWindow.require = mockWindowRequire;
|
|
});
|
|
});
|
|
|
|
describe('isEncryptionAvailable', () => {
|
|
it('should return true when encryption is available', () => {
|
|
jest.resetModules();
|
|
const mockStorage = {
|
|
isEncryptionAvailable: jest.fn(() => true),
|
|
encryptString: jest.fn(),
|
|
decryptString: jest.fn()
|
|
};
|
|
mockWindow.require = jest.fn(() => ({ safeStorage: mockStorage }));
|
|
|
|
const { isEncryptionAvailable: checkAvail } = require('../src/utils/encryption-utils');
|
|
expect(checkAvail()).toBe(true);
|
|
|
|
// Restore
|
|
mockWindow.require = mockWindowRequire;
|
|
});
|
|
|
|
it('should return false when encryption is not available', () => {
|
|
jest.resetModules();
|
|
const mockStorage = {
|
|
isEncryptionAvailable: jest.fn(() => false),
|
|
encryptString: jest.fn(),
|
|
decryptString: jest.fn()
|
|
};
|
|
mockWindow.require = jest.fn(() => ({ safeStorage: mockStorage }));
|
|
|
|
const { isEncryptionAvailable: checkAvail } = require('../src/utils/encryption-utils');
|
|
expect(checkAvail()).toBe(false);
|
|
|
|
// Restore
|
|
mockWindow.require = mockWindowRequire;
|
|
});
|
|
|
|
it('should return false when safeStorage is null', () => {
|
|
jest.resetModules();
|
|
mockWindow.require = jest.fn(() => ({ safeStorage: null }));
|
|
|
|
const { isEncryptionAvailable: checkAvail } = require('../src/utils/encryption-utils');
|
|
expect(checkAvail()).toBe(false);
|
|
|
|
// Restore original mock
|
|
mockWindow.require = mockWindowRequire;
|
|
});
|
|
|
|
it('should return false when isEncryptionAvailable method is missing', () => {
|
|
jest.resetModules();
|
|
const mockStorage = {
|
|
// Missing isEncryptionAvailable method
|
|
encryptString: jest.fn(),
|
|
decryptString: jest.fn()
|
|
};
|
|
mockWindow.require = jest.fn(() => ({ safeStorage: mockStorage }));
|
|
|
|
const { isEncryptionAvailable: checkAvail } = require('../src/utils/encryption-utils');
|
|
expect(checkAvail()).toBe(false);
|
|
|
|
// Restore
|
|
mockWindow.require = mockWindowRequire;
|
|
});
|
|
});
|
|
|
|
describe('Platform Fallback Scenarios', () => {
|
|
beforeEach(() => {
|
|
jest.resetModules();
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Restore mock after each test
|
|
mockWindow.require = mockWindowRequire;
|
|
});
|
|
|
|
it('should handle electron module not being available', () => {
|
|
// Mock require to throw when loading electron
|
|
mockWindow.require = jest.fn(() => {
|
|
throw new Error('Electron not available');
|
|
});
|
|
|
|
// This should use the console.warn fallback
|
|
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|
|
|
// Load module with electron unavailable
|
|
const { encryptApiKey: encrypt, isEncryptionAvailable: checkAvail } = require('../src/utils/encryption-utils');
|
|
|
|
expect(checkAvail()).toBe(false);
|
|
|
|
const apiKey = 'test-key';
|
|
const result = encrypt(apiKey);
|
|
|
|
// Should return plaintext when electron is unavailable
|
|
expect(result).toBe(apiKey);
|
|
|
|
consoleSpy.mockRestore();
|
|
});
|
|
|
|
it('should handle decryption when safeStorage is null', () => {
|
|
mockWindow.require = jest.fn(() => ({ safeStorage: null }));
|
|
|
|
const { decryptApiKey: decrypt } = require('../src/utils/encryption-utils');
|
|
|
|
const encrypted = 'encrypted:aW52YWxpZA==';
|
|
|
|
expect(() => decrypt(encrypted)).toThrow('Failed to decrypt API key');
|
|
});
|
|
|
|
it('should log warning when encryption not available on first load', () => {
|
|
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|
|
|
mockWindow.require = jest.fn(() => {
|
|
throw new Error('Module not found');
|
|
});
|
|
|
|
// Require the module to trigger the warning
|
|
require('../src/utils/encryption-utils');
|
|
|
|
// Warning should be logged during module initialization
|
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|
expect.stringContaining('Electron safeStorage not available')
|
|
);
|
|
|
|
consoleSpy.mockRestore();
|
|
});
|
|
|
|
it('should gracefully handle plaintext keys when encryption unavailable', () => {
|
|
mockWindow.require = jest.fn(() => ({ safeStorage: null }));
|
|
|
|
const { encryptApiKey: encrypt, decryptApiKey: decrypt } = require('../src/utils/encryption-utils');
|
|
|
|
const apiKey = 'plain-api-key';
|
|
|
|
// Encrypt should return plaintext
|
|
const encrypted = encrypt(apiKey);
|
|
expect(encrypted).toBe(apiKey);
|
|
|
|
// Decrypt plaintext should return as-is
|
|
const decrypted = decrypt(apiKey);
|
|
expect(decrypted).toBe(apiKey);
|
|
});
|
|
|
|
it('should warn when falling back to plaintext storage', () => {
|
|
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|
|
|
const mockStorage = {
|
|
isEncryptionAvailable: jest.fn(() => false)
|
|
};
|
|
mockWindow.require = jest.fn(() => ({ safeStorage: mockStorage }));
|
|
|
|
const { encryptApiKey: encrypt } = require('../src/utils/encryption-utils');
|
|
|
|
encrypt('test-key');
|
|
|
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|
expect.stringContaining('Encryption not available')
|
|
);
|
|
|
|
consoleSpy.mockRestore();
|
|
});
|
|
});
|
|
});
|