server be & fe

This commit is contained in:
azykov@mail.ru
2026-09-29 13:50:01 +03:00
commit 657009bd6c
56 changed files with 6162 additions and 0 deletions
+81
View File
@@ -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]);
});
});