server be & fe
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { parseKeySecret } from '../src/config.js';
|
||||
import { ACCESS_KEY_LENGTH, decryptSecret, encryptSecret, generateAccessKey } from '../src/crypto.js';
|
||||
import { YamlFile } from '../src/yamlFile.js';
|
||||
import { tempDir } from './helpers.js';
|
||||
|
||||
describe('crypto', () => {
|
||||
it('generates 32-char alphanumeric keys', () => {
|
||||
const keys = new Set(Array.from({ length: 100 }, generateAccessKey));
|
||||
expect(keys.size).toBe(100);
|
||||
for (const key of keys) expect(key).toMatch(new RegExp(`^[A-Za-z0-9]{${ACCESS_KEY_LENGTH}}$`));
|
||||
});
|
||||
|
||||
it('round-trips with AES-256-GCM using a random IV', () => {
|
||||
const secret = randomBytes(32);
|
||||
const a = encryptSecret('hello', secret);
|
||||
const b = encryptSecret('hello', secret);
|
||||
expect(a).not.toBe(b);
|
||||
expect(decryptSecret(a, secret)).toBe('hello');
|
||||
});
|
||||
|
||||
it('rejects a wrong secret or tampered data', () => {
|
||||
const secret = randomBytes(32);
|
||||
const encrypted = encryptSecret('hello', secret);
|
||||
expect(() => decryptSecret(encrypted, randomBytes(32))).toThrow();
|
||||
const buf = Buffer.from(encrypted, 'base64');
|
||||
buf[buf.length - 1]! ^= 1;
|
||||
expect(() => decryptSecret(buf.toString('base64'), secret)).toThrow();
|
||||
});
|
||||
|
||||
it('validates KEY_ENCRYPTION_SECRET', () => {
|
||||
expect(parseKeySecret(randomBytes(32).toString('base64'))).toHaveLength(32);
|
||||
expect(() => parseKeySecret(randomBytes(16).toString('base64'))).toThrow(/32 bytes/);
|
||||
expect(() => parseKeySecret(undefined)).toThrow(/required/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('YamlFile', () => {
|
||||
const make = (file: string) =>
|
||||
new YamlFile<{ items: number[] }>(file, (raw) => (raw as { items: number[] }) ?? { items: [] }, () => ({ items: [] }));
|
||||
|
||||
it('returns empty data for a missing file and persists updates', async () => {
|
||||
const file = path.join(await tempDir(), 'sub', 'data.yaml');
|
||||
const store = make(file);
|
||||
expect(await store.read()).toEqual({ items: [] });
|
||||
await store.update((d) => d.items.push(1));
|
||||
expect(await make(file).read()).toEqual({ items: [1] });
|
||||
});
|
||||
|
||||
it('serializes concurrent updates', async () => {
|
||||
const store = make(path.join(await tempDir(), 'data.yaml'));
|
||||
await Promise.all(Array.from({ length: 20 }, (_, i) => store.update((d) => d.items.push(i))));
|
||||
expect((await store.read()).items).toHaveLength(20);
|
||||
});
|
||||
|
||||
it('does not write when the update throws', async () => {
|
||||
const file = path.join(await tempDir(), 'data.yaml');
|
||||
const store = make(file);
|
||||
await store.update((d) => d.items.push(1));
|
||||
await expect(
|
||||
store.update((d) => {
|
||||
d.items.push(2);
|
||||
throw new Error('boom');
|
||||
}),
|
||||
).rejects.toThrow('boom');
|
||||
expect((await store.read()).items).toEqual([1]);
|
||||
expect(await readFile(file, 'utf8')).not.toContain('2');
|
||||
});
|
||||
|
||||
it('picks up manual edits', async () => {
|
||||
const file = path.join(await tempDir(), 'data.yaml');
|
||||
const store = make(file);
|
||||
await store.update((d) => d.items.push(1));
|
||||
await new Promise((r) => setTimeout(r, 20));
|
||||
await writeFile(file, 'items: [7, 8]\n');
|
||||
expect((await store.read()).items).toEqual([7, 8]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user