Initial commit: DashCaddy v1.0
Full codebase including API server (32 modules + routes), dashboard frontend, DashCA certificate distribution, installer script, and deployment skills.
This commit is contained in:
165
dashcaddy-api/__tests__/credential-manager.test.js
Normal file
165
dashcaddy-api/__tests__/credential-manager.test.js
Normal file
@@ -0,0 +1,165 @@
|
||||
// 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');
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('migrateToEncrypted', () => {
|
||||
test('returns migration count', async () => {
|
||||
const result = await credentialManager.migrateToEncrypted();
|
||||
expect(result).toHaveProperty('migrated');
|
||||
expect(result).toHaveProperty('skipped');
|
||||
expect(result).toHaveProperty('total');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user