ba-auth_keystore_vault/tests/VaultKeyst.test.ts
Alan Bridgeman e0747f5405
All checks were successful
Publish to Private NPM Registry / publish (push) Successful in 36s
Initial Commit
2026-02-18 08:55:30 -06:00

261 lines
No EOL
12 KiB
TypeScript

import { jest, describe, it, expect, beforeEach, afterEach } from '@jest/globals';
import type * as NodeJose from 'node-jose';
import type vault from 'node-vault';
import type { VaultCredentials as VaultCredentialsType } from '../src/VaultCredentials.js';
import type { VaultKeys as VaultKeysType } from '../src/VaultKeys.js';
import type { VaultKeystore as VaultKeystoreType } from '../src/VaultKeystore.js';
type JestVaultClient = Partial<vault.client> & {
approleLogin: jest.Mock<(options?: vault.Option | undefined) => Promise<any>>,
write: jest.Mock<(path: string, data: any, requestOptions?: vault.Option | undefined) => Promise<any>>,
read: jest.Mock<(path: string, requestOptions?: vault.Option | undefined) => Promise<any>>,
list: jest.Mock<(path: string, requestOptions?: vault.Option | undefined) => Promise<any>>,
token: string
};
// Mocks
jest.unstable_mockModule('node-vault', () => ({
'default': jest.fn<(options?: vault.VaultOptions | undefined) => JestVaultClient>().mockReturnValue({
approleLogin: jest.fn<(options?: vault.Option | undefined) => Promise<any>>().mockResolvedValue({ auth: { client_token: 'fake-token' } }),
write: jest.fn<(path: string, data: any, requestOptions?: vault.Option | undefined) => Promise<any>>().mockResolvedValue({}),
read: jest.fn<(path: string, requestOptions?: vault.Option | undefined) => Promise<any>>(),
list: jest.fn<(path: string, requestOptions?: vault.Option | undefined) => Promise<any>>(),
token: ''
})
}));
jest.unstable_mockModule('@BridgemanAccessible/ba-logging', () => ({
logMessage: jest.fn<(message: string, level: 'debug' | 'error' | 'warn') => void>(),
LogLevel: { DEBUG: 'debug', ERROR: 'error', WARN: 'warn' },
}));
// Mock the BaseKeystore dependency to avoid importing logic we aren't testing
jest.unstable_mockModule('@BridgemanAccessible/ba-auth_keystore', () => ({
BaseKeystore: class BaseKeystore { keystore: any; },
Keystore: { addKeystoreType: jest.fn() }
}));
jest.unstable_mockModule('node-forge', () => ({
pki: {
rsa: {
generateKeyPair: jest.fn<(bits: number, callback: (err: any, keypair: { publicKey: any, privateKey: any }) => void) => void>().mockImplementation((bits, callback) => {
// Simulate async key generation
setTimeout(() => {
callback(null, {
publicKey: { n: 'modulus', e: 'exponent' },
privateKey: { d: 'privateExponent' }
});
}, 10);
})
}
}
}))
describe('Vault Integration Tests', () => {
// Mock Vault Client Instance
let nodeVault: jest.Mock<(options?: vault.VaultOptions | undefined) => JestVaultClient>;
let mockVaultClient: JestVaultClient;
let JWK: typeof NodeJose.JWK;
let VaultKeys: typeof VaultKeysType;
let VaultCredentials: typeof VaultCredentialsType;
let VaultKeystore: typeof VaultKeystoreType;
// Standard Test Env Vars
const envVars = {
VAULT_NAME: 'test-vault',
VAULT_ROLE_ID: 'role-id',
VAULT_SECRET_ID: 'secret-id',
VAULT_PORT: 8200,
};
beforeEach(async () => {
// Reset mocks
jest.clearAllMocks();
// Setup the Node-Vault mock return values
const nodeVaultModule = await import('node-vault');
nodeVault = nodeVaultModule.default as jest.Mock<(options?: vault.VaultOptions | undefined) => JestVaultClient>;
mockVaultClient = nodeVault();
const nodeJoseModule = await import('node-jose');
JWK = nodeJoseModule.default.JWK;
// Setup the VaultCredentials and VaultKeys imports (after mocks)
const vaultCredentialsModule = await import('../src/VaultCredentials.js');
VaultCredentials = vaultCredentialsModule.VaultCredentials;
const vaultKeysModule = await import('../src/VaultKeys.js');
VaultKeys = vaultKeysModule.VaultKeys;
const vaultKeystoreModule = await import('../src/VaultKeystore.js');
VaultKeystore = vaultKeystoreModule.VaultKeystore;
});
// --- 1. Testing VaultCredentials ---
describe('VaultCredentials', () => {
it('should initialize and perform appRole login', async () => {
// Call the init method
const creds = await VaultCredentials.init('localhost', 'role', 'secret', 8200);
// Check the vault client was created correctly
expect(nodeVault).toHaveBeenCalledWith(expect.objectContaining({ endpoint: 'http://localhost:8200' }));
expect(mockVaultClient.approleLogin).toHaveBeenCalledWith({ role_id: 'role', secret_id: 'secret' });
expect(creds.getVaultClient().token).toBe('fake-token');
});
});
// --- 2. Testing VaultKeys ---
describe('VaultKeys', () => {
it('should throw error if env vars are missing', async () => {
await expect(VaultKeys.init({ ...envVars, VAULT_NAME: '' })).rejects.toThrow('The VAULT_NAME variable must be set');
});
it('should initialize with empty local keys', async () => {
const vaultKeys = await VaultKeys.init(envVars);
// "all" is synchronous and reads from the local private array
expect(vaultKeys.all()).toEqual([]);
});
describe('loadKeys (Hydration)', () => {
it('should load keys from Vault and populate local state', async () => {
// 1. Setup a real JWK to return from the mock
const realKey = await JWK.createKey('oct', 256, { kid: 'test-key-1' });
const keyJson = realKey.toJSON(true);
// 2. Mock list() to return one key ID
mockVaultClient.list.mockResolvedValue({ data: { keys: ['test-key-1'] } });
// 3. Mock read() to return the key data
mockVaultClient.read.mockResolvedValue({ data: { data: keyJson } });
const vaultKeys = await VaultKeys.init(envVars);
await vaultKeys.loadKeys();
// 4. Verify list was called
expect(mockVaultClient.list).toHaveBeenCalledWith('secret/data/keys');
// 5. Verify read was called
expect(mockVaultClient.read).toHaveBeenCalledWith('secret/data/keys/test-key-1');
// 6. Verify local state is updated (The synchronous get/all should work now)
const keys = vaultKeys.all();
expect(keys.length).toBe(1);
expect(keys[0]?.kid).toBe('test-key-1');
});
it('should handle empty vault gracefully (404 on list)', async () => {
// Simulate a 404 from Vault (standard behavior when path doesn't exist yet)
const err: any = new Error('Not Found');
err.response = { statusCode: 404 };
mockVaultClient.list.mockRejectedValue(err);
const vaultKeys = await VaultKeys.init(envVars);
const keys = await vaultKeys.loadKeys();
expect(keys).toEqual([]);
expect(vaultKeys.all()).toEqual([]);
});
});
describe('add', () => {
it('should write to Vault AND update local state', async () => {
const vaultKeys = await VaultKeys.init(envVars);
// Input: Raw Key Data
const rawKey = { kty: 'oct', k: '1234', kid: 'new-key' };
// Call add
await vaultKeys.add(rawKey as any);
// 1. Check Vault Write
expect(mockVaultClient.write).toHaveBeenCalledWith(
'secret/data/keys/new-key',
expect.objectContaining({
data: expect.objectContaining({
kid: 'new-key'
})
})
);
// 2. Check Local State
const storedKey = vaultKeys.get('new-key');
expect(storedKey).toBeDefined();
expect(storedKey.kid).toBe('new-key');
});
});
describe('generate', () => {
it('should generate a key, write it to vault, and store it locally', async () => {
const vaultKeys = await VaultKeys.init(envVars);
// This uses the real node-jose generator logic
const generatedKey = await vaultKeys.generate('RSA', 2048, { use: 'sig' });
// Verify Vault interaction
expect(mockVaultClient.write).toHaveBeenCalled();
const writeCall = mockVaultClient.write.mock.calls[0];
// Ensure the path contains the generated KID
expect((writeCall as [path: string, data: any, requestOptions?: vault.Option | undefined])[0]).toContain(`secret/data/keys/${generatedKey.kid}`);
// Verify Local interaction
expect(vaultKeys.get(generatedKey.kid)).toBeDefined();
expect(generatedKey.kty).toBe('RSA');
});
});
describe('get (Synchronous)', () => {
it('should retrieve a key by kid from local state', async () => {
const vaultKeys = await VaultKeys.init(envVars);
// Manually inject a key via add() first to populate state
const addedKey = await vaultKeys.add(await JWK.createKey('oct', 256, {}));
// Retrieve
const retrieved = vaultKeys.get(addedKey.kid);
expect(retrieved).toBeDefined();
expect(retrieved.kid).toBe(addedKey.kid);
});
it('should throw if key is missing locally', async () => {
const vaultKeys = await VaultKeys.init(envVars);
expect(() => vaultKeys.get('non-existent')).toThrow('Key non-existent not found');
});
});
});
// --- 3. Testing VaultKeystore (Wrapper) ---
describe('VaultKeystore', () => {
it('should initialize keys and setup initial keys if vault is empty', async () => {
// Mock empty vault
mockVaultClient.list.mockRejectedValue({ response: { statusCode: 404 } });
const keystore = await VaultKeystore.init(envVars);
// It should have tried to list keys
expect(mockVaultClient.list).toHaveBeenCalled();
// Since list failed (empty), it calls setupInitialKeys -> generate -> write
// We expect at least 2 writes (signing key and encryption key)
expect(mockVaultClient.write).toHaveBeenCalledTimes(2);
});
it('should load existing keys and NOT generate new ones', async () => {
// Mock existing keys
mockVaultClient.list.mockResolvedValue({ data: { keys: ['existing-key'] } });
const realKey = await JWK.createKey('RSA', 2048, { kid: 'existing-key' });
mockVaultClient.read.mockResolvedValue({ data: { data: realKey.toJSON(true) } });
await VaultKeystore.init(envVars);
// Should read the key
expect(mockVaultClient.read).toHaveBeenCalled();
// Should NOT write new keys (generate)
expect(mockVaultClient.write).not.toHaveBeenCalled();
});
});
});