trojanhorse-js
Version:
A comprehensive JavaScript library for fetching, managing, and analyzing global threat intelligence from multiple open-source feeds and security news sources. Unlike its mythological namesake, this Trojan protects your digital fortress.
516 lines (403 loc) • 17.7 kB
text/typescript
/**
* @jest-environment node
*/
import { jest } from '@jest/globals';
import { SecureStorage } from '../SecureStorage';
import { TestUtils, SecurityTestUtils } from '../../../tests/setup';
import type { ThreatIndicator, StorageConfig } from '../../types';
// Mock IndexedDB for Node.js testing environment
const mockIndexedDB = {
open: jest.fn(),
deleteDatabase: jest.fn()
};
global.indexedDB = mockIndexedDB as any;
describe('SecureStorage Module', () => {
let secureStorage: SecureStorage;
let config: StorageConfig;
let testData: ThreatIndicator[];
beforeEach(async () => {
config = {
dbName: 'test-trojanhorse-db',
encryptionKey: TestUtils.generateTestApiKey(),
maxSizeBytes: 10 * 1024 * 1024, // 10MB
defaultTTL: 24 * 60 * 60 * 1000 // 24 hours
};
testData = [
TestUtils.generateTestThreatIndicator({ type: 'domain', value: 'test1.com' }),
TestUtils.generateTestThreatIndicator({ type: 'ip', value: '1.2.3.4' }),
TestUtils.generateTestThreatIndicator({ type: 'url', value: 'http://test.com/malware' })
];
// Mock successful DB operations
const mockDB = {
transaction: jest.fn().mockReturnValue({
objectStore: jest.fn().mockReturnValue({
add: jest.fn().mockResolvedValue(undefined),
get: jest.fn().mockResolvedValue(undefined),
put: jest.fn().mockResolvedValue(undefined),
delete: jest.fn().mockResolvedValue(undefined),
clear: jest.fn().mockResolvedValue(undefined),
getAll: jest.fn().mockResolvedValue([]),
count: jest.fn().mockResolvedValue(0)
})
}),
createObjectStore: jest.fn(),
close: jest.fn()
};
mockIndexedDB.open.mockResolvedValue({
result: mockDB,
onsuccess: null,
onerror: null
});
secureStorage = new SecureStorage(config);
});
afterEach(async () => {
if (secureStorage) {
await secureStorage.clear();
}
jest.clearAllMocks();
TestUtils.secureCleanup([config, testData]);
});
describe('Initialization', () => {
test('should initialize with valid configuration', async () => {
await secureStorage.initialize();
expect(secureStorage.isInitialized()).toBe(true);
expect(mockIndexedDB.open).toHaveBeenCalledWith(config.dbName, expect.any(Number));
});
test('should validate configuration parameters', () => {
const invalidConfigs = [
{ ...config, dbName: '' },
{ ...config, encryptionKey: '' },
{ ...config, maxSizeBytes: -1 },
{ ...config, defaultTTL: -1 }
];
invalidConfigs.forEach(invalidConfig => {
expect(() => new SecureStorage(invalidConfig)).toThrow(/invalid|configuration/i);
});
});
test('should handle database creation errors', async () => {
mockIndexedDB.open.mockRejectedValueOnce(new Error('DB Error'));
await expect(secureStorage.initialize()).rejects.toThrow(/db error/i);
});
test('should upgrade database schema when needed', async () => {
const upgradeMock = jest.fn();
mockIndexedDB.open.mockResolvedValueOnce({
result: {
createObjectStore: upgradeMock,
transaction: jest.fn(),
close: jest.fn()
},
onupgradeneeded: upgradeMock
});
await secureStorage.initialize();
// Should handle schema upgrades
expect(secureStorage.isInitialized()).toBe(true);
});
});
describe('Data Storage and Retrieval', () => {
beforeEach(async () => {
await secureStorage.initialize();
});
test('should store and retrieve data successfully', async () => {
const key = 'test-threats';
const stored = await secureStorage.store(key, testData);
expect(stored).toBe(true);
const retrieved = await secureStorage.get<ThreatIndicator[]>(key);
expect(retrieved).toEqual(testData);
});
test('should handle encryption/decryption transparently', async () => {
const key = 'encrypted-data';
const sensitiveData = { apiKey: TestUtils.generateTestApiKey() };
await secureStorage.store(key, sensitiveData);
const retrieved = await secureStorage.get(key);
expect(retrieved).toEqual(sensitiveData);
});
test('should return null for non-existent keys', async () => {
const result = await secureStorage.get('non-existent-key');
expect(result).toBeNull();
});
test('should handle TTL expiration', async () => {
const shortTTL = 100; // 100ms
const key = 'short-lived-data';
await secureStorage.store(key, testData, { ttl: shortTTL });
// Should exist immediately
let retrieved = await secureStorage.get(key);
expect(retrieved).toEqual(testData);
// Wait for expiration
await TestUtils.wait(shortTTL + 50);
// Should be expired and return null
retrieved = await secureStorage.get(key);
expect(retrieved).toBeNull();
});
test('should support custom tags for organization', async () => {
const tags = ['threat-intel', 'urlhaus', 'high-confidence'];
await secureStorage.store('tagged-data', testData, { tags });
const taggedItems = await secureStorage.getByTags(['urlhaus']);
expect(taggedItems.length).toBeGreaterThan(0);
});
test('should handle large data sets efficiently', async () => {
const largeDataSet = Array(1000).fill(0).map((_, i) =>
TestUtils.generateTestThreatIndicator({ value: `large-test-${i}.com` })
);
const startTime = Date.now();
await secureStorage.store('large-dataset', largeDataSet);
const retrieved = await secureStorage.get('large-dataset');
const endTime = Date.now();
expect(retrieved).toEqual(largeDataSet);
expect(endTime - startTime).toBeLessThan(5000); // Should complete within 5 seconds
});
});
describe('Data Modification and Deletion', () => {
beforeEach(async () => {
await secureStorage.initialize();
await secureStorage.store('test-data', testData);
});
test('should update existing data', async () => {
const updatedData = [...testData, TestUtils.generateTestThreatIndicator()];
const updated = await secureStorage.update('test-data', updatedData);
expect(updated).toBe(true);
const retrieved = await secureStorage.get('test-data');
expect(retrieved).toEqual(updatedData);
});
test('should delete data successfully', async () => {
const deleted = await secureStorage.deleteData('test-data');
expect(deleted).toBe(true);
const retrieved = await secureStorage.get('test-data');
expect(retrieved).toBeNull();
});
test('should handle deletion of non-existent keys', async () => {
const deleted = await secureStorage.deleteData('non-existent');
expect(deleted).toBe(false);
});
test('should clear all data', async () => {
await secureStorage.store('data1', testData);
await secureStorage.store('data2', testData);
await secureStorage.clear();
const data1 = await secureStorage.get('data1');
const data2 = await secureStorage.get('data2');
expect(data1).toBeNull();
expect(data2).toBeNull();
});
});
describe('Security Features', () => {
beforeEach(async () => {
await secureStorage.initialize();
});
test('should encrypt data at rest', async () => {
const sensitiveData = { secret: TestUtils.generateTestApiKey() };
await secureStorage.store('sensitive', sensitiveData);
// Check that raw storage doesn't contain plaintext
// This would require access to the underlying IndexedDB, which is mocked
// In a real implementation, you'd verify encryption by checking raw data
const retrieved = await secureStorage.get('sensitive');
expect(retrieved).toEqual(sensitiveData);
});
test('should be resistant to timing attacks', async () => {
const validKey = 'valid-key';
const invalidKey = 'invalid-key';
await secureStorage.store(validKey, testData);
const isTimingAttackResistant = await SecurityTestUtils.testTimingAttack(
async (key: string) => {
const result = await secureStorage.get(key);
return result !== null;
},
validKey,
invalidKey
);
expect(isTimingAttackResistant).toBe(true);
});
test('should clean up sensitive data from memory', () => {
const sensitiveKey = TestUtils.generateTestApiKey();
const isMemoryClean = SecurityTestUtils.testMemoryCleanup(() => {
// Simulate storage operations with sensitive data
const tempData = { key: sensitiveKey };
TestUtils.secureCleanup(tempData);
});
expect(isMemoryClean).toBe(true);
});
test('should validate data integrity', async () => {
const key = 'integrity-test';
await secureStorage.store(key, testData);
// Simulate data corruption (in real implementation)
// Here we just verify the integrity check mechanism exists
const retrieved = await secureStorage.get(key);
expect(retrieved).toEqual(testData);
});
test('should handle encryption key rotation', async () => {
const key = 'rotation-test';
await secureStorage.store(key, testData);
const newEncryptionKey = TestUtils.generateTestApiKey();
await secureStorage.rotateEncryptionKey(newEncryptionKey);
// Data should still be accessible after key rotation
const retrieved = await secureStorage.get(key);
expect(retrieved).toEqual(testData);
});
});
describe('Storage Management', () => {
beforeEach(async () => {
await secureStorage.initialize();
});
test('should track storage usage', async () => {
await secureStorage.store('usage-test', testData);
const quota = await secureStorage.getStorageQuota();
expect(quota).toBeDefined();
expect(quota.used).toBeGreaterThan(0);
expect(quota.available).toBeGreaterThan(0);
expect(quota.total).toBeGreaterThan(0);
expect(quota.percentage).toBeGreaterThanOrEqual(0);
expect(quota.percentage).toBeLessThanOrEqual(100);
});
test('should enforce storage limits', async () => {
const smallStorage = new SecureStorage({
...config,
maxSizeBytes: 1024 // Very small limit
});
await smallStorage.initialize();
const largeData = Array(1000).fill(0).map(() =>
TestUtils.generateTestThreatIndicator()
);
await expect(smallStorage.store('large-data', largeData))
.rejects.toThrow(/storage limit|quota/i);
});
test('should clean up expired entries automatically', async () => {
const shortTTL = 100;
await secureStorage.store('expired1', testData, { ttl: shortTTL });
await secureStorage.store('expired2', testData, { ttl: shortTTL });
await secureStorage.store('permanent', testData); // No TTL
await TestUtils.wait(shortTTL + 50);
const cleanedCount = await secureStorage.cleanupExpired();
expect(cleanedCount).toBe(2);
// Permanent data should remain
const permanent = await secureStorage.get('permanent');
expect(permanent).toEqual(testData);
});
test('should provide storage statistics', async () => {
await secureStorage.store('stats1', testData, { tags: ['test'] });
await secureStorage.store('stats2', testData, { tags: ['test', 'demo'] });
const quota = await secureStorage.getStorageQuota();
expect(quota.totalEntries).toBeGreaterThanOrEqual(2);
expect(quota.tagStats).toBeDefined();
expect(quota.tagStats.test).toBe(2);
expect(quota.tagStats.demo).toBe(1);
});
});
describe('Advanced Querying', () => {
beforeEach(async () => {
await secureStorage.initialize();
// Store test data with various tags and metadata
await secureStorage.store('threats1', testData, {
tags: ['malware', 'high-confidence']
});
await secureStorage.store('threats2', testData, {
tags: ['phishing', 'medium-confidence']
});
await secureStorage.store('threats3', testData, {
tags: ['malware', 'low-confidence']
});
});
test('should query by tags', async () => {
const malwareThreats = await secureStorage.getByTags(['malware']);
expect(malwareThreats.length).toBe(2);
const highConfidence = await secureStorage.getByTags(['high-confidence']);
expect(highConfidence.length).toBe(1);
});
test('should support complex tag queries', async () => {
// AND query (must have all tags)
const results = await secureStorage.getByTags(['malware', 'high-confidence'], 'AND');
expect(results.length).toBe(1);
// OR query (must have any tag)
const orResults = await secureStorage.getByTags(['phishing', 'high-confidence'], 'OR');
expect(orResults.length).toBe(2);
});
test('should query by time range', async () => {
const now = new Date();
const hourAgo = new Date(now.getTime() - 60 * 60 * 1000);
const recentEntries = await secureStorage.getByTimeRange(hourAgo, now);
expect(recentEntries.length).toBeGreaterThan(0);
});
test('should support pagination', async () => {
// Store more data for pagination testing
for (let i = 0; i < 20; i++) {
await secureStorage.store(`page-test-${i}`, testData);
}
const page1 = await secureStorage.getByTags(['test'], 'OR', { limit: 5, offset: 0 });
const page2 = await secureStorage.getByTags(['test'], 'OR', { limit: 5, offset: 5 });
expect(page1.length).toBe(5);
expect(page2.length).toBe(5);
expect(page1[0].key).not.toBe(page2[0].key);
});
});
describe('Performance and Optimization', () => {
beforeEach(async () => {
await secureStorage.initialize();
});
test('should handle concurrent operations', async () => {
const operations = Array(20).fill(0).map((_, i) =>
secureStorage.store(`concurrent-${i}`, testData)
);
const results = await Promise.all(operations);
// All operations should succeed
results.forEach(result => {
expect(result).toBe(true);
});
});
test('should implement caching for frequently accessed data', async () => {
const key = 'cached-data';
await secureStorage.store(key, testData);
// First access
const start1 = Date.now();
await secureStorage.get(key);
const end1 = Date.now();
// Second access (should be faster due to caching)
const start2 = Date.now();
await secureStorage.get(key);
const end2 = Date.now();
// Second access should be significantly faster
expect(end2 - start2).toBeLessThan(end1 - start1);
});
test('should batch operations for efficiency', async () => {
const batchData = Array(100).fill(0).map((_, i) => ({
key: `batch-${i}`,
value: testData
}));
const startTime = Date.now();
await secureStorage.batchStore(batchData);
const endTime = Date.now();
// Batch operation should be faster than individual operations
expect(endTime - startTime).toBeLessThan(5000);
// Verify all data was stored
for (let i = 0; i < 10; i++) { // Check subset
const retrieved = await secureStorage.get(`batch-${i}`);
expect(retrieved).toEqual(testData);
}
});
});
describe('Error Handling and Recovery', () => {
test('should handle database connection failures', async () => {
mockIndexedDB.open.mockRejectedValue(new Error('Connection failed'));
await expect(secureStorage.initialize()).rejects.toThrow(/connection/i);
});
test('should recover from corrupted data', async () => {
await secureStorage.initialize();
// Mock corrupted data scenario
const mockStore = {
get: jest.fn().mockRejectedValue(new Error('Data corrupted'))
};
// Should handle corruption gracefully
await expect(secureStorage.get('corrupted-key')).rejects.toThrow(/corrupt/i);
});
test('should handle quota exceeded errors', async () => {
const quotaError = new Error('QuotaExceededError');
quotaError.name = 'QuotaExceededError';
mockIndexedDB.open.mockRejectedValue(quotaError);
await expect(secureStorage.initialize()).rejects.toThrow(/quota/i);
});
test('should provide detailed error information', async () => {
try {
await secureStorage.get('test-error');
} catch (error) {
expect(error.message).toMatch(/storage|error/i);
expect(error.code).toBeDefined();
}
});
});
});