Files
EdgeSSH/tests/forward-rules.test.ts
T

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), /主机不存在/);
});