moving
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled

This commit is contained in:
Oleg Maslov
2026-09-02 10:10:29 +02:00
commit 0c3e2ead3b
3841 changed files with 970576 additions and 0 deletions

View File

@@ -0,0 +1,281 @@
/**
* Vault REST API Route Tests
*
* Tests all 4 vault endpoints using a real VaultStore on a temp directory:
* GET /api/vault — list secrets (no values)
* POST /api/vault — add/update a secret
* DELETE /api/vault/:name — delete a secret
* POST /api/vault/:name/reveal — decrypt and return the full value
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import path from 'node:path';
import fs from 'node:fs';
import os from 'node:os';
import Fastify from 'fastify';
import { VaultStore } from '@waggle/core';
import { vaultRoutes } from '../../src/local/routes/vault.js';
/** Shape of a secret summary in the GET /api/vault response (no value exposed). */
interface VaultSecretSummary {
name: string;
type: string;
updatedAt: string;
isCommon: boolean;
}
function createTestServer(vault: VaultStore) {
const server = Fastify({ logger: false });
server.decorate('vault', vault);
server.register(vaultRoutes);
return server;
}
describe('Vault Routes', () => {
let tmpDir: string;
let vault: VaultStore;
let server: ReturnType<typeof Fastify>;
beforeEach(() => {
tmpDir = path.join(os.tmpdir(), `waggle-vault-routes-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
fs.mkdirSync(tmpDir, { recursive: true });
vault = new VaultStore(tmpDir);
server = createTestServer(vault);
});
afterEach(async () => {
await server.close();
fs.rmSync(tmpDir, { recursive: true, force: true });
});
// ── GET /api/vault ───────────────────────────────────────────────
describe('GET /api/vault', () => {
it('returns empty list initially', async () => {
const res = await server.inject({ method: 'GET', url: '/api/vault' });
expect(res.statusCode).toBe(200);
const body = res.json();
expect(body.secrets).toEqual([]);
expect(body.suggestedKeys).toBeDefined();
expect(Array.isArray(body.suggestedKeys)).toBe(true);
});
it('returns secrets after adding', async () => {
vault.set('ANTHROPIC_API_KEY', 'sk-ant-test-123', { credentialType: 'api_key' });
vault.set('MY_CUSTOM_KEY', 'custom-value-456');
const res = await server.inject({ method: 'GET', url: '/api/vault' });
expect(res.statusCode).toBe(200);
const body = res.json();
expect(body.secrets).toHaveLength(2);
const anthropicSecret = body.secrets.find((s: VaultSecretSummary) => s.name === 'ANTHROPIC_API_KEY');
expect(anthropicSecret).toBeDefined();
expect(anthropicSecret.type).toBe('api_key');
expect(anthropicSecret.updatedAt).toBeDefined();
// Ensure value is NOT exposed in list
expect(anthropicSecret.value).toBeUndefined();
});
it('sets isCommon flag correctly', async () => {
vault.set('ANTHROPIC_API_KEY', 'sk-ant-test');
vault.set('MY_PRIVATE_KEY', 'secret');
const res = await server.inject({ method: 'GET', url: '/api/vault' });
const body = res.json();
const anthropic = body.secrets.find((s: VaultSecretSummary) => s.name === 'ANTHROPIC_API_KEY');
const custom = body.secrets.find((s: VaultSecretSummary) => s.name === 'MY_PRIVATE_KEY');
// ANTHROPIC_API_KEY is not in the new COMMON_KEYS (now uses 'anthropic' as provider ID)
expect(anthropic.isCommon).toBe(false);
expect(custom.isCommon).toBe(false);
});
it('suggestedKeys excludes keys already in vault', async () => {
vault.set('anthropic', 'sk-ant-test');
const res = await server.inject({ method: 'GET', url: '/api/vault' });
const body = res.json();
expect(body.suggestedKeys).not.toContain('anthropic');
expect(body.suggestedKeys).toContain('openai');
expect(body.suggestedKeys).toContain('TAVILY_API_KEY');
});
it('suggestedKeys contains common keys when vault is empty', async () => {
const res = await server.inject({ method: 'GET', url: '/api/vault' });
const body = res.json();
expect(body.suggestedKeys).toContain('anthropic');
expect(body.suggestedKeys).toContain('openai');
expect(body.suggestedKeys).toContain('GITHUB_TOKEN');
expect(body.suggestedKeys.length).toBeGreaterThan(20);
// Also has categorized suggestions
expect(body.suggestedSecrets).toBeDefined();
expect(body.suggestedSecrets.length).toBeGreaterThan(0);
expect(body.suggestedSecrets[0].category).toBe('LLM Providers');
});
});
// ── POST /api/vault ──────────────────────────────────────────────
describe('POST /api/vault', () => {
it('adds a new secret', async () => {
const res = await server.inject({
method: 'POST',
url: '/api/vault',
payload: { name: 'OPENAI_API_KEY', value: 'sk-test-key-abc' },
});
expect(res.statusCode).toBe(200);
const body = res.json();
expect(body.success).toBe(true);
expect(body.name).toBe('OPENAI_API_KEY');
// Verify it was actually stored
const stored = vault.get('OPENAI_API_KEY');
expect(stored).not.toBeNull();
expect(stored!.value).toBe('sk-test-key-abc');
});
it('adds a secret with type metadata', async () => {
const res = await server.inject({
method: 'POST',
url: '/api/vault',
payload: { name: 'GITHUB_TOKEN', value: 'ghp_token', type: 'bearer' },
});
expect(res.statusCode).toBe(200);
const stored = vault.get('GITHUB_TOKEN');
expect(stored!.metadata?.credentialType).toBe('bearer');
});
it('updates an existing secret', async () => {
vault.set('MY_KEY', 'old-value');
const res = await server.inject({
method: 'POST',
url: '/api/vault',
payload: { name: 'MY_KEY', value: 'new-value' },
});
expect(res.statusCode).toBe(200);
const stored = vault.get('MY_KEY');
expect(stored!.value).toBe('new-value');
});
it('rejects missing name', async () => {
const res = await server.inject({
method: 'POST',
url: '/api/vault',
payload: { value: 'some-value' },
});
expect(res.statusCode).toBe(400);
// validateBody returns a generic error + structured zod issues naming the
// offending field (path), rather than embedding the field in the message.
const body = res.json();
expect(body.error).toBe('Invalid request body');
expect(JSON.stringify(body.issues)).toContain('name');
});
it('rejects missing value', async () => {
const res = await server.inject({
method: 'POST',
url: '/api/vault',
payload: { name: 'SOME_KEY' },
});
expect(res.statusCode).toBe(400);
const body = res.json();
expect(body.error).toBe('Invalid request body');
expect(JSON.stringify(body.issues)).toContain('value');
});
});
// ── DELETE /api/vault/:name ──────────────────────────────────────
describe('DELETE /api/vault/:name', () => {
it('deletes an existing secret', async () => {
vault.set('TO_DELETE', 'doomed');
const res = await server.inject({
method: 'DELETE',
url: '/api/vault/TO_DELETE',
});
expect(res.statusCode).toBe(200);
const body = res.json();
expect(body.deleted).toBe(true);
expect(body.name).toBe('TO_DELETE');
// Verify deletion
expect(vault.has('TO_DELETE')).toBe(false);
});
it('returns deleted:false for non-existent secret', async () => {
const res = await server.inject({
method: 'DELETE',
url: '/api/vault/NONEXISTENT',
});
expect(res.statusCode).toBe(404);
const body = res.json();
expect(body.error).toBe('Secret not found');
expect(body.name).toBe('NONEXISTENT');
});
});
// ── POST /api/vault/:name/reveal ─────────────────────────────────
describe('POST /api/vault/:name/reveal', () => {
it('reveals the decrypted value', async () => {
vault.set('SECRET_KEY', 'super-secret-value', { credentialType: 'api_key' });
const res = await server.inject({
method: 'POST',
url: '/api/vault/SECRET_KEY/reveal',
});
expect(res.statusCode).toBe(200);
const body = res.json();
expect(body.name).toBe('SECRET_KEY');
expect(body.value).toBe('super-secret-value');
expect(body.type).toBe('api_key');
});
it('returns default type when no credentialType metadata', async () => {
vault.set('PLAIN_KEY', 'plain-value');
const res = await server.inject({
method: 'POST',
url: '/api/vault/PLAIN_KEY/reveal',
});
expect(res.statusCode).toBe(200);
const body = res.json();
expect(body.type).toBe('api_key');
});
it('returns 404 for non-existent secret', async () => {
const res = await server.inject({
method: 'POST',
url: '/api/vault/NONEXISTENT/reveal',
});
expect(res.statusCode).toBe(404);
expect(res.json().error).toBe('Secret not found');
});
});
});
// ── Module Export Check ──────────────────────────────────────────────
describe('Vault Routes Module', () => {
it('exports vaultRoutes function', async () => {
const mod = await import('../../src/local/routes/vault.js');
expect(mod.vaultRoutes).toBeDefined();
expect(typeof mod.vaultRoutes).toBe('function');
});
});