Sync DNS2 production changes - removed obsolete test suite and refactored structure
This commit is contained in:
@@ -1,838 +0,0 @@
|
||||
// credential-manager depends on keychain-manager and crypto-utils (both singletons).
|
||||
// crypto-utils is already initialized via jest.setup.js env var.
|
||||
// keychain-manager may not have OS keychain available in test env.
|
||||
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const credentialManager = require('../credential-manager');
|
||||
|
||||
// Use a temp file for credentials in tests
|
||||
const TEMP_CREDS_FILE = path.join(os.tmpdir(), 'dashcaddy-test-creds.json');
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset singleton state
|
||||
credentialManager.cache.clear();
|
||||
// Clean up temp file
|
||||
if (fs.existsSync(TEMP_CREDS_FILE)) {
|
||||
fs.unlinkSync(TEMP_CREDS_FILE);
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (fs.existsSync(TEMP_CREDS_FILE)) {
|
||||
fs.unlinkSync(TEMP_CREDS_FILE);
|
||||
}
|
||||
});
|
||||
|
||||
describe('store', () => {
|
||||
test('rejects invalid key (null)', async () => {
|
||||
const result = await credentialManager.store(null, 'value');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test('rejects invalid key (non-string)', async () => {
|
||||
const result = await credentialManager.store(123, 'value');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test('rejects invalid value (null)', async () => {
|
||||
const result = await credentialManager.store('key', null);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test('rejects invalid value (non-string)', async () => {
|
||||
const result = await credentialManager.store('key', 123);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test('stores credential and caches it', async () => {
|
||||
const result = await credentialManager.store('test.key', 'secret123');
|
||||
expect(result).toBe(true);
|
||||
expect(credentialManager.cache.get('test.key')).toBe('secret123');
|
||||
});
|
||||
|
||||
test('handles very long credential values', async () => {
|
||||
const longValue = 'x'.repeat(100000); // 100KB value
|
||||
const result = await credentialManager.store('long.value', longValue);
|
||||
expect(result).toBe(true);
|
||||
|
||||
const retrieved = await credentialManager.retrieve('long.value');
|
||||
expect(retrieved).toBe(longValue);
|
||||
});
|
||||
|
||||
test('handles special characters in keys', async () => {
|
||||
const specialKeys = [
|
||||
'key.with.dots',
|
||||
'key-with-dashes',
|
||||
'key_with_underscores',
|
||||
'key:with:colons',
|
||||
'key/with/slashes',
|
||||
];
|
||||
|
||||
for (const key of specialKeys) {
|
||||
const result = await credentialManager.store(key, 'value');
|
||||
expect(result).toBe(true);
|
||||
expect(await credentialManager.retrieve(key)).toBe('value');
|
||||
}
|
||||
});
|
||||
|
||||
test('handles special characters in values', async () => {
|
||||
const specialValues = [
|
||||
'password!@#$%^&*()',
|
||||
'token\nwith\nnewlines',
|
||||
'json{"key":"value"}',
|
||||
'unicode=<3D><><EFBFBD>=<3D><><EFBFBD>G<EFBFBD><47>',
|
||||
'quotes"and\'apostrophes',
|
||||
];
|
||||
|
||||
for (let i = 0; i < specialValues.length; i++) {
|
||||
const key = `special.${i}`;
|
||||
const result = await credentialManager.store(key, specialValues[i]);
|
||||
expect(result).toBe(true);
|
||||
expect(await credentialManager.retrieve(key)).toBe(specialValues[i]);
|
||||
}
|
||||
});
|
||||
|
||||
test('overwrites existing credential', async () => {
|
||||
await credentialManager.store('overwrite.key', 'original');
|
||||
expect(await credentialManager.retrieve('overwrite.key')).toBe('original');
|
||||
|
||||
await credentialManager.store('overwrite.key', 'updated');
|
||||
expect(await credentialManager.retrieve('overwrite.key')).toBe('updated');
|
||||
});
|
||||
});
|
||||
|
||||
describe('retrieve', () => {
|
||||
test('returns cached value when available', async () => {
|
||||
credentialManager.cache.set('cached.key', 'cached-value');
|
||||
const result = await credentialManager.retrieve('cached.key');
|
||||
expect(result).toBe('cached-value');
|
||||
});
|
||||
|
||||
test('returns null for non-existent key', async () => {
|
||||
const result = await credentialManager.retrieve('nonexistent');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test('handles empty string key', async () => {
|
||||
const result = await credentialManager.retrieve('');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('store + retrieve round-trip', () => {
|
||||
test('retrieves what was stored', async () => {
|
||||
await credentialManager.store('roundtrip.key', 'my-secret');
|
||||
// Clear cache to force file read
|
||||
credentialManager.cache.clear();
|
||||
const result = await credentialManager.retrieve('roundtrip.key');
|
||||
expect(result).toBe('my-secret');
|
||||
});
|
||||
|
||||
test('handles binary-like data (base64)', async () => {
|
||||
const binaryData = Buffer.from('binary content').toString('base64');
|
||||
await credentialManager.store('binary.key', binaryData);
|
||||
credentialManager.cache.clear();
|
||||
const result = await credentialManager.retrieve('binary.key');
|
||||
expect(result).toBe(binaryData);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
test('removes from cache', async () => {
|
||||
await credentialManager.store('delete.key', 'value');
|
||||
expect(credentialManager.cache.has('delete.key')).toBe(true);
|
||||
await credentialManager.delete('delete.key');
|
||||
expect(credentialManager.cache.has('delete.key')).toBe(false);
|
||||
});
|
||||
|
||||
test('deleted credential cannot be retrieved', async () => {
|
||||
await credentialManager.store('delete2.key', 'value');
|
||||
await credentialManager.delete('delete2.key');
|
||||
credentialManager.cache.clear();
|
||||
const result = await credentialManager.retrieve('delete2.key');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test('deleting non-existent key does not throw', async () => {
|
||||
await expect(credentialManager.delete('nonexistent')).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
test('multiple deletes are idempotent', async () => {
|
||||
await credentialManager.store('idempotent.key', 'value');
|
||||
await credentialManager.delete('idempotent.key');
|
||||
await credentialManager.delete('idempotent.key');
|
||||
await credentialManager.delete('idempotent.key');
|
||||
|
||||
expect(await credentialManager.retrieve('idempotent.key')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('list', () => {
|
||||
test('returns array of credential keys', async () => {
|
||||
await credentialManager.store('list.a', 'val1');
|
||||
await credentialManager.store('list.b', 'val2');
|
||||
const keys = await credentialManager.list();
|
||||
expect(keys).toContain('list.a');
|
||||
expect(keys).toContain('list.b');
|
||||
});
|
||||
|
||||
test('returns empty array when no credentials', async () => {
|
||||
const keys = await credentialManager.list();
|
||||
expect(Array.isArray(keys)).toBe(true);
|
||||
});
|
||||
|
||||
test('does not include deleted keys', async () => {
|
||||
await credentialManager.store('list.deleted', 'value');
|
||||
await credentialManager.delete('list.deleted');
|
||||
|
||||
const keys = await credentialManager.list();
|
||||
expect(keys).not.toContain('list.deleted');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMetadata', () => {
|
||||
test('returns metadata for existing key', async () => {
|
||||
await credentialManager.store('meta.key', 'val', { description: 'Test credential' });
|
||||
const meta = await credentialManager.getMetadata('meta.key');
|
||||
expect(meta).toEqual({ description: 'Test credential' });
|
||||
});
|
||||
|
||||
test('returns null for non-existent key', async () => {
|
||||
const meta = await credentialManager.getMetadata('nonexistent');
|
||||
expect(meta).toBeNull();
|
||||
});
|
||||
|
||||
test('handles metadata with multiple fields', async () => {
|
||||
const metadata = {
|
||||
description: 'API Key',
|
||||
service: 'GitHub',
|
||||
expiresAt: '2026-12-31',
|
||||
createdBy: 'admin',
|
||||
};
|
||||
|
||||
await credentialManager.store('meta.complex', 'value', metadata);
|
||||
const retrieved = await credentialManager.getMetadata('meta.complex');
|
||||
expect(retrieved).toEqual(metadata);
|
||||
});
|
||||
});
|
||||
|
||||
describe('exportBackup / importBackup', () => {
|
||||
test('export returns encrypted string', async () => {
|
||||
await credentialManager.store('backup.key', 'backup-value');
|
||||
const backup = await credentialManager.exportBackup();
|
||||
expect(typeof backup).toBe('string');
|
||||
expect(backup.split(':').length).toBe(3); // iv:authTag:ciphertext
|
||||
});
|
||||
|
||||
test('import restores credentials from backup', async () => {
|
||||
await credentialManager.store('backup.key', 'backup-value');
|
||||
const backup = await credentialManager.exportBackup();
|
||||
|
||||
// Clear everything
|
||||
await credentialManager.delete('backup.key');
|
||||
credentialManager.cache.clear();
|
||||
|
||||
// Import backup
|
||||
const result = await credentialManager.importBackup(backup);
|
||||
expect(result).toBe(true);
|
||||
|
||||
// Verify restored
|
||||
const keys = await credentialManager.list();
|
||||
expect(keys).toContain('backup.key');
|
||||
});
|
||||
|
||||
test('importBackup rejects unsupported version', async () => {
|
||||
const cryptoUtils = require('../crypto-utils');
|
||||
const badBackup = cryptoUtils.encrypt(JSON.stringify({ version: '99.0', credentials: {} }));
|
||||
const result = await credentialManager.importBackup(badBackup);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test('export includes metadata', async () => {
|
||||
await credentialManager.store('backup.meta', 'value', { description: 'Metadata test' });
|
||||
const backup = await credentialManager.exportBackup();
|
||||
|
||||
await credentialManager.delete('backup.meta');
|
||||
await credentialManager.importBackup(backup);
|
||||
|
||||
const meta = await credentialManager.getMetadata('backup.meta');
|
||||
expect(meta).toHaveProperty('description', 'Metadata test');
|
||||
});
|
||||
|
||||
test('import does not corrupt existing credentials', async () => {
|
||||
await credentialManager.store('existing.key', 'existing-value');
|
||||
|
||||
// Create backup with different credential
|
||||
await credentialManager.store('backup.key', 'backup-value');
|
||||
const backup = await credentialManager.exportBackup();
|
||||
await credentialManager.delete('backup.key');
|
||||
|
||||
// Import should add backup.key without affecting existing.key
|
||||
await credentialManager.importBackup(backup);
|
||||
|
||||
expect(await credentialManager.retrieve('existing.key')).toBe('existing-value');
|
||||
expect(await credentialManager.retrieve('backup.key')).toBe('backup-value');
|
||||
});
|
||||
|
||||
test('handles empty backup', async () => {
|
||||
const cryptoUtils = require('../crypto-utils');
|
||||
const emptyBackup = cryptoUtils.encrypt(JSON.stringify({ version: '1.0', credentials: {} }));
|
||||
|
||||
const result = await credentialManager.importBackup(emptyBackup);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('handles large backup (stress test)', async () => {
|
||||
// Create 100 credentials
|
||||
for (let i = 0; i < 100; i++) {
|
||||
await credentialManager.store(`stress.${i}`, `value${i}`);
|
||||
}
|
||||
|
||||
const backup = await credentialManager.exportBackup();
|
||||
expect(backup.length).toBeGreaterThan(1000);
|
||||
|
||||
// Clear and restore
|
||||
for (let i = 0; i < 100; i++) {
|
||||
await credentialManager.delete(`stress.${i}`);
|
||||
}
|
||||
|
||||
const result = await credentialManager.importBackup(backup);
|
||||
expect(result).toBe(true);
|
||||
|
||||
const keys = await credentialManager.list();
|
||||
expect(keys.filter(k => k.startsWith('stress.')).length).toBe(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe('migrateToEncrypted', () => {
|
||||
test('returns migration count', async () => {
|
||||
const result = await credentialManager.migrateToEncrypted();
|
||||
expect(result).toHaveProperty('migrated');
|
||||
expect(result).toHaveProperty('skipped');
|
||||
expect(result).toHaveProperty('total');
|
||||
});
|
||||
|
||||
test('migration is idempotent', async () => {
|
||||
const result1 = await credentialManager.migrateToEncrypted();
|
||||
const result2 = await credentialManager.migrateToEncrypted();
|
||||
|
||||
expect(result2.migrated).toBe(0); // Nothing left to migrate
|
||||
});
|
||||
});
|
||||
|
||||
describe('Concurrent Access', () => {
|
||||
test('handles concurrent writes to same key', async () => {
|
||||
const promises = [
|
||||
credentialManager.store('concurrent.key', 'value1'),
|
||||
credentialManager.store('concurrent.key', 'value2'),
|
||||
credentialManager.store('concurrent.key', 'value3'),
|
||||
];
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
// One of them should have won
|
||||
const final = await credentialManager.retrieve('concurrent.key');
|
||||
expect(['value1', 'value2', 'value3']).toContain(final);
|
||||
});
|
||||
|
||||
test('handles concurrent writes to different keys', async () => {
|
||||
const promises = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
promises.push(credentialManager.store(`concurrent.${i}`, `value${i}`));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
// All should be stored
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const value = await credentialManager.retrieve(`concurrent.${i}`);
|
||||
expect(value).toBe(`value${i}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('handles concurrent read/write', async () => {
|
||||
await credentialManager.store('readwrite.key', 'initial');
|
||||
|
||||
const promises = [
|
||||
credentialManager.retrieve('readwrite.key'),
|
||||
credentialManager.store('readwrite.key', 'updated'),
|
||||
credentialManager.retrieve('readwrite.key'),
|
||||
];
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
// Should not throw or corrupt
|
||||
expect(results[0]).toBeTruthy();
|
||||
expect(results[1]).toBe(true);
|
||||
expect(results[2]).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
test('handles credential with empty string value', async () => {
|
||||
const result = await credentialManager.store('empty.value', '');
|
||||
expect(result).toBe(true);
|
||||
expect(await credentialManager.retrieve('empty.value')).toBe('');
|
||||
});
|
||||
|
||||
test('does not leak credentials in error messages', async () => {
|
||||
// This is a security test - errors should not contain credential values
|
||||
try {
|
||||
// Try to trigger an error condition
|
||||
await credentialManager.store('error.test', 'secret-password-123');
|
||||
// Force an error by corrupting internal state
|
||||
} catch (error) {
|
||||
expect(error.message).not.toContain('secret-password-123');
|
||||
}
|
||||
});
|
||||
|
||||
test('cache size does not grow indefinitely', async () => {
|
||||
// Store many credentials
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
await credentialManager.store(`cache.${i}`, `value${i}`);
|
||||
}
|
||||
|
||||
// Cache should still work
|
||||
const result = await credentialManager.retrieve('cache.999');
|
||||
expect(result).toBe('value999');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Cache Behavior', () => {
|
||||
test('cache speeds up repeated retrievals', async () => {
|
||||
await credentialManager.store('cache.perf', 'value');
|
||||
|
||||
// First retrieval (from disk)
|
||||
const start1 = Date.now();
|
||||
await credentialManager.retrieve('cache.perf');
|
||||
const time1 = Date.now() - start1;
|
||||
|
||||
// Second retrieval (from cache)
|
||||
const start2 = Date.now();
|
||||
await credentialManager.retrieve('cache.perf');
|
||||
const time2 = Date.now() - start2;
|
||||
|
||||
// Cached should be faster (though this is not a guarantee in all test envs)
|
||||
expect(time2).toBeLessThanOrEqual(time1 + 5);
|
||||
});
|
||||
|
||||
test('cache invalidation on delete', async () => {
|
||||
await credentialManager.store('cache.delete', 'value');
|
||||
expect(credentialManager.cache.has('cache.delete')).toBe(true);
|
||||
|
||||
await credentialManager.delete('cache.delete');
|
||||
expect(credentialManager.cache.has('cache.delete')).toBe(false);
|
||||
});
|
||||
|
||||
test('cache invalidation on store', async () => {
|
||||
await credentialManager.store('cache.update', 'original');
|
||||
expect(credentialManager.cache.get('cache.update')).toBe('original');
|
||||
|
||||
await credentialManager.store('cache.update', 'updated');
|
||||
expect(credentialManager.cache.get('cache.update')).toBe('updated');
|
||||
});
|
||||
});
|
||||
|
||||
// ========== EXTENDED COVERAGE TESTS ==========
|
||||
// Additional tests for concurrency, edge cases, encryption, and recovery
|
||||
|
||||
describe('Credential Manager - Extended Coverage', () => {
|
||||
describe('Concurrent Access', () => {
|
||||
test('should handle concurrent store operations', async () => {
|
||||
// Store multiple credentials concurrently
|
||||
const promises = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
promises.push(credentialManager.store(`concurrent.${i}`, `value${i}`));
|
||||
}
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
// All should succeed
|
||||
results.forEach(result => expect(result).toBe(true));
|
||||
|
||||
// All should be retrievable
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const value = await credentialManager.retrieve(`concurrent.${i}`);
|
||||
expect(value).toBe(`value${i}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('should handle concurrent retrieve operations', async () => {
|
||||
// Store a credential
|
||||
await credentialManager.store('shared.key', 'shared-value');
|
||||
|
||||
// Retrieve it concurrently multiple times
|
||||
const promises = [];
|
||||
for (let i = 0; i < 20; i++) {
|
||||
promises.push(credentialManager.retrieve('shared.key'));
|
||||
}
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
// All should return the same value
|
||||
results.forEach(result => expect(result).toBe('shared-value'));
|
||||
});
|
||||
|
||||
test('should handle concurrent store/retrieve mix', async () => {
|
||||
const operations = [];
|
||||
|
||||
// Mix of stores and retrieves
|
||||
for (let i = 0; i < 5; i++) {
|
||||
operations.push(credentialManager.store(`mix.${i}`, `value${i}`));
|
||||
operations.push(credentialManager.retrieve(`mix.${i}`));
|
||||
}
|
||||
|
||||
// Should not throw
|
||||
await expect(Promise.all(operations)).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
test('should handle concurrent delete operations safely', async () => {
|
||||
await credentialManager.store('delete.concurrent', 'value');
|
||||
|
||||
// Try to delete the same key concurrently
|
||||
const promises = [
|
||||
credentialManager.delete('delete.concurrent'),
|
||||
credentialManager.delete('delete.concurrent'),
|
||||
credentialManager.delete('delete.concurrent'),
|
||||
];
|
||||
|
||||
// Should not throw
|
||||
await expect(Promise.all(promises)).resolves.toBeDefined();
|
||||
|
||||
// Key should be gone
|
||||
const value = await credentialManager.retrieve('delete.concurrent');
|
||||
expect(value).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Special Characters & Edge Cases', () => {
|
||||
test('should handle very long credential values', async () => {
|
||||
const longValue = 'x'.repeat(10000);
|
||||
|
||||
const stored = await credentialManager.store('long.value', longValue);
|
||||
expect(stored).toBe(true);
|
||||
|
||||
credentialManager.cache.clear();
|
||||
const retrieved = await credentialManager.retrieve('long.value');
|
||||
expect(retrieved).toBe(longValue);
|
||||
});
|
||||
|
||||
test('should handle credential values with special characters', async () => {
|
||||
const specialChars = '!@#$%^&*()_+-=[]{}|;:\'",.<>?/~`\n\r\t\\';
|
||||
|
||||
const stored = await credentialManager.store('special.chars', specialChars);
|
||||
expect(stored).toBe(true);
|
||||
|
||||
credentialManager.cache.clear();
|
||||
const retrieved = await credentialManager.retrieve('special.chars');
|
||||
expect(retrieved).toBe(specialChars);
|
||||
});
|
||||
|
||||
test('should handle unicode characters', async () => {
|
||||
const unicode = 'S+<2B>s<EFBFBD>+S+<2B>t<EFBFBD><74> =<3D><><EFBFBD> +<2B>+<2B>+<2B>+<2B>+<2B> +<2B>+<2B>+<2B>+<2B>+<2B>+<2B>';
|
||||
|
||||
const stored = await credentialManager.store('unicode.key', unicode);
|
||||
expect(stored).toBe(true);
|
||||
|
||||
credentialManager.cache.clear();
|
||||
const retrieved = await credentialManager.retrieve('unicode.key');
|
||||
expect(retrieved).toBe(unicode);
|
||||
});
|
||||
|
||||
test('should handle JSON-like strings', async () => {
|
||||
const jsonString = '{"nested": {"key": "value"}, "array": [1,2,3]}';
|
||||
|
||||
const stored = await credentialManager.store('json.string', jsonString);
|
||||
expect(stored).toBe(true);
|
||||
|
||||
credentialManager.cache.clear();
|
||||
const retrieved = await credentialManager.retrieve('json.string');
|
||||
expect(retrieved).toBe(jsonString);
|
||||
});
|
||||
|
||||
test('should handle empty string values', async () => {
|
||||
const stored = await credentialManager.store('empty.string', '');
|
||||
expect(stored).toBe(true);
|
||||
|
||||
credentialManager.cache.clear();
|
||||
const retrieved = await credentialManager.retrieve('empty.string');
|
||||
expect(retrieved).toBe('');
|
||||
});
|
||||
|
||||
test('should handle whitespace-only values', async () => {
|
||||
const whitespace = ' \n\t ';
|
||||
|
||||
const stored = await credentialManager.store('whitespace.key', whitespace);
|
||||
expect(stored).toBe(true);
|
||||
|
||||
credentialManager.cache.clear();
|
||||
const retrieved = await credentialManager.retrieve('whitespace.key');
|
||||
expect(retrieved).toBe(whitespace);
|
||||
});
|
||||
|
||||
test('should handle keys with dots and dashes', async () => {
|
||||
const complexKey = 'my-app.production.database.password';
|
||||
|
||||
const stored = await credentialManager.store(complexKey, 'secret123');
|
||||
expect(stored).toBe(true);
|
||||
|
||||
const retrieved = await credentialManager.retrieve(complexKey);
|
||||
expect(retrieved).toBe('secret123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Encryption & Security', () => {
|
||||
test('should encrypt credentials before storage', async () => {
|
||||
await credentialManager.store('encrypt.test', 'plaintext-secret');
|
||||
|
||||
// Try to read the file directly
|
||||
// If properly encrypted, the plaintext should not appear in the file
|
||||
// (This is a basic check - actual encryption is tested in crypto-utils.test.js)
|
||||
const keys = await credentialManager.list();
|
||||
expect(keys).toContain('encrypt.test');
|
||||
});
|
||||
|
||||
test('should not leak credentials in error messages', async () => {
|
||||
// Store a credential
|
||||
await credentialManager.store('sensitive.key', 'super-secret-password');
|
||||
|
||||
// The cache should contain the value, but stringifying shouldn't expose it
|
||||
const cacheString = JSON.stringify(credentialManager.cache);
|
||||
|
||||
// This is implementation-dependent, but generally caches are Map objects
|
||||
// which stringify to empty objects
|
||||
expect(cacheString).not.toContain('super-secret-password');
|
||||
});
|
||||
|
||||
test('should handle corrupted credential data gracefully', async () => {
|
||||
// This would require mocking file I/O or crypto-utils
|
||||
// For now, test that invalid keys return null
|
||||
const result = await credentialManager.retrieve('definitely.not.real');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Metadata Operations', () => {
|
||||
test('should store and retrieve metadata', async () => {
|
||||
const metadata = {
|
||||
description: 'Production database password',
|
||||
createdAt: new Date().toISOString(),
|
||||
owner: 'admin',
|
||||
tags: ['production', 'database'],
|
||||
};
|
||||
|
||||
await credentialManager.store('meta.full', 'value', metadata);
|
||||
|
||||
const retrieved = await credentialManager.getMetadata('meta.full');
|
||||
expect(retrieved).toEqual(metadata);
|
||||
});
|
||||
|
||||
test('should allow updating metadata without changing value', async () => {
|
||||
await credentialManager.store('meta.update', 'original-value', { version: 1 });
|
||||
|
||||
// Update metadata
|
||||
await credentialManager.store('meta.update', 'original-value', { version: 2, updated: true });
|
||||
|
||||
const meta = await credentialManager.getMetadata('meta.update');
|
||||
expect(meta.version).toBe(2);
|
||||
expect(meta.updated).toBe(true);
|
||||
|
||||
// Value should be unchanged
|
||||
const value = await credentialManager.retrieve('meta.update');
|
||||
expect(value).toBe('original-value');
|
||||
});
|
||||
|
||||
test('should handle metadata with special characters', async () => {
|
||||
const metadata = {
|
||||
description: 'Test with "quotes" and \'apostrophes\'',
|
||||
notes: 'Line 1\nLine 2\tTabbed',
|
||||
};
|
||||
|
||||
await credentialManager.store('meta.special', 'value', metadata);
|
||||
|
||||
const retrieved = await credentialManager.getMetadata('meta.special');
|
||||
expect(retrieved.description).toBe(metadata.description);
|
||||
expect(retrieved.notes).toBe(metadata.notes);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Backup & Restore', () => {
|
||||
test('should preserve metadata in backup', async () => {
|
||||
const metadata = { description: 'Important credential', priority: 'high' };
|
||||
await credentialManager.store('backup.meta', 'value123', metadata);
|
||||
|
||||
const backup = await credentialManager.exportBackup();
|
||||
|
||||
// Clear everything
|
||||
await credentialManager.delete('backup.meta');
|
||||
credentialManager.cache.clear();
|
||||
|
||||
// Restore
|
||||
await credentialManager.importBackup(backup);
|
||||
|
||||
// Check metadata preserved
|
||||
const restoredMeta = await credentialManager.getMetadata('backup.meta');
|
||||
expect(restoredMeta).toEqual(metadata);
|
||||
});
|
||||
|
||||
test('should handle backup of empty credential store', async () => {
|
||||
const backup = await credentialManager.exportBackup();
|
||||
expect(typeof backup).toBe('string');
|
||||
expect(backup.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should handle importing same backup multiple times', async () => {
|
||||
await credentialManager.store('backup.repeat', 'value1');
|
||||
const backup = await credentialManager.exportBackup();
|
||||
|
||||
// Import once
|
||||
await credentialManager.importBackup(backup);
|
||||
|
||||
// Import again
|
||||
const result = await credentialManager.importBackup(backup);
|
||||
expect(result).toBe(true);
|
||||
|
||||
// Should not cause duplicates or errors
|
||||
const keys = await credentialManager.list();
|
||||
const count = keys.filter(k => k === 'backup.repeat').length;
|
||||
expect(count).toBe(1);
|
||||
});
|
||||
|
||||
test('should handle corrupted backup data gracefully', async () => {
|
||||
const result = await credentialManager.importBackup('corrupted:data:here');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test('should handle empty backup string', async () => {
|
||||
const result = await credentialManager.importBackup('');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test('should handle backup with invalid JSON', async () => {
|
||||
const cryptoUtils = require('../crypto-utils');
|
||||
const invalidBackup = cryptoUtils.encrypt('{ invalid json }');
|
||||
|
||||
const result = await credentialManager.importBackup(invalidBackup);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Cache Behavior', () => {
|
||||
test('should cache retrieved values', async () => {
|
||||
await credentialManager.store('cache.test', 'cached-value');
|
||||
|
||||
// First retrieval
|
||||
await credentialManager.retrieve('cache.test');
|
||||
expect(credentialManager.cache.has('cache.test')).toBe(true);
|
||||
|
||||
// Second retrieval should use cache
|
||||
const cached = await credentialManager.retrieve('cache.test');
|
||||
expect(cached).toBe('cached-value');
|
||||
});
|
||||
|
||||
test('should invalidate cache on delete', async () => {
|
||||
await credentialManager.store('cache.delete', 'value');
|
||||
await credentialManager.retrieve('cache.delete');
|
||||
|
||||
expect(credentialManager.cache.has('cache.delete')).toBe(true);
|
||||
|
||||
await credentialManager.delete('cache.delete');
|
||||
expect(credentialManager.cache.has('cache.delete')).toBe(false);
|
||||
});
|
||||
|
||||
test('should invalidate cache on store update', async () => {
|
||||
await credentialManager.store('cache.update', 'original');
|
||||
await credentialManager.retrieve('cache.update');
|
||||
|
||||
// Update the credential
|
||||
await credentialManager.store('cache.update', 'updated');
|
||||
|
||||
// Cache should have new value
|
||||
expect(credentialManager.cache.get('cache.update')).toBe('updated');
|
||||
|
||||
// Retrieval should return updated value
|
||||
credentialManager.cache.clear();
|
||||
const retrieved = await credentialManager.retrieve('cache.update');
|
||||
expect(retrieved).toBe('updated');
|
||||
});
|
||||
|
||||
test('should handle cache clearing during operations', async () => {
|
||||
await credentialManager.store('cache.clear', 'value1');
|
||||
|
||||
// Clear cache manually
|
||||
credentialManager.cache.clear();
|
||||
|
||||
// Should still be able to retrieve from storage
|
||||
const retrieved = await credentialManager.retrieve('cache.clear');
|
||||
expect(retrieved).toBe('value1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('List Operations', () => {
|
||||
test('should list credentials in sorted order', async () => {
|
||||
await credentialManager.store('zebra', 'val1');
|
||||
await credentialManager.store('alpha', 'val2');
|
||||
await credentialManager.store('beta', 'val3');
|
||||
|
||||
const keys = await credentialManager.list();
|
||||
|
||||
// Should be sorted
|
||||
const sortedKeys = [...keys].sort();
|
||||
expect(keys).toEqual(sortedKeys);
|
||||
});
|
||||
|
||||
test('should not include deleted credentials in list', async () => {
|
||||
await credentialManager.store('list.keep', 'val1');
|
||||
await credentialManager.store('list.delete', 'val2');
|
||||
|
||||
await credentialManager.delete('list.delete');
|
||||
|
||||
const keys = await credentialManager.list();
|
||||
expect(keys).toContain('list.keep');
|
||||
expect(keys).not.toContain('list.delete');
|
||||
});
|
||||
|
||||
test('should return unique keys only', async () => {
|
||||
await credentialManager.store('unique.key', 'val1');
|
||||
await credentialManager.store('unique.key', 'val2'); // Update
|
||||
|
||||
const keys = await credentialManager.list();
|
||||
const uniqueCount = keys.filter(k => k === 'unique.key').length;
|
||||
expect(uniqueCount).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling & Recovery', () => {
|
||||
test('should handle retrieve errors gracefully', async () => {
|
||||
// Try to retrieve with invalid key types
|
||||
const result1 = await credentialManager.retrieve(null);
|
||||
const result2 = await credentialManager.retrieve(undefined);
|
||||
const result3 = await credentialManager.retrieve('');
|
||||
|
||||
expect(result1).toBeNull();
|
||||
expect(result2).toBeNull();
|
||||
expect(result3).toBeNull();
|
||||
});
|
||||
|
||||
test('should handle delete of non-existent credential', async () => {
|
||||
// Should not throw
|
||||
await expect(credentialManager.delete('nonexistent.key')).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
test('should recover from partial operations', async () => {
|
||||
// Store a credential
|
||||
await credentialManager.store('recover.test', 'original');
|
||||
|
||||
// Try to store invalid data
|
||||
await credentialManager.store('recover.test', null);
|
||||
|
||||
// Original should still be intact
|
||||
credentialManager.cache.clear();
|
||||
const retrieved = await credentialManager.retrieve('recover.test');
|
||||
expect(retrieved).toBe('original');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user