74 lines
4.3 KiB
TypeScript
74 lines
4.3 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
import { DatabaseSync } from 'node:sqlite';
|
|
import { test } from 'node:test';
|
|
import type { Env } from '../src/types.ts';
|
|
import { decryptHost } from '../src/accounts/crypto.ts';
|
|
import { forwardRulesRoute, validateForwardRule } from '../src/accounts/forward-rules.ts';
|
|
|
|
const hostId = '00000000-0000-4000-8000-000000000001';
|
|
const input = { name: ' 服务 A ', hostId, port: 8080, mode: 'trusted' };
|
|
|
|
function fixture() {
|
|
const sqlite = new DatabaseSync(':memory:');
|
|
sqlite.exec('CREATE TABLE hosts (id TEXT PRIMARY KEY, account_id TEXT NOT NULL)');
|
|
sqlite.exec(readFileSync(new URL('../migrations/0004_forward_rules.sql', import.meta.url), 'utf8'));
|
|
sqlite.prepare('INSERT INTO hosts(id, account_id) VALUES (?, ?)').run(hostId, 'owner');
|
|
const prepare = (sql: string) => {
|
|
let params: (string | number)[] = [];
|
|
const query = sqlite.prepare(sql);
|
|
const statement = {
|
|
bind(...values: (string | number)[]) { params = values; return statement; },
|
|
async first() { return query.get(...params) ?? null; },
|
|
async all() { return { results: query.all(...params) }; },
|
|
async run() { return { meta: { changes: Number(query.run(...params).changes) } }; },
|
|
};
|
|
return statement;
|
|
};
|
|
const env = { ENCRYPTION_KEY: Buffer.alloc(32, 3).toString('base64'), DB: { prepare } } as unknown as Env;
|
|
const request = async (method = 'GET', id = '', body?: unknown, owner = 'owner') => {
|
|
const path = `/api/forward-rules${id ? `/${id}` : ''}`;
|
|
return forwardRulesRoute(new Request(`https://example.com${path}`, {
|
|
method, headers: { 'Content-Type': 'application/json' }, body: body === undefined ? undefined : JSON.stringify(body),
|
|
}), env, owner, path);
|
|
};
|
|
return { sqlite, env, request };
|
|
}
|
|
|
|
test('规则只接受有效名称、所属主机、端口与连接方式', () => {
|
|
assert.deepEqual(validateForwardRule(input), { ...input, name: '服务 A' });
|
|
for (const invalid of [
|
|
{ ...input, name: ' ' }, { ...input, name: 'A'.repeat(81) }, { ...input, name: 'x\n' },
|
|
{ ...input, hostId: 'other' }, { ...input, port: 0 }, { ...input, port: 65536 },
|
|
{ ...input, port: '8080' }, { ...input, port: 22.5 }, { ...input, mode: 'unknown' },
|
|
]) assert.throws(() => validateForwardRule(invalid), /规则名称|主机|端口|转发方式/);
|
|
});
|
|
|
|
test('规则云端加密、按账户隔离并允许增改删', async (context) => {
|
|
const f = fixture(); context.after(() => f.sqlite.close());
|
|
const created = await f.request('POST', '', input);
|
|
assert.equal(created.status, 201);
|
|
const { rule } = await created.json() as { rule: { id: string; name: string; port: number } };
|
|
const row = f.sqlite.prepare('SELECT encrypted_payload FROM forward_rules WHERE id = ?').get(rule.id)!;
|
|
assert.equal(String(row.encrypted_payload).includes('服务 A'), false);
|
|
await assert.rejects(decryptHost(String(row.encrypted_payload), f.env.ENCRYPTION_KEY, 'owner', rule.id));
|
|
assert.equal((await (await f.request()).json() as { rules: unknown[] }).rules.length, 1);
|
|
assert.deepEqual((await (await f.request('GET', '', undefined, 'other')).json() as { rules: unknown[] }).rules, []);
|
|
await assert.rejects(f.request('POST', '', input, 'other'), /主机不存在/);
|
|
await assert.rejects(f.request('PUT', rule.id, input, 'other'), /主机不存在/);
|
|
await assert.rejects(f.request('DELETE', rule.id, undefined, 'other'), /规则不存在/);
|
|
await f.request('PUT', rule.id, { ...input, name: '更新', port: 3000, mode: 'isolated' });
|
|
const listed = (await (await f.request()).json() as { rules: Array<{ name: string; port: number; mode: string }> }).rules;
|
|
assert.deepEqual([listed[0].name, listed[0].port, listed[0].mode], ['更新', 3000, 'isolated']);
|
|
await f.request('DELETE', rule.id);
|
|
assert.equal((await (await f.request()).json() as { rules: unknown[] }).rules.length, 0);
|
|
});
|
|
|
|
test('主机删除后旧规则仍可列出,但无法保存新的无效引用', async (context) => {
|
|
const f = fixture(); context.after(() => f.sqlite.close());
|
|
await f.request('POST', '', input);
|
|
f.sqlite.prepare('DELETE FROM hosts WHERE id = ?').run(hostId);
|
|
assert.equal((await (await f.request()).json() as { rules: unknown[] }).rules.length, 1);
|
|
await assert.rejects(f.request('POST', '', input), /主机不存在/);
|
|
});
|