262 lines
8.8 KiB
TypeScript
262 lines
8.8 KiB
TypeScript
/**
|
|
* Vault edge case tests — corrupted files, missing files,
|
|
* concurrent read/write safety, and large value storage.
|
|
*/
|
|
|
|
import { describe, it, expect, afterEach } from 'vitest';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { VaultStore } from '../src/vault.js';
|
|
|
|
describe('VaultStore edge cases', () => {
|
|
const tempDirs: string[] = [];
|
|
|
|
function makeTempDir(): string {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-vault-edge-'));
|
|
tempDirs.push(dir);
|
|
return dir;
|
|
}
|
|
|
|
afterEach(() => {
|
|
for (const dir of tempDirs) {
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
tempDirs.length = 0;
|
|
});
|
|
|
|
// ── Corrupted vault.json ────────────────────────────────────────────
|
|
|
|
describe('corrupted vault.json', () => {
|
|
it('handles invalid JSON gracefully — returns null for get', () => {
|
|
const dir = makeTempDir();
|
|
const vault = new VaultStore(dir);
|
|
|
|
// Store a valid secret first
|
|
vault.set('test-key', 'test-value');
|
|
expect(vault.get('test-key')!.value).toBe('test-value');
|
|
|
|
// Corrupt the vault.json file with invalid JSON
|
|
const vaultPath = path.join(dir, 'vault.json');
|
|
fs.writeFileSync(vaultPath, '{ this is not valid JSON !!!', 'utf-8');
|
|
|
|
// get should return null (not crash)
|
|
expect(vault.get('test-key')).toBeNull();
|
|
});
|
|
|
|
it('handles invalid JSON gracefully — list returns empty array', () => {
|
|
const dir = makeTempDir();
|
|
const vault = new VaultStore(dir);
|
|
|
|
// Corrupt the vault.json file
|
|
const vaultPath = path.join(dir, 'vault.json');
|
|
fs.writeFileSync(vaultPath, '<xml>not json</xml>', 'utf-8');
|
|
|
|
// list should return empty (not crash)
|
|
expect(vault.list()).toEqual([]);
|
|
});
|
|
|
|
it('handles invalid JSON gracefully — has returns false', () => {
|
|
const dir = makeTempDir();
|
|
const vault = new VaultStore(dir);
|
|
|
|
// Corrupt the vault.json file
|
|
const vaultPath = path.join(dir, 'vault.json');
|
|
fs.writeFileSync(vaultPath, '}}broken{{', 'utf-8');
|
|
|
|
// has should return false (not crash)
|
|
expect(vault.has('anything')).toBe(false);
|
|
});
|
|
|
|
it('can write new secrets after corruption — overwrites corrupted file', () => {
|
|
const dir = makeTempDir();
|
|
const vault = new VaultStore(dir);
|
|
|
|
// Corrupt the vault.json file
|
|
const vaultPath = path.join(dir, 'vault.json');
|
|
fs.writeFileSync(vaultPath, 'CORRUPT!', 'utf-8');
|
|
|
|
// set should overwrite corrupted file with valid data
|
|
vault.set('recovery-key', 'recovered-value');
|
|
|
|
// Should be able to read back the new value
|
|
const entry = vault.get('recovery-key');
|
|
expect(entry).not.toBeNull();
|
|
expect(entry!.value).toBe('recovered-value');
|
|
|
|
// Verify the file is now valid JSON
|
|
const raw = fs.readFileSync(vaultPath, 'utf-8');
|
|
expect(() => JSON.parse(raw)).not.toThrow();
|
|
});
|
|
});
|
|
|
|
// ── Missing vault.json ──────────────────────────────────────────────
|
|
|
|
describe('missing vault.json', () => {
|
|
it('creates a new vault.json on next write when file is missing', () => {
|
|
const dir = makeTempDir();
|
|
const vault = new VaultStore(dir);
|
|
const vaultPath = path.join(dir, 'vault.json');
|
|
|
|
// Ensure no vault.json exists initially
|
|
if (fs.existsSync(vaultPath)) {
|
|
fs.unlinkSync(vaultPath);
|
|
}
|
|
expect(fs.existsSync(vaultPath)).toBe(false);
|
|
|
|
// Write a secret — should create vault.json
|
|
vault.set('new-key', 'new-value');
|
|
|
|
expect(fs.existsSync(vaultPath)).toBe(true);
|
|
const entry = vault.get('new-key');
|
|
expect(entry).not.toBeNull();
|
|
expect(entry!.value).toBe('new-value');
|
|
});
|
|
|
|
it('get returns null when vault.json does not exist', () => {
|
|
const dir = makeTempDir();
|
|
const vault = new VaultStore(dir);
|
|
const vaultPath = path.join(dir, 'vault.json');
|
|
|
|
// Ensure no vault.json exists
|
|
if (fs.existsSync(vaultPath)) {
|
|
fs.unlinkSync(vaultPath);
|
|
}
|
|
|
|
expect(vault.get('missing')).toBeNull();
|
|
});
|
|
|
|
it('list returns empty array when vault.json does not exist', () => {
|
|
const dir = makeTempDir();
|
|
const vault = new VaultStore(dir);
|
|
const vaultPath = path.join(dir, 'vault.json');
|
|
|
|
// Ensure no vault.json exists
|
|
if (fs.existsSync(vaultPath)) {
|
|
fs.unlinkSync(vaultPath);
|
|
}
|
|
|
|
expect(vault.list()).toEqual([]);
|
|
});
|
|
});
|
|
|
|
// ── Concurrent reads during write ──────────────────────────────────
|
|
|
|
describe('concurrent reads during write', () => {
|
|
it('read during setAsync does not corrupt data', async () => {
|
|
const dir = makeTempDir();
|
|
const vault = new VaultStore(dir);
|
|
|
|
// Pre-populate with a known value
|
|
vault.set('existing', 'original-value');
|
|
|
|
// Start an async write, then immediately read
|
|
const writePromise = vault.setAsync('new-key', 'new-value');
|
|
const readResult = vault.get('existing');
|
|
|
|
await writePromise;
|
|
|
|
// The original key should still be readable (not corrupted)
|
|
expect(readResult).not.toBeNull();
|
|
expect(readResult!.value).toBe('original-value');
|
|
|
|
// The new key should also be present
|
|
const newEntry = vault.get('new-key');
|
|
expect(newEntry).not.toBeNull();
|
|
expect(newEntry!.value).toBe('new-value');
|
|
|
|
// The existing key should still be intact
|
|
const existingAfter = vault.get('existing');
|
|
expect(existingAfter).not.toBeNull();
|
|
expect(existingAfter!.value).toBe('original-value');
|
|
});
|
|
|
|
it('multiple rapid reads interleaved with writes produce consistent results', async () => {
|
|
const dir = makeTempDir();
|
|
const vault = new VaultStore(dir);
|
|
|
|
// Write initial data
|
|
vault.set('stable', 'stable-value');
|
|
|
|
// Fire off writes and reads in rapid succession
|
|
const writePromises = [];
|
|
for (let i = 0; i < 5; i++) {
|
|
writePromises.push(vault.setAsync(`rapid-${i}`, `value-${i}`));
|
|
}
|
|
|
|
// Interleave reads
|
|
const readResults: (string | null)[] = [];
|
|
for (let i = 0; i < 5; i++) {
|
|
const entry = vault.get('stable');
|
|
readResults.push(entry?.value ?? null);
|
|
}
|
|
|
|
await Promise.all(writePromises);
|
|
|
|
// All reads of the stable key should return the correct value
|
|
for (const val of readResults) {
|
|
expect(val).toBe('stable-value');
|
|
}
|
|
|
|
// All written keys should be present
|
|
for (let i = 0; i < 5; i++) {
|
|
const entry = vault.get(`rapid-${i}`);
|
|
expect(entry).not.toBeNull();
|
|
expect(entry!.value).toBe(`value-${i}`);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ── Large value storage ────────────────────────────────────────────
|
|
|
|
describe('large value storage', () => {
|
|
it('stores and retrieves a 1MB string correctly', () => {
|
|
const dir = makeTempDir();
|
|
const vault = new VaultStore(dir);
|
|
|
|
// Generate a 1MB string (1,048,576 characters)
|
|
const largeValue = 'A'.repeat(1024 * 1024);
|
|
expect(largeValue.length).toBe(1024 * 1024);
|
|
|
|
vault.set('large-secret', largeValue);
|
|
|
|
const entry = vault.get('large-secret');
|
|
expect(entry).not.toBeNull();
|
|
expect(entry!.value).toBe(largeValue);
|
|
expect(entry!.value.length).toBe(1024 * 1024);
|
|
});
|
|
|
|
it('large value is encrypted in vault.json — plaintext not present', () => {
|
|
const dir = makeTempDir();
|
|
const vault = new VaultStore(dir);
|
|
|
|
// Use a distinctive pattern that would be easy to find if unencrypted
|
|
const largeValue = 'SECRET_MARKER_'.repeat(10000);
|
|
vault.set('large-encrypted', largeValue);
|
|
|
|
const rawContent = fs.readFileSync(path.join(dir, 'vault.json'), 'utf-8');
|
|
expect(rawContent).not.toContain('SECRET_MARKER_');
|
|
|
|
// Verify the encrypted field has the correct format
|
|
const parsed = JSON.parse(rawContent);
|
|
expect(parsed['large-encrypted'].encrypted).toMatch(/^[0-9a-f]+:[0-9a-f]+:[0-9a-f]+$/);
|
|
});
|
|
|
|
it('large value survives round-trip through different VaultStore instances', () => {
|
|
const dir = makeTempDir();
|
|
|
|
// Write with one instance
|
|
const vault1 = new VaultStore(dir);
|
|
const largeValue = 'B'.repeat(1024 * 1024);
|
|
vault1.set('large-roundtrip', largeValue);
|
|
|
|
// Read with a different instance (same key file)
|
|
const vault2 = new VaultStore(dir);
|
|
const entry = vault2.get('large-roundtrip');
|
|
expect(entry).not.toBeNull();
|
|
expect(entry!.value).toBe(largeValue);
|
|
expect(entry!.value.length).toBe(1024 * 1024);
|
|
});
|
|
});
|
|
});
|