Full codebase including API server (32 modules + routes), dashboard frontend, DashCA certificate distribution, installer script, and deployment skills.
166 lines
5.5 KiB
JavaScript
166 lines
5.5 KiB
JavaScript
// 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');
|
|
});
|
|
});
|