Initial Commit
All checks were successful
Publish to Private NPM Registry / publish (push) Successful in 36s
All checks were successful
Publish to Private NPM Registry / publish (push) Successful in 36s
This commit is contained in:
commit
e0747f5405
14 changed files with 4785 additions and 0 deletions
261
tests/VaultKeyst.test.ts
Normal file
261
tests/VaultKeyst.test.ts
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue