mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-09 06:06:08 +00:00
9b91f0f42e
Fold the standalone 3x-ui-docs project (Next.js 16 + Fumadocs, deployed to docs.sanaei.dev) into docs/ so the panel and its documentation share a single source of truth, the way sing-box keeps its docs in-tree. The old repo becomes redundant and can be retired. - Import the full site under docs/ (app, components, content, lib, public, scripts, config). The self-contained pnpm project sits alongside the existing engineering notes with no filename collisions. - Re-point "Edit on GitHub" links from MHSanaei/3x-ui-docs to this repo's docs/content/docs path (docs/lib/shared.ts, docs/app/.../page.tsx). - Add docs-ci.yml and docs-deploy.yml under .github/workflows/, scoped to docs/** and run with working-directory: docs, since GitHub only runs workflows from the repo-root .github/. deploy-static.yml's GitHub Pages publish (CNAME docs.sanaei.dev) carries over unchanged. Follow-up (outside this commit): attach the docs.sanaei.dev custom domain to this repository's Pages (or set the Vercel project's root directory to docs), confirm the site is live from the monorepo, then delete MHSanaei/3x-ui-docs.
128 lines
4.7 KiB
TypeScript
128 lines
4.7 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
buildBalancer,
|
|
buildRule,
|
|
buildObservatory,
|
|
buildRouting,
|
|
buildRoutingJson,
|
|
type RoutingInput,
|
|
} from './routing';
|
|
|
|
describe('buildBalancer', () => {
|
|
it('emits tag, selector, and strategy.type; omits fallbackTag when empty', () => {
|
|
const b = buildBalancer({ tag: 'lb', selector: ['proxy', 'hk-'], strategy: 'roundRobin' });
|
|
expect(b.tag).toBe('lb');
|
|
expect(b.selector).toEqual(['proxy', 'hk-']);
|
|
expect((b.strategy as Record<string, unknown>).type).toBe('roundRobin');
|
|
expect('fallbackTag' in b).toBe(false);
|
|
});
|
|
|
|
it('includes fallbackTag when set', () => {
|
|
const b = buildBalancer({ tag: 'lb', selector: ['a'], strategy: 'random', fallbackTag: 'direct' });
|
|
expect(b.fallbackTag).toBe('direct');
|
|
});
|
|
});
|
|
|
|
describe('buildRule', () => {
|
|
it('emits only the matchers that are set, with outboundTag', () => {
|
|
const r = buildRule({ domain: ['geosite:google'], target: { kind: 'outbound', tag: 'warp' } });
|
|
expect(r.type).toBe('field');
|
|
expect(r.domain).toEqual(['geosite:google']);
|
|
expect('ip' in r).toBe(false);
|
|
expect(r.outboundTag).toBe('warp');
|
|
expect('balancerTag' in r).toBe(false);
|
|
});
|
|
|
|
it('uses balancerTag (and not outboundTag) when the target is a balancer', () => {
|
|
const r = buildRule({ ip: ['geoip:cn'], target: { kind: 'balancer', tag: 'lb' } });
|
|
expect(r.balancerTag).toBe('lb');
|
|
expect('outboundTag' in r).toBe(false);
|
|
expect(r.ip).toEqual(['geoip:cn']);
|
|
});
|
|
|
|
it('passes port + network through and carries ruleTag', () => {
|
|
const r = buildRule({
|
|
port: '443,8443',
|
|
network: 'tcp,udp',
|
|
ruleTag: 'r1',
|
|
target: { kind: 'outbound', tag: 'direct' },
|
|
});
|
|
expect(r.port).toBe('443,8443');
|
|
expect(r.network).toBe('tcp,udp');
|
|
expect(r.ruleTag).toBe('r1');
|
|
});
|
|
});
|
|
|
|
describe('buildObservatory', () => {
|
|
it('observatory mode emits probeURL default + enableConcurrency, no burst', () => {
|
|
const o = buildObservatory({ mode: 'observatory', subjectSelector: ['proxy'] });
|
|
const obs = (o as Record<string, Record<string, unknown>>).observatory;
|
|
expect(obs.probeURL).toBe('https://www.google.com/generate_204');
|
|
expect(obs.subjectSelector).toEqual(['proxy']);
|
|
expect(obs.enableConcurrency).toBe(true);
|
|
expect('burstObservatory' in o).toBe(false);
|
|
});
|
|
|
|
it('burst mode emits pingConfig.destination default and no observatory', () => {
|
|
const o = buildObservatory({ mode: 'burst', subjectSelector: ['proxy'] });
|
|
const burst = (o as Record<string, Record<string, Record<string, unknown>>>).burstObservatory;
|
|
expect(burst.pingConfig.destination).toBe('https://www.google.com/generate_204');
|
|
expect('observatory' in o).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('buildRouting', () => {
|
|
const base: RoutingInput = {
|
|
balancers: [{ tag: 'lb', selector: ['proxy'], strategy: 'random' }],
|
|
rules: [{ domain: ['geosite:category-ads-all'], target: { kind: 'outbound', tag: 'block' } }],
|
|
};
|
|
|
|
it('nests rules and balancers under routing', () => {
|
|
const out = buildRouting(base) as { routing: { rules: unknown[]; balancers: unknown[] } };
|
|
expect(out.routing.rules).toHaveLength(1);
|
|
expect(out.routing.balancers).toHaveLength(1);
|
|
});
|
|
|
|
it('puts observatory at the TOP level (not under routing) for a leastPing balancer', () => {
|
|
const out = buildRouting({
|
|
...base,
|
|
balancers: [{ tag: 'lb', selector: ['proxy'], strategy: 'leastPing' }],
|
|
}) as Record<string, Record<string, unknown>>;
|
|
expect(out.observatory).toBeDefined();
|
|
expect('observatory' in out.routing).toBe(false);
|
|
});
|
|
|
|
it('uses a burstObservatory for a leastLoad balancer', () => {
|
|
const out = buildRouting({
|
|
...base,
|
|
balancers: [{ tag: 'lb', selector: ['proxy'], strategy: 'leastLoad' }],
|
|
}) as Record<string, unknown>;
|
|
expect(out.burstObservatory).toBeDefined();
|
|
});
|
|
|
|
it('carries domainStrategy when set', () => {
|
|
const out = buildRouting({ ...base, domainStrategy: 'IPIfNonMatch' }) as {
|
|
routing: Record<string, unknown>;
|
|
};
|
|
expect(out.routing.domainStrategy).toBe('IPIfNonMatch');
|
|
});
|
|
|
|
it('does not auto-add an observatory for random/roundRobin balancers', () => {
|
|
const out = buildRouting(base) as Record<string, unknown>;
|
|
expect('observatory' in out).toBe(false);
|
|
expect('burstObservatory' in out).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('buildRoutingJson', () => {
|
|
it('round-trips and is 2-space indented', () => {
|
|
const input: RoutingInput = {
|
|
balancers: [],
|
|
rules: [{ ip: ['geoip:private'], target: { kind: 'outbound', tag: 'direct' } }],
|
|
};
|
|
const json = buildRoutingJson(input);
|
|
expect(json).toContain('\n "');
|
|
expect(JSON.parse(json)).toEqual(buildRouting(input));
|
|
});
|
|
});
|