test: expand credential-manager edge case coverage

This commit is contained in:
2026-03-22 02:37:32 -07:00
parent 6775dc154b
commit 41a0cdee7e

View File

@@ -52,6 +52,56 @@ describe('store', () => {
expect(result).toBe(true); expect(result).toBe(true);
expect(credentialManager.cache.get('test.key')).toBe('secret123'); 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=ƒöÉ=ƒöæG£à',
'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', () => { describe('retrieve', () => {
@@ -65,6 +115,11 @@ describe('retrieve', () => {
const result = await credentialManager.retrieve('nonexistent'); const result = await credentialManager.retrieve('nonexistent');
expect(result).toBeNull(); expect(result).toBeNull();
}); });
test('handles empty string key', async () => {
const result = await credentialManager.retrieve('');
expect(result).toBeNull();
});
}); });
describe('store + retrieve round-trip', () => { describe('store + retrieve round-trip', () => {
@@ -75,6 +130,14 @@ describe('store + retrieve round-trip', () => {
const result = await credentialManager.retrieve('roundtrip.key'); const result = await credentialManager.retrieve('roundtrip.key');
expect(result).toBe('my-secret'); 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', () => { describe('delete', () => {
@@ -92,6 +155,19 @@ describe('delete', () => {
const result = await credentialManager.retrieve('delete2.key'); const result = await credentialManager.retrieve('delete2.key');
expect(result).toBeNull(); 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', () => { describe('list', () => {
@@ -107,6 +183,14 @@ describe('list', () => {
const keys = await credentialManager.list(); const keys = await credentialManager.list();
expect(Array.isArray(keys)).toBe(true); 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', () => { describe('getMetadata', () => {
@@ -120,6 +204,19 @@ describe('getMetadata', () => {
const meta = await credentialManager.getMetadata('nonexistent'); const meta = await credentialManager.getMetadata('nonexistent');
expect(meta).toBeNull(); 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', () => { describe('exportBackup / importBackup', () => {
@@ -153,6 +250,61 @@ describe('exportBackup / importBackup', () => {
const result = await credentialManager.importBackup(badBackup); const result = await credentialManager.importBackup(badBackup);
expect(result).toBe(false); 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', () => { describe('migrateToEncrypted', () => {
@@ -162,4 +314,525 @@ describe('migrateToEncrypted', () => {
expect(result).toHaveProperty('skipped'); expect(result).toHaveProperty('skipped');
expect(result).toHaveProperty('total'); 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+ásÑ+S+ûtòî =ƒÜÇ +à+¦+¡+¿+º +º+ä+¦+º+ä+à';
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');
});
});
}); });